图像处理入门020 | 图像二值化:阈值分割基础 —— cv2.threshold 五种类型与自适应阈值铺垫

第020期 | 图像处理算法入门系列 | 2026-08-29

关键词:图像二值化、阈值分割、cv2.threshold、Trackbar、自适应阈值、文档扫描


一、引言

第019期我们用 8 种方法 把彩色图变成灰度图,今天进入真正的分割:二值化(Image Binarization)。

二值化是图像处理里出场率最高的操作,没有之一。OCR 预处理、车牌识别、文档扫描、产品瑕疵检测、医学细胞计数 ------ 几乎所有"检测目标轮廓"的算法,第一步都是二值化。

text 复制代码
二值化的本质:用一个阈值 T 把灰度图像切成"前景 / 背景"两堆。

  像素值 > T    →  255(白色,前景)
  像素值 ≤ T    →  0(黑色,背景)

看似一行代码能搞定的事,里面却藏着五种"切法",每种切法对结果的语义完全不同。

本期我们就把 cv2.threshold五种 type 参数 一字排开讲清楚,再把整个"从直方图选 T 到 Trackbar 实时调整"的完整流程跑一遍,最后用一个"光照不均"的反例,引出下期的 Otsu 与自适应阈值。


二、核心概念与原理

2.1 二值化的数学定义

设输入灰度图像为 I(x, y) ∈ [0, 255],阈值为 T,最大值为 maxval(通常 255),则通用映射为:

text 复制代码
dst(x, y) = f(I(x, y), T, maxval)

OpenCV 提供了 5 种映射规则 f(·),对应不同的实际用途。

2.2 全局阈值 vs 局部阈值

text 复制代码
全局阈值:用一个 T 处理整张图
  - 优点:快、稳定、可解释
  - 缺点:光照不均时一片 T 不够用(实验5会演示)

局部/自适应阈值:每个像素根据邻域确定自己的 T
  - 优点:适应光照变化
  - 缺点:参数更多、计算更慢
  - 本期先讲全局,023 期讲自适应、076 期讲 Otsu 自动选 T

2.3 cv2.threshold 函数签名

python 复制代码
ret, dst = cv2.threshold(src, thresh, maxval, type)

参数:
  src      : 输入灰度图(uint8)
  thresh   : 阈值 T(0-255)
  maxval   : 大于 T 时赋的值(一般写 255)
  type     : 5 种阈值类型之一(flags.THRESH_*)
返回:
  ret  : 算法实际使用的阈值(OTSU 等会自动算 T 并回传)
  dst  : 输出二值图

三、五种阈值类型详解

Type 名 规则 典型用途
THRESH_BINARY > T → maxval;否则 → 0 标准二值化(OCR/检测)
THRESH_BINARY_INV > T → 0;否则 → maxval 反二值化(白底黑字文档→黑底白字)
THRESH_TRUNC > T → T;否则不变 把亮像素"砍"到 T(高光抑制)
THRESH_TOZERO > T → 不变;否则 → 0 只保留亮区(细节提取)
THRESH_TOZERO_INV > T → 0;否则不变 只保留暗区(找阴影、烧洞检测)

3.1 完整代码:五种类型一次跑完

python 复制代码
import cv2
import numpy as np

# 读一张灰度图(BGR → GRAY)
img = cv2.imread('test.png', cv2.IMREAD_GRAYSCALE)

types = [
    cv2.THRESH_BINARY,
    cv2.THRESH_BINARY_INV,
    cv2.THRESH_TRUNC,
    cv2.THRESH_TOZERO,
    cv2.THRESH_TOZERO_INV,
]
for t in types:
    ret, dst = cv2.threshold(img, 127, 255, t)
    cv2.imwrite(f'out_{t}.png', dst)

3.2 实验效果(合成暗背景 + 亮目标)

可以直观看到:

  • BINARY 提取出全部亮目标;
  • BINARY_INV 把图"染色"成背景------便于后续按位运算;
  • TRUNC 把亮区"压平"到 T 值,可以丢掉强光;
  • TOZERO 是"亮区细节选择器",保护亮区像素不被截断;
  • TOZERO_INV 把亮区抹黑,留下的就是原本的暗区细节。

四、阈值 T 的取值:扫描实验

cv2.threshold 不会替你选 T,这个 T 必须自己给。T 取错了,再好的算法也救不回来

4.1 扫描不同 T 看效果

text 复制代码
T 太低(< 谷底):  背景被错分进前景,引入噪声点
T 太高(> 谷底):  前景边缘碎裂,细小目标丢失
T ≈ 谷底:         前景背景分离度最大,分割最稳

4.2 直方图辅助:找双峰之间的"谷底"

理想阈值 = 直方图两峰之间的最小值处。这是 Otsu 自动算法的核心思想。

