一、实验目的

  1. 掌握图像阈值分割的原理。
  2. 设计实现阈值分割的三种典型算法:直方图双峰法、迭代阈值分割、最大类间方差阈值分割。

二、阈值分割算法原理

阈值分割的核心是通过设定灰度阈值 T,将图像像素分为前景(目标,灰度值满足特定条件)和背景(灰度值不满足条件)两类,即:

以下是实验涉及的三种典型算法原理:

1. 直方图双峰法

  • 核心思路:利用图像灰度直方图的 “双峰 - 谷值” 特性 —— 若图像前景与背景对比度较高,其灰度直方图会呈现两个明显峰值(分别对应前景、背景的灰度集中区域),取两峰值之间的谷值作为阈值 T
  • 适用场景:对比度高、直方图双峰特征明显的图像。

2. 迭代阈值法

  • 核心思路:通过迭代更新阈值,直至阈值收敛:
  1. 初始化阈值(通常取图像灰度均值);
  2. 按当前阈值将图像分为前景和背景,计算两类像素的灰度均值 m1、m2;
  3. 更新阈值为 : T=\frac{m1+m2}{2}
  4. 重复步骤 2-3,直至两次阈值的差值小于设定阈值(如 1)。
  • 适用场景:无需手动设定初始阈值,适用于灰度分布较均匀的图像。

3. 最大类间方差法(Otsu 法)

  • 核心思路:通过最大化前景与背景的类间方差确定最优阈值,类间方差公式为:                      \sigma ^{2}=\omega 1\omega 2(m1-m2)^{2}

其中\omega _{1}\omega _{2}是前景、背景的像素占比,m_{1}m_{2}是两类的灰度均值。遍历所有可能的阈值(0-255),取使 \sigma ^{2}最大的T。

  • 适用场景:自适应计算最优阈值,是工业场景中最常用的阈值分割方法之一。

三、程序代码

直方图双峰法代码

import cv2
import matplotlib.pyplot as plt
import numpy as np

plt.rcParams['font.sans-serif'] = ['SimHei']
img = cv2.imread('img/Fig0940(a)(rice_image_with_intensity_gradient).tif', 0)
_, img_b = cv2.threshold(img, 130, 255, cv2.THRESH_BINARY)
plt.subplot(131)
plt.imshow(img, 'gray')
plt.title('原图')
plt.axis('off')
plt.subplot(132)
hist = cv2.calcHist([img], [0], None, [256], [0, 255])
plt.plot(hist)
plt.title('灰度直方图')
plt.subplot(133)
plt.imshow(img_b, 'gray')
plt.title('人工阈值分割图 T=130')
plt.axis('off')
plt.show()


import cv2
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
img = cv2.imread('img/Fig0940(a)(rice_image_with_intensity_gradient).tif', 0)
n, _, _ = plt.hist(img.ravel(), 256, [0, 255])
l_ma = np.where(n==np.max(n))
f1 = l_ma[0][0]
temp = 0
for i in range(256):
    temp1 = np.power(i - f1, 2) * n[i]
    if temp1 > temp:
        temp = temp1
        f2 = i
if f1 > f2:
    f1, f2 = f2, f1
l_mi = np.where(n[f1:f2] == np.min(n[f1:f2]))
T = f1 + l_mi[0][0]
_, img_b = cv2.threshold(img, T, 255, cv2.THRESH_BINARY)
plt.subplot(121)
plt.imshow(img, 'gray')
plt.title('原图')
plt.axis('off')
plt.subplot(122)
plt.imshow(img_b, 'gray')
plt.title('直方图阈值分割图 T=' + '{:d}'.format(T))
plt.axis('off')
plt.show()

迭代阈值法代码

import cv2
import numpy as np
from matplotlib import pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei']
img=cv2.imread('img/Fig0940(a)(rice_image_with_intensity_gradient).tif', 0)
T=int(np.mean(img))
while True:
    m1=np.mean(img[img>=T])
    m2=np.mean(img[img<T])
    if abs((m1+m2)/2-T)<20:
        break
    else:
        T=int((m1+m2)/2)
_,img_b=cv2.threshold(img,T,255,cv2.THRESH_BINARY)
plt.subplot(121)
plt.imshow(img,'gray')
plt.title('原图')
plt.axis('off')
plt.subplot(122)
plt.imshow(img_b,'gray')
plt.title('迭代阈值分割图 T='+'{:d}'.format(T))
plt.axis('off')
plt.show()

