
接到一个双目视觉项目,要做一次严格审计,找里面的各种 bug。最典型的症状用户一句话就点了:「灰阶 14 阶并没有出现」。手上的素材是两张灰阶台阶卡的照片,拍摄条件也交代得很清楚——单摄像头、增益 1 到 2 倍、线性模式、曝光调亮过、黑电平 128。
这篇只写其中最硬的一条线:暗端的灰阶台阶为什么看不见,以及顺着它又带出的第二个问题。
「14 阶没出现」要先分清三种死法
暗台阶看不见,听上去是一件事,其实至少有三种完全不同的死法,而且对应的处理手段互相矛盾:
- CLIP 硬截断:暗部被直接削平到纯黑,特征是那几块的均值≈0、方差≈0;
- 噪声地板:信号本身还在,但淹在噪声里,特征是低均值、高方差;
- NR 过度:降噪把相邻的暗台阶抹成了一片,特征是相邻台阶均值合并、方差反而很低。
三种病,药方相反——该抬曝光的你去收噪声,只会更糟。所以第一步不是改参数,是先判断它到底是哪一种。我写了个判别器(_audit_grayscale.py):取灰阶条带的一行亮度剖面,按平台自动切出每个台阶,量最暗那几块的均值、方差、以及相邻台阶的均值差。

实测:不是被削平,是被整体压暗
把两张 6 月 21 日的灰阶图喂进去,结论比较干脆。最暗那块的窗口,min 到 1、均值 50 出头、std 在 38–41——方差不为 0,就排除了硬 clip;每个台阶自己的方差又都很低(0 到 3 点几),相邻台阶的均值差最小也有 6 到 18,也不是 NR 把等值台阶抹平那种。
真正扎眼的是台阶数。一张只稳稳分出约 11 个台阶,暗端从第 11 阶往后直接并成一整块,宽度 386 像素;另一张曝光略好,分到 13 阶左右再并块。换句话说,过了中段以后,台阶和台阶之间的亮度差已经小到分不出来了——暗部被整体压暗,所有暗台阶被挤进了太少的码值里,第 14 阶自然「没出现」。这既不是某个模块把它削平,也不是把它淹掉,而是曝光和暗部压制的合力。

配置这边对得上:黑电平就落在 OB 段的 cofs=128(基准增益档),和用户说的「黑电平 128」一致。方向也就清楚了——抬曝光、松暗部压制,把暗台阶之间的码值差拉开。
重拍验证:第 14 阶回来了
实验室按这个方向重拍了一组(6 月 23 日,调试预设从 5-1 换到 5-4)。同一个脚本再跑,这回稳稳分出 17 个台阶,第 14 阶明明白白地回来了;最暗块的 std 从 38 升到 48.8——暗部被抬亮,噪声也跟着浮上来,这正是抬曝光该有的副作用,说明方向没错。

顺手带出的第二个 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)这一环,不过那是另一条线,这轮没细查。

一点方法论上的实话
这个项目原始的审计会话日志没保留下来。所以上面这些数字不是从当时的记录里抄的,而是 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:暗部被整体压暗,让灰阶暗阶消失;以及锐化过冲,在把暗部修亮之后冒出光晕。它俩其实是一对需要一起拧的参数。顺带一提,双目的第二颗摄像头这一轮没进来,用的全是单摄灰阶——立体那部分是后话。