Halcon对比度增强算子选型与调参实战

发布时间:2026/11/3 20:08:46
Halcon对比度增强算子选型与调参实战

Halcon 里跟增强图像对比度相关的算子,数下来不超过十个:scale_image、scale_image_max、equ_histo_image、emphasize、illuminate,再加上自己用mean_image拼出来的掩膜方案,基本就是全部家当。真正让人卡住的地方通常不是记不住这些名字,而是不知道该在流程的哪一步动手、参数凭什么取这个值、改完之后分割效果为什么反而更差。我在几个检测工位上反复调过这类参数,也见过不少把图"调好看"了但阈值彻底失效的案例,所以这篇就按实际调参的顺序来拆:问题定位、算子选型、参数计算、场景实录、问题排查。刚上手 Halcon 的新手可以照着代码抄,做过一两年、想把预处理链路做稳的同行,重点看参数取值逻辑和踩坑那一节。

1. 增强对比度在检测链路里到底扮演什么角色

1.1 目标是稳定分割,并非让图好看

先立一个认知:工业视觉里的对比度增强,服务对象是后面的分割和特征提取算子,不是人眼。这两个目标的取向经常是相反的。人眼喜欢整体通透、层次分明的图,但threshold、dyn_threshold、edges_sub_pix这些算子只关心两件事:目标与背景的灰度差够不够大,噪声的灰度波动够不够小。一张被拉得很亮、视觉上很舒服的图,如果噪声也被一起放大到和目标同一量级,阈值就彻底没法选了。

我在一个检测工位上遇到过很典型的情况:原始图里目标灰度大概在 60 到 110 之间,背景在 40 到 70 之间,两者有重叠区。直接threshold无论怎么调,要么漏检边缘,要么把背景纹理一起吃进来。这时候scale_image把 60 到 110 这段拉开到 20 到 230,重叠区被推到可见范围之外,固定阈值一次就能分干净。整个过程里图看起来是"发灰发硬"的,但分割结果稳。所以调参时我基本不看窗口里那张图好不好看,只看dev_display出来的区域边界和area_center的稳定性。

反过来说,如果下游用的是dyn_threshold配mean_image这种局部比较的方式,全局对比度增强的收益就没那么大了,因为局部比较本身对绝对灰度不敏感。这种时候该做的是光照校正而不是灰度拉伸,方向选错了,后面参数怎么调都是白费。

1.2 全局增强与局部增强的岔路口

这是选型的第一个分岔。判断依据只有一个:光照在整幅图上是不是均匀的。

光照均匀(比如同轴光、背光、暗场这种稳定照明),灰度分布的问题纯粹是动态范围没占满,用全局方法最省事,scale_image一次性搞定,参数可解释、可复现、可写进配置文件。光照不均匀(环形光打在高反光件上、大视野镜头边缘衰减、条形光留下的亮带),全局拉伸会把亮的地方直接推到饱和、暗的地方还是黑的,问题反而更严重。这时候必须上局部方法,先把低频的照明分量估出来扣掉,再放大剩下的细节。

判断均匀性有个不用写代码的土办法:在一个空背景下拍图,看背景区域的min_max_gray结果。如果整幅背景的灰度极差超过 40(8bit),基本可以判定存在明显的照明梯度,全局拉伸要谨慎。这个判据粗糙但够用,现场调试时比画直方图快。

1.3 一条能复用的三步预处理链

顺序错了,后面全乱。我现在的固定链路是:降噪 → 光照校正 → 对比度拉伸。

降噪放最前,是因为后面两步都会放大噪声。emphasize的Factor调到 3,噪声标准差也跟着乘 3,如果之前没压过,拉伸完之后噪点灰度能超过目标本身的灰度差,阈值彻底选不出来。先做一次median_image或轻量gauss_filter,代价是丢一点边缘锐度,换成后面参数空间的稳定,很划算。

光照校正放中间,是因为它改变的是低频分量,不改变整体动态范围。做完之后图的灰度分布是"压平"的,此时再拉伸,Mult的计算才有意义——没校正就拉伸,算出来的Mult是照着一个带斜坡的分布去凑的,换个位置拍图就得重算。

拉伸放最后,紧贴分割算子。这样每一张图进来,链条输出的灰度范围都是一致的,阈值可以固定下来不用逐图调整。这条链路我在字符识别和表面缺陷两个方向上都在用,改的只是中间那步的参数。

2. Halcon 对比度增强算子逐个拆解与选型对照

2.1 scale_image:唯一需要自己算参数的线性拉伸

