3. 图像预处理与叶片区域提取
病斑分割算法真正处理的并不是“叶片”,而是整幅田间图像。图像里往往同时存在土壤、地膜、杂草、阴影、滴灌带、手指甚至相邻叶片,病斑像素只占很小一部分。如果直接对原图做阈值分割或聚类,背景中的黄褐色土壤很容易被误判为病斑,杂草的绿色也会混入叶片区域。因此,在进入病斑分割之前,必须先完成一套稳定的预处理链:把彩色图像转换到更适合分离颜色信息的色彩空间,去除与叶片无关的背景,提取完整叶片轮廓,用形态学操作修补缺陷,最后裁剪出只包含叶片的感兴趣区域(ROI)。这一节按这条处理链逐步展开,并给出可复现的参数示例。
色彩空间转换:为“绿色”和“病斑”找到合适的坐标
RGB 三个通道高度相关,受光照强度影响大:同一片叶子在阳光下和阴影中,R、G、B 数值可以相差几十甚至上百,但颜色本质没变。因此预处理的第一步通常是转换到对亮度不敏感的色彩空间。常用选择有三类:HSV 把颜色分解为色调 H、饱和度 S、明度 V,适合用 H 通道圈定绿色叶片;Lab 空间用 a、b 两个色度通道描述“绿—红”“蓝—黄”方向,其中 a 通道负值代表绿色,对叶片与土壤的区分很有效;ExG(超绿指数)则是面向植被的简化指数,计算式为 ExG = 2G − R − B,数值越大越偏向植被。
以 OpenCV 实现为例,读入图像后先做一次轻度高斯滤波抑制噪点,再分别得到 HSV 与 Lab:
import cv2, numpy as np
img = cv2.imread("leaf_01.jpg")
img = cv2.GaussianBlur(img, (5, 5), 0)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
b, g, r = cv2.split(img.astype(np.int16))
exg = 2 * g - r - b # 超绿指数,范围约 -510 ~ 510
exg = np.clip(exg, 0, 255).astype(np.uint8)
需要注意 OpenCV 中 H 通道的取值范围是 0~179(对应 0~360°),S、V 为 0~255。绿色在 H 上大致落在 35~85 区间,但不同作物、不同白平衡下会有偏移,实际使用时应先在样本图上用鼠标读取叶片与背景的 H 值,再确定阈值,而不是照搬固定数值。
背景去除与叶片轮廓分割
得到合适的通道后,背景去除本质上是一个二值化问题。对 ExG 图或 Lab 的 a 通道做 Otsu 自动阈值,可以得到“植被/非植被”的粗略掩膜;对 HSV 则常用区间阈值 cv2.inRange 圈定绿色。两种方式各有短板:Otsu 在背景与叶片亮度接近时会失效,固定 H 区间在偏黄老叶上会漏检。实践中可以把两者取交集或并集,再用面积筛选剔除小连通域。
二值掩膜往往并不干净:叶缘有锯齿、叶面有反光孔洞、叶柄处可能断裂。此时用形态学开运算去除细小噪点,用闭运算填补孔洞,是成本最低的修补手段。核尺寸要与图像分辨率匹配——例如 2000×1500 像素的手机照片,叶片宽度约 800 像素,用 5×5 或 7×7 的椭圆核比较合适;核过大(如 25×25)会把相邻叶片粘连,反而破坏轮廓。
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = [c for c in cnts if cv2.contourArea(c) > 5000]
leaf = max(cnts, key=cv2.contourArea) # 取最大连通域作为主叶片
这里有两个判断依据值得强调。第一,面积阈值 5000 像素只是示例,应按实际分辨率换算成“最小可接受叶片面积”,例如要求叶片至少占画面 2%。第二,取最大连通域适用于单叶平铺拍摄;若图像中有多片叶子,应保留全部满足面积条件的轮廓,并分别建立 ROI,而不是简单取最大者。
形态学处理与轮廓精修
开闭运算之后,轮廓仍可能存在凹陷或毛刺。若后续要计算叶面积,轮廓的凹凸会直接影响结果,因此常再做一次轮廓平滑:用 cv2.approxPolyDP 做多边形逼近,或对轮廓点做滑动平均。另一种做法是先填充轮廓内部得到实心掩膜,再与原始掩膜求交,从而消除内部孔洞而不改变外缘。对于叶柄细长、容易断裂的情况,可先做一次小核闭运算连接,再提取轮廓。
需要提醒的是,形态学操作会系统性地改变叶片边界:闭运算使轮廓外扩,开运算使轮廓内缩。若面积测量精度要求较高(例如误差需控制在 3% 以内),应记录所用核尺寸与迭代次数,并在标定环节用已知面积的标准圆片验证偏差方向与大小,必要时对结果做补偿。
感兴趣区域裁剪与掩膜输出
最后一步是把叶片从整幅图中“抠”出来。先由轮廓计算外接矩形,再向外扩若干像素作为边距,避免叶缘被切掉:
x, y, w, h = cv2.boundingRect(leaf)
pad = 10
x0, y0 = max(x - pad, 0), max(y - pad, 0)
x1 = min(x + w + pad, img.shape[1])
y1 = min(y + h + pad, img.shape[0])
roi_img = img[y0:y1, x0:x1]
roi_mask = np.zeros(img.shape[:2], np.uint8)
cv2.drawContours(roi_mask, [leaf], -1, 255, -1)
roi_mask = roi_mask[y0:y1, x0:x1]
裁剪后应同时保留两样东西:ROI 彩色图与 ROI 二值掩膜。后续病斑分割只在掩膜内部进行,掩膜外的像素一律置零,这样土壤和杂草就不会再干扰病斑判断。裁剪坐标 (x0, y0) 也要记录,以便把 ROI 中的病斑面积换算回原图坐标系,或与多期图像对齐。
把上述步骤串起来,一条完整的预处理函数应当返回 ROI 图像、ROI 掩膜、裁剪偏移和轮廓点集四项结果。这四项分别服务于后续的病斑分割、面积统计、坐标还原和形状分析。参数不要写死在代码里,而应集中为配置字典,便于针对不同作物、不同拍摄设备批量调整。例如水稻叶片细长、小麦叶片窄,形态学核尺寸和最小面积阈值都应相应减小;而手机近景拍摄的烟草叶片宽大,核尺寸可适当增大。
完成本节处理后,得到的 ROI 掩膜已经把问题从“整幅田间图像”缩小到“一片叶子的内部”。接下来的病斑分割只需在这个受限区域内区分健康组织与病斑组织,背景干扰被大幅削弱,阈值和分类器的稳定性都会明显提高。建议在正式处理批量图像前,先抽取 5~10 张涵盖晴天、阴天、不同叶龄的样本,逐一检查掩膜质量并记录参数,形成本批次图像的预处理配置,再统一应用到全部图像上。