
1. 先把深度图和伪彩图这件事说清楚1.1 深度图不是一张普通灰度图做机器视觉的同学应该都有体会深度图Depth Map和普通2D灰度图完全是两码事。灰度图记录的是物体表面的反射光强度而深度图记录的是相机到物体每个点的距离或者是结构光、双目视觉、ToF传感器解算出来的空间Z坐标。Halcon里读进来的深度图绝大多数情况下是单通道的uint2类型也就是16位无符号整型。这意味着它的灰度范围是从0到65535而不是普通8位灰度图的0到255。实际项目中传感器返回的深度范围可能只有几毫米到几百毫米映射到16位空间里真正有用的数据可能只落在某一段区间比如2000到5000。这种情况下你直接用read_image读进来用Halcon的Image窗口显示看到的基本就是一片漆黑或者一片灰蒙蒙的画面什么细节都看不出来。这也是为什么“Halcon深度图转伪彩图”这个需求在项目里特别常见——倒不是说伪彩之后数据本身发生了什么质变而是人眼对灰度的分辨能力非常有限通常只能看出几十个等级的灰度差异但对彩色的敏感程度却要高得多。深度图转伪彩图本质上就是把人眼看不清的灰度信息映射成人眼容易分辨的彩色信息。1.2 伪彩图到底改了什么信息伪彩图并没有给图像增加任何新的物理量它只是做了一次灰度值到RGB颜色的映射。原来深度值2000的地方可能映射成深蓝色深度值5000的地方可能映射成红色。中间的所有值按照一条预设的颜色曲线colormap逐级过渡。我经常拿温度云图来类比你看天气预报的卫星云图温度高低并不会直接显示数字而是用蓝色到红色的色带表示一眼就能看出哪里冷哪里热。Halcon的伪彩处理和这个逻辑一模一样只是数据源从温度变成了深度值。这种可视化处理在项目里的价值主要体现在三个方面第一个是快速直观地检查深度数据质量比如有没有无效点、有没有跳变噪声第二个是方便和客户、产线人员沟通毕竟你不可能让产线师傅盯着65536个灰度级别判断产品好坏但让他看红绿蓝的色块差异他马上就能理解第三个是配合深度学习或者传统图像处理做缺陷检测时伪彩增强后的图可以直接作为训练样本特征会比原来的暗色调图像明显得多。2. 核心环节灰度映射与colormap选型2.1 先拉伸还是先转类型顺序很关键很多新手第一次做深度图转伪彩会直接用convert_image_type把uint2转成byte结果发现图像变成了一片白或者一片黑然后就开始怀疑自己的数据有问题。其实问题出在convert_image_type这个算子是按截断方式工作的比如把uint2转成byte它默认会把0到65535的整个范围压缩到0到255如果有效深度只占整个范围的很小一段压缩完之后基本就是一团模糊的灰对比度极低。正确做法是先做灰度拉伸把有用的深度范围映射到0到65535的完整动态范围或者直接映射到0到255然后再做类型转换和伪彩映射。常用的算子组合是min_max_gray获取有效灰度范围再用scale_image做线性拉伸。这里有一个细节值得注意min_max_gray如果直接用0作为百分比会把图像里极少数的异常点比如传感器失效产生的高亮点或纯黑点也纳入统计范围导致拉伸效果被这些离群值牵着走。实际项目中我一般用1到5的百分比值把这些极限值忽略掉只针对主体数据范围做拉伸出来的效果会稳定很多。2.2 colormap不是越花哨越好Halcon里做伪彩常用的算子有两个流派一个是apply_color_map配合现成的颜色映射表比如jet、temperature、iron之类的另一个是自己定义LUT查找表用lut算子和一个三通道的查找表图像实现自定义映射。apply_color_map用起来是最省事的代码很简单但选哪种colormap是有讲究的。我做深度图可视化时一般遵守这么几条经验如果是做3D高度图展示比如芯片引脚平面度检测或者连接器针脚共面性测量我习惯用jet或者rainbow。这类colormap是蓝绿黄红的渐变冷色调代表低处暖色调代表高处视觉直觉非常自然客户也最容易接受。如果是做表面缺陷检测比如划痕、凹坑、凸起我会偏向用temperature或者自定义的黑白红渐变。因为划痕和凹坑在深度上往往只有很小的差异需要把目标缺陷区域和正常区域的色差拉得足够大同时背景尽量暗这样缺陷一眼就能跳出来。如果是做最终交付报告用的图我会自己定义LUT把背景无效像素设为灰色有效深度用绿到紫的渐变显得专业且不刺眼。这里插一句很多国外视觉公司出的报告图都是这种风格国内项目方也逐渐接受这种视觉效果。3. HDevelop里完整实现深度图转伪彩图3.1 环境准备与算子选择开始写代码之前先确认你的Halcon环境已经安装好并且能正常读图。如果你手头没有真实的深度相机数据可以用Halcon自带的示例深度图也可以自己用sim_occlusion这类算子合成一个高度起伏明显的测试图效果同样能说明问题。这一步涉及的算子链是固定的* 1. 读取深度图uint2类型 read_image (DepthImage, your_depth_image.tiff) * 2. 获取图像尺寸和有效区域信息 get_image_size (DepthImage, Width, Height) * 3. 统计有效灰度范围 min_max_gray (DepthImage, DepthImage, 2, MinGray, MaxGray, RangeGray) * 4. 灰度拉伸到0~255 scale_image (DepthImage, ScaledImage, 255.0 / (MaxGray - MinGray), -MinGray * 255.0 / (MaxGray - MinGray)) * 5. 转换类型为byte convert_image_type (ScaledImage, ByteImage, byte) * 6. 应用伪彩映射 apply_color_map (ByteImage, ColorMappedImage, jet)这里我需要专门解释一下scale_image的两个参数。scale_image的定义是目标像素值 原始像素值 × Mult Add。所以如果你想把深度区间[MinGray, MaxGray]映射到[0, 255]Mult就是255除以区间宽度Add就是负的MinGray乘以Mult。上面的代码里-MinGray * 255.0 / (MaxGray - MinGray)就是这么推导出来的不需要死记硬背现场演算一遍就清楚了。3.2 进阶方案带无效点过滤的鲁棒写法实际项目中深度图经常会有无效点。ToF相机对黑色吸光物体、反光物体容易输出0值或者极大值结构光相机对遮挡区域也会出现空洞。如果这些无效点不处理拉伸就会受影响伪彩图会看到大片异常的纯色块。我的做法是先用阈值把有效范围框出来再做拉伸映射* 假设有效深度范围是200~2000之外的都认为是无效点 threshold (DepthImage, ValidRegion, 200, 2000) reduce_domain (DepthImage, ValidRegion, ValidDepth) * 在有效区域内统计灰度范围 min_max_gray (ValidDepth, ValidDepth, 3, MinGrayValid, MaxGrayValid, RangeGrayValid) * 拉伸到0~255 scale_image (DepthImage, ScaledImage, 255.0 / (MaxGrayValid - MinGrayValid), -MinGrayValid * 255.0 / (MaxGrayValid - MinGrayValid)) * 把无效区域单独置灰 paint_gray (ScaledImage, 128, PaintedImage, Region, 0)这里有个小技巧paint_gray可以把指定区域填充成固定灰度值。我把无效区域填充成128的中灰色这样伪彩图里无效区域就会显示为中性色不会干扰有效区域的色带观察。3.3 用更直观的scale_image_max简化流程如果你的项目对拉伸的精度要求不那么苛刻只是想在调试阶段快速看个大概Halcon还提供了一个更省事的算子scale_image_max。它会自动统计图像的最大灰度范围并做满幅拉伸一步到位不需要手动计算Mult和Add。scale_image_max (DepthImage, ScaledImage) convert_image_type (ScaledImage, ByteImage, byte) apply_color_map (ByteImage, ColorMappedImage, rainbow)三行代码就能看到一幅像模像样的伪彩图。但这个算子有个毛病就是它会被个别极端噪声点干扰导致整体对比度被拉低。所以我的习惯是调试阶段用scale_image_max快速验证数据是否正常正式处理流程里还是用min_max_gray加scale_image的组合把离群值过滤掉再拉伸。3.4 自定义LUT实现专利色调映射最后补充一下自定义LUT的做法。有时候客户会指定颜色风格或者你需要把N个深度区间分别标成不同颜色来做定性分析现成的colormap就不够用了。Halcon自定义伪彩的核心是生成一张查找表图像这张表是一个N行1列的三通道图像通过gen_empty_obj配合人工赋值或者从外部文件读取RGB值来生成。然后用lut算子应用到目标图上* 生成一个256行1列的RGB图片作为查找表 gen_image_const (LookupTable, rgb, 256, 1, 0) * 这里省略具体逐行赋值的代码实际项目中用set_grayval循环填充 set_grayval (LookupTable, Row, 0, [R, G, B]) * 应用自定义LUT lut (ByteImage, CustomColorImage, LookupTable)自定义LUT的好处是自由度极高比如你可以把深度值前50%统一压缩成蓝色系后50%展开成红色系突出高低落差也可以把某个特定深度范围标亮其他区域压暗。缺点是需要手写填充逻辑调起来比apply_color_map慢。我的建议是能不用就不用除非客户明确要求特定配色或者你要做产品级的色彩标准化否则jet和rainbow足够打天下了。4. 实战场景复盘伪彩图在项目落地中的价值4.1 场景一3D视觉药片缺粒检测说一个我实际做过的项目药板包装缺粒检测。药板的透明薄膜在2D灰度图下几乎看不出药片是否缺失即便用背光也很难稳定判断但用3D深度相机配合Halcon处理后药片区域的高度和周围薄膜有明显差异。这个项目的调试过程中伪彩图帮了大忙。深度图原始显示下药片和薄膜的灰度差异肉眼几乎不可分辨阈值分割怎么做都找不到合适的参数。但把深度图转成伪彩图后药片区域在colormap下呈现出明显的暖色薄膜区域则是冷色对比极其明显。我直接在伪彩图上做了颜色阈值分割用decompose3拆出红绿蓝通道做通道差值缺陷区域一下就锁定了。这里我想强调一个思路伪彩图不只是给人看的它也可以直接作为图像处理的输入。虽然从信息论角度讲伪彩图本身没有增加信息量但是因为它把细微的灰度差放大成了明显的色差在某些场景下反而让传统算法更容易处理。4.2 场景二高度测量时反而要警惕伪彩图伪彩图不是万能的我踩过一个坑在一个连接器针脚共面性检测项目里开发阶段为了方便观察直接把深度图转成伪彩然后在伪彩图上做一系列形态学处理和测量。结果发现测量数据波动很大重复精度达不到要求。后来排查了半天才意识到问题伪彩图经过了三步处理——拉伸、类型转换、颜色映射每一步都可能引入舍入误差。尤其是从uint2转到byte这一步深度数据的原始精度已经被压缩掉了原来毫米级别的精度信息丢失了很多。之后的测量计算基于这种损失精度的数据结果自然不稳定。所以我的经验是伪彩图只用于显示、调试和交付报告真正的量化测量必须基于原始uint2深度图做。如果你确实需要以图像作为处理载体也应该在拉伸后的byte灰度图上测量而不是在伪彩图的RGB通道上测量。这个原则很重要希望做3D视觉的朋友引以为戒。4.3 场景三批量转换脚本与流程自动化深度图转伪彩还有一个常见的落地场景就是离线批量处理。比如产线上每天产生几千张深度图需要全部转成伪彩图提供给质检团队复核或者用于深度学习模型训练集的构建。这种情况下我不会把HDevelop里的操作一步步手动点按钮执行而是把处理流程封装成一个脚本通过Halcon的批处理模式跑。也可以用C#或C调用Halcon的算子库在Windows服务或者工厂上位机软件里批量跑。热词里提到的“C#直接调用Halcon”其实走的就是这个方向用Halcon .NET接口加载hdev脚本或者直接调算子一条流水线下来输入文件夹、遍历、处理、输出全程无人值守。批量处理时我强烈建议在文件名上做文章比如保留原始深度图的文件名和采集时间戳再拼一个_pseudo后缀作为伪彩图的文件名。这样后续如果发现某一张伪彩图异常可以快速追溯到原始深度数据和当时的产线参数排查起来效率高很多。5. 常见问题与排查技巧实录5.1 伪彩图整体偏蓝或偏红动态范围没拉开这个现象最常见的原因是拉伸范围不对。比如你用了全图范围的min_max_gray但图像角落有极少量的高灰度噪点把MaxGray拉到了一个异常大的值导致有效区域的灰度被压缩到了一小段区间映射之后自然是单一色调。解决方法是把min_max_gray的百分比参数从0调整到2到5把极限值剔除出统计范围。如果你还不知道合理的深度范围是多少可以先跑一次min_max_gray看输出值再结合自己的传感器量程来判断。比如ToF相机的量程是0到2000mm但你统计出来的MaxGray是3000那就说明有异常值必须过滤。5.2 深度图有大量黑色孔洞伪彩图也是一块块斑驳黑色孔洞一般是深度图里的无效像素也就是深度值为0的区域。这种区域不做处理伪彩图就会在有效区域之间穿插很多黑色或灰色的空洞严重影响视觉效果。我的处理办法前面提到过先通过阈值把无效区域分离出来在伪彩映射之后用paint_gray统一填充成中灰色或者干脆用interpolate类的算子把空洞区域插值填平。具体用哪种方式取决于下游需求如果只是做人眼检查填充成中性灰就够了如果伪彩图要喂给深度学习模型那最好把空洞区域做插值补全避免模型学到“空洞异常”这种错误的关联。5.3 深度图有类似水波纹的条纹噪声转伪彩后更明显条纹噪声往往是传感器的多路径反射或者运动伪影造成的。原始灰度图下可能还看不太出来但一旦转成高对比度的伪彩图这种噪声会被放大得非常刺眼。遇到这种情况建议在转伪彩之前做一步滤波。Halcon里可以用median_image做中值滤波去掉离散噪点或者用smooth_image搭配大一点的mask做平滑。但我需要提醒一下平滑滤波在去掉噪声的同时也会抹掉微小的深度细节如果后续还要做高精度测量滤波的强度要控制得很谨慎不要为了视觉好看牺牲数据真实性。5.4 伪彩图显示卡顿窗口缩放很吃力如果你处理的深度图分辨率很高比如2000万像素级别的3D点云投影图伪彩映射本身很快但Halcon窗口的交互缩放会明显卡顿。这个时候不要折磨自己直接把伪彩图保存成PNG或JPEG用普通图片查看器打开体验会好很多。Halcon的图形窗口本来就是为调试设计的不是为海量数据做高频交互浏览的。另外批量保存伪彩图时建议用PNG格式而不是JPEG。JPEG是有损压缩在色带过渡区域会产生明显的压缩伪影虽然不影响人眼判断但如果后续要用这些图做深度学习训练压缩伪影会引入额外的干扰因素。6. 深度图转伪彩的几个高阶扩展方向6.1 从伪彩图到热力图叠加显示有些项目不只是要一张伪彩图而是要把伪彩的深度信息叠加到2D彩色图像上形成一种混合显示效果。比如在电子产品外壳检测里甲方希望看到真实物体纹理的同时又能看到深度差异在对应位置的颜色标记。Halcon里做这种叠加最直接的方式是把RGB图和伪彩图各自转成三通道然后按一定比例做加权融合用overpaint_gray或者add_weighted_images这类算子就能实现。我在一个外观检测项目里就用过这个方案最后展示给客户的效果图是在产品正常外观基础上叠加了半透明的深度色带既保留了纹理信息又能看到凹陷区域泛红、凸起区域泛蓝客户反馈非常直观。6.2 把伪彩图作为深度学习训练的输入另一个值得尝试的方向是把深度图转伪彩后作为深度学习模型的输入。很多人以为深度学习输入只能是普通的RGB图实际上对于3D视觉任务把深度图编码成伪彩图再训练模型是业界常用的策略。我个人的实践感受是直接用原始uint2深度图训练网络的收敛速度和精度往往不如用伪彩图训练来得好。原因在于很多预训练模型比如基于ImageNet的图像分类网络的输入分布是针对RGB图设计的伪彩图在数据分布上更接近这些模型见过的数据。当然这不是说伪彩图一定能赢但这确实是一个工程上性价比很高的做法尤其当你不想从头训练一个接收单通道深度图的网络时。6.3 动态范围压缩从线性映射到非线性映射前面讲的方法都是线性映射也就是说深度值从Min到Max均匀地映射到0到255。但实际场景里有时候你更关心某一个区间的细节其他区间反而希望压缩掉。比如一个冰箱门密封条检测项目所有待测区域的深度差异可能只有10mm但整个图像的深度范围却有1000mm。如果用线性映射10mm的差异只占整体色带的1%人眼根本分辨不出来。这时候就需要用非线性映射把关心的10mm区间映射到大部分色带上其余范围压缩到两端。Halcon里实现非线性映射的方式比较灵活可以用look_up_table这个算子施加任意形状的灰度映射曲线也可以先自己计算一个映射数组转成LUT再应用。我通常的做法是把深度值先归一化到0到1用一个分段函数做映射优先放大中间区间然后再缩放回0到255。这种定制的动态范围压缩能力在工业检测项目里非常实用因为真实产线上往往就是“整体差异大关键细节差异小”的情况。7. 关于这段实操最后再交代几句从最早在一堆纯黑图像里对着uint2数据束手无策到现在能根据项目需求灵活选择拉伸策略、colormap和输出格式我算是把“深度图转伪彩图”这件事的各种边角都摸过一遍。我个人的体会是这个处理本身不复杂真正决定项目质量的往往是那些不起眼的细节有没有过滤异常值、拉伸区间选得对不对、无效区域有没有处理、输出格式是无损还是有损这些小事在单张图上看着无所谓在批量处理和大规模应用中就会被无限放大。分享一个小技巧作为收尾当你第一次拿到一个全新传感器的深度图不知道有效深度范围是多少时别急着写完整的处理链。先跑一行min_max_gray加上histo_to_thresh看一下灰度直方图分布花两分钟搞清楚数据的真实分布再决定后面的拉伸策略。这个习惯帮我省掉了无数次来回调试的时间。深度图转伪彩图说到底是对数据分布的理解而不是对某个算子的死记硬背。