scale_image做的是纯线性映射,公式就一行:g' = g * Mult + Add。它不做任何自适应,图进来什么样,出来就是按你给的系数缩放平移后的样子。这个"笨"恰恰是它最大的价值——参数固定,结果可预测,同一批产品换个时间拍图,只要照明没变,结果就是一致的。

与之对比,所有自适应方法(直方图均衡、自动拉伸)都有个共同毛病:映射函数随图变。良品和不良品的直方图不一样,映射出来的灰度就不一样,固定阈值立刻失效。产线上要求"同一套参数跑一整班",scale_image是最容易满足这个要求的。

它的代价是必须手工算参数。Mult太大,亮部会顶到 255 被硬截断,原来有层次的区域变成一片死白;Add为负是常态,因为要把暗端抬到 0 以上。这两个数怎么来,第 3 节会走一遍完整计算。

2.2 scale_image_max:一行代码,但容易破坏阈值

scale_image_max自动把图像拉伸到当前像素类型的满量程,8bit 图就是 0 到 255。写起来极其省事,一行搞定,很多示例代码里都能看到。但它在产线上有个很隐蔽的坑:拉伸依据的是全图的最小值和最大值,而这两个值通常由噪声或者个别坏点决定。

我吃过一次亏:一幅图大部分区域灰度在 70 到 120 之间,角落有个反光点灰度 250,还有个暗角灰度 5。scale_image_max按 5 到 250 去拉伸,Mult只有 1.04,实际上等于没增强。反过来,如果图里有一个纯黑坏点(0)和最亮处 130,Mult会被算成 1.96,噪声跟着翻倍。这就是它不可控的地方——参数不是由你决定,是由图里的极值决定的。

要用也不是不行,前提是先把极值处理掉:用一个略小于图像尺寸的矩形做reduce_domain裁掉边缘,或者先median_image压掉孤立点,再调scale_image_max。但既然都要做这些,不如直接上scale_image把参数握在自己手里。我现在的做法是把它当调试工具用——快速看一眼这张图的动态范围大概占了多少,正式代码里不出现。

2.3 equ_histo_image:直方图均衡的适用边界

equ_histo_image做直方图均衡,把灰度分布强行拉成近似均匀。它对提升纹理和边缘的可见性效果很好,尤其是那种大面积同灰度、只有细微纹理差异的表面(铸件、织物、粗糙金属),均衡之后纹理的层次一下就出来了。

但它在测量类流程里要慎用。原因是映射是非线性的,而且是全局的、随图变的。同一类零件,光照稍微变一点,直方图峰值位置移动,映射曲线整体平移,同一个灰度值在两幅图里映射到完全不同的结果。下游只要用了固定阈值,良品率就开始飘。更麻烦的是它会放大暗区的噪声——暗区灰度本来集中在 20 到 40 这一小段,均衡时这一段被拉开到 0 到 100,噪声标准差跟着涨好几倍。

我的经验边界是:下游做纹理分析、频域分析、亚像素边缘定位,可以用;下游做固定灰度阈值分割、灰度特征比对,别用。如果一定要用又得保留可复现性,可以先在所有样本上算出一个平均的 LUT,用lut_trans固化成查找表,后面所有图都套同一张表。这样映射关系固定了,代价是对新样本的适应性差一些,但对产线上批次稳定的场景完全够用。

2.4 emphasize:局部高频放大的主力

emphasize是局部增强里最常用的一个。按文档给的算式,它先对图做一次MaskWidth × MaskHeight的均值滤波得到低频分量,然后按(Image - Mean) * Factor + Image计算输出。也就是说,它保留了原图的亮度基底,把"原图减去局部均值"这部分高频成分乘上Factor再叠回去。

这个结构决定了它的两个特性。第一,它不改变整体亮度,只放大局部起伏,所以对光照均匀性没有改善,光斑还是光斑。第二,Factor = 1的时候输出是2*Image - Mean,相当于标准的一次锐化,Factor = 0才是无操作。很多人第一次用看到Factor建议值从 1.0 起,以为 1.0 是"不增强",调完发现边缘炸了,就是没注意这个偏移。

MaskWidth、MaskHeight的取值是这里唯一的学问,第 3.3 节会专门讲。简单说一句结论:窗口要明显大于你要增强的特征尺寸,否则特征会被算进均值里,反而被削弱。

2.5 illuminate:先扣照明再放细节

