图像分割基础:阈值分割、边缘检测与形态学操作

各位同学,欢迎来到第四讲。今天我们要聊的,是叶片表面缺陷自动标注里最核心的几把“手术刀”——图像分割。说白了,就是把缺陷从背景里“切”出来。

我刚开始做视觉项目那会儿,总觉得深度学习万能。后来发现,很多工业场景下,传统图像分割方法反而更稳、更快。尤其是叶片这种高反光、纹理复杂的表面,纯靠深度学习容易过拟合。嗯,今天咱们就把基础打牢。

核心逻辑:图像分割 = 找到目标区域 + 剔除干扰 + 提取特征。阈值分割负责“粗切”,边缘检测负责“精修”,形态学操作负责“善后”。

图像分割知识体系 阈值分割 全局阈值 / 自适应阈值 边缘检测 Sobel / Canny 形态学操作 腐蚀 / 膨胀 / 开闭运算 应用场景:叶片划痕分割 → 边缘提取 → 缺陷区域填充 → 最终标注 ① 粗分割 ② 精定位 ③ 后处理 最终输出:二值掩码(Mask)

一、阈值分割:把灰度图变成黑白图

阈值分割,是所有分割方法里最直观的。你设定一个灰度值T,大于T的算一类,小于T的算另一类。就这么简单。

但实际项目中,难点在于“T怎么选”。我记得有一次做叶片裂纹检测,裂纹和背景的灰度差只有10个像素值。全局阈值死活分不开。后来换了自适应阈值,才把裂纹“揪”出来。

1.1 全局阈值

适合光照均匀的场景。比如叶片在标准光源下,背景是纯色。

import cv2
import numpy as np

img = cv2.imread('leaf_defect.jpg', 0)
# 全局阈值,T=127
ret, thresh = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
cv2.imshow('Global Threshold', thresh)

我的习惯:先用直方图看看灰度分布。如果双峰明显,全局阈值就够用。如果单峰或者平缓,赶紧换自适应。

1.2 自适应阈值

说白了,就是让算法根据局部区域自动算T。光照不均的叶片表面,这招特别好使。

# 自适应阈值,blockSize=11,C=2
adaptive_thresh = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY, 11, 2
)
cv2.imshow('Adaptive Threshold', adaptive_thresh)

避坑指南:blockSize必须是奇数。C值越大,分割越“松”。我曾经设C=10,结果把背景噪声全当成了缺陷。后来调小到2,效果才正常。

二、边缘检测:找到缺陷的“轮廓”

阈值分割只能得到区域,但缺陷的形状、边界信息,还得靠边缘检测。你想想看,叶片上的划痕、凹坑,本质上都是灰度突变的地方。边缘检测就是抓这些突变。

2.1 Sobel算子

Sobel算子是基础的一阶导数算子。它计算x方向和y方向的梯度,然后合成。优点是快,缺点是对噪声敏感。

# Sobel边缘检测
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
sobel_combined = cv2.magnitude(sobel_x, sobel_y)
sobel_combined = np.uint8(np.clip(sobel_combined, 0, 255))
cv2.imshow('Sobel Edge', sobel_combined)

经验之谈:ksize=3最常用。ksize=5会丢失细节,但噪声更少。我一般先试3,如果噪声太多再换5。

2.2 Canny边缘检测

Canny是工业界的老大哥。它做了非极大值抑制和双阈值处理,边缘更干净、更连续。我个人习惯,只要不是实时性要求极高,都用Canny。

# Canny边缘检测,阈值50和150
edges = cv2.Canny(img, 50, 150)
cv2.imshow('Canny Edge', edges)

为什么Canny效果好?因为它先平滑去噪,再找梯度,最后用两个阈值筛选。低阈值以下的不要,高阈值以上的保留,中间的看是否连接。嗯,这个逻辑很聪明。

调参技巧:低阈值:高阈值 ≈ 1:2 或 1:3。比如(30, 90)或(50, 150)。我遇到过叶片表面纹理很密,阈值设低了全是纹理,设高了又漏掉缺陷。后来用(40, 120)才平衡。

三、形态学操作:给分割结果“美容”

分割完的图像,往往有毛刺、空洞、孤立点。形态学操作就是用来处理这些“瑕疵”的。说白了,就是腐蚀、膨胀、开运算、闭运算这四板斧。

操作 作用 典型场景
腐蚀 缩小前景区域,去除小噪点 去掉叶片上的孤立亮点
膨胀 扩大前景区域,填充小空洞 连接断裂的缺陷边缘
开运算 先腐蚀后膨胀,去除毛刺 平滑缺陷边界
闭运算 先膨胀后腐蚀,填充空洞 填补缺陷内部的孔洞
# 形态学操作示例
kernel = np.ones((5,5), np.uint8)

# 腐蚀
erosion = cv2.erode(thresh, kernel, iterations=1)
# 膨胀
dilation = cv2.dilate(thresh, kernel, iterations=1)
# 开运算
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
# 闭运算
closing = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)

注意:kernel大小和迭代次数别贪多。我曾经为了去噪,迭代了5次腐蚀,结果缺陷本身也被“吃”掉了。一般1-2次就够了。

四、实战组合拳:叶片缺陷分割流水线

好了,咱们把今天学的串起来。一个典型的叶片缺陷自动标注流程是这样的:

  1. 灰度化:把彩色图转成单通道,减少计算量。
  2. 自适应阈值分割:得到缺陷的粗分割区域。
  3. Canny边缘检测:提取缺陷的精确边界。
  4. 形态学闭运算:填充边缘内部的空洞,让缺陷区域连续。
  5. 轮廓提取:用cv2.findContours拿到缺陷的轮廓坐标,用于后续标注。
# 完整流水线示例
def leaf_defect_segmentation(image_path):
    img = cv2.imread(image_path, 0)
    # 1. 自适应阈值
    thresh = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY_INV, 11, 2)
    # 2. Canny边缘
    edges = cv2.Canny(thresh, 30, 90)
    # 3. 闭运算填充
    kernel = np.ones((3,3), np.uint8)
    closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations=2)
    # 4. 轮廓提取
    contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    return contours

核心思路:阈值分割做“粗定位”,边缘检测做“精修”,形态学做“善后”。三步走,缺一不可。

嗯,今天的内容就到这儿。这些方法看着基础,但我在实际项目中,80%的叶片缺陷都能用这套组合拳搞定。剩下的20%,才需要上深度学习。所以,别小看传统方法。


公众号:蓝海资料掘金营,微信deep3321