9  灰度变换与直方图

产线上最常听到的抱怨之一是”图像太灰了”:目标和背景明明灰度不同,肉眼看上去却挤成一团。遇到这种图像,第一反应往往是调对比度——而”调对比度”在算法层面对应一类最简单的图像运算:点运算(point operation)。点运算的定义是:输出像素的灰度只依赖同一位置输入像素的灰度,即 \(s = T(r)\),与任何邻域无关。这与 章节 6 的空间滤波形成鲜明对照——滤波看邻域,点运算只看自己。对 8 位图像而言,一切点运算都可以归结为同一个实现形态:查找表(lookup table, LUT)——预先把 \(T(0)\)\(T(255)\) 共 256 个输出值算好存进一张表,处理时每个像素查一次表即可。本章的灰度拉伸、伽马校正、直方图均衡化,本质上都只是在回答一个问题:这张 256 项的表该怎么填。

LUT 的工程价值在于把”任意复杂的灰度映射”统一成每像素一次 \(O(1)\) 的查表:先对 256 个灰度级各算一次 \(T(r)\),再扫一遍图像。无论 \(T\) 是分段线性、幂函数还是 CDF,运行成本完全相同。

本章沿用一幅合成的低对比场景(480×360)贯穿全部实验:正弦纹理背景上放置一块恒定灰度的平坦板、一个暗矩形、一个亮圆、两条 2 px 细线和一组分辨率条纹,另有两个锚块把灰度区间精确钉在 90 与 150——整幅图像的灰度只占据 \([90,150]\),仅用了 8 位动态范围的不到四分之一。图 9.1 给出了场景和它的直方图。

(a) 低对比场景
(b) 处理前直方图
图 9.1: 低对比测试场景及其直方图。(a) 全部灰度被压缩在 \([90,150]\) 内,目标与背景肉眼难辨;(b) 直方图证实了这一点:所有计数挤在横轴中段一个窄带里,左右两侧大片灰度级完全空置。

9.1 直方图:图像的灰度账本

直方图(histogram)是对图像灰度分布的统计:第 \(k\) 个 bin 的计数 \(h(k)\) 等于灰度值为 \(k\) 的像素个数。把计数除以总像素数 \(N\),得到归一化直方图 \(p(k)=h(k)/N\)——它正是 章节 2 中”经验分布”的具体实例:若把”随机抽取一个像素的灰度”看作随机变量,\(p(k)\) 就是这个随机变量分布的经验估计。直方图丢弃了一切空间信息(把图像打乱重排,直方图不变),却把曝光与对比度的全部信息浓缩在一条曲线里。

读直方图是一项基本功。计数堆在左端贴着 0,说明欠曝(暗部被裁剪);堆在右端贴着 255,说明过曝(高光饱和,被裁掉的信息任何软件都救不回来——这要回到 章节 4 的照明设计去解决);而像 图 9.1 (b) 这样挤在中段窄带里,则是典型的低对比:曝光没问题,但场景本身反差小或镜头眩光抬高了暗部。对本章场景,用 SDK 统计得到:均值 114.78,灰度标准差 12.55,范围恰为 \([90,150]\)。标准差只有 12.55——对一幅 8 位图像来说,这就是”灰成一团”的定量说法。

9.2 线性拉伸

既然灰度只占 \([r_{\min}, r_{\max}]=[90,150]\),最直接的修法就是把这个区间线性地撑满 \([0,255]\)

\[ s = (r - r_{\min}) \cdot \frac{255}{r_{\max} - r_{\min}}. \]

这是一条斜率为 \(255/60 = 4.25\) 的直线——每个像素的灰度(连同噪声)被放大 4.25 倍。图 9.2 是拉伸结果:纹理、细线、条纹组全部清晰可辨,统计上均值 105.30、标准差从 12.55 跃升到 53.34,范围占满 \([0,255]\)

(a) 线性拉伸结果
(b) 拉伸后直方图
图 9.2: 线性拉伸把 \([90,150]\) 映射到 \([0,255]\)。(a) 对比度大幅改善,所有结构清晰可见;(b) 直方图被拉开成梳状(comb)——柱与柱之间留有规律的空隙。

拉伸后的直方图(图 9.2 (b))呈现教科书式的梳状伪影:输入只有 61 个离散灰度级,整数 LUT 把它们映射到 \([0,255]\) 中至多 61 个输出级——其余近 200 个 bin 注定为空。拉伸没有创造任何新信息,只是把原有的 61 级灰度在数轴上摊开;可分辨的灰度级数一个也没有增加。这也提醒我们:看到梳状直方图,基本可以断定这张图被做过数字拉伸。

实践中直接取全图最小/最大值做拉伸并不鲁棒——单个坏点就能把 \(r_{\min}\)\(r_{\max}\) 拽到极端值,使拉伸失效。工程上常用截断百分位拉伸:丢弃最暗与最亮各 \(p\%\)(如 1%)的像素后再取区间端点,以牺牲极少数像素的代价换取对离群值的免疫。

9.3 伽马校正

