一、实验目的
1.实现Canny边缘检测算法
2.对图像使用阈值分割,用人工阈值选取、迭代法、OSTU法
二、实验原理
2.1 Canny边缘检测
Canny边缘检测是一种多级边缘检测算法,旨在精确提取图像中的边缘,同时尽量减少误检和漏检,它是通过逐步的噪声过滤、梯度计算、边缘细化及双阈值策略实现对图像边缘的精确有效提取,是计算机视觉和图像处理领域广泛应用的边缘检测技术。
Canny算法的具体步骤:
去噪:滤波的目的是平滑一些纹理较弱的非边缘区域,以便得到更准确的边缘。一般使用高斯滤波器对图像进行预处理,消除噪声干扰,确保边缘检测的准确性。高斯核大小需权衡去噪效果与边缘保真度,滤波器的核越大,边缘信息对于噪声的敏感度就越低,但同时边缘检测的定位错误也会随之增加,常用的尺寸为5x5。
梯度计算:计算图像中每个像素点的梯度幅度和方向,梯度方向代表边缘走向,梯度幅度反映边缘强度。常用Sobel算子或类似方法实现,分别求得水平和垂直方向的梯度分量Gx、Gy,进而得到梯度幅值和方向(一般就近取八个方向:水平、垂直、对角线)。
非极大值抑制:沿着梯度方向检查每个像素,判断当前像素点是否是周围像素点中具有相同梯度方向的最大值,并根据判断结果决定是否抑制该点(归0),以此细化边缘,排除非边缘点的干扰。
双阈值检测与边缘连接:设定高低两个阈值,将梯度幅度与之比较。高于高阈值的像素被视为强边缘;介于高低阈值之间的像素为候选边缘,需进一步分析其与强边缘的连通性;低于低阈值的像素则舍弃。通过分析候选边缘与确定边缘的连接性(与强边缘相连则保留,否则将其视为弱边缘并抑制),决定最终边缘,确保检测到连续且有意义的边缘,同时排除孤立的噪声点。
2.2 阈值分割
阈值分割技术,作为图像处理领域中最基础且广泛应用的图像分割手段之一,其核心在于精心选定的灰度阈值,用于区分图像中的前景与背景。这一技术的本质是依据像素的灰度级将其划分为两类或多类,假设具有相似灰度值的像素属于同一对象或区域,一般通过直方图分析来识别最佳阈值。
优点:不仅能大幅度减少图像数据的复杂度,便于后续的图像分析与处理,还在于其实现的简便性。这种方法无需复杂的模型构建或迭代优化过程,在对计算资源有限或实时性要求高的应用场景中十分重要。
缺点:存在局限性,比如在目标与背景灰度相互重叠、光照不均或图像质量不佳的情况下,单一阈值往往难以准确分割。
阈值分割局限性——图例
从上图我们可以看出,人物和背景的灰度相似度高,出现了相互重叠的情况,阈值分割不能很好的将人物前景和背景分割开来。
2.2.1 人工阈值分割
人工阈值选取是最直接和基础的阈值确定方法,该方法依据用户经验或对灰度直方图的观察来手动设定固定的阈值T,所有灰度值大于T的像素归为一类,小于等于T的像素归为一类。人工阈值选取简单直观,但是要根据操作者的主观判断,缺乏通用性和自动化能力。
2.2.2 迭代法阈值分割
迭代阈值分割算法是一种自动寻找最佳阈值的方法。该方法是先设定一个初始阈值,然后根据这个阈值将图像二值化,接着计算两类(前景和背景)的平均灰度值,再根据新的平均灰度重新调整阈值,重复这一过程直到阈值收敛(小于某个精度值)。迭代法试图最小化两类之间的灰度差异(有点像K均值聚类,不断地找最优聚类点以最小化类内差异,最大化类间差异),达到一个较好的分割效果。这种方法自动化程度较高,能适应一定程度的图像变化,但可能需要较多迭代次数才能收敛,且对于复杂图像可能找不到理想的分割点。
2.2.3 OSTU法阈值分割
OSTU法又称最大类间方差法,是一种全局自动阈值选择算法,其核心思想是寻找一个阈值,使得两类(前景和背景)的类间方差(即两组像素灰度值的均值之差的平方与各自像素数量的乘积之和)最大化。类间方差越大,说明两类之间的区分度越高,所选阈值越优。OTSU法完全自动,无需人工干预,适用于图像中目标与背景有明显灰度差异的情况。该方法计算效率较高,且分割效果在很多情况下优于固定阈值和简单迭代法。然而,对于类间灰度分布接近或图像噪声较大的情况,OTSU法的表现可能不尽理想。
是所有样本的总数。
是第i类样本的均值
是所有样本的总体均值
三、实验内容
3.1 Canny边缘检测
# Canny边缘检测算法 HT = 200 # 高阈值 LT = 100 # 低阈值 # 高斯模糊 blur_dst = cv2.GaussianBlur(dst, (5, 5), 0.5) # 计算x,y方向梯度,梯度范围大于uint8,用uint16存储 dx = cv2.Sobel(blur_dst, cv2.CV_16S, 1, 0) dy = cv2.Sobel(blur_dst, cv2.CV_16S, 0, 1) # 计算梯度幅值,方向 M = abs(dx) + abs(dy) theta = np.arctan2(dx, dy) # 根据梯度方向进行NMS,结果存入M_supp M_supp = np.zeros(M.shape) for i in range(1, M.shape[0] - 1): for j in range(1, M.shape[1] - 1): x = dx[i, j] y = dy[i, j] angle = theta[i, j] / np.pi mag = M[i, j] # 检查当前幅值是否大于八邻域方向上的两个幅值 # 若是,则保留该局部极大幅值 if abs(angle) <= 1 / 8. or abs(angle) >= 7 / 8.: if mag >= M[i - 1, j] and mag >= M[i + 1, j]: M_supp[i, j] = mag elif abs(angle - 1 / 2.) <= 1 / 8. or abs(angle + 1 / 2.) <= 1 / 8.: if mag >= M[i, j - 1] and mag >= M[i + 1, j + 1]: M_supp[i, j] = mag elif abs(angle - 3 / 4.) <= 1 / 8. or abs(angle + 1 / 4.) <= 1 / 8.: if mag >= M[i + 1, j - 1] and mag >= M[i - 1, j + 1]: M_supp[i, j] = mag else: if mag >= M[i + 1, j + 1] and mag >= M[i - 1, j - 1]: M_supp[i, j] = mag # 对NMS后的幅值及边缘图外侧补0,防止后续递归函数越界 M_supp = np.pad(M_supp, ((1, 1)), 'constant') edge = np.zeros(M_supp.shape, dtype=np.uint8) # 递归函数,从(i,j)点的八邻域找到幅值大于LT的点并设置为边缘 def trace(M_supp, edge, i, j, LT): if edge[i, j] == 0: edge[i, j] = 255 for di, dj in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: if M_supp[i + di, j + dj] >= LT: trace(M_supp, edge, i + di, j + dj, LT) # 枚举NMS后的幅值中每一个点,找到幅值大于HT的边缘点 for i in range(1, M_supp.shape[0] - 1): for j in range(1, M_supp.shape[1] - 1): if M_supp[i, j] >= HT: trace(M_supp, edge, i, j, LT) # 去除外侧补的0 edge = edge[1:-1, 1:-1] # 绘制Canny后的图像 plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) plt.imshow(dst, cmap='gray') plt.title('Gray Image') plt.axis('off') plt.subplot(1, 2, 2) plt.imshow(edge, cmap='gray') plt.title('Canny_self Edge') plt.axis('off') plt.savefig("Origin vs. Canny_self Image.png") plt.show()
# 调用cv2的Canny进行边缘检测 # 读取图片并转换为灰度图 src = cv2.imread(".venv/test1.png") dst = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # 进行Canny边缘检测 edge = cv2.Canny(dst, threshold1=100, threshold2=200) # cv2.imshow("Gray Image", dst) # cv2.imshow("Canny", edge) # cv2.waitKey(0) plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) plt.imshow(dst, cmap='gray') plt.title('Gray Image') plt.axis('off') plt.subplot(1, 2, 2) plt.imshow(edge, cmap='gray') plt.title('Canny Edge') plt.axis('off') plt.savefig("Origin vs. Canny Image.png") plt.show()
Origin vs. Canny Image.png
Origin vs. Canny_self Image.png
该通过上述自编Canny算法和cv的Canny算法得到的边缘检测图像进行对比,标准的Canny边缘检测图像显示了更多的细节和复杂的边缘,捕捉到了如头发丝、面部细节和小的水花等细微特征。自定义的Canny边缘检测图像捕捉到的细节和边缘较少。它更关注主体的主要轮廓和较大的特征,而遗漏了一些在标准 Canny 边缘检测中可见的细节。
标准的 Canny 边缘检测方法提供了比 自定义的Canny边缘检测更详细和清晰的边缘图。自定义的Canny边缘算法捕捉的细节较少,边缘不够明确。这种差异可能是由于两种实现之间在阈值、梯度计算或边缘跟踪方法上的不同造成的。
3.2 人工阈值分割
# 读取图片并转换为灰度图 src = cv2.imread('.venv/test3.png') src = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # 图像的灰度直方图 hist = cv2.calcHist([src], [0], None, [256], [0, 256]) plt.figure() plt.title('Grayscale Histogram') plt.xlabel('Bins') plt.ylabel('# of Pixels') plt.plot(hist) plt.xlim([0, 256]) # 确保x轴范围从0到256 # 保存直方图图像 plt.savefig('histogram_gray.png')
灰度直方图
# 人工选取阈值法 threshold_t = 166 dst_s = src.copy() dst_s[dst_s >= threshold_t] = 255 dst_s[dst_s < threshold_t] = 0# 人工阈值分割 plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) plt.imshow(src, cmap='gray') plt.title('Original Image') plt.axis('off') plt.subplot(1, 2, 2) plt.imshow(dst_s, cmap='gray') plt.title('Arti-Thresholding Image') plt.axis('off') plt.savefig("Origin vs. Arti-Thresholding Image.png") plt.show()
Origin vs. Arti-Thresholding Image.png
3.3 迭代法阈值分割
def iter_image(src, max_iters=100, convergence_threshold=1): """ :param src: 输入图像 :param max_iters: 最大迭代次数 :param convergence_threshold: 收敛阈值,两次迭代差值小于该值结束迭代 :return: 分割后图像 """ image = src.copy() height, width = image.shape threshold = image.mean() for _ in range(max_iters): # 根据阈值划分前景和背景 binary = image > threshold forgeground = image[binary] background = image[~binary] # 计算前景和背景的均值 mean_f = forgeground.mean() if forgeground.size > 0 else 0 mean_b = background.mean() if background.size > 0 else 0 # 更新阈值 new_threshold = (mean_f + mean_b) / 2 # 检测是否达到收敛条件 if abs(new_threshold - threshold) < convergence_threshold: break threshold = new_threshold image[image < threshold] = 0 image[image > threshold] = 255 return image# 迭代法阈值分割 dst_i = iter_image(src) plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) plt.imshow(src, cmap='gray') plt.title('Original Image') plt.axis('off') plt.subplot(1, 2, 2) plt.imshow(dst_i, cmap='gray') plt.title('Iter-Thresholding Image') plt.axis('off') plt.savefig("Origin vs. Iter-Thresholding Image.png") plt.show()
Origin vs. Iter-Thresholding Image.png
3.4 OSTU法阈值分割
def ostu(src): gray_img = src.copy() h = gray_img.shape[0] w = gray_img.shape[1] threshold_g = 0 max_g = 0 # 遍历所有可能阈值 for t in range(255): # 计算g所需变量 n0 = gray_img[np.where(gray_img < t)] n1 = gray_img[np.where(gray_img >= t)] w0 = len(n0) / (h * w) w1 = len(n1) / (h * w) u0 = np.mean(n0) if len(n0) > 0 else 0. u1 = np.mean(n1) if len(n1) > 0 else 0. # 获得当前阈值下的g g = w0 * w1 * (u0 - u1) ** 2 # 判断是否为最优 if g > max_g: max_g = g threshold_g = t # 将最优阈值下的图像二值化,得到分割结果 gray_img[gray_img < threshold_g] = 0 gray_img[gray_img > threshold_g] = 255 cv2.imshow("gray_threshold_ostu", gray_img) cv2.waitKey(0) return gray_img# OSTU阈值分割 dst_o = ostu(src) dst_o = cv2.cvtColor(dst_o, cv2.COLOR_BGR2RGB) plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) plt.imshow(src, cmap='gray') plt.title('Original Image') plt.axis('off') plt.subplot(1, 2, 2) plt.imshow(dst_o, cmap='gray') plt.title('OSTU-Thresholding Image') plt.axis('off') plt.savefig("Origin vs. OSTU-Thresholding Image.png") plt.show()
Origin vs. OSTU-Thresholding Image.png
尽管所有三种阈值方法成功地将人物与背景分离,形成了清晰的前景与背景区域,但它们各有特点。人工阈值方法凭借预设的经验值展现了较优的分割效果。相比之下,OSTU和迭代法更细腻地保留了人物的细节信息。然而,这些额外的细节在二值化过程中可能偶尔导致前后景区分的混淆。综上所述,这三种方法均达到了较为满意的分割成果。
四、实验总结
通过本次实验,我系统地学习和实践了Canny边缘检测和几种阈值分割方法:
Canny边缘检测提供了一种高效的边缘检测技术,能够准确提取图像中的边缘信息。实验中,我通过调节高低阈值,深入理解了边缘的细化和连接过程。
人工阈值分割让我们掌握了手动设定阈值的基本方法,通过观察图片和灰度直方图,我体会到了阈值选择对分割效果的直接影响。
迭代法阈值分割和OSTU法阈值分割则展示了自动化阈值选择的优势,迭代法通过不断优化阈值,提高了分割的准确性,而OSTU法则通过最大化类间方差,自动确定最佳阈值,大大简化了分割过程。通过与人工阈值分割对比,它们具有通用性和自动化的能力,并且分割效果优异。
本次实验通过对灰度图像的边缘检测及阈值处理的实践,让我全面了解了边缘检测算法的原理及其在图像处理中的应用。通过对Canny边缘检测算法的编写,我认识到边缘检测算法的具体实现方式和细节处理,对图像的边缘检测有了更深的认识。其中与标准Canny算法的对比,也让我认识到图像边缘检测可以在细节捕捉上做的更为精细。
此外,通过实践这三种不同的阈值分割技术,我不仅获得了对阈值分割原理的深刻理解,还进一步领悟了如何根据具体情况选择合适的阈值来优化图像的边缘特征提取,总体而言,本次实验不仅提升了我的技术能力,还扩展了我对计算机视觉技术应用的认知。