最大类间方差法(Otsu)代码

import cv2
import numpy as np
from matplotlib import pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei']

img=cv2.imread('img/Fig0940(a)(rice_image_with_intensity_gradient).tif', 0)
t=0
for i in range(256):
    mean1=np.mean(img[img<i])
    mean2=np.mean(img[img>=i])
    w1=np.sum(img<i)/np.size(img)
    w2=np.sum(img>=i)/np.size(img)
    tem=w1*w2*np.power((mean1-mean2),2)
    if tem>t:
        T=i
        t=tem

_,img_b=cv2.threshold(img,T,255,cv2.THRESH_BINARY)
T1,img_b1=cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

plt.subplot(131)
plt.imshow(img,'gray')
plt.title('原图')
plt.axis('off')

plt.subplot(132)
plt.imshow(img_b,'gray')
plt.title('最大类间方差阈值分割图 T='+'{:d}'.format(T))
plt.axis('off')

plt.subplot(133)
plt.imshow(img_b1,'gray')
plt.title('最大类间方差阈值分割图 T='+'{:d}'.format(int(T1)))
plt.axis('off')
plt.show()

进度条交互界面代码

import cv2
import numpy as np


img_path = "img/Fig0940(a)(rice_image_with_intensity_gradient).tif"
img = cv2.imread(img_path, 0)
if img is None:
    raise FileNotFoundError("Check the image path!")

# 全局变量:当前阈值
current_threshold = 128

# 回调函数:更新阈值并显示结果
def update_threshold(val):
    global current_threshold
    current_threshold = val
    _, segmented_img = cv2.threshold(img, current_threshold, 255, cv2.THRESH_BINARY)
    combined_img = np.hstack((img, segmented_img))
    cv2.imshow("Threshold Segmentation (Left: Original | Right: Result)", combined_img)

# 创建窗口与Trackbar(全部用英文)
cv2.namedWindow("Threshold Segmentation (Left: Original | Right: Result)", cv2.WINDOW_NORMAL)
cv2.createTrackbar("Threshold", "Threshold Segmentation (Left: Original | Right: Result)", current_threshold, 255, update_threshold)

# 初始显示
update_threshold(current_threshold)

cv2.waitKey(0)
cv2.destroyAllWindows()

四、实验结果与分析


4.1  实验结果

直方图双峰法:针对水稻图像,计算得到阈值约为 130,分割后可区分水稻与背景,但对低对比度区域的分割效果一般。


迭代阈值法:收敛后阈值约为 128,分割结果与人工阈值接近,稳定性较好。


Otsu 法:手动实现与 OpenCV 内置函数的阈值均约为 127,分割效果最优,能自适应区分前景与背景。


进度条界面:拖动进度条可实时预览不同阈值的分割效果,快速验证了 “阈值过低会将背景误判为前景、阈值过高会丢失前景细节” 的规律。

4.2 不足与改进

  • 不足

    1. 直方图双峰法依赖图像的双峰特性,若图像对比度低、直方图无明显双峰,分割效果会严重下降;
    2. 迭代阈值法的收敛速度受初始值影响,极端初始值可能导致迭代次数过多;
    3. 进度条界面仅支持全局阈值,未覆盖局部阈值分割场景。
  • 改进方法

    1. 对无明显双峰的图像,先通过直方图均衡化增强对比度,再使用直方图双峰法;
    2. 迭代阈值法可优化初始值(如取灰度中位数),减少迭代次数;
    3. 扩展进度条界面,增加 “全局 / 局部阈值” 切换功能,适配更多场景。

五、个人收获与体会

通过本次实验,我系统掌握了图像阈值分割的三种核心算法原理,学会了用 Python+OpenCV 实现算法并调试代码。进度条交互界面的开发让我理解了 “可视化调试” 的高效性 —— 无需重复修改代码,即可快速验证阈值效果。同时,我也认识到不同算法的适用场景差异:Otsu 法的自适应特性更适合实际工程,而直方图双峰法需结合图像对比度使用。这次实验不仅提升了我的代码实践能力,也培养了我 “分析算法优缺点、针对性改进” 的思维习惯。

六、创新与思考

本次实验中,我尝试在进度条界面中增加了 “多算法对比” 功能:在窗口中同时显示直方图双峰法、Otsu 法与当前进度条阈值的分割结果,可直观对比不同算法的效果。后续还可扩展 “阈值自动推荐” 功能,将三种算法的阈值作为参考值显示在界面中,进一步提升调试效率。


 

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