illuminate的算式是(Image - Mean) * Factor + Mean,和emphasize只差最后加回去的是均值还是原图。这个差别带来的效果完全不同:它把每个像素都往局部均值上拉,等于把照射不均的低频分量直接扣除,输出图的整体亮度被抹平成一条水平线。

所以它的主战场是光照不均。环形光在圆柱面上留下的亮带、大视野镜头边角的暗角、条形光在平面上留下的渐变,这些用illuminate一遍就能压下去。压完之后再上scale_image拉伸,Mult才算得准。

参数上,MaskWidth、MaskHeight要比照明变化的尺度大得多,通常从 51 起步,51、101、151 这样往上试。窗口太小,均值会跟着零件本身的灰度变化走,等于把零件也当成照明给削了。Factor在 1.0 附近是无操作,往大调增强局部对比,往小调相当于做了一次大尺度平滑。我常用 1.2 到 2.5 这个区间。

这两个算子经常被混用,我给个最简的区分方式:图整体亮度不均,用illuminate;图整体亮度均匀但细节发闷,用emphasize。拿不准的时候两个都跑一遍,看哪个的输出直方图更接近双峰,就用哪个。

2.6 频域路线与手工掩膜:什么时候值得多花这点功夫

还有一条路是自己拼:mean_image或gauss_filter得到低频,sub_image相减得到高频,mult_image放大,add_image叠回。这条路的优势是完全可控——低通用高斯还是均值、高频乘多少、叠回去用哪个基底,每一步都能单独调。emphasize本质就是这条链的封装,只是把中间步骤藏起来了。

真正需要手工搭的场合有两种。一种是要用非对称的处理,比如只增强水平方向的纹理(连接器的针脚划痕多是横向的),这时候用一个横向长、纵向短的掩膜做mean_image,再用sub_image相减,方向性就出来了,现成算子做不到。另一种是要做多尺度融合:分别用窗口 5、15、45 各算一遍高频,按不同权重叠加,小窗口管细节、大窗口管结构,比单一尺度稳得多。

再往上一层是 FFT 路线,fft_image转频域、gen_gauss_filter造滤波器、convol_fft滤波、fft_image_inv转回来。这套东西在照明梯度特别强(低频能量集中在极低频)的时候确实好用,高通一卡,照明分量整个被切掉。但它的代价是计算开销大、参数难解释、边界效应需要额外处理(fft_image的图尺寸要求是 2 的幂或者特定长度)。产线节拍紧的时候我基本不考虑,优先用illuminate顶着。

选型上做个对照方便查阅:

算子作用范围参数是否随图自适应适合的下游主要副作用
scale_image全局线性否,参数需手算固定阈值分割Mult过大会饱和截断
scale_image_max全局自适应是,由极值决定仅调试观察噪声/坏点主导拉伸
equ_histo_image全局非线性是纹理、频域分析阈值不可复用、暗区噪声放大
emphasize局部高频否边缘、划痕、细纹理噪声同步放大
illuminate局部低频扣除否光照不均场景窗口过小会削平零件
手工掩膜链局部,可定向否方向性纹理、多尺度步骤多,参数相互耦合

3. 参数怎么算:拿一张实拍图走完整流程

3.1 先把问题量化,别靠眼睛猜

调参之前一定先做统计。眼睛看到的"对比度低"和真实的灰度分布经常不是一回事。我固定用三行代码开局:

read_image (Image, 'part_01') rgb1_to_gray (Image, Gray) gen_rectangle1 (ROI, 120, 200, 860, 1180) reduce_domain (Gray, ROI, ImageROI) min_max_gray (ImageROI, ImageROI, 2, Min, Max, Range)

min_max_gray的Percent参数很关键,传 2 表示忽略首尾各 2% 的像素再取极值。这一步就是防噪声和坏点的——不做这个忽略,一个过曝点就能把Max拉到 255,算出来的Mult直接偏小一半。Percent取多少看噪声水平,噪点多的图取 3 到 5,干净的图取 1 就够。

除了极值,我还会看一眼均值和中值。如果Min=58、Max=124,均值只有 70,说明分布严重偏向暗端,这时候光拉伸不够,Add要一起调。再往下才是写拉伸参数。

3.2 scale_image 的完整计算过程

接着上一步的结果往下算。假设Min = 58、Max = 124,实际范围只有 66 个灰度级,占满量程的 26%。目标是映射到 10 到 240 这个区间,不顶到 0 和 255。

为什么不映射到 0 到 255?因为要留余量。饱和截断是不可逆的,一旦顶到 255,那一片区域的灰度信息全丢了,后面再想做亚像素定位就没有梯度可算。留 10 个灰度级的下限和 15 个的上限,能吸收大部分噪声波动和批次差异。