线性拉伸对所有灰度一视同仁;若想有选择地提亮暗部或压暗亮部,就需要非线性映射。最常用的是幂律形式的伽马校正(gamma correction)

\[ s = 255 \cdot \left(\frac{r}{255}\right)^{\gamma}. \]

曲线的直觉很简单:\(\gamma<1\) 时曲线上凸,暗部斜率大于 1(被拉开、提亮)、亮部斜率小于 1(被压缩);\(\gamma>1\) 时正相反,压暗部、扩亮部。两端 \(0\mapsto 0\)\(255\mapsto 255\) 始终不动,改变的只是中间调的分配。用一条 0–255 灰度斜坡实测 SDK 的约定:\(\gamma=0.5\)\(64\to 128\)\(128\to 181\)\(192\to 221\)——输出恒大于输入,与上式(输入先归一化再取幂)一致。

(a) \(\gamma=0.5\)(提亮暗部)
(b) \(\gamma=2.0\)(压暗暗部)
图 9.3: 在拉伸图(图 9.2 (a))上施加两种伽马。(a) \(\gamma=0.5\) 整体提亮,暗矩形内部细节浮现;(b) \(\gamma=2.0\) 整体压暗,暗矩形近乎全黑,亮部层次反而展开。

在拉伸图上实测(图 9.3):暗矩形内部(拉伸后均值 21.4)经 \(\gamma=0.5\) 提到 71.8,经 \(\gamma=2.0\) 则压到只剩 2.3;亮圆内部(245.6)经 \(\gamma=0.5\) 仅微升到 250.0、经 \(\gamma=2.0\) 微降到 236.8。数字印证了曲线形状:伽马的”力气”几乎全花在远离两端的那一侧——\(\gamma<1\) 大幅搬动暗部、几乎不动高光,\(\gamma>1\) 反之。

“伽马”一词源于显示链路:传统显示器的亮度响应近似 \(V^{2.2}\),所以消费图像(sRGB)在存储时预先做了 \(\gamma\approx 1/2.2\) 的反向编码。而工业相机默认输出线性灰度——像素值正比于光通量,这正是测量算法想要的。在测量流水线中引入伽马意味着破坏线性光度关系,应当慎之又慎;它的正当用途是显示与人工目视检查。

9.4 直方图均衡化

拉伸和伽马都需要人工选参数;直方图均衡化(histogram equalization)则让图像自己决定映射——目标是让输出直方图尽量平坦。推导只需一步:要把分布”摊平”,密集的灰度段应当被拉开、稀疏的灰度段应当被压缩,而累积分布函数(cumulative distribution function, CDF)恰好天然具备这个性质。定义 \(\mathrm{CDF}(r)=\sum_{k\le r} p(k)\),取映射

\[ s = T(r) = 255 \cdot \mathrm{CDF}(r), \]

\(T\) 单调不减(不会颠倒灰度次序),且其局部斜率正比于该处的直方图密度 \(p(r)\)——像素扎堆的地方斜率最陡、被拉得最开。对本章场景做均衡化,结果见 图 9.4:标准差进一步升到 74.28(均值 131.30),对比度甚至超过线性拉伸。

(a) 均衡化结果
(b) 均衡化后直方图
图 9.4: 直方图均衡化。(a) 对比度最大化,但平坦板与背景出现明显的颗粒感;(b) 直方图在整个灰度轴上铺开(离散图像无法做到严格平坦,呈现高低不一的稀疏柱)。

但请仔细看 图 9.4 (a) 中右上角那块”平坦板”:它在原图中是恒定灰度 122 加 \(\sigma=2\) 的噪声,如今布满了颗粒。定量测量这块板内部的标准差给出本章最重要的一组数字:原图 2.02,线性拉伸后 8.46(4.19 倍,恰约等于 LUT 的常数增益 \(255/60\)),而均衡化后达到 18.02——放大 8.93 倍,是线性拉伸的两倍还多。原因正在 CDF 的斜率里:平坦板的几千个像素全部挤在 122 附近的两三个 bin 中,那里的 \(p(r)\) 极高,\(T\) 的局部斜率因而最陡——均衡化在哪里像素最密集,就在哪里把灰度差(连同噪声)放大得最凶,而平坦区恰恰是像素最密集的地方。

这决定了均衡化的工程定位:它是”展示给人看”的工具——目视检查、报告配图、人工复核界面都很合适;但在测量与缺陷检测流水线中应当慎用。章节 7 的自动阈值方法建立在”前景/背景各自灰度集中”的统计前提上,均衡化恰好系统性地破坏这一前提:平坦区噪声被放大近 9 倍后,原本干净的背景会冒出大量越过阈值的伪响应。

