ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ROS sensor_msgs::Image:step与encoding避坑

ROS sensor_msgs::Image:step与encoding避坑 前阵子帮人看一个相机节点rqt_image_view里画面看着挺正常可下游做标定的时候死活对不上把帧存成 PNG 一看画面被斜着切开了——上半截和下半截错开几十个像素像被人用刀斜着一划。这种图我这几年见过不下五次每一次的根因最后都落到同一个字段上step。sensor_msgs::Image这个只有六个字段的消息类型表面上看简陋得不像个正经图像格式但它其实是整个机器人感知链路里最核心的一根血管相机驱动往里塞数据标定节点、检测模型、深度转点云、录制回放全都靠它传。你对它的理解深一寸踩坑的概率就少一大截。这篇东西我打算按字段怎么理解 → 取数怎么取 → 发布怎么发 → 出错怎么查 → 怎么跟周边消息配合这条线走一遍中间会穿插我自己调试时踩过的坑和那些文档里不会写的东西。刚接触图像话题的新手可以照着抄代码已经跑通流程但总在奇怪地方翻车的同学重点看第二、第四节那里大概率和你的问题有关。文中代码以 C 和 Python 双份给出ROS1 的sensor_msgs::Image和 ROS2 的sensor_msgs/msg/Image我会标出差异点。1. 把Image消息拆开六个字段各自管什么1.1 header、height、width最没有争议的三个字段Image消息的完整定义在 ROS1 里是这样Header header uint32 height uint32 width string encoding uint8 is_bigendian uint32 step uint8[] dataheader里装着seqROS2 已经删掉了、stamp和frame_id。前两个字段height和width就是像素行列数height是行数width是每行的像素个数注意是像素个数不是字节数这一点后面算step的时候特别容易搞混。一个 1280×720 的相机width1280、height720没什么好说的。header的三个子字段里真正容易出事的不是stamp而是frame_id。做点云拼接、手眼标定、多传感器融合的同学应该有体会如果你的frame_id写的是camera而 TF 树里注册的是camera_link那么所有依赖 TF 的节点都会安静地失败或者更糟用了一个过期变换算出一堆看着像样的错误结果。我在一个项目里就被这个坑过整整一个下午——图像路径本身完全正确问题是标定结果偏了十几厘米最后发现是驱动里frame_id拼字符串的时候多了个斜杠TF 查找回退到了一个祖先坐标系上。stamp的来源也值得较真。相机驱动里两种写法ros::Time::now()或者相机硬件时间戳比如通过 PTP/GPS 同步过来的时间。前者意味着你的图像时间戳带着驱动线程调度、USB 传输、序列化的全部抖动几毫秒到几十毫秒不等后者才是真正拍下这一帧的时刻。做视觉惯性融合、多相机同步的时候用now()是致命的因为抖动会让优化器以为相机在抖动重投影误差怎么调都下不去。判断方法很简单把/cam/image_raw的 stamp 和帧间间隔打印出来如果相邻帧的时间差在 33.3ms 上下跳比如 28ms、39ms、31ms基本就是用now()了硬件时间戳通常稳定在 33.3ms 附近波动小于 1ms。1.2 step不是width乘通道数行填充这件事这是整篇里最值钱的一段。step的定义是整行占用的字节数而不是有效像素占用的字节数。对于驱动直接从内存里搬来的图像step经常大于width × 每像素字节数因为相机、共享内存或某些硬件接口会对每行做对齐填充padding。比如 1290 宽的 mono8 图像DMA 可能按 4 字节或者 16 字节对齐实际step变成 1296 或者 1312那多出来的 6 到 22 个字节就是垃圾数据不属于任何像素。为什么这个区别要命因为它决定了你reshape的时候第二个维度填多少。绝大多数人第一次手搓解析代码时会这么写# 错误写法 img np.frombuffer(msg.data, dtypenp.uint8).reshape(msg.height, msg.width, 3)只要step width * 3这段代码完全正确也正因为如此它能在很多相机上长期存活直到你换了一台相机或者提高了分辨率然后画面就斜了。正确写法是要让第二维等于step再切掉多余部分buf np.frombuffer(msg.data, dtypenp.uint8) if msg.step msg.width * 3: img buf.reshape(msg.height, msg.width, 3) else: img buf.reshape(msg.height, msg.step)[:, : msg.width * 3].reshape(msg.height, msg.width, 3)C 侧同理OpenCV 的cv::Mat构造函数本身就带step参数这也是为什么cv_bridge天然不出这个错// 直接引用消息内存不拷贝 cv::Mat img(msg.height, msg.width, CV_8UC3, msg.data.data(), msg.step);注意第四个参数是msg.step而不是省略。很多人写cv::Mat(msg.height, msg.width, CV_8UC3, msg.data.data())这时候 OpenCV 会自己算一个width*3的行距——只要原始数据有填充画面立刻斜切。这个错误的症状非常有辨识度图像整体是对得上的但每一行往右或往左累积偏移越往下偏得越多看起来像被平行四边形拉伸了。我第一次遇到的时候以为是相机坏了还专门换了根线。顺带说一句step在 C 里可以用sensor_msgs::image_encodings里的辅助函数算出来避免自己手算通道数出错#include sensor_msgs/image_encodings.h uint32_t step msg.width * sensor_msgs::image_encodings::numChannels(msg.encoding) * sensor_msgs::image_encodings::bitDepth(msg.encoding) / 8;numChannels(rgb8)返回 3bitDepth(rgb8)返回 8两个函数配合就能覆盖全部标准编码不用你自己维护一张映射表。1.3 encoding字符串一份被低估的转换契约encoding不是给人看的备注它是驱动和下游之间的一份格式契约。ROS 标准里定义了一批编码我按用途归一下类类别典型取值每像素字节常见来源灰度mono8、8UC11工业相机、红外彩色rgb8、bgr8、rgba8、bgra83 或 4USB 相机、仿真高位灰度mono16、16UC12深度图、16bit 科学相机浮点单通道32FC14深度图、视差图多通道数值8UC3、16UC3、32FC2等可变光流、特征图拜耳马赛克bayer_rggb8、bayer_bggr8等1原始 raw 输出YUV 系列yuv422、uyvy等2部分工业相机这里有几个必须说清的坑。第一mono8和8UC1在内存布局上完全一样都是单通道 8 位但cv_bridge对两者的处理路径不同某些版本里8UC1不会被当成颜色格式做彩色转换时会直接抛异常。稳妥做法是能给mono8就给mono88UC1留给纯数值处理。第二mono16和16UC1的关系同理深度图用16UC1更通用但有些老版本的cv_bridge只认mono16遇到编码不支持的时候不妨把这两个换一下试试。第三rgb8和bgr8只差通道顺序但如果你在驱动里写rgb8而实际塞的是 BGR 数据下游用 OpenCV 直接显示就会红蓝互换——人脸变蓝脸天空变橙天这个错误肉眼一秒钟就能发现但如果下游是喂给神经网络的那就可能变成一个永远训不上去的玄学问题。1.4 is_bigendian与data字节序和那条看不见的尾巴is_bigendian只有 0 和 1 两个值0 表示小端。x86 主机、绝大多数 ARM 板子、几乎所有 USB 相机输出的都是小端所以这个字段在实际项目里 99% 是 0。它的存在意义在于跨平台场景如果你从一台大端机器上收到了16UC1深度图不处理字节序直接按uint16解析得到的结果会是一个离谱的数字0x1234 被解释成 0x3412。解析时怎么处理Python 里用np.frombuffer(msg.data, dtypeu2 if msg.is_bigendian else u2)C 里在读取后调用cv::Mat的字节交换或者ntohs。我个人的做法是驱动侧强制统一成小端在接收侧做一次断言assert(msg.is_bigendian 0)一旦有设备破坏假设就尽早暴露比在下游慢慢发现数值不对要好查得多。data字段本身是uint8[]长度应该等于step × height。这个等式值得在接收端加个检查我见过驱动在分辨率切换的一瞬间width/height已经更新但data还是上一帧的长度结果下游reshape直接抛异常把整个节点干掉了。加一行长度校验能省掉很多半夜起来看日志的时间。2. 三种取数路径零拷贝、拷贝和自己动手2.1 toCvShare的零拷贝到底省了什么cv_bridge提供两个主要入口toCvCopy和toCvShare。名字已经说明了一切但真正决定用哪个的不是省内存这么简单。toCvShare(msg)返回CvImageConstPtr它构造的cv::Mat直接指向msg-data那块内存不做任何像素拷贝。节省的量级一张 1920×1080 的rgb8图像是 6.2MB30fps 下每秒 187MB如果每个处理节点都拷一次内存带宽和缓存压力都会上去。更重要的是延迟——多一次 6MB 的memcpy大约要 1 到 2 毫秒在一个要求 30Hz 的闭环里这不是小数目。但零拷贝有两条铁律违反任何一条都会出事。第一msg的生命周期必须覆盖你使用cv::Mat的全程因为toCvShare内部持有的是消息的智能指针只要你还拿着CvImageConstPtr消息就不会被释放。第二绝对不能修改这块内存。曾经有同事在共享的Mat上直接cv::circle画了个标记结果下游所有订阅者收到的图像上都多了这个圈——因为共享的是同一份底层 buffer包括发布者自己重发的那份。需要改动就用toCvCopy或者自己mat.clone()。还有一个容易被忽略的细节toCvShare只在目标编码和源编码一致或者用passthrough时才真正零拷贝。如果你传了desired_encodingbgr8而消息是rgb8内部必须做通道交换那就不可避免地要分配一块新内存这时候它的行为和toCvCopy没什么区别只是返回值类型不同。所以用 toCvShare 更快这个说法是有前提的。2.2 toCvCopy什么时候反而是更优解我自己的选型标准很粗暴这块图像要不要被改写、要不要活过回调函数。如果要往图上画框、叠字或者要把图像塞进一个跨越回调生命周期的队列、存到某个长生命周期的成员变量里那就老老实实toCvCopy。多花一两个毫秒换一个清晰的所有权模型非常值。另一个必须用拷贝的场景是图像尺寸和类型不固定的时候。toCvShare返回的Mat引用着原始消息而cv::Mat本身是引用计数的浅拷贝如果你把多个CvImageConstPtr存进一个std::vector看起来只占了一份数据的内存实际上每一份都各自锁住了一条完整的消息30fps 下这个 vector 能在一秒内吃掉几百兆。这个内存泄漏非常隐蔽因为top里看到的是某个处理节点在缓慢涨和图像话题本身没有直接关联。Python 里对应的是imgmsg_to_cv2它跟toCvCopy更像返回的是新分配的数组import cv_bridge bridge cv_bridge.CvBridge() cv_img bridge.imgmsg_to_cv2(msg, desired_encodingbgr8)注意 Python 下没有真正的零拷贝路径toCvShare系列在 Python 里不可用所以如果你在 Python 里做高帧率处理瓶颈可能不在算法而在转换上。一个常用的绕过手段是用np.frombuffer自己做见下一节。2.3 自己用numpy解析更快也更危险绕过cv_bridge直接用 numpy 解析msg.data在 Python 里通常能快 3 到 10 倍——cv_bridge的 Python 绑定每次调用都要经过一层 C 到 Python 的数据复制和编码判断开销不小。基本写法import numpy as np def imgmsg_to_numpy(msg): bpp 3 if msg.encoding in (rgb8, bgr8) else (4 if msg.encoding in (rgba8, bgra8) else 1) buf np.frombuffer(msg.data, dtypenp.uint8) expected msg.height * msg.step if buf.size ! expected: raise ValueError(fdata 长度 {buf.size} 与 step*height {expected} 不一致) if msg.step msg.width * bpp: img buf.reshape(msg.height, msg.width, bpp) if bpp 1 else buf.reshape(msg.height, msg.width) else: img buf.reshape(msg.height, msg.step)[:, : msg.width * bpp] img img.reshape(msg.height, msg.width, bpp) if bpp 1 else img return img三个注意点。第一np.frombuffer返回的是只读视图底层就是消息的内存千万不要往上面写写了会直接抛ValueError: assignment destination is read-only或者在某些情况下悄悄污染你手上这条消息。要改就加.copy()。第二如果你的msg.data在某个 ROS 版本里是list而不是bytesROS1 的 Python 实现在不同版本上有差异np.frombuffer会报类型错误这时候退回np.array(msg.data, dtypenp.uint8)但它会多一次完整拷贝。第三16UC1和32FC1要走不同的dtypeif msg.encoding 16UC1: arr np.frombuffer(msg.data, dtypeu2).reshape(msg.height, msg.width) elif msg.encoding 32FC1: arr np.frombuffer(msg.data, dtypef4).reshape(msg.height, msg.width)深度图的32FC1里无效像素通常用NaN表示处理之前先np.isnan过一遍否则求均值、做直方图这些操作会把整张图变成NaN。2.4 发布侧怎么少花钱缓冲区复用与格式选择现在换个方向说说发。一个 30fps 的相机节点如果每一帧都新建一条消息、新分配一块data你会得到每秒钟 30 次大块内存申请和释放每次 6MB。这在短时间跑没问题跑上几个小时内存碎片和分配器开销就会体现出来。稳妥的做法是复用缓冲区sensor_msgs::ImagePtr img_msg boost::make_sharedsensor_msgs::Image(); img_msg-height h; img_msg-width w; img_msg-encoding bgr8; img_msg-is_bigendian 0; img_msg-step w * 3; img_msg-data.resize(img_msg-step * h); // 只扩容一次 while (ros::ok()) { // 直接把相机帧拷进 img_msg-data 已有内存 memcpy(img_msg-data.data(), frame_ptr, img_msg-data.size()); img_msg-header.stamp camera_timestamp; pub.publish(img_msg); }ROS1 下publish还会做一次序列化所以复用省下的是分配这一环。ROS2 里如果发布者和订阅者在同一个进程内use_intra_process_comms配合std::unique_ptr和std::move可以做到真正的进程内零拷贝一旦跨进程就还是要走序列化。这个区别在设计节点拓扑时很关键把相机驱动和第一个重处理节点放进同一个组合节点里能省掉一轮完整的 6MB 序列化和反序列化。分辨率的选择也是一笔账。我列个表方便估算按 30fps 计算分辨率编码单帧大小带宽640×480mono80.3MB9MB/s640×480rgb80.9MB27MB/s1280×720rgb82.6MB79MB/s1920×1080rgb86.0MB181MB/s1920×1080mono164.0MB121MB/s这张表值得贴在工位上。当有人问为什么我加个压缩话题就顺了答案就在最后一列。另外如果你的用途只是给人看、给日志留档或者链路带宽确实吃紧用CompressedImageJPEG/PNG能压到原图的 5% 到 10%代价是引入压缩伪影——做特征匹配、光流、标定的链路不要用做可视化可以放心用。3. 排错实录从画面症状反推根因这一节我尽量按看到什么 → 想到什么 → 怎么验证 → 怎么修的顺序写因为排错这件事的关键不是记住答案而是建立症状到原因的映射。3.1 画面斜切或呈平行四边形step不匹配症状我已经在前面描述过上下对齐正常但越往下偏移越大整幅图像被剪切。三步验证打印msg.width、msg.height、msg.step算一下step / (width * 期望通道数)。如果是 1说明没有填充问题在别处如果大于 1比如 1.004、1.02就是填充。用rqt_image_view看同一个话题。如果它显示正常说明消息本身没问题是你自己的解析代码没用step如果它也斜那问题在发布侧step字段填错了。发布侧排查检查填step的那行代码是不是写成了width * 3而实际数据有对齐填充。这种情况要用相机 SDK 返回的stride或者直接算data.size() / height。修复很简单读的一侧按step切写的一侧把真实的行距填进去。我个人的习惯是在发布前加一句断言assert(msg.data.size() msg.step * msg.height);3.2 红蓝互换rgb8与bgr8的认知错位症状是显示出来的人脸发蓝、红色物体发青。原因只有两个声明rgb8但塞进去的是 BGR 数据或者反过来。验证方法用一行数值取图像左上角第一个像素找画面里明确是纯红色的区域看三个通道值。纯红在rgb8里是(255, 0, 0)在bgr8里是(0, 0, 255)。修的时候别急着改驱动先想清楚整条链路。我的建议是立规矩驱动发布什么编码就用什么编码声明不管什么编码进入自己的处理函数时统一转成bgr8。因为 OpenCV 的imshow、imwrite都按 BGR 来解释三通道图统一到bgr8能让所有可视化代码不用动脑。转换用cv_bridge的desired_encoding一行搞定cv_bridge::CvImageConstPtr cv_ptr cv_bridge::toCvShare(msg, bgr8);代价是多一次通道交换对 30fps 的链路可以忽略。3.3 编码不支持一条完整的排查链报错信息长这样不同版本措辞略有差异Encoding is not supported、is not a color format、Unsupported conversion from X to Y。这类错误的本质是你要求的源编码 → 目标编码转换不在cv_bridge的内置转换表里。排查链路我是这么走的第一先确认消息里真实的encoding字符串别信自己的记忆。ros2 topic echo --once --field encoding /cam/image_raw或者rostopic echo -n1 /cam/image_raw/encoding一秒钟的事。第二查这张内置表能做什么。cv_bridge支持的转换大致是三类彩色格式之间互换rgb8/bgr8/rgba8/bgra8、非彩色到彩色mono8/mono16/拜耳格式 → 彩色、以及同通道数之间的位深转换8UC1和16UC1之间做缩放。像8UC3 → 16UC1、32FC1 → bgr8这种跨类别转换它不支持你必须自己拆通道再做归一化。第三如果只是mono16和16UC1、mono8和8UC1这种同物异名的情况直接换成另一个名字试一次很多版本只认其中一个。第四拜耳格式要做去马赛克。如果相机输出bayer_rggb8别自己写插值用image_proc节点它会把/cam/image_raw变成/cam/image_mono和/cam/image_color两个话题去马赛克的算法和参数都调好了。3.4 深度图读出来全零或者数值离谱深度图的问题通常是三种之一。第一种单位搞错了。16UC1的深度图值通常是毫米比如 1500 表示 1.5 米而32FC1是米。如果你把16UC1当成米来用会发现所有物体都在一千米开外视锥一调整个场景就乱了。第二种无效值没过滤。16UC1里 0 常常表示测不到直接拿去做点云会生成一大堆落在原点的假点32FC1里是NaN。第三种字节序问题前面说过。我用得最多的一个自检片段是这样几行就能把问题定死d np.frombuffer(msg.data, dtypeu2).reshape(msg.height, msg.width).astype(np.float32) valid d 0 print(有效像素比例, valid.mean(), 中位数(米), np.median(d[valid]) / 1000.0 if valid.any() else None)正常深度场景里有效像素比例应该在 0.5 以上中位数在 0.5 到 5 米之间。如果有效比例低于 0.1先怀疑镜头遮挡或者曝光如果中位数是几十上百那大概率是单位算错了。4. 和周边消息的配合CameraInfo、深度与同步4.1 CameraInfo不是可选项Image单独发出来只能看要用就得配上CameraInfo。后者的关键字段是K3×3 内参矩阵、D畸变系数、P投影矩阵、distortion_modelplumb_bob、rational_polynomial、equidistant等、width、height以及binning_x/binning_y/roi这三组描述这张图相对标定时的原始图做了什么裁剪和降采样的字段。这里有个高频错误相机驱动配置成640×480但标定是在1280×960下做的标定文件里的K是按全分辨率算的直接用就会让所有投影偏一倍。正确做法是驱动在发CameraInfo的时候按降采样比例缩放K和P或者下游自己用camera_calibration的P矩阵处理。判断方法把CameraInfo.width/height和Image.width/height打印出来对比不一致就一定有问题。另一个细节是P矩阵和K矩阵的分工。K是原始内参D是畸变P是已经去掉畸变之后的投影矩阵做立体匹配、三角化的时候用它。用image_proc的rectify输出的/cam/image_rect配的就是P用原始图/cam/image_raw配的就是K加D。两套组合不能混用混用会让标定误差看着不大但三维重建结果歪得很明显。4.2 深度图的编码选择16UC1还是32FC1这两个编码我都用选哪个取决于你的数据链路。16UC1的优势是省一半内存每像素 2 字节 vs 4 字节还能直接交给depth_image_proc转点云缺点是精度受限毫米为单位的量程最多到 65.5 米uint16上限而且没有小数——1.5 米和 1.501 米表示不出来。32FC1精度足够能表示NaN和inf这类语义代价是带宽翻倍、部分版本的depth_image_proc需要额外配置。我的默认选择是传感器原生输出什么就发什么。RealSense、奥比中光这类深度相机原生给的是16UC1毫米那就别转转了就是白白多一轮精度损失和带宽。只有当你要做视差图、要跟32FC1的光流做对齐时才在消费侧转换。转换时注意两点除以 1000 得到米把 0 替换成NaN否则后续所有统计都会被 0 拉偏。4.3 CompressedImage在链路上的位置经常有人问压缩话题能不能替代原始话题。答案是不能它俩不是替代关系而是分层关系。Image是原始像素CompressedImage是原始像素的一次有损/无损编码前者适合算法消费后者适合传输和录包。具体地CompressedImage只有一个format字符串和一个dataformat在 ROS1 里常见的是jpeg、pngROS2 的compressed_image_transport会写成rgb8; jpeg compressed bgr8这种源编码 压缩方式 目标编码的复合形式。这也解释了为什么CompressedImage不需要stepJPEG 内部不存在行填充这个概念。什么时候该用录包空间紧张一小时 1080p 的原始包能到 600GB 以上、网络链路带宽有限、可视化给人看。什么时候不该用标定、特征提取、光流、任何对像素级精度敏感的场合。JPEG 的质量损失在平坦区域看不出来但在边缘和高频纹理上会引入块状伪影角点检测的重复率会明显下降。4.4 多路图像对齐message_filters的实战参数相机图像和CameraInfo、深度图和彩色图、双目左右目这些都需要做时间对齐。ROS 里标准工具是message_filters用它的ApproximateTimeSynchronizermessage_filters::Subscribersensor_msgs::Image sub_rgb(nh, /cam/image_raw, 5); message_filters::Subscribersensor_msgs::Image sub_depth(nh, /cam/depth/image_raw, 5); typedef message_filters::sync_policies::ApproximateTimesensor_msgs::Image, sensor_msgs::Image SyncPolicy; message_filters::SynchronizerSyncPolicy sync(SyncPolicy(10), sub_rgb, sub_depth); sync.registerCallback(boost::bind(cb, _1, _2));这里面有三个参数值得根据实际情况调。第一是队列长度两个订阅者都设 5 到 10 就够设太大只会让延迟堆积而且当一路话题突然掉线时你会先收到一堆缓冲的旧帧才察觉。第二是ApproximateTime的setMaxIntervalDurationROS2 里叫max_interval_duration它决定两帧相隔多久还算同一时刻。默认不设的话会匹配到间隔很大的帧对双目场景下会直接导致深度算错。我的经验值是按相机帧率的 1/3 设30fps 就设 10ms。第三是use_sim_time回放 rosbag 的时候必须打开否则所有时间戳都是墙上时间同步器永远匹配不上你会看到回调一次都不触发但话题有条数在涨。顺带提一个我踩过的坑如果一路图像走的是CompressedImage而另一路是ImageApproximateTimeSynchronizer照样能工作因为它只看 header 的 stamp不看消息类型。但两路的时间戳来源如果一个是硬件时间戳、一个是ros::Time::now()那同步窗口再大也匹配不准。统一时间戳来源比调同步参数有用一百倍。5. 一些用久了才形成的习惯写到这里把几个我用了几年之后固定下来的做法列一下都不是什么高深技巧但确实帮我省过大量时间。驱动节点的第一件事是打印消息元信息。相机一上电先rostopic echo -n1把width、height、encoding、step、is_bigendian全看一眼确认step width × 每像素字节数或者至少知道填充量是多少。这个习惯是从一次现场调试养成的客户现场的相机型号和实验室不一样step有填充代码在实验室跑得好好的一到现场就斜。现在我把这几行打印放在驱动启动时的 INFO 日志里一眼就能发现换设备带来的差异。下游节点的第一件事是加输入校验。我写过的每个图像回调开头都有这么几句检查data.size()是否等于step * height检查encoding是否在自己的白名单里检查width/height是否大于 0。看起来啰嗦但它把输入异常和算法出错这两类问题彻底分开了。没有这个校验一个上游的坏帧会让下游抛异常崩溃你去查日志只能看到算法函数里的某一行完全不知道是输入的问题。还有一条frame_id和stamp在开发早期就要理清楚别等到做融合了再回头改。我见过太多项目在后期被迫做一次全链路时间戳改造改动的节点数量和回归测试成本都远超早期就把这件事做对的代价。具体做法很朴素驱动里把frame_id从参数服务器读不要硬编码时间戳用相机 SDK 给的硬件时间TF 树里确保这个frame_id有对应的静态变换。这三件事一次做对后面能省掉几十次为什么标定对不上的排查。最后是一个关于编码的小建议。在项目初期就定下一条规则并写进文档所有发布Image的话题encoding只允许出现在一个白名单里比如mono8、bgr8、16UC1、32FC1四种需要别的格式就在进入自己模块时转换。规则本身对不对不重要重要的是它让整条链路上的编码组合数从几十种降到几种转换错误和编码不支持的概率会断崖式下降。我在一个多相机项目里推行这条规则之后跟图像格式相关的工单从每周两三张降到了几乎为零省下来的时间够我把标定精度再优化一轮。
返回列表