计算过程:

Mult = (TargetMax - TargetMin) / (Max - Min) = (240 - 10) / (124 - 58) = 230 / 66 = 3.4848 Add = TargetMin - Min * Mult = 10 - 58 * 3.4848 = 10 - 202.12 = -192.12

验算一下端点:58 * 3.4848 - 192.12 ≈ 10.0,124 * 3.4848 - 192.12 ≈ 240.0,对得上。写成代码:

TargetMin := 10 TargetMax := 240 Mult := (TargetMax - TargetMin) / (Max - Min) Add := TargetMin - Min * Mult scale_image (Gray, ImageScaled, Mult, Add)

有两个细节容易翻车。第一,Max - Min如果是个很小的值(比如图上大部分是纯色,极差只有 5),Mult会飙到 46,噪声乘 46 倍直接变成满屏雪花。这种情况要设一个下限保护:if (Max - Min < 20) Mult := 1.0,或者干脆判定这张图无效、走异常流程。第二,如果相机输出的是 12bit 的uint2图,满量程不是 255 而是 4095,TargetMax要按 4000 左右去设,照抄 255 的话图会被压得极暗。这个坑我在第一次接 12bit 相机的时候踩过,调了半天以为是镜头问题。

3.3 emphasize 的窗口参数怎么定

这是整套参数里最容易凭感觉乱填的地方。我用的判据只有一条:MaskWidth、MaskHeight取目标特征尺寸的 3 到 5 倍。

原理很清楚。emphasize输出里被放大的部分是(Image - Mean) * Factor。如果窗口远大于特征,均值里几乎不含特征本身,Image - Mean在特征处就是特征相对背景的完整灰度差,乘上Factor后特征被完整放大。如果窗口小于特征,特征的中心区域会被算进均值,Image - Mean在中心处接近 0,放大出来是个空心的环——边缘被强调,中间被吃掉,看起来像浮雕。这个现象很有辨识度,看到浮雕感就说明窗口给得太小了。

举个例子:检测金属件表面的细划痕,实测宽度 3 到 6 个像素。窗口取 15 到 25 就合适,相当于特征宽度的 3 到 4 倍。如果划痕宽度有 20 个像素(比较粗的压痕),窗口就得给到 61 到 101,仍然给 15 的话中心会被削空。

窗口必须是奇数,Halcon 内部会按中心对称处理,偶数会有半像素偏移。Factor上,我从 1.5 起试,一般到 3.0 就够,超过 4 之后噪声涨得比信号快,得不偿失。手上有噪声数据的话可以算一下:emphasize后噪声标准差约等于原标准差乘以Factor,只要这个值还明显小于目标与背景的灰度差,就是安全的。

顺带说illuminate的窗口逻辑是反的:它要扣掉的是照明不均,照明变化的尺度往往占半幅图,所以窗口要往大了取,51、101、151 这样试,取到能覆盖住照明梯度的周期。这两个算子的窗口取法方向相反,混用的时候特别容易搞错。

3.4 Halcon 里没有 CLAHE,但可以近似实现

自适应直方图均衡(分块均衡再插值融合)在很多开源库里都有,Halcon 没有直接对应算子。我试过两种近似方案,都能用。

第一种是多尺度emphasize叠加:分别用窗口 9、27、81 各跑一次emphasize,Factor分别取 2.0、1.6、1.2,然后用add_image加权融合(权重 0.5、0.3、0.2)。小窗口补细节,大窗口补结构,整体观感接近分块均衡,但参数可解释,不会出现块状伪影。

第二种是分块scale_image加重叠:把图切成 4×4 的块,每块单独统计min_max_gray单独算Mult、Add,块与块之间留 20% 重叠,处理完再拼回去。拼接缝在重叠区做线性过渡可以压掉。这个方法算出来的结果最接近真正的自适应均衡,但开销大、代码长,除非前两种压不住,我一般不选。

4. 三个典型场景的完整实操记录

4.1 场景一:低对比度金属件划痕

原始图是灰度范围 58 到 124 的金属表面,划痕灰度比背景高 8 到 15 个灰度级,噪声标准差约 3。直接阈值选不出稳定的MinGray。处理链路:

