Otsu算法:从原理到实战,实现图像二值化的自动阈值计算 1. 项目概述从“猜”阈值到“算”阈值的跨越在图像处理的世界里二值化是个绕不开的基础操作。无论是想从一张发票照片里提取文字还是从医学CT影像中分离出病灶区域第一步往往就是把一张灰度图变成非黑即白的二值图。这个“变”的过程核心就是找一个“门槛”——阈值。像素灰度值高于这个门槛的我们让它变成白色255低于的就变成黑色0。听起来很简单对吧但问题来了这个门槛到底该设成多少新手最常见的做法是“拍脑袋”或者“试错法”。比如面对一张光照不均的文档照片你可能先试128中间值发现背景没干净文字也断了再试150背景干净了但文字变细了试100文字是保住了但背景的噪点全出来了。整个过程就像在黑暗中摸索效率低效果还不稳定。尤其是在需要批量处理成千上万张图像或者图像质量参差不齐时手动调阈值简直就是一场噩梦。Otsu算法也叫最大类间方差法或大津算法就是为了终结这场噩梦而生的。它不是一个让你去“设置”阈值的工具而是一个能自动“计算”出最佳阈值的数学方法。它的核心思想极其优雅既然二值化是把像素分成了前景我们感兴趣的目标如文字、物体和背景两类那么最理想的阈值就应该让这两类之间的差异最大同时让每一类内部的像素尽可能相似。用统计学的语言来说就是最大化类间方差。我第一次接触这个算法是在处理一批古文献的扫描件时。文献纸张泛黄、墨迹深浅不一还有各种污渍。手动调阈值调到眼花效果还时好时坏。直到用上Otsu一键处理大部分图片的文字提取效果立刻变得清晰、连贯一下子把我从重复劳动中解放了出来。从此它就成了我图像处理工具箱里的“定海神针”。无论你是做OCR预处理、医学图像分析还是简单的背景分割理解并掌握Otsu算法都能让你从“凭感觉”的初级阶段跃升到“有理论依据”的实践层面。2. 核心原理拆解方差背后的分类哲学要真正用好Otsu算法不能只停留在调用cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)这一行代码上。理解其背后的数学原理不仅能让你在算法失效时知道如何排查更能让你在更复杂的场景下比如多阈值分割举一反三。2.1 从直方图到概率分布Otsu算法的所有计算都基于图像的灰度直方图。假设我们有一张8位灰度图灰度级L2560到255。直方图h[i]就表示灰度值为i的像素有多少个。算法第一步是把这直方图归一化成一个概率分布p(i) h(i) / N 其中N是图像总像素数i 0, 1, ..., L-1。这样p(i)就代表了随机抽取一个像素其灰度值为i的概率。整个图像的平均灰度一阶矩μ_T也就很容易计算μ_T Σ(i * p(i))对i从0到L-1求和。现在我们假设选取一个阈值k它将所有像素分成了两类C0和C1C0: 灰度值在 [0, k] 的像素通常视为背景。C1: 灰度值在 [k1, L-1] 的像素通常视为前景。2.2 类内相似与类间差异的数学表达有了分类我们就可以计算每一类出现的概率权重ω0 P(C0) Σ p(i) i从0到k。 背景类像素占总像素的比例ω1 P(C1) Σ p(i) i从k1到L-1。 前景类像素占总像素的比例 显然ω0 ω1 1。每一类的平均灰度也可以算出μ0 Σ (i * p(i)) / ω0 i从0到k。 背景类的平均灰度μ1 Σ (i * p(i)) / ω1 i从k1到L-1。 前景类的平均灰度整个图像的总平均灰度μ_T其实就是这两类平均灰度的加权和μ_T ω0 * μ0 ω1 * μ1。这个关系在后面推导时会用到。Otsu算法的目标是让两类“分得最开”。在统计学里衡量数据分布离散程度的一个经典指标是方差。这里我们定义类间方差Between-class varianceσ_B²σ_B² ω0 * (μ0 - μ_T)² ω1 * (μ1 - μ_T)²这个公式非常直观它衡量的是两类各自的均值与图像总均值之间的差异的加权平方和。如果背景和前景的灰度值本身相差很大比如白纸黑字那么μ0和μ1就会分别远离μ_T导致σ_B²很大。反之如果图像本身对比度很低前景背景灰度接近μ0和μ1都靠近μ_Tσ_B²就会很小。注意这里有一个非常重要的等价推导。利用μ_T ω0 * μ0 ω1 * μ1的关系我们可以把类间方差公式简化为σ_B² ω0 * ω1 * (μ0 - μ1)²这个形式更简洁计算量也更小。它直接告诉我们最大化类间方差等价于最大化前景与背景两类权重的乘积再乘以两类均值之差的平方。当两类权重相等ω0ω10.5且均值差距最大时σ_B²取得理论最大值。这完美对应了我们的直觉最好的分割应该让前景和背景在“数量”和“灰度值”上都区分明显。2.3 算法步骤与计算优化理解了目标函数σ_B²Otsu算法的步骤就清晰了计算灰度直方图统计图像中每个灰度级的像素个数。归一化直方图得到概率分布p(i)。遍历所有候选阈值k从1到L-20和255作为阈值通常无意义。对于每个k a. 计算累积权重ω0(k)和ω1(k)ω1(k)1-ω0(k)。 b. 计算累积均值μ0(k)和μ1(k)。这里可以利用递推公式高效计算避免每次从头求和。 c. 计算当前k下的类间方差σ_B²(k) ω0(k) * ω1(k) * [μ0(k) - μ1(k)]²。寻找最佳阈值找到使σ_B²(k)最大的那个k值即为Otsu算法计算出的最佳全局阈值。在实际编程中步骤4的递推计算是关键。我们可以维护两个累积量ω0随着k增加每次加上p(k)即可更新。μ0的累积和sum0随着k增加每次加上k * p(k)然后μ0 sum0 / ω0。 这样整个遍历过程的时间复杂度是O(L)对于256个灰度级来说计算速度极快这也是Otsu算法得以广泛应用的原因之一。3. 实战应用与代码解析理论再漂亮也得落地才行。下面我们抛开库函数亲手实现一遍Otsu算法并对比OpenCV等库的实现看看在实际应用中需要注意哪些细节。3.1 从零实现Otsu算法我们用Python和NumPy来手动实现这能帮你彻底吃透计算过程的每一个环节。import numpy as np import cv2 from matplotlib import pyplot as plt def otsu_threshold_manual(image): 手动实现Otsu阈值计算 Args: image: 输入灰度图像 (numpy array) Returns: best_threshold: 计算得到的最佳阈值 between_variances: 所有阈值对应的类间方差列表用于可视化 # 1. 计算灰度直方图 # 假设是8位图像bins为256 hist, bin_edges np.histogram(image.flatten(), bins256, range(0, 256)) # 2. 归一化得到概率分布 pixel_count image.size prob hist / pixel_count # 初始化变量 best_threshold 0 max_variance 0.0 between_variances [] # 记录每个k的方差方便观察 # 总平均灰度 total_mean np.sum(np.arange(256) * prob) # 初始化累积量 omega0 0.0 # C0类的累积概率 sum0 0.0 # C0类的累积灰度值和 # 3. 遍历所有可能的阈值k (1 到 254) for k in range(1, 255): # 递推更新C0类的累积概率和累积灰度值和 omega0 prob[k] sum0 k * prob[k] # 如果omega0为0或1跳过此时一类为空无意义 if omega0 1e-10 or omega0 1 - 1e-10: between_variances.append(0) continue # 计算C0类的平均灰度 mean0 sum0 / omega0 # 计算C1类的概率和平均灰度 omega1 1.0 - omega0 # C1类的累积灰度值和 总灰度值和 - C0类累积灰度值和 # 总灰度值和 total_mean * pixel_count但这里用概率形式更简单 # mean1 (total_mean - omega0 * mean0) / omega1 sum1 total_mean - omega0 * mean0 mean1 sum1 / omega1 # 4. 计算类间方差使用简化公式 variance_b omega0 * omega1 * (mean0 - mean1) ** 2 between_variances.append(variance_b) # 5. 更新最佳阈值 if variance_b max_variance: max_variance variance_b best_threshold k return best_threshold, between_variances # 测试用例 # 生成一张简单的双峰图像深色背景亮色前景 np.random.seed(42) background np.random.normal(70, 10, (200, 300)).astype(np.uint8) # 背景~N(70,10) foreground np.random.normal(180, 10, (100, 200)).astype(np.uint8) # 前景~N(180,10) test_img np.zeros((300, 400), dtypenp.uint8) test_img[50:250, 100:300] foreground # 将前景块放入图像中心 # 计算阈值 thresh_manual, variances otsu_threshold_manual(test_img) print(f手动实现Otsu计算的最佳阈值: {thresh_manual}) # 使用OpenCV的Otsu方法进行验证 thresh_cv2, binary_cv2 cv2.threshold(test_img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) print(fOpenCV Otsu计算的最佳阈值: {thresh_cv2}) # 可视化 fig, axes plt.subplots(2, 2, figsize(12, 10)) axes[0,0].imshow(test_img, cmapgray) axes[0,0].set_title(原始测试图像) axes[0,0].axis(off) axes[0,1].hist(test_img.flatten(), bins50, colorblue, alpha0.7) axes[0,1].axvline(xthresh_manual, colorred, linestyle--, labelfOtsu Threshold{thresh_manual}) axes[0,1].set_xlabel(灰度值) axes[0,1].set_ylabel(像素频数) axes[0,1].set_title(图像直方图与阈值) axes[0,1].legend() axes[1,0].plot(range(1,255), variances, g-) axes[1,0].axvline(xthresh_manual, colorred, linestyle--) axes[1,0].set_xlabel(候选阈值 k) axes[1,0].set_ylabel(类间方差 σ_B²) axes[1,0].set_title(类间方差随阈值变化曲线) axes[1,0].grid(True) # 应用阈值进行二值化 binary_manual np.where(test_img thresh_manual, 255, 0).astype(np.uint8) axes[1,1].imshow(binary_manual, cmapgray) axes[1,1].set_title(f手动二值化结果 (阈值{thresh_manual})) axes[1,1].axis(off) plt.tight_layout() plt.show()运行这段代码你会看到生成的测试图像、其双峰直方图、类间方差随阈值变化的曲线这条曲线通常会有一个明显的峰值以及最终的二值化结果。手动计算的结果应该与OpenCV的结果完全一致或相差1以内因实现细节可能略有不同。3.2 OpenCV中的Otsu与高效使用技巧在实际项目中我们当然不会每次都自己写。OpenCV中的cv2.threshold()函数是标准用法。但直接调用之外有些技巧能让你用得更好import cv2 import numpy as np # 标准用法 img cv2.imread(document.jpg, cv2.IMREAD_GRAYSCALE) # 注意THRESH_OTSU 必须与 THRESH_BINARY 或 THRESH_BINARY_INV 组合使用 thresh_val, binary_img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) print(fOtsu自动计算的阈值: {thresh_val}) # 技巧1预处理的重要性 # Otsu假设直方图是双峰的。如果图像噪声很大直方图峰谷不明显效果会变差。 # 常见的预处理是高斯模糊可以平滑噪声使直方图分布更集中。 img_blurred cv2.GaussianBlur(img, (5,5), 0) thresh_val_blur, binary_img_blur cv2.threshold(img_blurred, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 对比 blur 前后的阈值和效果通常 blur 后的结果更鲁棒。 # 技巧2处理高光或阴影区域局部Otsu # 全局Otsu对光照不均的图像效果不佳。此时可以使用局部自适应阈值如 cv2.adaptiveThreshold。 # 但 adaptiveThreshold 本身不支持 Otsu 方法。一个折中方案是分块处理。 height, width img.shape block_size 64 binary_local np.zeros_like(img) for y in range(0, height, block_size): for x in range(0, width, block_size): block img[y:yblock_size, x:xblock_size] if block.size 0: # 防止边缘块为空 thresh_local, block_binary cv2.threshold(block, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) binary_local[y:yblock_size, x:xblock_size] block_binary # 注意分块处理可能在块边界产生不连续效应后续可能需要形态学操作平滑。 # 技巧3获取并分析直方图 hist cv2.calcHist([img], [0], None, [256], [0,256]) # 可以手动绘制直方图观察是否具有双峰特性预判Otsu效果。实操心得cv2.threshold()的返回值中第一个值thresh_val就是Otsu算法计算出的阈值。这个值对于后续分析非常有用。比如你可以记录一批图像的处理阈值如果某张图的阈值异常偏高或偏低可能意味着这张图本身质量有问题如过曝、欠曝需要进行单独检查或不同的预处理。4. 优势、局限与适用场景分析没有一种算法是万能的Otsu也不例外。清楚它的能力和边界才能把它用在最合适的地方。4.1 核心优势完全自动无需参数这是它最吸引人的地方。你不需要凭经验去猜一个阈值算法基于图像自身的统计特性给出“最优”解。对于批量处理任务自动化程度极高。计算高效算法复杂度是O(L)对于256灰度级计算几乎是瞬时的。即使在资源受限的嵌入式设备如K230这类AIoT芯片上也能实时运行。理论基础扎实基于最大类间方差有明确的数学解释和优化目标结果可解释性强。对双峰直方图图像效果卓越当图像的前景和背景在灰度直方图上能形成两个分离的波峰时Otsu找到的阈值通常就位于波谷处分割效果非常理想。典型的例子就是白纸黑字的文档、在均匀背景下拍摄的物体等。4.2 主要局限性及应对策略Otsu的局限性主要源于其“全局”和“基于直方图形状”的假设。局限性场景问题描述可能的现象应对策略光照不均匀图像不同区域亮度差异大导致全局单一阈值失效。同一物体亮处被分为前景暗处被分为背景。1. 使用局部自适应阈值如cv2.adaptiveThreshold。2. 先进行光照校正如同态滤波、Retinex算法再用Otsu。3. 采用分块Otsu如上一节所示。直方图非双峰前景和背景灰度重叠严重或图像内容复杂直方图呈单峰或多峰。Otsu阈值可能位于错误位置导致大量误分割。1. 增加预处理如对比度拉伸、直方图均衡化尝试分离峰谷。2. 考虑多阈值Otsu将图像分为多类。3. 转向更复杂的算法如基于熵的方法、迭代阈值法或深度学习分割模型如UNet。前景/背景面积悬殊一类像素数量远多于另一类如小目标检测。算法倾向于保护大类小目标可能被忽略。1. 尝试反转图像后再应用Otsu。2. 使用加权Otsu为目标类赋予更高权重。3. 在目标区域使用ROI感兴趣区域单独分割。噪声敏感图像中含有大量椒盐噪声或高斯噪声。直方图被噪声污染波峰波谷模糊不清。1.滤波去噪预处理是关键。中值滤波对椒盐噪声效果好高斯滤波对高斯噪声有效。2. 结合小波阈值去噪等更高级方法预处理图像。4.3 典型应用场景指南根据其特性Otsu算法在以下场景中表现出色文档图像二值化扫描或拍摄的文档、书籍、票据。前提是光照相对均匀纸张背景与文字墨水对比度明显。这是Otsu的“主场”。工业视觉中的简单目标分割在受控的工业环境下产品与传送带背景颜色对比强烈时用于定位产品轮廓。医学图像的初步分割在某些模态的医学图像中如细胞涂片、部分X光片目标与背景对比度较高时可作为快速初筛或预处理步骤。例如在“口腔疾病图像分割系统”或“脊柱侧弯深度学习图像分割”的流程中Otsu可能用于从原始影像中初步分离出感兴趣的大区域作为更精细分割如深度学习模型的输入。遥感图像的土地覆盖分类初探对于简单的植被、水体、建筑分类当不同地物在特定波段灰度差异大时可用Otsu进行快速粗略分割。作为更复杂算法的预处理或组成部分例如在深度学习时代Otsu仍可用于生成训练数据的粗略标签或是在模型推理后对概率图进行二值化处理的一个可选方法。个人经验我经常将Otsu作为一个“基准线”方法。接手一个新的图像分割任务时我会先用Otsu试一下。如果效果不错问题就简单解决了如果效果不好通过分析它失败的原因看直方图、看分割结果我能快速判断问题的症结是光照、噪声还是目标本身与背景难以区分从而指引我选择更合适的进阶方法。它就像一个高效的“诊断工具”。5. 进阶话题与变种算法当你熟练掌握了经典Otsu后可以进一步探索其改进和变种算法以应对更复杂的场景。5.1 多阈值OtsuMulti-level Otsu经典Otsu是二分类的。但有时我们需要将图像分成多个类别比如遥感图像中的水体、植被、裸地。多阈值Otsu将寻找多个阈值使多个类之间的总类间方差最大。假设我们要找M-1个阈值将图像分成M类。目标函数变为最大化总的类间方差σ_B²它是各个类之间方差的和。计算量会随着阈值数量指数级增长穷举搜索。通常采用最大类间方差法的推广公式并利用动态规划或优化算法来求解以避免组合爆炸。在OpenCV中并没有直接的多阈值Otsu函数。你可以使用skimage.filters中的threshold_multiotsu。from skimage import filters, io, color import matplotlib.pyplot as plt # 读取图像并转为灰度 image color.rgb2gray(io.imread(multi_class.jpg)) # 应用多阈值Otsu指定类别数 thresholds filters.threshold_multiotsu(image, classes3) print(f多阈值Otsu计算得到的阈值: {thresholds}) # 根据阈值生成区域 regions np.digitize(image, binsthresholds) # 可视化 plt.imshow(regions, cmapjet) plt.show()5.2 二维Otsu与二维熵阈值法经典Otsu只利用了像素的灰度信息一维直方图。二维Otsu则同时考虑了像素的灰度值和其邻域平均灰度值。每个像素对应一个二维点灰度值邻域均值。算法在二维平面上寻找一个阈值向量将二维直方图划分成4个象限同样以最大化类间方差为目标。二维Otsu对噪声的鲁棒性更强因为噪声像素的灰度值可能异常但其邻域均值可能将其拉回正常范围。但计算量也显著增加。类似地还有二维最大熵阈值法它基于信息论目标是最大化分割后图像的信息量。5.3 Otsu与其他技术的结合在实际的复杂系统中Otsu很少单独使用而是作为流水线中的一环Otsu 形态学操作这是非常经典的组合。Otsu进行初始分割后结果可能包含小孔洞、毛刺或断裂。通过后续的开运算先腐蚀后膨胀可以去除小噪声点闭运算先膨胀后腐蚀可以填充小孔洞、连接断点。import cv2 kernel np.ones((3,3), np.uint8) binary_cleaned cv2.morphologyEx(binary_img, cv2.MORPH_CLOSE, kernel) # 闭运算填充 binary_cleaned cv2.morphologyEx(binary_cleaned, cv2.MORPH_OPEN, kernel) # 开运算去噪Otsu 作为深度学习的前/后处理预处理在标注数据不足时用Otsu生成粗糙的伪标签用于预训练或辅助训练深度学习模型如UNet。后处理深度学习模型如语义分割网络输出的是每个像素属于各类别的概率图。对于二分类问题可以直接对概率图使用Otsu自动确定二值化的阈值这比手动设定一个固定阈值如0.5可能更适应不同的图像。5.4 在资源受限设备上的考量在诸如K230这类边缘AI芯片上部署时需要权衡效果和效率。优势Otsu算法本身计算量小内存占用低主要是一个长度为256的直方图数组非常适合边缘设备。挑战如果图像很大计算整图的直方图仍有开销。可以考虑下采样先将图像缩小在小图上计算Otsu阈值再应用到原图。分块并行计算如果芯片有多个计算核心可以将图像分块各块独立计算阈值适用于光照均匀场景或计算局部直方图后合并。查找表LUT优化对于固定流程可以将部分中间计算结果预先存储。“K230脱机调阈值”的启示这个热词暗示了在嵌入式场景下阈值可能需要动态调整。Otsu的“自动”特性在这里很有价值。你可以设计一个系统定期如每天/每次启动用Otsu计算当前环境下的最佳阈值并更新到系统中实现自适应的“脱机调参”避免因环境光变化导致固定阈值失效。6. 常见问题与调试技巧实录即使理解了原理在实际编码和调试中还是会遇到各种问题。下面是我踩过的一些坑和总结的排查思路。6.1 问题排查速查表问题现象可能原因排查步骤与解决方案计算出的阈值明显不合理如全黑/全白图的阈值是0或2551. 图像本身对比度极低直方图几乎单峰。2. 代码实现有误如直方图计算范围错误、方差计算溢出。1.可视化直方图plt.hist(img.flatten(), bins50)。检查是否无双峰特征。2.检查输入图像确认是单通道灰度图img.shape为(H,W)。如果是三通道先转换cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)。3.调试代码在手动实现的循环中打印出每个k对应的ω0, ω1, μ0, μ1检查是否有异常值如除零。Otsu分割结果噪声很多图像本身噪声大导致直方图在波谷处不干净。1.预处理滤波先对原图进行高斯模糊cv2.GaussianBlur(img, (5,5), 0)或中值滤波cv2.medianBlur(img, 5)。2.观察滤波后的直方图看波峰波谷是否更清晰。目标物体部分丢失或背景分割不干净1. 光照不均。2. 前景/背景内部灰度不均匀。3. 目标与背景灰度有重叠。1.尝试局部阈值换用cv2.adaptiveThreshold。2.尝试色彩空间如果目标是彩色的尝试在HSV的V通道或Lab的L通道应用Otsu可能比灰度图效果更好。3.结合边缘信息可以先做Canny边缘检测再结合Otsu结果进行逻辑运算。OpenCV的cv2.threshold返回的阈值是0调用方式错误未正确组合标志。正确写法retval, binary cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)。注意THRESH_OTSU是一个标志需要与THRESH_BINARY或THRESH_BINARY_INV用按位或(|)组合。传入的阈值参数此处是0会被忽略。处理速度慢图像分辨率过高。1.缩放图像对于实时性要求高的场景先将图像缩放到固定大小如640x480再处理。2.检查直方图计算cv2.calcHist对于大图可能慢确保只对灰度图操作。对于超大规模图像可以随机采样像素点来估算直方图。6.2 调试与可视化技巧直方图是灵魂任何时候对Otsu效果有疑问第一件事就是画出图像的灰度直方图并把你计算出的阈值用竖线标在图上。一眼就能看出阈值是否位于预期的波谷位置。plt.figure(figsize(10,4)) plt.subplot(121) plt.imshow(img, cmapgray) plt.title(Original Image) plt.axis(off) plt.subplot(122) plt.hist(img.ravel(), bins256, range(0,256), colorblue, alpha0.7) plt.axvline(xthresh_val, colorred, linestyle--, linewidth2, labelfOTSU Thresh{thresh_val:.1f}) plt.xlabel(Pixel Intensity) plt.ylabel(Frequency) plt.title(Grayscale Histogram) plt.legend() plt.tight_layout() plt.show()类间方差曲线在手动实现时把每个候选阈值k对应的类间方差σ_B²(k)画出来。一个健康的、适合Otsu的图像这条曲线应该有一个尖锐的、唯一的峰值。如果曲线平坦或有多个峰值说明Otsu可能不是最佳选择或者需要预处理。分块测试对于怀疑光照不均的图片可以手动将图片分成4x4的网格对每个小块分别用Otsu计算阈值并二值化然后拼回。观察不同块的阈值差异。如果差异很大比如超过50那就证实了光照不均的问题必须采用局部方法。6.3 一个综合案例处理光照不均的名片图像假设我们有一张在侧面光下拍摄的名片照片左边亮右边暗。import cv2 import numpy as np import matplotlib.pyplot as plt # 模拟或读取一张光照不均的图像 # 这里用梯度模拟光照不均 height, width 400, 600 gradient np.tile(np.linspace(0.5, 1.2, width), (height, 1)) # 从左到右变亮 uniform_bg np.ones((height, width)) * 120 text_region np.random.randint(30, 60, (height, width)) # 创建名片背景深色文字 img_simulated (uniform_bg - text_region * (text_region 50)).astype(np.uint8) # 添加光照梯度 img_uneven np.clip(img_simulated * gradient, 0, 255).astype(np.uint8) # 添加一些文字图案简单模拟 cv2.putText(img_uneven, NAME, (150, 100), cv2.FONT_HERSHEY_SIMPLEX, 2, 80, 3) cv2.putText(img_uneven, TEL: 123-4567, (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, 60, 2) # 方法1直接全局Otsu会失败 thresh_global, binary_global cv2.threshold(img_uneven, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 方法2高斯模糊后全局Otsu img_blur cv2.GaussianBlur(img_uneven, (5,5), 0) thresh_blur, binary_blur cv2.threshold(img_blur, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 方法3局部自适应阈值高斯加权 binary_adaptive cv2.adaptiveThreshold(img_uneven, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 块大小11常数2 # 可视化比较 fig, axes plt.subplots(2, 3, figsize(15, 10)) axes[0,0].imshow(img_uneven, cmapgray) axes[0,0].set_title(Original (Uneven Illumination)) axes[0,0].axis(off) axes[0,1].hist(img_uneven.ravel(), 256, [0,256], colorblue) axes[0,1].axvline(thresh_global, colorred, linestyle--) axes[0,1].set_title(fGlobal Histogram\nOtsu Thresh{thresh_global:.0f}) axes[0,2].imshow(binary_global, cmapgray) axes[0,2].set_title(Global Otsu Result\n(Poor)) axes[0,2].axis(off) axes[1,0].imshow(binary_blur, cmapgray) axes[1,0].set_title(Otsu after Gaussian Blur\n(Slightly Better)) axes[1,0].axis(off) axes[1,1].imshow(binary_adaptive, cmapgray) axes[1,1].set_title(Adaptive Thresholding\n(Best for this case)) axes[1,1].axis(off) # 方法4尝试分块Otsu简易版 binary_block np.zeros_like(img_uneven) block_h, block_w 100, 100 for y in range(0, height, block_h): for x in range(0, width, block_w): block img_uneven[y:yblock_h, x:xblock_w] if block.size 0: t, b cv2.threshold(block, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) binary_block[y:yblock_h, x:xblock_w] b axes[1,2].imshow(binary_block, cmapgray) axes[1,2].set_title(Block-wise Otsu\n(Boundary Artifacts)) axes[1,2].axis(off) plt.tight_layout() plt.show()运行这段代码你会清晰地看到全局Otsu由于光照不均导致直方图严重偏离双峰计算出的阈值无法兼顾亮部和暗部结果要么左边文字消失要么右边背景变黑。模糊后Otsu略有改善但本质问题未解决。局部自适应阈值效果最好因为它为每个像素点根据其邻域单独计算阈值完美适应了光照变化。分块Otsu效果介于两者之间但在块边界处会产生明显的“棋盘格”效应。这个案例直观地告诉我们Otsu不是银弹理解其适用前提全局、双峰直方图比盲目调用更重要。当它失效时分析直方图和图像特性才能找到正确的解决路径。