灰阶 14 阶没出现:一次相机成像审计里挖出的两个 bug

一个双目视觉项目让我做严格审计,最典型的症状是「灰阶 14 阶并没有出现」。暗台阶看不见有三种完全不同的死法,我写了个判别器把它分清,定位到暗部被整体压暗;按方向重拍后第 14 阶回来了,却又把另一个毛病——锐化过冲——放了出来。

灰阶 14 阶没出现:一次相机成像审计里挖出的两个 bug
封面:一台工业相机对着一张从白到黑的灰阶台阶卡,放大镜在检查暗端
图 0:封面,AI 生成示意图(不是实拍)。

接到一个双目视觉项目,要做一次严格审计,找里面的各种 bug。最典型的症状用户一句话就点了:「灰阶 14 阶并没有出现」。手上的素材是两张灰阶台阶卡的照片,拍摄条件也交代得很清楚——单摄像头、增益 1 到 2 倍、线性模式、曝光调亮过、黑电平 128。

这篇只写其中最硬的一条线:暗端的灰阶台阶为什么看不见,以及顺着它又带出的第二个问题。

「14 阶没出现」要先分清三种死法

暗台阶看不见,听上去是一件事,其实至少有三种完全不同的死法,而且对应的处理手段互相矛盾:

  1. CLIP 硬截断:暗部被直接削平到纯黑,特征是那几块的均值≈0、方差≈0;
  2. 噪声地板:信号本身还在,但淹在噪声里,特征是低均值、高方差;
  3. NR 过度:降噪把相邻的暗台阶抹成了一片,特征是相邻台阶均值合并、方差反而很低。

三种病,药方相反——该抬曝光的你去收噪声,只会更糟。所以第一步不是改参数,是先判断它到底是哪一种。我写了个判别器(_audit_grayscale.py):取灰阶条带的一行亮度剖面,按平台自动切出每个台阶,量最暗那几块的均值、方差、以及相邻台阶的均值差。

灰阶暗部诊断:三种死法的判别流程
图 1:把「暗台阶看不见」分成 CLIP / 噪声地板 / NR 过度三类,各看一组指标;右边是实测落点。图中数字为 2026-10-15 用脱敏副本脚本在原始截图上重跑所得。

实测:不是被削平,是被整体压暗

把两张 6 月 21 日的灰阶图喂进去,结论比较干脆。最暗那块的窗口,min 到 1、均值 50 出头、std 在 38–41——方差不为 0,就排除了硬 clip;每个台阶自己的方差又都很低(0 到 3 点几),相邻台阶的均值差最小也有 6 到 18,也不是 NR 把等值台阶抹平那种。

真正扎眼的是台阶数。一张只稳稳分出约 11 个台阶,暗端从第 11 阶往后直接并成一整块,宽度 386 像素;另一张曝光略好,分到 13 阶左右再并块。换句话说,过了中段以后,台阶和台阶之间的亮度差已经小到分不出来了——暗部被整体压暗,所有暗台阶被挤进了太少的码值里,第 14 阶自然「没出现」。这既不是某个模块把它削平,也不是把它淹掉,而是曝光和暗部压制的合力。

6 月 21 日的灰阶图:暗端并成一片
图 2:6-21 原始灰阶(裁到卡面)。白区 A–M 台阶分明,过了中间 M 往后(约 11 阶起)迅速并成一片黑,第 14 阶所在的位置已经分不出台阶。

配置这边对得上:黑电平就落在 OB 段的 cofs=128(基准增益档),和用户说的「黑电平 128」一致。方向也就清楚了——抬曝光、松暗部压制,把暗台阶之间的码值差拉开。

重拍验证:第 14 阶回来了

实验室按这个方向重拍了一组(6 月 23 日,调试预设从 5-1 换到 5-4)。同一个脚本再跑,这回稳稳分出 17 个台阶,第 14 阶明明白白地回来了;最暗块的 std 从 38 升到 48.8——暗部被抬亮,噪声也跟着浮上来,这正是抬曝光该有的副作用,说明方向没错。

6 月 23 日重拍:暗台阶重新分开
图 3:6-23 重拍(裁到卡面)。11–15 这几阶重新拉开,和图 2 同一位置对比明显。

顺手带出的第二个 bug:锐化过冲

审计既然在看边缘,就顺手查了锐化。配置里 EDGE 是开着的,overshoot_str 和 undershoot_str 都给到 256、th_overshoot 128,这套参数偏猛,理论上容易在黑白台阶交界处压出过冲光晕。可是在 6-21 那两张上测,平均过冲几乎是 0(0.6% 和 −0.6%)——光晕没显出来。原因也简单:原图本来就偏软、偏暗,锐化没东西可咬。

等 6-23 重拍(预设 5-4 把 th_overshoot 从 128 抬到 180,还把 edge_map_lut 的底从 0 抬到 76,让锐化在更平的区域也介入)之后再测,平均过冲一下冲到 33.7%,不少边缘在 30%–85%,这就是肉眼可见的锐化光晕了。也就是说:把暗部修亮的同一步,顺带把锐化的毛病放了出来。 这俩得一起调——暗部要抬、锐化要收,否则按下葫芦起了瓢。

至于镜头,这是个大广角,实验室场景那张能看到明显的桶形畸变,ISP 里也确实有 LDC(几何畸变校正加一张 2D LUT)这一环,不过那是另一条线,这轮没细查。

实验室测试场景里的分辨率 / 畸变卡
图 4:测试场景里的分辨率 / 畸变卡(裁自实验室全景)。卡框四边的弯曲说明镜头桶形畸变不小,对应 ISP 里的 LDC 环节。

一点方法论上的实话

这个项目原始的审计会话日志没保留下来。所以上面这些数字不是从当时的记录里抄的,而是 2026-10-15 我用保留下来的两个审计脚本(_audit_grayscale.py / _audit_edges.py)在原始截图上重跑得到的,完整输出放在素材里。好处是结论可复现;代价是「当时具体怎么一步步得出」只能照脚本和配置重建,我不敢声称和原过程逐字一致。

还有一处存疑留给后面核:脚本注释里写的配置值(edge_enh=240、overshoot/undershoot_str=128)和我在 .cfg 里实际读到的(edge_enh=128、overshoot_str=256、th_edge_low=240、th_overshoot=128)对不上,很可能脚本是对着某个分增益档或另一版预设写的。这条记进了待确认。

说到底,这轮审计落定的是两个 bug:暗部被整体压暗,让灰阶暗阶消失;以及锐化过冲,在把暗部修亮之后冒出光晕。它俩其实是一对需要一起拧的参数。顺带一提,双目的第二颗摄像头这一轮没进来,用的全是单摄灰阶——立体那部分是后话。

← 上一篇 给跟随露营车接一路 4K 摄像头:PolarFire 传感器桥的进度记录 下一篇 → 开机卡一分多钟:一台不许省电的解算工作站,前后剥了四层