read_image (Image, 'metal_01') rgb1_to_gray (Image, Gray) median_image (Gray, ImageMedian, 'circle', 2, 'mirrored') min_max_gray (ImageMedian, ImageMedian, 2, Min, Max, Range) Mult := 230.0 / (Max - Min) Add := 10 - Min * Mult scale_image (ImageMedian, ImageScaled, Mult, Add) emphasize (ImageScaled, ImageEmphasize, 21, 21, 2.0) threshold (ImageEmphasize, Region, 180, 255) connection (Region, ConnectedRegions) select_shape (ConnectedRegions, Selected, 'area', 'and', 120, 8000)

几个取值说明。median_image用半径 2 的圆形结构元,是为了压掉传感器的椒盐噪声又不糊掉划痕——半径再大一点,3 像素宽的细划痕就被中值滤掉了。emphasize窗口给 21,对应划痕宽度 4 到 6 像素的 3 到 4 倍。阈值下限 180,是拉伸后背景灰度大约在 150 到 175 之间波动、划痕在 200 以上,取 180 留了一点余量。

这套参数跑了大概两周的样本,漏检集中在划痕起点处——那里灰度差只有 5 左右,拉伸后勉强到 190,被阈值切掉了尾部。后来把阈值降到 172、同时把select_shape的面积下限从 120 提到 200,靠面积特征把背景噪声挡掉,漏检就基本没有了。这个调整思路值得记一下:降阈值必然带进更多假点,用形状特征去补,比硬扛着阈值不放要好。

4.2 场景二:背光不均匀的字符识别

背光拍摄的字符,中间的字符区域过曝、边缘偏暗,全图灰度从 30 跨到 250。这里绝对不能用全局拉伸。处理链路:

read_image (Image, 'char_01') rgb1_to_gray (Image, Gray) illuminate (Gray, ImageIlluminated, 101, 101, 1.8) median_image (ImageIlluminated, ImageMedian, 'circle', 1, 'mirrored') scale_image (ImageMedian, ImageScaled, 1.8, -40) threshold (ImageScaled, Region, 110, 255)

illuminate用 101×101 的窗口,是因为照明梯度在图上跨越了将近 300 像素,窗口太小的话均值会跟着字符本身跳,压不平。Factor取 1.8,把局部对比提上来。做完这一步,图的整体亮度被压平到同一水平,后面scale_image的Mult才有一个统一的值可算——这里是 1.8 配Add = -40,因为我提前在几张图上统计过illuminate输出的典型范围。

有个细节得提:illuminate之后图像的灰度范围其实变窄了(因为低频被抠掉),如果直接接threshold,阈值上下限的容差会比想象中小,图与图之间稍微有点波动就翻车。我一般会在illuminate后面再加一次统计,把阈值做成自适应:min_max_gray拿到范围,阈值下限取Min + 0.45 * Range,这样换一批产品不用重调。

4.3 场景三:高反光表面的暗缺陷

最后这个场景最难。铝件表面做了抛光,有强烈的镜面反射,图上几条亮带灰度直接 255,缺陷是暗色的凹坑,灰度比周围低 20 左右。难点在于亮带附近的目标被"洗掉"了。

这里emphasize和illuminate都不太合适——emphasize会把亮带边缘进一步锐化,产生假的暗线;illuminate的窗口压不住这么陡的梯度。我的做法是改用局部自适应阈值,配合一次温和的对比度拉伸:

read_image (Image, 'alu_01') rgb1_to_gray (Image, Gray) scale_image (Gray, ImageScaled, 1.6, -20) mean_image (ImageScaled, ImageMean, 31, 31) dyn_threshold (ImageScaled, ImageMean, RegionDynThresh, 12, 'dark') connection (RegionDynThresh, ConnectedRegions) select_shape (ConnectedRegions, Selected, 'area', 'and', 60, 3000)

dyn_threshold的思路是拿每个像素和它邻域的均值比,只关心局部差异,亮带这样的缓变区域被自动忽略。Offset取 12,意思是比局部均值低 12 个灰度以上才算缺陷,这个值要大于噪声波动、小于缺陷的真实灰度差,我一般是噪声标准差的 3 到 4 倍。mean_image的 31×31 窗口要大于缺陷尺寸、小于亮带宽度。

这个方案里scale_image只是把信噪比抬一点,Mult给到 1.6 就够了,再大亮带处就彻底饱和,dyn_threshold在饱和区没法工作——255 减去 255 是 0,什么也检测不出来。这也是为什么高反光表面要把曝光压下来拍,靠后期增强是补不回来的。

5. 常见问题和排查速查

5.1 增强之后噪声爆炸

