2、黑白图像的本质:二值图像与灰度图像的区别、直方图分析、阈值分割原理
好,咱们直接切入正题。黑白图像,听起来很简单对吧?但在我刚入行那会儿,还真被它坑过。有一次做文档扫描件的预处理,我直接把一张灰度图当二值图去处理,结果算法跑出来的结果惨不忍睹。后来我才意识到——黑白图像其实是个大家族,里面至少住着两位性格迥异的成员:二值图像和灰度图像。
2.1 二值图像 vs 灰度图像:到底差在哪?
说白了,二值图像每个像素只有两个值:0 或 1,黑或白。没有中间地带。你想想看,就像开关一样,要么开要么关。而灰度图像呢?每个像素可以有 0 到 255 共 256 个级别,从纯黑到纯白,中间有 254 个灰阶过渡。
我个人习惯用一张表格来对比它们:
| 特性 | 二值图像 | 灰度图像 |
|---|---|---|
| 像素深度 | 1 bit | 8 bit(常见) |
| 取值空间 | {0, 1} | [0, 255] |
| 信息量 | 极低,仅轮廓 | 丰富,含纹理细节 |
| 存储大小 | 小 | 大(约 8 倍) |
| 典型应用 | OCR、形态学处理 | 边缘检测、特征提取 |
嗯,这里要注意:二值图像不是灰度图像的一种特殊情况。它们是两种完全不同的数据结构。灰度图像可以转换成二值图像,但转换过程中会丢失大量信息。我在项目中遇到过一位同事,直接把灰度图当成二值图去算连通域,结果每个像素都成了独立的区域——那画面太美我不敢看。
2.2 直方图分析:图像的数字指纹
直方图是什么?说白了就是统计每个灰度级别上出现了多少个像素。横轴是灰度值(0~255),纵轴是像素数量。你想想看,一张图像的所有像素分布,用一根根柱子就画出来了。
我每次拿到一张新图像,第一件事就是看它的直方图。为什么?因为直方图能告诉我很多信息:
- 图像是否过暗或过亮:直方图集中在左侧说明偏暗,集中在右侧说明偏亮。
- 对比度高低:直方图分布越宽,对比度越高;越窄,对比度越低。
- 是否存在明显的双峰:这是做阈值分割的重要依据。
举个例子,一张正常的自然图像,直方图通常像连绵起伏的山脉。而一张二值图像的直方图,就只有两根柱子——一根在 0 位置,一根在 255 位置。中间空空如也。这就是为什么我说直方图是图像的数字指纹,一眼就能看出图像的本质。
核心要点:灰度图像的直方图是连续分布的,二值图像的直方图只有两个峰值。如果你看到直方图中间有大量像素,那它一定不是二值图像。
2.3 阈值分割原理:从灰度到二值的桥梁
阈值分割,说白了就是设定一个门槛,把灰度图像变成二值图像。公式很简单:
if pixel_value > threshold:
output = 255 # 白色
else:
output = 0 # 黑色
但问题来了——这个 threshold 怎么选?
我曾经犯过一个低级错误:所有图像都用固定阈值 128。结果遇到一张偏暗的文档扫描件,文字和背景全混在一起了。嗯,从那以后我再也不敢用固定阈值了。
2.3.1 全局阈值法
最经典的是Otsu 算法(大津法)。它的核心思想是:找一个阈值,让前景和背景的类间方差最大。说白了,就是让两坨像素各自内部尽量紧凑,两坨之间尽量分开。
用代码实现很简单:
import cv2
import numpy as np
# 读取灰度图像
img = cv2.imread('document.jpg', cv2.IMREAD_GRAYSCALE)
# Otsu 自动阈值
_, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 显示结果
cv2.imshow('Binary', binary)
cv2.waitKey(0)
Otsu 算法有个前提:直方图必须是双峰的。如果图像光照不均匀,或者前景背景面积相差太大,Otsu 的效果就会打折扣。我在项目中遇到过一张医学 X 光片,直方图是单峰的,Otsu 直接失效了。
2.3.2 局部阈值法
当全局阈值搞不定的时候,就得请出局部阈值法了。最常用的是自适应阈值:
# 自适应阈值(局部)
adaptive_binary = cv2.adaptiveThreshold(
img, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
blockSize=11, # 局部窗口大小
C=2 # 减去常数
)
它的原理是:每个像素的阈值由它周围一个小窗口内的像素决定。这样就能应对光照不均匀的情况。你想想看,一张照片左边亮右边暗,全局阈值肯定不行,但局部阈值就能自适应调整。
我的经验:blockSize 一般取奇数,比如 11、15、21。C 值越大,分割结果越「白」,反之越「黑」。调试时可以先从 C=2 开始,再根据效果微调。
2.3.3 手动阈值与交互式调整
有些场景下,自动算法就是不如人眼判断。比如老照片修复、艺术图像处理。这时候我会写一个简单的滑动条工具:
def nothing(x):
pass
cv2.namedWindow('Threshold')
cv2.createTrackbar('Value', 'Threshold', 127, 255, nothing)
while True:
thresh_val = cv2.getTrackbarPos('Value', 'Threshold')
_, binary = cv2.threshold(img, thresh_val, 255, cv2.THRESH_BINARY)
cv2.imshow('Threshold', binary)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
嗯,手动调整虽然笨,但有时候就是最靠谱的。我记得有一次处理一份民国时期的报纸,背景泛黄、字迹模糊,Otsu 和自适应阈值都搞不定,最后就是靠手动调阈值一张一张处理的。
避坑指南:我曾经把一张灰度图直接保存为 .bmp 格式,然后当成二值图去读。结果发现文件大小一点没变——因为 BMP 格式默认保存为 8 位灰度,即使像素只有 0 和 255,它也是灰度图,不是二值图。要保存真正的二值图,请使用 PNG 格式,或者用 OpenCV 的 cv2.imwrite 时指定参数。
2.4 总结一下
这一章我们聊了三件事:
- 二值图像和灰度图像的本质区别——一个只有黑白,一个有 256 级灰阶。
- 直方图分析——看直方图就能判断图像类型和质量。
- 阈值分割原理——从全局 Otsu 到局部自适应,再到手动调整。
说白了,黑白图像处理的核心就是如何从灰度信息中提取出有意义的二值结构。这个能力,是后续所有图像分析的基础。下一章我们会聊到噪声处理——嗯,那又是一个让人头疼的话题。
公众号:蓝海资料掘金营,微信deep3321