mirror of
https://github.com/orbbec/OrbbecSDK_ROS2.git
synced 2026-10-11 15:09:48 +08:00
feat: benchmark camera topics from subscribers
This commit is contained in:
@@ -4,11 +4,14 @@
|
|||||||
Support: ROS2
|
Support: ROS2
|
||||||
name: common_benchmark_node.py
|
name: common_benchmark_node.py
|
||||||
function: A ROS2 node to monitor and log the performance of an Orbbec camera node:
|
function: A ROS2 node to monitor and log the performance of an Orbbec camera node:
|
||||||
frame rates, delays, CPU and RAM usage, packet/frame loss statistics.
|
subscriber-side frame rates, end-to-end delays, CPU and RAM usage,
|
||||||
|
and estimated frame loss statistics.
|
||||||
usage:
|
usage:
|
||||||
ros2 run orbbec_camera common_benchmark_node.py --run_time 20 --csv_file /tmp/cam_log.csv
|
ros2 run orbbec_camera common_benchmark_node.py --run_time 20 --csv_file /tmp/cam_log.csv
|
||||||
You can also pass an ideal frame rate for drop detection: --ideal_fps 30
|
You can also pass an ideal frame rate for drop detection: --ideal_fps 30
|
||||||
Monitor multiple cameras: --camera_names camera,camera01
|
Monitor multiple cameras: --camera_names camera,camera01
|
||||||
|
Select topics explicitly: --topics color,depth
|
||||||
|
Compressed images and point clouds require full topic names.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -21,7 +24,7 @@ import csv
|
|||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from orbbec_camera_msgs.msg import DeviceStatus
|
from orbbec_camera_msgs.msg import DeviceStatus
|
||||||
from sensor_msgs.msg import Image
|
from sensor_msgs.msg import CompressedImage, Image, PointCloud2
|
||||||
|
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
@@ -29,11 +32,14 @@ CAMERA_NODE_NAMES = ["component_container", "orbbec_camera_node", "nodelet"]
|
|||||||
MONITORED_STREAMS = (
|
MONITORED_STREAMS = (
|
||||||
"color",
|
"color",
|
||||||
"depth",
|
"depth",
|
||||||
|
"ir",
|
||||||
"left_ir",
|
"left_ir",
|
||||||
"right_ir",
|
"right_ir",
|
||||||
"left_color",
|
"left_color",
|
||||||
"right_color",
|
"right_color",
|
||||||
)
|
)
|
||||||
|
DISCOVERY_INTERVAL_SECONDS = 0.1
|
||||||
|
DISCOVERY_DURATION_SECONDS = 1.0
|
||||||
DOCUMENTATION_URL = (
|
DOCUMENTATION_URL = (
|
||||||
"https://orbbec.github.io/OrbbecSDK_ROS2/en/source/camera_devices/"
|
"https://orbbec.github.io/OrbbecSDK_ROS2/en/source/camera_devices/"
|
||||||
"6_benchmark/benchmark_tools.html"
|
"6_benchmark/benchmark_tools.html"
|
||||||
@@ -107,29 +113,62 @@ def estimate_dropped_frames(dt, expected_interval):
|
|||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
|
|
||||||
class TopicTracker:
|
class TopicTracker:
|
||||||
def __init__(self, logger=None):
|
def __init__(self, logger=None, sample_start_time=None):
|
||||||
self.received = 0
|
self.received = 0
|
||||||
|
self.sample_start_time = sample_start_time or time.monotonic()
|
||||||
self.last_time = None
|
self.last_sample_time = self.sample_start_time
|
||||||
|
self.last_sample_received = 0
|
||||||
|
self.last_header_stamp = None
|
||||||
|
self.estimated_interval = None
|
||||||
self.drop_frames = 0
|
self.drop_frames = 0
|
||||||
|
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
def on_msg(self, header, avg_fps):
|
def on_msg(self, header, ros_receive_time, ideal_fps):
|
||||||
stamp = header.stamp.sec + header.stamp.nanosec * 1e-9
|
"""Record one received message and return its age in milliseconds."""
|
||||||
|
header_stamp = header.stamp.sec + header.stamp.nanosec * 1e-9
|
||||||
self.received += 1
|
self.received += 1
|
||||||
observed_fps = None
|
delay_ms = None
|
||||||
|
|
||||||
if self.last_time is not None:
|
if header_stamp > 0:
|
||||||
dt = stamp - self.last_time
|
delay_ms = (ros_receive_time - header_stamp) * 1000.0
|
||||||
if dt > 0:
|
if self.last_header_stamp is not None:
|
||||||
observed_fps = 1.0 / dt
|
header_dt = header_stamp - self.last_header_stamp
|
||||||
if avg_fps > 0:
|
if header_dt > 0:
|
||||||
expected_interval = 1.0 / avg_fps
|
expected_interval = (
|
||||||
self.drop_frames += estimate_dropped_frames(dt, expected_interval)
|
1.0 / ideal_fps
|
||||||
|
if ideal_fps and ideal_fps > 0.0
|
||||||
|
else self.estimated_interval
|
||||||
|
)
|
||||||
|
if expected_interval is not None:
|
||||||
|
self.drop_frames += estimate_dropped_frames(
|
||||||
|
header_dt, expected_interval
|
||||||
|
)
|
||||||
|
self.update_estimated_interval(header_dt)
|
||||||
|
self.last_header_stamp = header_stamp
|
||||||
|
|
||||||
self.last_time = stamp
|
return delay_ms
|
||||||
return stamp, observed_fps
|
|
||||||
|
def sample_fps(self, sample_time):
|
||||||
|
"""Calculate current and average subscriber throughput."""
|
||||||
|
window_elapsed = sample_time - self.last_sample_time
|
||||||
|
total_elapsed = sample_time - self.sample_start_time
|
||||||
|
if window_elapsed <= 0.0 or total_elapsed <= 0.0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
window_received = self.received - self.last_sample_received
|
||||||
|
current_fps = window_received / window_elapsed
|
||||||
|
average_fps = self.received / total_elapsed
|
||||||
|
self.last_sample_time = sample_time
|
||||||
|
self.last_sample_received = self.received
|
||||||
|
return current_fps, average_fps
|
||||||
|
|
||||||
|
def update_estimated_interval(self, interval):
|
||||||
|
"""Learn the nominal source interval while excluding likely frame gaps."""
|
||||||
|
if self.estimated_interval is None or interval < 0.75 * self.estimated_interval:
|
||||||
|
self.estimated_interval = interval
|
||||||
|
elif interval <= 1.5 * self.estimated_interval:
|
||||||
|
self.estimated_interval = 0.9 * self.estimated_interval + 0.1 * interval
|
||||||
|
|
||||||
def frames_loss_rate(self):
|
def frames_loss_rate(self):
|
||||||
total = self.received + self.drop_frames
|
total = self.received + self.drop_frames
|
||||||
@@ -142,17 +181,27 @@ class TopicTracker:
|
|||||||
|
|
||||||
|
|
||||||
class CameraMonitorNode(Node):
|
class CameraMonitorNode(Node):
|
||||||
def __init__(self, run_time, csv_file="camera_monitor_log.csv", ideal_fps: float = 0.0, camera_names=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
run_time,
|
||||||
|
csv_file="camera_monitor_log.csv",
|
||||||
|
ideal_fps: float = 0.0,
|
||||||
|
camera_names=None,
|
||||||
|
topics=None,
|
||||||
|
):
|
||||||
super().__init__("camera_monitor_node")
|
super().__init__("camera_monitor_node")
|
||||||
|
|
||||||
self.run_time = run_time
|
self.run_time = run_time
|
||||||
self.start_time = time.time()
|
self.discovery_start_time = time.time()
|
||||||
|
self.discovery_start_monotonic = None
|
||||||
|
self.start_time = None
|
||||||
self.process = psutil.Process(os.getpid())
|
self.process = psutil.Process(os.getpid())
|
||||||
self.first_data_collected = False
|
self.first_data_collected = False
|
||||||
self.camera_names = parse_camera_names(camera_names)
|
self.camera_names = parse_camera_names(camera_names)
|
||||||
self.node_names = {camera_name: "Not Found" for camera_name in self.camera_names}
|
self.node_names = {camera_name: "Not Found" for camera_name in self.camera_names}
|
||||||
self.total_node_name = "Not Found"
|
self.total_node_name = "Not Found"
|
||||||
# If > 0, use this ideal fps value for drop-frame detection instead of the reported average
|
# If > 0, use this ideal FPS for drop detection instead of learning the
|
||||||
|
# nominal interval from received image timestamps.
|
||||||
self.ideal_fps = float(ideal_fps) if ideal_fps is not None else 0.0
|
self.ideal_fps = float(ideal_fps) if ideal_fps is not None else 0.0
|
||||||
self.finished = False
|
self.finished = False
|
||||||
|
|
||||||
@@ -166,22 +215,26 @@ class CameraMonitorNode(Node):
|
|||||||
"stats": defaultdict(make_stat),
|
"stats": defaultdict(make_stat),
|
||||||
"cpu_stats": make_stat(),
|
"cpu_stats": make_stat(),
|
||||||
"ram_stats": make_stat(),
|
"ram_stats": make_stat(),
|
||||||
"trackers": {
|
"trackers": {},
|
||||||
stream: TopicTracker(logger=self.get_logger())
|
|
||||||
for stream in MONITORED_STREAMS
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.total_cpu_stats = make_stat()
|
self.total_cpu_stats = make_stat()
|
||||||
self.total_ram_stats = make_stat()
|
self.total_ram_stats = make_stat()
|
||||||
|
self.topic_subscriptions = {}
|
||||||
|
self.topic_configs = {}
|
||||||
|
self.discovered_streams = {}
|
||||||
|
self.discovery_complete = False
|
||||||
|
self.discovery_timer = None
|
||||||
|
self.timer = None
|
||||||
|
self.requested_streams = self.parse_requested_topics(topics)
|
||||||
|
|
||||||
# CSV
|
# CSV
|
||||||
self.csv_file = csv_file
|
self.csv_file = csv_file
|
||||||
self.csv_fh = open(self.csv_file, "w", newline="")
|
self.csv_fh = open(self.csv_file, "w", newline="")
|
||||||
self.csv_writer = csv.writer(self.csv_fh)
|
self.csv_writer = csv.writer(self.csv_fh)
|
||||||
self.csv_writer.writerow(self.build_csv_header())
|
|
||||||
|
|
||||||
# subscriptions
|
# Device status subscriptions are always present. Data subscriptions
|
||||||
|
# are created once after automatic discovery or explicit selection.
|
||||||
for camera_name in self.camera_names:
|
for camera_name in self.camera_names:
|
||||||
ns = self.camera_namespace(camera_name)
|
ns = self.camera_namespace(camera_name)
|
||||||
self.create_subscription(
|
self.create_subscription(
|
||||||
@@ -190,26 +243,26 @@ class CameraMonitorNode(Node):
|
|||||||
lambda msg, name=camera_name: self.status_callback(msg, name),
|
lambda msg, name=camera_name: self.status_callback(msg, name),
|
||||||
5
|
5
|
||||||
)
|
)
|
||||||
for stream in MONITORED_STREAMS:
|
|
||||||
self.create_subscription(
|
|
||||||
Image,
|
|
||||||
f"{ns}/{stream}/image_raw",
|
|
||||||
lambda msg, name=camera_name, stream_name=stream: self.image_callback(
|
|
||||||
msg, name, stream_name
|
|
||||||
),
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
|
|
||||||
# timer runs every 1s to update system stats, log csv and print status
|
if self.requested_streams:
|
||||||
self.timer = self.create_timer(1.0, self.timer_callback)
|
self.finish_topic_discovery(self.requested_streams)
|
||||||
|
else:
|
||||||
|
self.discovery_start_monotonic = time.monotonic()
|
||||||
|
self.discovery_timer = self.create_timer(
|
||||||
|
DISCOVERY_INTERVAL_SECONDS, self.update_topic_discovery
|
||||||
|
)
|
||||||
|
|
||||||
def timer_callback(self):
|
def timer_callback(self):
|
||||||
|
if not self.discovery_complete:
|
||||||
|
return
|
||||||
|
|
||||||
elapsed = time.time() - self.start_time
|
elapsed = time.time() - self.start_time
|
||||||
if elapsed > self.run_time:
|
if elapsed > self.run_time:
|
||||||
self.finish()
|
self.finish()
|
||||||
rclpy.shutdown()
|
rclpy.shutdown()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.update_topic_fps_stats()
|
||||||
camera_sys_stats, total_cpu, total_ram, self.total_node_name = self.get_camera_stats()
|
camera_sys_stats, total_cpu, total_ram, self.total_node_name = self.get_camera_stats()
|
||||||
for camera_name in self.camera_names:
|
for camera_name in self.camera_names:
|
||||||
camera = self.cameras[camera_name]
|
camera = self.cameras[camera_name]
|
||||||
@@ -230,7 +283,8 @@ class CameraMonitorNode(Node):
|
|||||||
return
|
return
|
||||||
self.finished = True
|
self.finished = True
|
||||||
|
|
||||||
elapsed = time.time() - self.start_time
|
timer_start = self.start_time or self.discovery_start_time
|
||||||
|
elapsed = time.time() - timer_start
|
||||||
try:
|
try:
|
||||||
self.csv_fh.close()
|
self.csv_fh.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -241,6 +295,150 @@ class CameraMonitorNode(Node):
|
|||||||
def camera_namespace(self, camera_name):
|
def camera_namespace(self, camera_name):
|
||||||
return "/" + camera_name.strip("/")
|
return "/" + camera_name.strip("/")
|
||||||
|
|
||||||
|
def image_topic_name(self, camera_name, stream):
|
||||||
|
return f"{self.camera_namespace(camera_name)}/{stream}/image_raw"
|
||||||
|
|
||||||
|
def make_raw_image_config(self, camera_name, stream):
|
||||||
|
return {
|
||||||
|
"camera_name": camera_name,
|
||||||
|
"topic_id": stream,
|
||||||
|
"topic_name": self.image_topic_name(camera_name, stream),
|
||||||
|
"msg_type": Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_full_topic(self, topic_name):
|
||||||
|
normalized_topic = "/" + topic_name.strip("/")
|
||||||
|
for camera_name in self.camera_names:
|
||||||
|
namespace_prefix = self.camera_namespace(camera_name) + "/"
|
||||||
|
if not normalized_topic.startswith(namespace_prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
relative_name = normalized_topic[len(namespace_prefix):]
|
||||||
|
for stream in MONITORED_STREAMS:
|
||||||
|
raw_name = f"{stream}/image_raw"
|
||||||
|
if relative_name == raw_name:
|
||||||
|
return self.make_raw_image_config(camera_name, stream)
|
||||||
|
if relative_name == f"{raw_name}/compressed":
|
||||||
|
return {
|
||||||
|
"camera_name": camera_name,
|
||||||
|
"topic_id": f"{stream}_compressed",
|
||||||
|
"topic_name": normalized_topic,
|
||||||
|
"msg_type": CompressedImage,
|
||||||
|
}
|
||||||
|
if relative_name == f"{raw_name}/compressedDepth":
|
||||||
|
return {
|
||||||
|
"camera_name": camera_name,
|
||||||
|
"topic_id": f"{stream}_compressed_depth",
|
||||||
|
"topic_name": normalized_topic,
|
||||||
|
"msg_type": CompressedImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
point_cloud_ids = {
|
||||||
|
"depth/points": "depth_points",
|
||||||
|
"depth_registered/points": "depth_registered_points",
|
||||||
|
}
|
||||||
|
if relative_name in point_cloud_ids:
|
||||||
|
return {
|
||||||
|
"camera_name": camera_name,
|
||||||
|
"topic_id": point_cloud_ids[relative_name],
|
||||||
|
"topic_name": normalized_topic,
|
||||||
|
"msg_type": PointCloud2,
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_requested_topics(self, topics):
|
||||||
|
if not topics:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
requested = str(topics).replace(";", ",").split(",")
|
||||||
|
selected = {}
|
||||||
|
for value in requested:
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
if value in MONITORED_STREAMS:
|
||||||
|
for camera_name in self.camera_names:
|
||||||
|
config = self.make_raw_image_config(camera_name, value)
|
||||||
|
selected[(camera_name, config["topic_id"])] = config
|
||||||
|
continue
|
||||||
|
|
||||||
|
config = self.parse_full_topic(value)
|
||||||
|
if config is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported topic '{value}'. Specify a raw or compressed image topic, "
|
||||||
|
"or a depth/points or depth_registered/points topic under a configured "
|
||||||
|
"camera namespace."
|
||||||
|
)
|
||||||
|
selected[(config["camera_name"], config["topic_id"])] = config
|
||||||
|
return selected
|
||||||
|
|
||||||
|
def find_published_image_streams(self):
|
||||||
|
published_streams = {}
|
||||||
|
for camera_name in self.camera_names:
|
||||||
|
for stream in MONITORED_STREAMS:
|
||||||
|
config = self.make_raw_image_config(camera_name, stream)
|
||||||
|
key = (camera_name, config["topic_id"])
|
||||||
|
if self.count_publishers(config["topic_name"]) > 0:
|
||||||
|
published_streams[key] = config
|
||||||
|
return published_streams
|
||||||
|
|
||||||
|
def update_topic_discovery(self):
|
||||||
|
if self.discovery_complete:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.discovered_streams.update(self.find_published_image_streams())
|
||||||
|
elapsed = time.monotonic() - self.discovery_start_monotonic
|
||||||
|
if elapsed >= DISCOVERY_DURATION_SECONDS:
|
||||||
|
self.finish_topic_discovery(self.discovered_streams)
|
||||||
|
|
||||||
|
def finish_topic_discovery(self, selected_streams):
|
||||||
|
self.topic_configs = dict(selected_streams)
|
||||||
|
sample_start_time = time.monotonic()
|
||||||
|
for key, config in self.topic_configs.items():
|
||||||
|
camera_name = config["camera_name"]
|
||||||
|
topic_id = config["topic_id"]
|
||||||
|
self.cameras[camera_name]["trackers"][topic_id] = TopicTracker(
|
||||||
|
logger=self.get_logger(), sample_start_time=sample_start_time
|
||||||
|
)
|
||||||
|
self.topic_subscriptions[key] = self.create_subscription(
|
||||||
|
config["msg_type"],
|
||||||
|
config["topic_name"],
|
||||||
|
lambda msg, name=camera_name, selected_topic_id=topic_id: self.topic_callback(
|
||||||
|
msg, name, selected_topic_id
|
||||||
|
),
|
||||||
|
5,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.csv_writer.writerow(self.build_csv_header())
|
||||||
|
self.csv_fh.flush()
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.discovery_complete = True
|
||||||
|
self.timer = self.create_timer(1.0, self.timer_callback)
|
||||||
|
if self.discovery_timer is not None:
|
||||||
|
self.discovery_timer.cancel()
|
||||||
|
|
||||||
|
def topics_for_camera(self, camera_name):
|
||||||
|
return [
|
||||||
|
config
|
||||||
|
for config in self.topic_configs.values()
|
||||||
|
if config["camera_name"] == camera_name
|
||||||
|
]
|
||||||
|
|
||||||
|
def update_topic_fps_stats(self):
|
||||||
|
sample_time = time.monotonic()
|
||||||
|
for config in self.topic_configs.values():
|
||||||
|
camera = self.cameras[config["camera_name"]]
|
||||||
|
topic_id = config["topic_id"]
|
||||||
|
fps_sample = camera["trackers"][topic_id].sample_fps(sample_time)
|
||||||
|
if fps_sample is None:
|
||||||
|
continue
|
||||||
|
current_fps, average_fps = fps_sample
|
||||||
|
self.update_sample_stat(
|
||||||
|
camera["stats"][f"{topic_id}_fps"],
|
||||||
|
current_fps,
|
||||||
|
average=average_fps,
|
||||||
|
)
|
||||||
|
|
||||||
def cmdline_has_camera_namespace(self, cmdline_args, camera_name):
|
def cmdline_has_camera_namespace(self, cmdline_args, camera_name):
|
||||||
ns = self.camera_namespace(camera_name)
|
ns = self.camera_namespace(camera_name)
|
||||||
candidates = [
|
candidates = [
|
||||||
@@ -321,48 +519,30 @@ class CameraMonitorNode(Node):
|
|||||||
|
|
||||||
camera["prev_online"] = msg.device_online
|
camera["prev_online"] = msg.device_online
|
||||||
|
|
||||||
# Update every image stream from the native DeviceStatus fields.
|
def topic_callback(self, msg, camera_name: str, topic_id: str):
|
||||||
for stream in MONITORED_STREAMS:
|
self.first_data_collected = True
|
||||||
self.update_stats(
|
|
||||||
camera["stats"],
|
|
||||||
f"{stream}_fps",
|
|
||||||
getattr(msg, f"{stream}_frame_rate_cur"),
|
|
||||||
getattr(msg, f"{stream}_frame_rate_min"),
|
|
||||||
getattr(msg, f"{stream}_frame_rate_max"),
|
|
||||||
getattr(msg, f"{stream}_frame_rate_avg"),
|
|
||||||
)
|
|
||||||
self.update_stats(
|
|
||||||
camera["stats"],
|
|
||||||
f"{stream}_delay",
|
|
||||||
getattr(msg, f"{stream}_delay_ms_cur"),
|
|
||||||
getattr(msg, f"{stream}_delay_ms_min"),
|
|
||||||
getattr(msg, f"{stream}_delay_ms_max"),
|
|
||||||
getattr(msg, f"{stream}_delay_ms_avg"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def image_callback(self, msg: Image, camera_name: str, stream: str):
|
|
||||||
if stream not in MONITORED_STREAMS:
|
|
||||||
return
|
|
||||||
camera = self.cameras[camera_name]
|
camera = self.cameras[camera_name]
|
||||||
tracker = camera["trackers"][stream]
|
camera["data_collected"] = True
|
||||||
# Prefer a user-specified ideal fps for drop detection when provided.
|
tracker = camera["trackers"][topic_id]
|
||||||
fps_to_use = (
|
ros_receive_time = self.get_clock().now().nanoseconds * 1e-9
|
||||||
self.ideal_fps
|
delay_ms = tracker.on_msg(
|
||||||
if self.ideal_fps and self.ideal_fps > 0.0
|
msg.header,
|
||||||
else camera["stats"][f"{stream}_fps"]["avg"]
|
ros_receive_time,
|
||||||
|
self.ideal_fps,
|
||||||
)
|
)
|
||||||
tracker.on_msg(msg.header, fps_to_use)
|
|
||||||
|
|
||||||
def update_stats(self, stats, key, cur, min_val, max_val, avg_val):
|
if delay_ms is not None:
|
||||||
if min_val <= 1e-3 or avg_val < 0: # ignore invalid data
|
self.update_sample_stat(
|
||||||
return
|
camera["stats"][f"{topic_id}_delay"], delay_ms
|
||||||
s = stats[key]
|
)
|
||||||
s["cur"] = (cur)
|
|
||||||
s["count"] += 1
|
def update_sample_stat(self, stat, value, average=None):
|
||||||
s["sum"] += avg_val
|
stat["cur"] = value
|
||||||
s["avg"] = s["sum"] / s["count"] if s["count"] > 0 else 0.0
|
stat["count"] += 1
|
||||||
s["min"] = min(s["min"], min_val)
|
stat["sum"] += value
|
||||||
s["max"] = max(s["max"], max_val)
|
stat["avg"] = average if average is not None else stat["sum"] / stat["count"]
|
||||||
|
stat["min"] = min(stat["min"], value)
|
||||||
|
stat["max"] = max(stat["max"], value)
|
||||||
|
|
||||||
def update_sys_stat(self, stat_dict, value, online=True):
|
def update_sys_stat(self, stat_dict, value, online=True):
|
||||||
stat_dict["cur"] = value
|
stat_dict["cur"] = value
|
||||||
@@ -379,7 +559,7 @@ class CameraMonitorNode(Node):
|
|||||||
row = [round(elapsed, 2)]
|
row = [round(elapsed, 2)]
|
||||||
for camera_name in self.camera_names:
|
for camera_name in self.camera_names:
|
||||||
camera = self.cameras[camera_name]
|
camera = self.cameras[camera_name]
|
||||||
row.extend(self.build_camera_csv_values(camera))
|
row.extend(self.build_camera_csv_values(camera_name, camera))
|
||||||
|
|
||||||
row.extend([
|
row.extend([
|
||||||
round(self.total_cpu_stats["cur"], 2), round(self.total_cpu_stats["avg"], 2),
|
round(self.total_cpu_stats["cur"], 2), round(self.total_cpu_stats["avg"], 2),
|
||||||
@@ -391,30 +571,42 @@ class CameraMonitorNode(Node):
|
|||||||
|
|
||||||
def build_csv_header(self):
|
def build_csv_header(self):
|
||||||
header = ["time(s)"]
|
header = ["time(s)"]
|
||||||
camera_fields = ["connection_type", "status_online", "disconnects"]
|
for camera_name in self.camera_names:
|
||||||
for stream in MONITORED_STREAMS:
|
header.extend(
|
||||||
camera_fields.extend(
|
|
||||||
[
|
[
|
||||||
f"{stream}_fps_cur",
|
f"{camera_name}_connection_type",
|
||||||
f"{stream}_fps_avg",
|
f"{camera_name}_status_online",
|
||||||
f"{stream}_fps_min",
|
f"{camera_name}_disconnects",
|
||||||
f"{stream}_fps_max",
|
]
|
||||||
f"{stream}_delay_cur",
|
)
|
||||||
f"{stream}_delay_avg",
|
for config in self.topics_for_camera(camera_name):
|
||||||
f"{stream}_delay_min",
|
topic_id = config["topic_id"]
|
||||||
f"{stream}_delay_max",
|
header.extend(
|
||||||
f"{stream}_frames_loss",
|
[
|
||||||
f"{stream}_frames_loss_rate(%)",
|
f"{camera_name}_{topic_id}_fps_cur",
|
||||||
|
f"{camera_name}_{topic_id}_fps_avg",
|
||||||
|
f"{camera_name}_{topic_id}_fps_min",
|
||||||
|
f"{camera_name}_{topic_id}_fps_max",
|
||||||
|
f"{camera_name}_{topic_id}_delay_cur",
|
||||||
|
f"{camera_name}_{topic_id}_delay_avg",
|
||||||
|
f"{camera_name}_{topic_id}_delay_min",
|
||||||
|
f"{camera_name}_{topic_id}_delay_max",
|
||||||
|
f"{camera_name}_{topic_id}_sub_lost_count",
|
||||||
|
f"{camera_name}_{topic_id}_sub_lost_rate(%)",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
header.extend(
|
||||||
|
[
|
||||||
|
f"{camera_name}_cpu_cur",
|
||||||
|
f"{camera_name}_cpu_avg",
|
||||||
|
f"{camera_name}_cpu_min",
|
||||||
|
f"{camera_name}_cpu_max",
|
||||||
|
f"{camera_name}_ram_cur",
|
||||||
|
f"{camera_name}_ram_avg",
|
||||||
|
f"{camera_name}_ram_min",
|
||||||
|
f"{camera_name}_ram_max",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
camera_fields.extend(
|
|
||||||
[
|
|
||||||
"cpu_cur", "cpu_avg", "cpu_min", "cpu_max",
|
|
||||||
"ram_cur", "ram_avg", "ram_min", "ram_max",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
for camera_name in self.camera_names:
|
|
||||||
header.extend([f"{camera_name}_{field}" for field in camera_fields])
|
|
||||||
|
|
||||||
header.extend([
|
header.extend([
|
||||||
"total_cpu_cur", "total_cpu_avg", "total_cpu_min", "total_cpu_max",
|
"total_cpu_cur", "total_cpu_avg", "total_cpu_min", "total_cpu_max",
|
||||||
@@ -422,7 +614,7 @@ class CameraMonitorNode(Node):
|
|||||||
])
|
])
|
||||||
return header
|
return header
|
||||||
|
|
||||||
def build_camera_csv_values(self, camera):
|
def build_camera_csv_values(self, camera_name, camera):
|
||||||
def safe(k):
|
def safe(k):
|
||||||
v = camera["stats"].get(k, {})
|
v = camera["stats"].get(k, {})
|
||||||
return (
|
return (
|
||||||
@@ -437,11 +629,12 @@ class CameraMonitorNode(Node):
|
|||||||
camera["prev_online"],
|
camera["prev_online"],
|
||||||
camera["disconnect_count"],
|
camera["disconnect_count"],
|
||||||
]
|
]
|
||||||
for stream in MONITORED_STREAMS:
|
for config in self.topics_for_camera(camera_name):
|
||||||
tracker = camera["trackers"][stream]
|
topic_id = config["topic_id"]
|
||||||
|
tracker = camera["trackers"][topic_id]
|
||||||
if camera["prev_online"]:
|
if camera["prev_online"]:
|
||||||
values.extend(safe(f"{stream}_fps"))
|
values.extend(safe(f"{topic_id}_fps"))
|
||||||
values.extend(safe(f"{stream}_delay"))
|
values.extend(safe(f"{topic_id}_delay"))
|
||||||
else:
|
else:
|
||||||
values.extend(["N/A"] * 8)
|
values.extend(["N/A"] * 8)
|
||||||
values.extend(
|
values.extend(
|
||||||
@@ -487,25 +680,30 @@ class CameraMonitorNode(Node):
|
|||||||
rows = []
|
rows = []
|
||||||
for camera_name in self.camera_names:
|
for camera_name in self.camera_names:
|
||||||
camera = self.cameras[camera_name]
|
camera = self.cameras[camera_name]
|
||||||
for stream in MONITORED_STREAMS:
|
for config in self.topics_for_camera(camera_name):
|
||||||
fps_key = f"{stream}_fps"
|
topic_id = config["topic_id"]
|
||||||
delay_key = f"{stream}_delay"
|
fps_key = f"{topic_id}_fps"
|
||||||
topic_name = f"/{camera_name}/{stream}/image_raw"
|
delay_key = f"{topic_id}_delay"
|
||||||
|
topic_name = config["topic_name"]
|
||||||
if not camera["prev_online"]:
|
if not camera["prev_online"]:
|
||||||
rows.append([camera_name, topic_name, *["N/A"] * 10])
|
rows.append([camera_name, topic_name, *["N/A"] * 10])
|
||||||
else:
|
else:
|
||||||
fps_vals = format_stats(camera["stats"][fps_key])
|
fps_vals = format_stats(camera["stats"][fps_key])
|
||||||
delay_vals = format_stats(camera["stats"][delay_key])
|
delay_vals = format_stats(camera["stats"][delay_key])
|
||||||
tracker = camera["trackers"][stream]
|
tracker = camera["trackers"][topic_id]
|
||||||
|
|
||||||
frames_loss = tracker.drop_frames
|
frames_loss = tracker.drop_frames
|
||||||
frames_loss_rate = round(tracker.frames_loss_rate() * 100.0, 3)
|
frames_loss_rate = round(tracker.frames_loss_rate() * 100.0, 3)
|
||||||
rows.append([camera_name, topic_name, *fps_vals, *delay_vals, frames_loss, frames_loss_rate])
|
rows.append([camera_name, topic_name, *fps_vals, *delay_vals, frames_loss, frames_loss_rate])
|
||||||
|
|
||||||
header_bottom = ["Camera", "Topic", "fps_cur", "fps_avg", "fps_min", "fps_max", "delay_cur(ms)", "delay_avg(ms)", "delay_min(ms)", "delay_max(ms)", "Pub_lost_count", "Pub_lost_rate(%)"]
|
header_bottom = [
|
||||||
|
"Camera", "Topic", "fps_cur", "fps_avg", "fps_min", "fps_max",
|
||||||
|
"delay_cur(ms)", "delay_avg(ms)", "delay_min(ms)", "delay_max(ms)",
|
||||||
|
"Sub_lost_count", "Sub_lost_rate(%)",
|
||||||
|
]
|
||||||
|
|
||||||
os.system("clear")
|
os.system("clear")
|
||||||
print("Orbbec Camera Benchmark\n")
|
print("Orbbec Camera Subscriber Benchmark\n")
|
||||||
print(tabulate([header_bottom] + rows, tablefmt="fancy_grid"))
|
print(tabulate([header_bottom] + rows, tablefmt="fancy_grid"))
|
||||||
|
|
||||||
sys_rows = []
|
sys_rows = []
|
||||||
@@ -542,13 +740,36 @@ def main(argv=None):
|
|||||||
)
|
)
|
||||||
parser.add_argument("--run_time", type=str, default="10s", help="Total run time for monitoring, e.g., 10s, 5m, 1h.")
|
parser.add_argument("--run_time", type=str, default="10s", help="Total run time for monitoring, e.g., 10s, 5m, 1h.")
|
||||||
parser.add_argument("--csv_file", type=str, default="camera_monitor_log.csv")
|
parser.add_argument("--csv_file", type=str, default="camera_monitor_log.csv")
|
||||||
parser.add_argument("--ideal_fps", type=float, default=0.0, help="Optional ideal frame rate to use for drop detection (overrides reported avg).")
|
parser.add_argument(
|
||||||
|
"--ideal_fps",
|
||||||
|
type=float,
|
||||||
|
default=0.0,
|
||||||
|
help=(
|
||||||
|
"Optional ideal frame rate for subscriber-side drop detection; "
|
||||||
|
"otherwise it is learned from image timestamps."
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument("--camera_names", type=str, default="camera", help="Comma-separated camera namespaces, e.g., camera,camera01,camera02.")
|
parser.add_argument("--camera_names", type=str, default="camera", help="Comma-separated camera namespaces, e.g., camera,camera01,camera02.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--topics",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help=(
|
||||||
|
"Comma-separated raw stream names or full raw/compressed image and point cloud "
|
||||||
|
"topics. Automatic discovery only selects raw image topics."
|
||||||
|
),
|
||||||
|
)
|
||||||
cli_args, _ = parser.parse_known_args(argv)
|
cli_args, _ = parser.parse_known_args(argv)
|
||||||
|
|
||||||
rclpy.init(args=argv)
|
rclpy.init(args=argv)
|
||||||
run_time = parse_duration(cli_args.run_time)
|
run_time = parse_duration(cli_args.run_time)
|
||||||
node = CameraMonitorNode(run_time, cli_args.csv_file, ideal_fps=cli_args.ideal_fps, camera_names=cli_args.camera_names)
|
node = CameraMonitorNode(
|
||||||
|
run_time,
|
||||||
|
cli_args.csv_file,
|
||||||
|
ideal_fps=cli_args.ideal_fps,
|
||||||
|
camera_names=cli_args.camera_names,
|
||||||
|
topics=cli_args.topics,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rclpy.spin(node)
|
rclpy.spin(node)
|
||||||
|
|||||||
Reference in New Issue
Block a user