最常见。表现是阈值分割出一堆碎点,connection之后连了几千个区域。根本原因是增强前的信噪比就不够,emphasize或equ_histo_image把噪声和信号一起放大了。

排查顺序:先算一次intensity看噪声标准差,如果超过目标与背景灰度差的 1/3,说明原始信噪比不足,任何增强都是在放大噪声,得回去改照明和曝光。如果信噪比还行,那就是增强参数过猛,Factor从 3.0 降到 2.0 试试,或者在增强前加一次median_image。

有个细节:median_image放在增强前比放在增强后有效得多。增强后的噪声已经是高频信号的一部分,中值滤波分不清哪个是噪声哪个是划痕,容易把细特征一起抹掉。

5.2 参数只挪了一点点,效果跳变

8bit 图像的饱和截断是主因。scale_image的Mult从 3.4 调到 3.6,看起来只差 6%,但原本落在 250 附近的一大片像素会被推到 255 以上截断,那一片区域的梯度信息全部消失,edges_sub_pix直接报错或者给出乱跳的结果。

对策是留足余量,TargetMax不要设到 250 以上。我一般用 240,讲究一点用 230。另外调参时用min_max_gray复核一下输出的实际范围,超了就说明给大了。

另一个跳变来源是emphasize的窗口奇偶性。相邻的 20 和 21 输出差异不大,但 21 和 22 之间会有一个半像素的偏移感,边缘位置会整体挪一点。这个在亚像素测量里是致命的,所以窗口永远用奇数,而且要固定。

5.3 常见问题速查表

现象最可能的原因处理办法
分割出大量碎点增强前信噪比不足,Factor过大先median_image,Factor降到 2.0 以下
目标中心被掏空、边缘发亮emphasize窗口小于目标尺寸窗口放大到特征尺寸的 3 至 5 倍
大片区域全白无层次scale_image的Mult过大,饱和截断降低TargetMax,复核输出范围
换一张图阈值就失效用了自适应算子,映射随图变化改用scale_image固定参数,或固化 LUT
光照不均压不下去illuminate窗口太小窗口从 51 加到 101 或 151
边缘出现假线条emphasize放大了亮带边缘改用dyn_threshold局部比较
12bit 图变得极暗满量程按 255 算了TargetMax按 4000 左右设

5.4 几条不太写进文档的实操心得

第一,参数一定要写进配置文件,不要写死在代码里。同一类产品换批次、换供应商,最佳Mult可能差 0.3,硬编码在代码里意味着每次都要重新编译部署。我现在的做法是把Mult、Add、窗口尺寸、Factor全放到一个 ini 或者 json 里,现场工程师可以直接改。

第二,每次改参数都在同一组固定的样本图(至少 20 张,覆盖良品和各类不良)上跑一遍,统计检出率和误检率。只在一张图上调参数,等于在过拟合。我见过太多参数在一张样例图上完美、上线第一天就崩的情况。

第三,留一条关掉增强的退路。增强是为了解决问题,不是为了炫技。有些场景照明做好了,原始图的分割就很干净,硬加一层emphasize只是给自己增加调参负担。每次加一层处理,都要问自己:不加这层,结果差多少?差得不多就去掉。

6. 我在实际调参里的几点体会

调这类参数这几年,最大的变化是从"看着图顺眼"变成"看着数据说话"。现在每次拿到一张新图,第一件事永远是min_max_gray和intensity两行统计,灰度范围、均值、标准差三个数出来,该用哪个算子、参数大概在什么量级,心里就有谱了。反倒是直接上手拖参数的做法,绕的弯路最多——改完之后效果好一点或者差一点,说不清是哪个变量起的作用,最后调出一组"能用但不知道为什么能用"的参数,换个产品就推倒重来。

还有一个体会是,emphasize和illuminate这两个算子看起来简单,实际上手最容易出错的地方都在窗口尺寸的语义上。它们不是"滤波器尺寸"这种和效果无关的工程参数,而是直接定义了"什么算低频、什么算高频"的分界线,等于在告诉算法什么叫细节。所以每次定窗口,我都会先量一下目标特征的实际像素尺寸,再乘 3 到 5,而不是随手填个 15 或者 21。这个习惯养成之后,参数一次就调对的概率高了很多。

至于对比度增强本身能解决多少问题,说句实在话,照明能解决的部分就别交给算法。我在一个工位上花了两天调illuminate的参数,最后发现问题出在光源角度偏了 5 度,把光源支架挪了一下,原始图的均匀性直接好了大半,之前调的全白费。硬件上花十分钟,软件上省两天,这笔账我后来算得很清楚。