资讯详情

资讯详情

从RGB-D相机到彩色PCD文件:PCL点云XYZRGB保存实战

上一篇写了PCD文件的基础读写这篇我们顺着“点云学习”的路线继续往前走目标很明确获取并保存彩色点云PCD文件。简单说就是把三维空间里的每个点从原来的XYZ三个坐标扩展成XYZ加上RGB颜色六个数值有些还会加Alpha最终落到一个.pcd后缀的文件里。第一次接触点云的人可能觉得这事不复杂无非是多存三个数但真正动手会发现颜色从哪来、怎么和深度对齐、为什么保存完颜色不对每一步都有讲究。这篇内容适合刚接触PCL、手里有RGB-D相机或者已经有一份点云数据但不知道怎么生成彩色点云的开发者照着做完你就能得到一份可以直接用于可视化、配准和分割的彩色点云文件。1. 彩色点云的本质从XYZ到XYZRGB开始写代码之前我建议先花几分钟把数据本身弄明白。很多新手直接跳到“怎么保存”结果在文件内容上卡了半天其实是没搞懂PCD里存的到底是什么。1.1 为什么纯几何点云“不够用”纯XYZ点云类似于一张黑白轮廓图。扫描一个房间墙壁、地面、桌面都出来了但墙壁上贴了一张深色海报还是白色墙面地面是浅色地砖还是深色木地板只看几何信息很难判断。除非曲面重建做得极其精细否则这些语义信息几乎拿不到。加上RGB之后情况就完全不一样了。彩色信息在实际项目里直接影响几类工作语义分割区分地毯和地板边界、墙上的涂鸦和墙体本身颜色是强特征目标识别交通场景里用颜色辅助区分行人的衣服、车辆的涂装SLAM回环检测同样一个走廊几何相似度很高但颜色分布不同就能更可靠地避免误匹配三维重建和纹理映射重建出的模型还能把原始照片纹理贴回去这个纹理来源就是彩色点云的颜色值。所以很多应用场景里XYZRGB点云是比XYZ点云实用得多的数据形式。打个比方你拿着尺子量出箱子长宽高这只能说明形状打开手机相机的瞬间你同时拿到了形状和外观这就是彩色点云相对纯几何点云的增量价值。1.2 PCD文件头里到底写了什么PCD是PCLPoint Cloud Library定义的文件格式结构上是“文本头 数据体”。一个典型的彩色PCD文件打开后大概长这样# .PCD v0.7 - Point Cloud Data file format VERSION 0.7 FIELDS x y z rgb SIZE 4 4 4 4 TYPE F F F F COUNT 1 1 1 1 WIDTH 640 HEIGHT 480 VIEWPOINT 0 0 0 1 0 0 0 POINTS 307200 DATA ascii 0.123 0.456 1.234 1.67117e07 ...这里有几个字段值得专门说一下FIELDS每列数据的名字。彩色点云通常是x y z rgb如果用的是RGBA点类型则会看到x y z rgba。SIZE每个字段占的字节数。x、y、z是float占4字节rgb也是4字节因为它的本质是一个把R、G、B三个8位整数打包后的32位整数。TYPE字段类型F代表float。WIDTH和HEIGHT如果HEIGHT大于1点云是有序的可以按图像行列理解如果HEIGHT1表示点云是无序的WIDTH就是点数。POINTS总点数应该等于WIDTH*HEIGHT。很多新手看到数据行里rgb这一列的数值是1.67117e07这种浮点数会很困惑这算哪门子颜色其实它确实是浮点数但内在含义是打包后的颜色整数。以红色RGB(255,0,0)为例打包成整数是0xFF0000十进制16711680而16711680这个整数按位复制到float里用小端法存储后展示出来就变成了类似1.67117e07的数。这不是数据错了而是PCD用float容器装了一个整数。明白这一点后面处理颜色编码的坑就会少踩很多。2. RGB-D相机获取彩色点云的完整思路知道了彩色点云长什么样下一步就是怎么把颜色和三维坐标凑到一起。市面上能直接输出彩色点云的设备其实不少但绝大多数工作流都要自己动手把深度图和彩色图融合这一步绕不开。2.1 从深度图到三维坐标的投影公式RGB-D相机获取的是深度图每个像素的值代表从相机光心到物体表面的距离。深度图本身还是二维图像需要配合相机内参把每个像素投影到三维空间。假设深度图上像素坐标是(u, v)该处深度值为d单位毫米或米相机内参为fx、fy、cx、cy则相机坐标系下的三维坐标可以用下面公式计算Z d / depth_scale X (u - cx) * Z / fx Y (v - cy) * Z / fy其中depth_scale是深度值单位换算系数很多设备输出的原始深度单位是毫米此时depth_scale1000.0算出来的Z以米为单位。举个例子假设fx600fy600cx320cy240某个点在深度图中的像素坐标是(300, 250)深度值为1200毫米那么Z 1200 / 1000 1.2米 X (300 - 320) * 1.2 / 600 -0.04米 Y (250 - 240) * 1.2 / 600 0.02米也就是说深度图里这一个像素对应相机前方偏左下方的一个三维点。把整张深度图每个有效像素都做一次这样的变换就得到了一帧几何点云。这时候点云只有XYZ还没有颜色下一步就是去彩色图里取色。2.2 彩色图与深度图的对齐做错了边缘会重影很多人以为RGB-D相机的彩色图和深度图天然对应直接按相同像素坐标取颜色就行结果发现物体边缘像描边一样错位颜色渗到背景里。原因很简单彩色相机和深度传感器通常不是同一个镜头两者之间存在物理位置差异和视差同一物体在两幅图中的投影位置并不完全一样。正确做法是先做对齐。所谓对齐就是把深度图重新投影到彩色相机的坐标系下让深度图的每个像素坐标和彩色图像素坐标一一对应。现在大多数RGB-D相机的SDK都提供了现成的对齐接口学习阶段直接用SDK自带功能就好不用自己手写外参标定和重投影。拿到对齐后的深度图之后再去彩色图相同坐标处取RGB颜色和几何才能基本吻合。我这里说的“基本吻合”是有代价的。深度图对齐到彩色图后深度图边缘会产生一些无效像素这些像素在后续生成点云时需要过滤掉。另外离相机很近的物体视差变化很大即使SDK对齐也可能有误差采集时尽量保持物体和相机距离适中别贴太近拍。2.3 什么时候需要激光雷达加相机的融合路线RGB-D相机是室内近距离场景最合适的选择结构光和ToF方案在户外或强光下容易失效而且测距范围普遍有限超过五六米精度就明显下降。在自动驾驶、测绘这类场景中大家会采用激光雷达加相机的方案激光雷达给高精度稀疏深度相机给颜色两者通过外参标定和时间同步把激光点投影到图像上取色。这条路线的工程复杂度比RGB-D方案高不少标定稍微有点偏差彩色和几何就会错位。除非你是做多传感器融合方向的否则入门阶段先不要从这条路开始。先把RGB-D相机流程跑通理解深度图、彩色图、内参、对齐这些概念后面真要上激光雷达加相机思路也是相通的。3. 用PCL把彩色点云写入PCD可直接抄的代码PCL是点云处理领域绕不开的库PCD格式就是它定义的所以首选方案自然是用PCL来写。这一节给出一份可以直接复制运行的C代码并解释为什么这么写。3.1 PointXYZRGB结构体的正确填充姿势PCL里面常见的点类型是pcl::PointXYZRGB它内部除了x、y、z三个float坐标外还提供了r、g、b三个无符号字符成员以及一个打包后的rgb字段。手动操作时直接给r、g、b赋值是最直观的PCL在保存时会自动完成打包不用自己去拼float。我习惯把“深度图转彩色点云”封装成一个函数输入是两张cv::Mat深度图和已经对齐的彩色图以及相机内参输出是pcl::PointCloudpcl::PointXYZRGB::Ptr。代码里还做了无效深度过滤避免把相机原点的错误点或NaN点写入点云。#include pcl/point_types.h #include pcl/point_cloud.h #include pcl/io/pcd_io.h #include opencv2/opencv.hpp #include limits pcl::PointCloudpcl::PointXYZRGB::Ptr buildColoredCloud( const cv::Mat depth_raw, // uint16_t单位毫米 const cv::Mat color, // 已对齐的BGR彩色图 float fx, float fy, float cx, float cy) { pcl::PointCloudpcl::PointXYZRGB::Ptr cloud(new pcl::PointCloudpcl::PointXYZRGB()); cloud-width color.cols; cloud-height color.rows; cloud-is_dense false; cloud-points.resize(cloud-width * cloud-height); for (int v 0; v color.rows; v) { for (int u 0; u color.cols; u) { pcl::PointXYZRGB pt cloud-points.at(v * color.cols u); uint16_t raw depth_raw.atuint16_t(v, u); float z static_castfloat(raw) / 1000.0f; if (raw 0 || z 0.1f || z 8.0f) { pt.x pt.y pt.z std::numeric_limitsfloat::quiet_NaN(); pt.r 0; pt.g 0; pt.b 0; continue; } pt.x (u - cx) * z / fx; pt.y (v - cy) * z / fy; pt.z z; cv::Vec3b bgr color.atcv::Vec3b(v, u); pt.r bgr[2]; // 注意OpenCV默认是BGR赋值时要调过来 pt.g bgr[1]; pt.b bgr[0]; } } return cloud; }注意两点一是函数输入里的彩色图必须是已经对齐到深度坐标系或者同一坐标系的图否则像素坐标对不上二是OpenCV读取彩色图后默认通道顺序是BGR赋值给PCL的r、g、b时要做通道交换否则保存出来的颜色会红蓝互换。如果你得到的不是逐通道的颜色数据而是一个已经打包好的float型rgb字段例如从某些驱动或者话题消息里拿到的数据那就需要解码。解码逻辑是把这个float按位看回uint32_tuint32_t packed *reinterpret_castuint32_t*(point.rgb); uint8_t r (packed 16) 0xFF; uint8_t g (packed 8) 0xFF; uint8_t b packed 0xFF;这个解包逻辑在很多点云处理代码里都会遇到建议直接背下来。3.2 保存函数怎么选ASCII、Binary还是压缩模式点云填充完成后保存就很简单了。PCL提供了三个常用接口// ASCII模式人可以直接看 pcl::io::savePCDFileASCII(colored_ascii.pcd, *cloud); // Binary模式体积小读取快 pcl::io::savePCDFileBinary(colored_binary.pcd, *cloud); // Binary压缩模式文件最小但读写需要额外解压 pcl::io::savePCDFileBinaryCompressed(colored_compressed.pcd, *cloud);三种模式差异很大我平时这样选择保存模式文件体积读取速度适用场景ASCII大慢调试阶段、想直接用文本编辑器检查数据Binary中快日常采集和算法处理最推荐BinaryCompressed最小稍慢磁盘紧张、需要长期存档时学习阶段可以先存ASCII打开文件看一眼rgb列是不是合理的浮点数确认数据没问题之后再切Binary。生产中千万别用ASCII文件大几倍不说读写速度也会拖后腿尤其是一帧几十万点的点云。如果你的工作流是Python也可以直接用Open3D来保存彩色点云代码更短import open3d as o3d import numpy as np # points: Nx3colors: Nx3取值0~1 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points) pcd.colors o3d.utility.Vector3dVector(colors) o3d.io.write_point_cloud(colored.pcd, pcd)Open3D是Python点云生态里非常好用的库很多人习惯先用PCL做C算法再用Open3D做快速可视化和格式转换两者配合比单独用某一个顺手很多。4. 颜色错乱、白板点云、NaN入云保存PCD的排错清单代码写完了真正跑起来的时候大概率会遇到几个经典问题。我总结一下自己踩过和帮别人排查过的坑按排查顺序从小到大排列。4.1 颜色反转和全绿全黑RGB编码与字节序问题最常见的现象是点云形状完全正常但颜色整个红蓝互换原本红色的物体变成了蓝色。这种情况几乎可以肯定是通道顺序填错了。前面代码里已经强调过OpenCV读进来是BGR赋值给PCL点类型时要按RGB顺序填。如果你是从其他库拿到颜色数组先确认通道排列再填进点云。另一个更隐蔽的问题是直接用int强转float来保存颜色。很多人嫌麻烦写类似pt.rgb (float)(r 16 | g 8 | b)的代码结果保存出来的点云颜色有噪点、色块发花。原因很简单r 16 | g 8 | b这个整数往往大于2的24次方而float只有23位有效尾数超出精度的部分直接被截断丢掉了。正确做法是用union或者reinterpret_cast把整数按位复制到float里或者干脆用我之前代码里的方式直接给pt.r、pt.g、pt.b赋值让PCL自己处理打包。还有人说“我保存了但是点云看起来全是黑色的”。出现这种情况先别急着怀疑编码用文本编辑器打开PCD文件看几行数据如果rgb列数值都是0说明彩色图本身就是黑的或者彩色图没有成功读取。检查一下彩色图的路径、解码格式、是否对齐别在编码层面浪费时间。4.2 保存成功后只有坐标没有颜色类型和FIELDS字段检查如果你用的是pcl::PointCloudpcl::PointXYZ那编译阶段就不可能通过pt.r 255的赋值这类错误很容易发现。但有一种隐蔽情况代码用的是pcl::PointXYZRGB编译也过了保存也成功了打开文件却发现FIELDS这一行只有x y z没有rgb。这个问题多半发生在你手动创建点云对象、然后通过一些数据拷贝或者成员遍历操作时把点类型信息丢失了。保存之后先检查PCD文件头部如果FIELDS里没有rgb说明写文件的点云对象实际上退化了。还有一种情况是PCL版本之间的行为差异老版本对某些点类型的序列化支持不完整。解决思路就一个确保写入的文件对应的点云类型是pcl::PointCloudpcl::PointXYZRGB并用pcl::io::savePCDFile系列函数保存不要自己拼文件头。如果文件头有rgb字段但可视化工具打开后依然显示成黑白点云换一个软件检查一下。比如CloudCompare和PCL的可视化器对颜色字段的解析基本没问题但某些很轻量的查看器可能只读XYZ并不代表文件坏了。现象可能原因排查步骤红蓝互换通道顺序填反检查图像读取通道BGR改RGB颜色有噪点或花屏int直接强转float用union或reinterpret_cast不直接转文件头没有rgb字段点类型写成了PointXYZ改成PointXYZRGB再保存全是黑色彩色图没读到或全黑检查彩色图像素值确认非04.3 深度图空洞带来的NaN点云过滤与is_dense设置RGB-D相机在反光表面、黑色物体、玻璃、强光直射的地方深度值经常是0或者严重噪点。如果直接把0毫米当成有效深度输入公式算出来的点在相机原点上密密麻麻非常难看如果把这些无效点填成NaN保存的文件数据段里就会带NaN。带NaN的点云文件在部分算法库中会直接崩溃或者产生异常结果。PCL里几乎所有滤波、配准算法在计算前都会检查is_dense标志如果为false很多函数会拒绝执行或者需要先调用pcl::removeNaNFromPointCloud。我的习惯是生成点云时就把无效深度过滤掉代码里已经体现了raw 0 || z 0.1f || z 8.0f这些条件都可以按你的场景调整。这里有个取舍如果保留NaN点云依然保持有序结构尺寸固定为WIDTH*HEIGHT方便按图像索引取点如果直接push_back有效点点云变成无序点云体积更小但失去了行列结构。我是这样选的做可视化或者有后续基于图像索引的算法时保留NaN并置is_densefalse只做纯几何处理时直接丢弃无效点生成无序点云。不管哪种都要在保存前统计一下NaN比例做到心里有数。4.4 彩色点云边缘错位对齐问题的定位最后一种常见问题在视觉上特别明显整个点云形状是对的但物体边缘有一圈错位的颜色渗出来像是彩色照片和深度图叠印没对齐。这条之前提到过多半是深度图和彩色图没有对齐。排查链路是这样的先找一面有明显纹理的平面物体固定相机拍一帧分别保存深度图和彩色图在图像坐标系里把两张图叠起来看边缘偏移方向。如果偏移比较大说明采集时没有做对齐去SDK文档里找对齐接口如果偏移很小就在可接受范围内不用管。另外提醒一句物体靠相机越近视差引起的错位越明显实测时别把物体放得太近否则会被误判成对齐错误。5. 从单帧保存到批量落盘工程习惯建议算法验证完下一步就是批量采集数据了。这个阶段拼的不是代码功能而是工程习惯和数据管理能力做得不好后期想回溯问题会非常痛苦。5.1 批量保存的命名与元数据管理连续采集几十上百帧点云时文件命名要明确、有序。我一般用固定位数的帧号配合时间戳for (int i 0; i frame_count; i) { auto cloud buildColoredCloud(depth_i, color_i, fx, fy, cx, cy); char name[64]; std::snprintf(name, sizeof(name), frames/frame_%06d.pcd, i); pcl::io::savePCDFileBinary(name, *cloud); }不要用cloud_1.pcd这种没有补零规则的命名文件一多字符串排序会乱。再说保存相机内参这事很多新手完全忽略但内参直接影响后续所有几何计算。建议每批数据都单独存一份相机参数文件内容很简单fx: 615.0 fy: 615.0 cx: 320.0 cy: 240.0 depth_scale: 1000.0别以为内参是固定不变的。很多RGB-D相机在固件升级或分辨率切换之后内参会有细微变化如果项目做了几个月当时的标定参数早忘了没有这份文件就得重新标定。哪怕现在用不上也先存着这是成本最低的保险。5.2 文件体积、互转工具与后续使用延伸一帧640x480的深度图转成彩色点云有效点大约二十万到三十万个。用ASCII保存文件能到150MB以上Binary通常40MB上下BinaryCompressed可能只有20MB左右。批量采集几十分钟磁盘很快就满所以采集和算法处理阶段我基本只用Binary。工具链方面CloudCompare是我最常用的点云查看和处理软件它可以直接打开PCD文件也支持另存为PLY、LAS、XYZ等格式。如果下游工具不支持PCD先转成PLY是通用性比较好的选择。PCL自己也提供了格式转换的例子思路就是读进来点云再换个后缀保存。彩色点云保存好之后后面能做的事情就多了可以和ICP配准结合用颜色信息辅助几何对齐可以作为语义分割模型的输入特征也可以用于三维重建时的纹理映射。拿到一帧可靠的彩色PCD文件等于拿到了后续所有点云算法的基础数据这一步值得花时间磨扎实。最后分享一个我个人的习惯无论代码看起来多对保存完PCD后我一定会用pcl::io::loadPCDFile把文件读回来打印前三个点的xyz和rgb再统计NaN比例。这个习惯帮我挡住了大量低级错误比如写文件时通道顺序错了、相机内参传反了、彩色图没对齐。多花一分钟做验证省下的是后面几小时的无头绪排查。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →