简介:这是一套基于ROS2的海康网络RTSP摄像头驱动Demo,主要面向在Ubuntu20.04环境下开展机器人开发、视觉SLAM或目标检测的开发者,解决海康网络摄像头接入ROS2生态时缺乏即用驱动的问题。资源包整体约8KB,仅9个文件,以2个C++源文件、2个YAML参数文件、2个XML配置/启动文件为核心,另有头文件和说明文档,覆盖从节点实现、相机内参配置到colcon编译声明、launch一键启动的完整闭环。目前已有1762人学习下载。借助该驱动Demo,读者可以快速完成工作空间构建并启动海康RTSP摄像头,在ROS2中获取图像话题,方便后续接入图像处理或视觉应用;两套YAML参数可应对不同相机或标定场景,整体代码结构精简、无多余依赖,适合初学者理解ROS2驱动编写思路,也可作为二次开发时的精简基座与工程模板。
1. 用ROS2接网络摄像头,为什么我建议先跑通这个RTSP驱动Demo
在Ubuntu 20.04上做ROS2机器人视觉,最容易被卡住的一步往往是底层图像采集:网络摄像头不直接出USB帧,而是通过RTSP协议传输经过压缩的H.264或H.265流,ROS2原生不认这个格式。这个Demo提供了一条从RTSP拉流到ROS2图像话题的完整链路,解决了“能出图、能进算法、能持续跑”三个问题。适合正在上手ROS2图像采集、需要把现有监控摄像头接入机器人导航或视觉识别流程的开发者。它不是一个理论框架,而是一个可以改一改配置就上车的驱动节点。
2. RTSP拉流进ROS2:为什么选GStreamer而不是直接调OpenCV
2.1 RTSP与ROS2图像话题之间的“翻译”工作
RTSP(Real Time Streaming Protocol)本身并不是传输视频像素的协议,它只负责会话控制,真正传输数据的是RTP包,里面封装的可能是H.264、H.265、MJPEG等编码格式。ROS2的sensor_msgs/Image要求收到的是已经解码成标准RGB或BGR排列的像素数据。所以这个“翻译”工作至少包含三步:与RTSP服务器建立会话、接收RTP流并组帧、解码成原始像素。任何一步出问题,ros2 topic list里就不会出现图像话题,而且很多时候错误日志是空的,只能靠外围工具定位。
常见做法是让GStreamer充当这个翻译器。GStreamer的rtspsrc插件内置了RTSP客户端、RTP解包器和解码器,一条pipeline就能拿到解码后的视频帧。这个Demo正是基于这个思路:用GStreamer的pipeline拉流,通过appsink取出视频buffer,再用cv_bridge包装成ROS2 Image消息发布。相比自己用socket写RTP接收逻辑,GStreamer的成熟度高得多,而且支持硬件解码,CPU占用能降到软件解码的一半以下。对于要跑在工控机或ARM板上的机器人系统,这个差异往往是“能跑”和“跑不动”的分界线。
2.2 为什么不用OpenCV的VideoCapture直接拉流
很多初学者第一次接RTSP会直接写cv2.VideoCapture("rtsp://..."),在PC上偶尔能用,但放到机器人上容易出现三类问题。第一是延迟不可控,OpenCV内部起了一个解码线程,缓冲策略不透明,实测延迟在300ms到1s之间波动,对PID控制这类实时反馈任务就是灾难。第二是断线重连能力基本为零,Wi-Fi环境下一抖动,VideoCapture直接返回False,需要外部重启整个节点。第三是编码格式兼容性,有些摄像头输出H.265,OpenCV版本没带对应解码器时就静默失败,连错误日志都不给。
所以我在设计这个Demo时直接用GStreamer pipeline。rtspsrc提供了重连信号和缓冲设置,即使网络抖动,最多丢几帧,不会直接把进程干掉。而且GStreamer的pipeline可以用插件名切换硬件解码器,比如avdec_h264换成v4l2m2m或nvdec,这对嵌入式板子特别重要。下面你会看到,整个节点代码里真正和摄像头硬件相关的部分其实只有一条pipeline字符串,其他都是ROS2的发布逻辑。
2.3 Demo的节点结构与核心代码
这个Demo的ROS2包名叫rtsp_camera_driver,目录结构很常规:launch存放launch文件,config目录存放摄像头配置YAML,src/rtsp_camera_driver下面放节点源码。为了降低上手门槛,实现用Python写,核心是一个继承自rclpy.Node的类,在构造函数里读入参数,创建发布者,然后启动GStreamer pipeline。下面这段是简化的初始化与取帧循环:
import rclpy import cv2 import numpy as np from rclpy.node import Node from sensor_msgs.msg import Image from cv_bridge import CvBridge class RtspCameraNode(Node): def __init__(self): super().__init__('rtsp_camera_node') # 从参数服务器读取摄像头信息 self.declare_parameter('rtsp_uri', 'rtsp://admin:12345@192.168.1.64:554/Streaming/Channels/101') self.declare_parameter('frame_rate', 15.0) self.declare_parameter('image_width', 1280) self.declare_parameter('image_height', 720) self.publisher_ = self.create_publisher(Image, '/camera/image_raw', 10) self.bridge = CvBridge() # 使用OpenCV读取GStreamer管道输出,注意这里的appsink配置 cap = cv2.VideoCapture( "rtspsrc location={} latency=100 ! rtph264depay ! h264parse " "! avdec_h264 ! videoconvert ! appsink".format(self.get_parameter('rtsp_uri').value), cv2.CAP_GSTREAMER ) self.cap = cap这段代码的核心是OpenCV的VideoCapture与GStreamer pipeline的结合。cv2.VideoCapture第一参数不是RTSP地址,而是GStreamer pipeline字符串,第二参数指定CAP_GSTREAMER,这样OpenCV只负责从appsink读buffer,解码和网络传输都由GStreamer完成。latency=100表示把流延迟限制在100ms,数值太大会让实时性变差,太小又会出现花屏。rtph264depay是解封装RTP中的H.264负载,h264parse做H.264语法解析,avdec_h264是软件解码器,videoconvert负责把像素格式转成OpenCV期望的BGR。每个环节不能多也不能少,漏掉任何一个,appsink都可能收不到数据。
然后发布图像的部分,需要在循环里读取cap.read(),并调用self.publisher_.publish(self.bridge.cv2_to_imgmsg(frame, "bgr8"))。完整代码里我还加了时间戳校正,用ROS2的系统时间而不是摄像头返回的时间戳,这样在机器人上叠加里程计数据时不会出现不同步。这个节点的设计思路就是:GStreamer负责“拉流+解码”,cv_bridge负责“格式转换”,ROS2参数系统负责“可配置”,三层各司其职。
3. 在Ubuntu 20.04上跑通Demo:安装、编译、改参数、启动
3.1 环境准备:ROS2 Foxy与GStreamer
这个Demo基于ROS2 Foxy和Ubuntu 20.04,这是目前兼容性最好的一组组合。如果你的系统已经装好ROS2基础环境,那还需要确认几个包:GStreamer的基础插件、libav解码插件、Python绑定,以及ros2的cv_bridge。GStreamer装不全,运行时会报“no element 'rtspsrc'”,这是新手最容易卡住的第一个点。安装命令如下:
sudo apt update sudo apt install python3-pip python3-opencv sudo apt install libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 \ gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ gstreamer1.0-plugins-ugly gstreamer1.0-libav第一条命令里的python3-opencv是给cv_bridge用的,如果没有它会报找不到cv2。gstreamer1.0-plugins-bad和ugly包括了rtsp相关的很多插件,比如rtspsrc在good里面,但h264parse在bad里,avdec_h264在libav里。所以这四个插件包必须都装上,少一个都会在pipeline启动时报“Internal data stream error”。安装后可以先用gst-launch-1.0试一下摄像头流是否正常:
gst-launch-1.0 rtspsrc location=rtsp://admin:12345@192.168.1.64:554/Streaming/Channels/101 ! rtph264depay ! h264parse ! avdec_h264 ! videoconvert ! autovideosink如果能看到窗口画面,说明GStreamer链路没问题。很多朋友在这里出现黑屏或窗口关闭,多半是摄像头地址不对,或用户名密码里有特殊字符没有转义。后面我会专门讲转义这个坑。注意autovideosink在远程终端上会报没有DISPLAY,你需要连到带桌面的Linux环境,或者在本地跑,否则它无法弹出窗口。
3.2 编译ROS2包:colcon build与依赖补齐
把下载的Demo包解压到你的ROS2工作空间,比如~/ros2_ws/src目录下,然后确认包名和依赖。这个包的package.xml里声明了rclpy、sensor_msgs、cv_bridge这些依赖。编译时不需要额外的find_package,因为Python包不走cmake。但如果你后续要加C++节点,才需要改CMakeLists.txt。这里直接执行:
cd ~/ros2_ws/src/rtsp_camera_driver rosdep update rosdep install --from-paths . --ignore-src -r -y cd ~/ros2_ws colcon build --symlink-install --packages-select rtsp_camera_driver source install/setup.bashrosdep install会从system安装Python和ROS2依赖,如果它提示缺某个apt包,直接sudo apt install即可。--symlink-install符号链接安装很重要,它让launch文件和Python脚本不需要每次改完都重新build,改完直接生效。这是调试RTSP参数时最省时间的习惯,我一般都会建议先加这个参数。如果你发现rosdep install卡在等待锁或找不到某个包,可以先手动执行sudo apt install ros-foxy-cv-bridge ros-foxy-image-transport再重试。
3.3 修改摄像头参数:配置文件里的每一项
这个Demo把参数集中在config/rtsp_cam.yaml,这是为了launch时统一传参。打开文件后能看到类似下面的内容:
rtsp_camera_node: ros__parameters: rtsp_uri: "rtsp://admin:12345@192.168.1.64:554/Streaming/Channels/101" frame_width: 1280 frame_height: 720 frame_rate: 15.0 gst_latency_ms: 100 reconnect_timeout_s: 5.0 pixel_format: "bgr8"其中rtsp_uri是最需要小心的字段。很多摄像头默认用户名密码是admin和12345,但你得先确认摄像头的RTSP端口,有的设备是554,有的可能是8554,路径也不同,常见的是/Streaming/Channels/101。这些信息可以在摄像头Web管理页的“媒体设置”里找到。如果用户名密码里有@、:、/这类字符,必须做URL编码,例如密码是a@b,URL里要写成a%40b,否则GStreamer会误以为@后面是端口号。frame_width和frame_height建议先去摄像头网页端确认可用的主码流分辨率,设置成原生支持的值,不要随意填4K,否则解码压力大而且可能黑屏。gst_latency_ms是端到端延迟参数,100ms适合视觉定位,如果只是做监控可以调到300ms,图像会更稳定。
3.4 启动节点并验证图像话题
启动时直接使用launch文件,这个Demo的launch文件会启动节点并加载参数。命令行如下:
ros2 launch rtsp_camera_driver rtsp_cam.launch.py如果一切正常,终端会看到Opened pipeline之类的日志。然后用两个命令验证:看看话题是否存在,再看帧率是否正常:
ros2 topic hz /camera/image_raw ros2 run rqt_image_view rqt_image_viewros2 topic hz会输出类似average rate: 15.0 Hz的统计,这个数字应与你在yaml里设置的frame_rate一致。如果只有一两Hz,说明解码性能或网络带宽不足,按第4章的参数调整方式处理。rqt_image_view窗口里能看到实时画面,就说明整个链路已经通了。这时候你可以在自己的算法节点里订阅/camera/image_raw,直接用cv_bridge转成OpenCV矩阵做处理。注意如果你在同一个shell里同时跑launch和rqt,建议先source install/setup.bash再启动rqt,否则可能找不到包的安装路径。
4. 排雷手册:ROS2拉RTSP最容易踩的四个坑
4.1 坑一:launch后topic list里没有图像话题
现象:节点启动后没有报错,但ros2 topic list看不到/camera/image_raw。
原因:绝大多数情况是pipeline在启动阶段静默失败。GStreamer的错误信息被rclpy的日志系统吞掉,或者压根没有打印出来。更深一层的原因包括:摄像头RTSP服务拒绝连接、用户名密码错误、路径不存在。常见的是用户把摄像头Web管理页的HTTP端口当成了RTSP端口,填成了80或者8080。
解决:先手动跑一遍gst-launch-1.0,用相同地址验证流是否通。如果gst-launch能出画面,那就是ROS2节点内部的问题,这时在节点代码里把GStreamer的错误回调打开,输出一行self.get_logger().error(str(err))。我在这个Demo里已经把错误回调写进去了,启动后如果看到类似“Could not open resource”的提示,直接按提示去改URL。另外,检查RTSP URL中的路径大小写,很多摄像头是区分大小写的,Channel写成channel可能直接404。
4.2 坑二:图像出来了但帧率只有2帧
现象:rqt_image_view能看到画面,但画面像幻灯片,ros2 topic hz显示只有2Hz,而摄像头配置明明支持25帧。
原因:常见情况是软解码跟不上。1280x720的H.264流在树莓派或老款笔记本上,avdec_h264单线程解码也就20帧左右,加上Python GIL和图像拷贝开销,掉到2帧很正常。另一个原因是没有限制GStreamer的buffer大小,后台缓冲了大量未处理的帧,让实时帧率看起来很低。
解决:优先启用硬件解码。把pipeline里的avdec_h264换成你的板子支持的硬解码器,比如v4l2m2m(常见于RK3399等ARM板)或nvdec(NVIDIA板)。如果硬件解码不可用,可以降低分辨率到640x360,同时把appsink的max-buffers设为2,强制丢弃来不及处理的帧,命令如下:
gst-launch-1.0 rtspsrc location=<你的rtsp地址> latency=100 ! rtph264depay ! h264parse ! v4l2m2m ! videoconvert ! fakesink如果这条命令的CPU占用比软解低,说明硬解码生效了。然后在节点代码的appsink属性里加上max_buffers=2和drop=true,让旧帧被丢弃,保证话题输出稳定的新帧。还有一个容易忽视的点:把视频源的frame_rate参数调低到实际需要的帧率,比如10帧,会把解码器从“拼命追帧”状态解放出来。
4.3 坑三:网络一抖动,节点就直接退出
现象:Wi-Fi环境下运行几分钟,终端突然报“Pipeline error”并退出,ros2 daemon失去节点。
原因:RTSP是基于TCP/UDP的会话,网络抖动会导致RTP包乱序或丢失,GStreamer的rtspsrc默认在收不到数据一段时间后判定流结束并发送EOS信号。如果节点代码没有处理EOS信号,Python进程就会因pipeline执行完而正常退出。本质上不是崩溃,是我们的代码缺少重连逻辑。
解决:在节点代码里捕获EOS事件并执行重连。这个Demo已经实现了一个重连循环,核心逻辑是当cap.read()返回False时,先释放旧的VideoCapture实例,等待reconnect_timeout_s,然后重新用相同参数构造新的VideoCapture。注意不能直接重复open同一个实例,因为GStreamer的appsink内部状态没有完全重置。代码片段如下:
while rclpy.ok(): ret, frame = self.cap.read() if not ret: self.get_logger().warn('RTSP流中断,释放资源...') self.cap.release() time.sleep(self.reconnect_timeout_s) self.cap = cv2.VideoCapture(self.build_gst_pipeline(), cv2.CAP_GSTREAMER) continue self.publisher_.publish(self.bridge.cv2_to_imgmsg(frame, "bgr8"))重连时一定要调用release()再重新构造,否则GStreamer底层socket不会真正关闭,重连几十次之后会耗尽句柄导致打开失败。从那以后我每次写RTSP节点都会强制带上release逻辑。
4.4 坑四:时间戳全是0,算法一叠加就乱
现象:图像话题有帧有内容,但ros2 topic echo /camera/image_raw里的header.stamp.sec和nanosec都是0。
原因:很多RTSP摄像头不提供可靠的PTS(展示时间戳),GStreamer输出buffer的时间戳是系统接收时刻,但OpenCV在appsink里默认丢掉时间戳,所以cv_bridge转成的Image消息头部位是空。这不会影响单帧查看,但一旦做视觉里程计或传感器融合,时间对不齐,整个系统就崩了。
解决:严格来说,网络摄像头的时间戳只能代表“接收时刻”,如果需要精确同步,必须用PTP或硬件同步。但日常使用可以退而求其次,用ROS2节点收到帧的系统时间作为时间戳。这个Demo在配置里加了一个use_system_time参数,默认true。核心代码是:
msg = self.bridge.cv2_to_imgmsg(frame, "bgr8") now = self.get_clock().now().to_msg() msg.header.stamp = now msg.header.frame_id = 'camera_optical_frame'注意这里用的是self.get_clock().now()而不是time.time(),因为当系统开启了use_sim_time时,前者会跟随仿真时间推进,后者不会。这个细节会让你的算法在仿真和实机之间切换时省很多事。
5. 把Demo变成能持续跑的相机驱动:帧率控制、多摄像头与参数热加载
5.1 用videorate强制限流,避免算法跟不上
当摄像头输出30帧但你的视觉SLAM只需要10帧时,与其让算法丢弃廉价帧,不如在pipeline里加videorate插件。修改GStreamer pipeline,在appsink前增加videorate ! video/x-raw,framerate=10/1,GStreamer会按照目标帧率做时间过滤。这样网络传输和解码压力也降下来了,CPU占用能少20%。注意这个framerate必须用分数形式写,写成10/1而不是10。
5.2 同时接四个摄像头:复用一个节点还是起四个节点
如果你的机器需要同时看四个不同位置的RTSP摄像头,不建议在同一个进程里开四个读取线程——GStreamer的rtspsrc不是线程安全的,多线程竞争同一插件库会出现未知段错误。最稳妥的做法是启动四个节点,每个节点通过launch参数传入不同的rtsp_uri、frame_id和topic名。launch文件里可以用循环生成多个节点,这样代码只写一遍,参数都放在一个YAML里。每个摄像头分配独立的话题名,比如/camera1/image_raw到/camera4/image_raw。
5.3 实时调整分辨率而不重启节点
最后说一个我常用的调试技巧。用ros2 param set /rtsp_camera_node frame_width 640改参数不会生效,因为VideoCapture的pipeline在初始化时就固定了。要热切换,就得在参数回调里重新构造VideoCapture。这个Demo预留了on_set_parameters_callback接口,但默认注释掉了,你需要把重连逻辑抽成一个方法,在参数变化时调用。这个操作在调试不同场景时特别有用,比如白天用1080p,晚上只保留720p,不用重启节点就能切换。
整个链路的脉络其实很清楚:GStreamer负责拉流和解码,cv_bridge负责转成ROS2消息,colcon负责编译,launch负责参数注入,四个避坑点则保证了在多变网络环境下的稳定性。我后来接视觉项目,凡是要提网络摄像头,都会先打开这个Demo,把参数表里每一项过一遍,确认这台摄像头的RTSP路径、编码格式和延迟预算,再谈算法集成。这套习惯救了我很多次,也希望帮到你。
本文还有配套的精品资源,点击获取