☰
大津法二值化程序实战:从原理到代码,解决光照不均图像分割
2026/10/21 8:28:55 网站建设 项目流程

简介:这份资源提供大津法(OTSU)二值化算法的MATLAB实现程序,面向图像处理初学者、科研人员及工程开发者,用于解决图像分割中自动确定全局阈值的问题,可应用于文本识别、医学图像分析与工业检测等场景。压缩包为rar格式,仅含1个m文件,体积约1KB,属于轻量级脚本,便于直接阅读与移植。目前已有395人学习下载,说明其在入门实践中具有一定参考价值。通过研读代码,读者可掌握灰度直方图统计、阈值遍历、前景与背景平均灰度计算以及类间方差最大化等关键步骤,理解从理论公式到MATLAB落地的完整过程。程序经实验验证效果良好,能有效分离目标与背景,同时也可作为对比自适应阈值、局部阈值等方法的基线,帮助排查光照不均或噪声干扰下的分割问题。

1. 大津法二值化程序:从一张光照不均的零件图说起

前阵子帮朋友处理一批工业零件照片,同一批货,打光稍微偏一点,阈值就飘得没法看。手动调阈值调到怀疑人生,后来翻出这个「大津法二值化程序」,跑了一遍,效果确实稳。它干的事很纯粹:输入一张灰度图,自动算出一个全局阈值,把图像分成前景和背景两类,输出黑白二值图。适合谁用?做图像预处理、OCR 前处理、缺陷检测、文档扫描增强的开发者,尤其是那种不想在阈值调参上耗时间的场景。大津法(Otsu)本身不新鲜,但这个程序把「类间方差最大化」的推导落成了可读的代码,还带了实验对比,能直接看到它比固定阈值强在哪。下面我按「它怎么算 → 怎么跑 → 怎么改」的顺序拆一遍。

2. 核心算法拆解:类间方差最大化的每一步

2.1 大津法到底在优化什么

大津法的目标函数是类间方差。把图像的灰度直方图归一化后,假设阈值 t 把像素分成两类:C0(灰度 ≤ t)和 C1(灰度 > t)。两类的概率分别是 ω0、ω1,均值是 μ0、μ1,全局均值是 μT。类间方差定义为:

σ²_b(t) = ω0(t) · ω1(t) · [μ0(t) − μ1(t)]²

遍历所有可能的 t(0 到 255),取使 σ²_b 最大的那个 t 作为最终阈值。这个公式的直觉是:两类分得越开、各自占比越均衡,类间方差越大。程序里通常用等价形式 σ²_b = [μT·ω0 − μ(t)]² / (ω0·(1−ω0)) 来算,避免重复求 μ0、μ1,速度更快。

2.2 直方图与累积量计算

核心代码一般长这样,先统计直方图,再算累积概率和累积均值:

import numpy as np def otsu_threshold(gray): # gray: 单通道 uint8 图像 hist = np.bincount(gray.ravel(), minlength=256).astype(np.float64) total = gray.size prob = hist / total # 归一化直方图 omega = np.cumsum(prob) # ω0(t):前 t 类的累积概率 mu = np.cumsum(prob * np.arange(256)) # 累积均值 muT = mu[-1] # 全局均值 # 避免除零:只取 0 < omega < 1 的阈值 valid = (omega > 0) & (omega < 1) sigma_b = np.zeros(256) sigma_b[valid] = (muT * omega[valid] - mu[valid]) ** 2 / \ (omega[valid] * (1 - omega[valid])) best_t = int(np.argmax(sigma_b)) return best_t

逻辑说明:np.bincount统计每个灰度级出现次数,cumsum一次性算出所有阈值下的 ω0 和累积均值,避免嵌套循环。valid掩码排除 ω0=0 或 1 的退化情况,否则分母为零。argmax返回最大类间方差对应的灰度值。参数上,输入必须是单通道 uint8,如果是彩色图要先转灰度;返回的best_t直接拿去做cv2.threshold的 thresh 参数即可。

2.3 二值化输出与可视化

拿到阈值后,二值化本身一行就够:

import cv2 def binarize(image_path, out_path): img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(image_path) t = otsu_threshold(img) _, binary = cv2.threshold(img, t, 255, cv2.THRESH_BINARY) cv2.imwrite(out_path, binary) print(f"threshold={t}") return t

这里cv2.threshold的THRESH_BINARY表示大于 t 的置 255,小于等于 t 的置 0。如果前景是暗色、背景是亮色,可以换成THRESH_BINARY_INV。程序里通常还会把原图、直方图、二值图拼在一起显示,方便对比。常见做法是用 matplotlib 的 subplot 排成一行,直方图上画一条竖线标出阈值位置,一眼就能看出阈值选得合不合理。

3. 跑起来:环境、入口与实验对比

3.1 依赖与目录结构

这个程序依赖很轻,常见组合是 Python 3 + NumPy + OpenCV。安装就两条:

pip install numpy opencv-python

如果程序里带了实验对比图,可能还会用到 matplotlib:

pip install matplotlib

目录结构一般是这样,不同版本可能略有差异,但核心文件跑不掉:

otsu-binarization/ ├── main.py # 入口,读图、调用、保存 ├── otsu.py # 大津法核心实现 ├── utils.py # 直方图绘制、图像读写封装 ├── images/ # 测试图,通常放几张光照不均的样图 └── results/ # 输出二值图