均衡化的局部增益 \(T'(r) \propto p(r)\) 还解释了一个看似矛盾的现象:它对真实结构对比的提升与对平坦区噪声的放大来自同一机制,不可能只要前者不要后者。自适应变体(如 CLAHE 的 clip limit)正是通过限制密集 bin 的斜率来缓解这一矛盾。

9.5 SciVision 实现

本章实验涉及三个类:直方图统计 SciSvHistogram、拉伸与均衡化 SciSvHistEqualize、伽马 SciSvBrightness

SCIMV::SciSvHistogram histOp;
SciROI roi;                       // 默认构造 = UNDEF ROI,SDK 约定为整张图像
SciVarArray histData, histRange;
double avg, stdValue; int mn, mx, med, norm, total;
// lower=0, upper=255 统计范围;type=4 自定义 bin 数;nBins=256 即每灰度级一个 bin
histOp.CaculateHist(img, roi, 0, 255, 4, 256, &histData, &histRange,
                    &avg, &stdValue, &mn, &mx, &med, &norm, &total);

SCIMV::SciSvHistEqualize eq;
SciImage stretched, equalized;
// method=1 截断式线性拉伸;lowPct/highPct=0 即按 [minGray,maxGray]=[90,150] 拉满 [0,255]
eq.LineStretchHist(scene, roi, 1, 0.0, 0.0, 90, 150, &stretched);
// rangeValue=255 全范围均衡化;minStrength=0 / maxStrength=255 为默认强度区间
eq.EqualizeHist(scene, roi, 255, 0, 255, &equalized);

SCIMV::SciSvBrightness bright;
SciImage dst;
bright.AdjustGamma(img, roi, 0.5f, &dst);   // s = 255*(r/255)^gamma

CaculateHist 一次调用同时返回直方图数组与一组统计量,但有一个必须知道的坑:输出参数 stdValue 是 256 个 bin 计数的标准差,而不是像素灰度的标准差——对本章原图它返回 157.61,已超出 8 位灰度标准差的理论上限 127.5,显然不是灰度统计。本章正文引用的 12.55/53.34/74.28 均为从直方图自行计算的灰度标准差。LineStretchHistmethod=0(按全图 min-max 自动拉伸)实测对本图不做任何变换,疑似 SDK 缺陷,故采用 method=1 并显式给定 [minGray, maxGray]。另外,全图 ROI 不要用 GenRect1((0,0),(W-1,H-1)) 构造——该矩形按半开区间处理,最后一行一列共 839 个像素不被统计;用默认构造的 UNDEF ROI 表示整图即可。最后一个细节:SDK 拉伸与按公式手写的 LUT 逐像素对比最大差仅为 1(来自截断与四舍五入的取整差异),证实 LineStretchHist 内部正是一次查找表点运算。

完整可运行工程位于 code/gray_transforms_histograms/,固定随机种子保证可复现。

工业案例:“图像太暗”的两种修法

某产线反馈检测图像偏暗、对比度低,软件组先用直方图均衡化”修”了一版——画面立刻通透好看。两周后缺陷误报率翻倍:背景平坦区的传感器噪声被均衡化放大数倍(正是本章 2.02→18.02 的现场翻版),大量噪点越过缺陷判定阈值被报为伪缺陷。复盘后的正确做法分两步:第一步回到硬件链路——检查曝光时间、光圈与镜头,把模拟增益降下来,从源头拿到灰度范围足够、噪声受控的图像(参见 章节 4章节 1);第二步软件上只做参数受控的截断百分位线性拉伸,且拉伸后的噪声水平重新纳入阈值整定。教训只有一句话:“好看”与”好测”是两回事——人眼喜欢的高对比图像,未必是算法需要的低噪声图像。

9.6 小结

  • 点运算 = 查找表。输出只依赖同位置输入灰度的运算统称点运算,8 位图像上一律可实现为 256 项 LUT;拉伸、伽马、均衡化只是三种不同的填表策略。
  • 直方图是灰度的经验分布,丢掉空间信息却保留曝光与对比度的全部证据:贴左端是欠曝、贴右端是过曝(信息已不可恢复)、挤在中段窄带是低对比。
  • 线性拉伸不创造信息:把 61 级灰度摊开到 256 级,可分辨灰度数不变,直方图留下梳状空隙;端点应取截断百分位而非全图 min-max,以抵御离群值。
  • 伽马校正非线性地重新分配中间调\(\gamma<1\) 提暗压亮、\(\gamma>1\) 反之,两端不动;工业相机输出本是线性的,测量流水线引入伽马须慎重。
  • 均衡化按 CDF 填表,局部增益正比于直方图密度——平坦区像素最密集,噪声放大也最凶(本章实测 8.93 倍,远超线性拉伸的 4.19 倍)。它是给人看的增强工具,进入测量/检测流水线前要先评估对 章节 7 统计前提的破坏。

灰度变换与直方图处理(含全局直方图均衡化)的标准论述见 Gonzalez 与 Woods 的教材 (Gonzalez 和 Woods 2018);针对全局均衡化在平坦区放大噪声的弊病,Pizer 等人提出的自适应直方图均衡化及其限幅变体(CLAHE 的雏形)是经典的改进方案 (Pizer 等 1987)。灰度变换与直方图处理在工业流水线中的系统性论述(包括鲁棒拉伸与多项式灰度校正),可进一步阅读 Steger 等人的著作 (Steger, Ulrich, 和 Wiedemann 2018)