在实验3的合成图里,程序自动算出谷底 T ≈ 59,正好落在背景峰(约 30-60)与前景峰(>150)之间。


五、手写实现 + 性能对比

cv2.threshold 背后就是一行 np.where,完全可以自己写。

5.1 NumPy 向量化版本(推荐)

python 复制代码
import numpy as np

def binarize_manual(img, thresh=127):
    """手写实现 THRESH_BINARY:纯 NumPy,无 OpenCV。"""
    return np.where(img > thresh, 255, 0).astype(np.uint8)

5.2 与 OpenCV 对比:1280×960 性能基准

text 复制代码
1280×960 像素、平均耗时(毫秒,越低越好):

  OpenCV cv2.threshold :  0.35 ms
  NumPy 向量化        :  2.72 ms
  Python 双 for 循环  : 138.31 ms   ← 反面教材,比向量化慢约 50 倍

结论 :自己写完全够用,差别只在常数项。永远不要用双 for 循环处理像素


六、交互式阈值选择器(Trackbar)

固定 T 显然不够灵活,OpenCV 提供 cv2.createTrackbar 让你拖滑块实时调。

python 复制代码
import cv2

def interactive_threshold_viewer(img, win='Threshold Viewer'):
    """拖动滑块实时看 T 对结果的影响。"""
    def on_trackbar(val):
        _, dst = cv2.threshold(img, val, 255, cv2.THRESH_BINARY)
        fg = (dst > 0).mean() * 100
        # 把灰度图变成 BGR,加文字
        disp = cv2.cvtColor(dst, cv2.COLOR_GRAY2BGR)
        cv2.putText(disp, f'T={val:3d}  FG={fg:5.1f}%',
                    (10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
        cv2.imshow(win, disp)

    cv2.namedWindow(win, cv2.WINDOW_AUTOSIZE)
    cv2.createTrackbar('Threshold', win, 127, 255, on_trackbar)
    on_trackbar(127)    # 第一次手动触发刷新
    print('拖动滑块选择阈值 | S=保存当前帧 | ESC/Q=退出')
    while True:
        key = cv2.waitKey(30) & 0xFF
        if key in (27, ord('q')):
            break
        if key == ord('s'):
            t = cv2.getTrackbarPos('Threshold', win)
            _, dst = cv2.threshold(img, t, 255, cv2.THRESH_BINARY)
            cv2.imwrite(f'020_interactive_T{t}.png', dst)
            print(f'已保存 020_interactive_T{t}.png')
    cv2.destroyAllWindows()

# 本地运行(服务器无显示环境不可用):
# interactive_threshold_viewer(cv2.imread('test.png', cv2.IMREAD_GRAYSCALE))

完整版本(含 Trackbar)见本仓库 code/020_threshold_basic.py,脚本默认无头模式跑批实验,设置 RUN_INTERACTIVE=1 即可启动 GUI。


七、全局阈值的致命弱点:光照不均

现实中没有几张图是光照均匀的。看下面这张"光从左往右渐暗"的图:

  • 左侧(亮)的目标正常切出;
  • 右侧(暗)的目标几乎被错分到背景;
  • 单一 T=127 无法兼顾两侧。

解决方案有三

  1. 先做光照校正 (第110期去雾算法 + 同态滤波可以借鉴)

  2. Otsu 自动选 T(最大类间方差,第076期详讲)

  3. 自适应阈值(每个像素根据自己的邻域选 T,第023期详讲)

这是下两期要展开的内容,本期先埋个钩子。


八、完整可运行代码

本期所有图都来自这一份脚本 code/020_threshold_basic.py,无头环境直接 python 020_threshold_basic.py 即可复现:

python 复制代码
"""
第020期:图像二值化 ------ 阈值分割基础
环境:Python 3.8+, OpenCV 4.x/5.x, NumPy, Matplotlib
"""
import cv2, numpy as np, time, os
OUTPUT_DIR = os.path.join(os.path.dirname(__file__), 'output')
os.makedirs(OUTPUT_DIR, exist_ok=True)
# ... 完整代码已上传到本仓库 code/020_threshold_basic.py
# 包含五大模块:
#   exp1_threshold_types()   # 五种 type 对比图
#   exp2_threshold_sweep()   # T 取值扫描图
#   exp3_histogram_guide()   # 直方图辅助选 T
#   exp4_manual_vs_opencv()  # 手写实现 + 性能基准
#   exp5_global_fail()       # 光照不均案例(下期预告)
#   interactive_threshold_viewer()  # Trackbar 交互(本地 GUI)

直接运行:

bash 复制代码
python code/020_threshold_basic.py

脚本会生成 5 张可视化 PNG 到 code/output/020_*.png,并打印每次实验的关键参数。


九、常见问题 FAQ

Q1:cv2.threshold 输入必须是灰度图吗?

A:是的。如果传彩色图,OpenCV 会按每个通道独立做阈值,结果通常不是你想要的。先用 cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 转灰度。

Q2:返回的 ret 是干嘛用的?

A:如果你没指定 OTSU / TRIANGLE 等自动算法,ret 就是你传入的 thresh;用了 OTSU,ret 就是它自动算出的 T。很多教程把 ret 写成 _ 是合理的,但你要知道它在背后是有意义的。

Q3:为什么我的图像 ret 不等于我传的 thresh?

A:你可能用了 cv2.THRESH_OTSU 之类的 flag,OpenCV 会"替你改 T"。本期我们没有这个 flag,所以 ret 完全等于传入值。

Q4:cv2.threshold 与 cv2.inRange 是一回事吗?

A:不是。threshold 处理单通道灰度,T 是一个数;inRange 处理 HSV / BGR 多通道,可以一次给 [lo, hi] 区间,本质是范围掩码。

Q5:服务器/无显示环境能用 Trackbar 吗?

A:不能。Trackbar 依赖桌面 GUI。生产环境的做法:第1步用直方图找谷底 / 用 Otsu 自动算 T;第2步批量处理。

Q6:阈值化前要不要先做平滑?

A:通常建议先做轻度高斯模糊(σ=0.5~1.0)再阈值,噪点会被压下去,尤其是椒盐噪声。但别用太强的模糊,否则细小目标被抹掉。


十、课后练习

  1. 基础 :用 cv2.imread 读取一张你自己的照片,转灰度后做不同 T 的阈值分割,观察什么时候文字(前景)开始被错分。
  2. 进阶 :写一个函数,输入彩色图、给定 T,用 NumPy 而不调 cv2.threshold 完成 HSV 三通道各自独立二值化,再合并成"任一通道为前"的掩码(OR 逻辑)。
  3. 挑战 :实现一个"自动阈值扫描找最佳 T"的工具:对每个候选 T,计算"类间方差"(前景均值与背景均值差距),输出方差最大的那个 T,不要用 Otsu 算法(自己推导)。

十一、产品推荐

11.1 移动端

产品 亮点 适合场景
扫描全能王(CamScanner) 自动文档检测 + 透视校正 + 二值化增强 移动端文档扫描、合同照片转 PDF
Microsoft Lens Office 生态、二值化对比度自动调节 课堂板书、白板会议记录
Adobe Scan Adobe 生态、超像素二值化输出 商务合同、纸质文件归档
PicScan / PhotoScan 批量扫描、智能去阴影 老照片、海报电子化

11.2 专业级

产品 亮点 适合场景
ABBYY FineReader 工业级 OCR 预处理、版面分析 法律/财务文档数字化、批量扫描
Adobe Acrobat Pro OCR 引擎 + 批量二值化设置 企业文档归档、合规存档
ScanTailor / ScanTailor Advanced 开源文档分割流水线、社区维护 学术论文数字化
Kofax Power PDF 企业级扫描 + 工作流集成 银行/医疗表单自动化

本期代码仅用到 OpenCV + NumPy,全是开源免费工具,工业级部署可集成 ABBYY / Tesseract 的二值化模块作为预处理。


下期预告

第021期:直方图:图像像素分布的可视化 ------ 我们已经看到了双峰直方图,下一期深入到直方图均衡化(HE)和自适应均衡化(CLAHE)。这是图像增强最强的一招:从 s 型灰度分布"拉"成均匀分布,肤色变亮、纹理重现,是人像美颜、去雾的核心技术之一。

相关推荐
笑霸final2 小时前
不用上传服务器!用 Vue3 + ONNX Runtime Web 在浏览器本地实现智能抠图
运维·前端·图像处理·ai·onnx·canva可画·web性能优化
AI的探索之旅6 小时前
97 个 OpenCV 实例(十三):目标跟踪,CamShift 反向投影
人工智能·opencv·目标跟踪
A hao1 天前
户外LED广告牌需要什么防护等级IP级别?
大数据·网络·图像处理·人工智能·网络协议·tcp/ip·广告
Mr.Lu ‍1 天前
C++开发,使用openCV API对图片进行压缩
开发语言·c++·opencv
一只嵌入式爱好者1 天前
深入理解ISP
图像处理·isp
SamChan901 天前
Python+OpenCV检测PDF翻译后排版偏移:像素级格式一致性验证
python·opencv·ai·pdf·wpf
sali-tec2 天前
C# 基于OpenCv的视觉工作流-章106-差值追踪
图像处理·人工智能·opencv·算法·计算机视觉
磁场转动100万匹2 天前
OpenCV 答题卡识别判卷实战:从图像预处理到自动评分
人工智能·opencv·计算机视觉
阿图灵2 天前
OpenCV 绘图五件套:画圆、文本、线段、矩形与椭圆
图像处理·人工智能·python·opencv·计算机视觉·绘图