main.py里通常用argparse接收输入路径和输出路径,方便命令行调用。如果拿到的是 notebook 版本,那就直接从上往下执行 cell,改一下图片路径就行。

3.2 运行入口与参数

命令行跑法:

python main.py --input images/part_01.jpg --output results/part_01_bin.png

如果程序没做参数解析,那就改main.py里的硬编码路径。我一般会先确认三件事:图片是不是灰度读入的、输出目录存不存在、阈值有没有打印出来。阈值打印很重要,它是判断程序有没有正常工作的第一信号。正常零件图阈值大概在 100 到 180 之间,如果打出来是 0 或 255,基本就是图读错了或者全黑全白。

3.3 实验对比:大津法 vs 固定阈值

程序里带的实验通常拿同一张图跑三组:固定阈值 127、固定阈值 100、大津法自动阈值。对比结果一般用表格呈现:

方法阈值前景保留背景噪声适用场景
固定阈值127一般较多光照均匀
固定阈值100偏少少偏暗图
大津法自动完整少光照不均

从实验能看出,固定阈值在光照变化时要么丢前景要么留噪声,大津法因为每张图重新算阈值,稳定性明显好一截。这也是标题里说「实验证明很好」的底气所在。不过要注意,大津法假设直方图是双峰的,如果图像前景背景面积悬殊,或者直方图只有一个峰,效果会打折,这时候得考虑局部阈值或者自适应阈值。

4. 避坑与排查:跑大津法常翻的几回车

4.1 阈值算出来是 0 或 255

现象:打印阈值发现是 0 或 255,二值图全黑或全白。原因通常是图像读入失败返回 None,或者图像本身就是纯色。解决:先print(img.shape)确认读入成功,再print(img.min(), img.max())看灰度范围。如果 min 等于 max,说明图没内容,换图。

4.2 彩色图直接喂进去

现象:程序不报错但结果诡异,阈值飘忽。原因:大津法只对单通道有效,三通道图被当成二维数组处理,bincount行为不对。解决:读图时加cv2.IMREAD_GRAYSCALE,或者手动cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)。这一步漏了,后面全白搭。

4.3 直方图单峰导致阈值偏移

现象:前景背景面积差很大时,阈值偏向面积大的一类,小目标被吞掉。原因:大津法基于类间方差,面积悬殊时方差最大点会偏向大块区域。解决:先做 ROI 裁剪,把目标区域框出来再算阈值;或者改用局部大津法,分块计算。

4.4 除零警告

现象:NumPy 报RuntimeWarning: invalid value encountered in divide。原因:ω0 等于 0 或 1 时公式分母为零。解决:加valid掩码,只对 0 < ω0 < 1 的阈值计算,其余置零。上面代码里已经处理了,如果拿到的是旧版本,记得补上。

4.5 输出目录不存在导致保存失败

现象:cv2.imwrite返回 False,但没报错。原因:输出路径的目录不存在,OpenCV 不会自动创建。解决:保存前os.makedirs(os.path.dirname(out_path), exist_ok=True)。这个坑很隐蔽,因为 imwrite 失败不抛异常,只返回 False,不检查返回值根本发现不了。

5. 二次开发:把大津法塞进自己的流水线

5.1 封装成可复用函数

如果要在自己的项目里用,别每次复制粘贴,封装成一个模块:

class OtsuBinarizer: def __init__(self, invert=False): self.invert = invert def __call__(self, gray): t = otsu_threshold(gray) mode = cv2.THRESH_BINARY_INV if self.invert else cv2.THRESH_BINARY _, binary = cv2.threshold(gray, t, 255, mode) return binary, t

这样在流水线里binarizer = OtsuBinarizer(); binary, t = binarizer(gray)就行。invert参数应对前景暗背景亮的场景,不用改代码。

5.2 加局部大津法应对光照不均

全局大津法在光照渐变图上会翻车,常见改造是分块:

def local_otsu(gray, block_size=64): h, w = gray.shape out = np.zeros_like(gray) for y in range(0, h, block_size): for x in range(0, w, block_size): block = gray[y:y+block_size, x:x+block_size] if block.size == 0: continue t = otsu_threshold(block) _, out[y:y+block_size, x:x+block_size] = \ cv2.threshold(block, t, 255, cv2.THRESH_BINARY) return out

block_size是分块大小,太小会引入块状伪影,太大又退化成全局。我一般从 64 开始试,根据图像分辨率调整。如果块内全是背景,阈值会乱,可以加个判断:块内方差低于阈值就跳过,保持原样。

5.3 验证改造效果的方法

改完别凭感觉,拿几张典型图跑一遍,对比全局和局部的二值结果。我习惯做一个简单的指标:前景像素占比。如果局部大津法把前景占比从 5% 拉到 30%,那多半是噪声被误判成前景了。另一个办法是叠加原图看边缘,二值图的前景边界应该和原图目标边界吻合。验证通过再上流水线,别直接替换生产代码。

5.4 一个我踩过的坑

有次我把大津法用在低对比度图上,阈值算出来 128,看着正常,但二值图里目标断成好几截。后来发现是图像本身对比度太低,直方图两个峰几乎粘在一起,类间方差曲线很平,阈值稍微偏一点结果就差很多。从那以后我每次上大津法之前,都强制走一遍直方图可视化,确认双峰明显才用全局阈值,否则直接上局部或者先做对比度拉伸。这个习惯帮我省了不少返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询