4. 图像预处理与增强实战

苗木航拍或近景图像在进入计数与密度分析之前,往往存在三类干扰:光照不均(树冠阴影、逆光、云影)、噪声(传感器噪声、压缩伪影、风摆模糊)以及对比度不足(阴天、过曝导致冠层与背景灰度接近)。本节围绕一条可复现的预处理流水线展开,目标不是"把图变好看",而是让冠层与背景、苗木与裸土之间的可分性稳定提升,为后续分割、检测或密度估计提供一致输入。

一、先定顺序:为什么流水线不是随意堆叠

预处理各步骤之间存在明确的先后依赖。去噪应放在锐化之前,否则锐化会放大噪声;色彩空间转换应在光照校正之前完成,因为多数光照校正方法在亮度通道上操作更合理;CLAHE 应放在光照校正之后,否则局部均衡会把光照梯度"固化"进对比度里。数据增强则必须放在整条确定性预处理链之后,只对训练样本施加随机变换,验证集与推理输入保持确定性处理。一个稳妥的顺序是:色彩空间转换 → 去噪 → 自适应光照校正 → CLAHE → 锐化 → (仅训练)数据增强。

确定性预处理链(训练 / 验证 / 推理共用) 原始图像 BGR / RGB 色彩空间转换 BGR→LAB 取 L 通道 去噪 双边滤波 d=7, σ=50 自适应光照校正 大核高斯背景 L − 背景 + 128 CLAHE clip=2.0 tile=8×8 锐化 USM amount=1.2 数据增强(仅训练集) 随机翻转 / 旋转 ±15° 亮度抖动 ±10% / 随机裁剪 统一输入张量 归一化 → 分割 / 检测 / 密度估计 验证集 / 推理路径 跳过数据增强,其余五步参数完全一致; 保证训练与部署的输入分布对齐,避免"训练好看、上线漂移"。 关键参数与作用 • 双边滤波:保边去噪,σ_color 过大将抹平冠层边缘 • 光照校正核:应大于树冠直径,否则背景估计"吃掉"冠层 • CLAHE clipLimit:过高放大噪声,过低等于未均衡

二、色彩空间转换:把"亮度"单独拿出来

OpenCV 默认读入为 BGR。直接对三通道做均衡或校正,会同时改变色相,导致绿色冠层偏色。更合理的做法是转到 LAB 或 HSV,只处理亮度分量。LAB 的 L 通道与人眼亮度感知近似线性,适合做光照校正与 CLAHE;a、b 通道保留色彩信息,处理完再合并回 BGR。示例代码如下(参数为教学示例值):

import cv2, numpy as np

img = cv2.imread("seedling.jpg")            # BGR
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
L, a, b = cv2.split(lab)                    # L: 0~255 (uint8)

需要注意 uint8 的 L 通道在后续做减法时容易下溢截断,建议先转 float32 再运算,最后裁剪回 0~255。

三、去噪:保边比"去干净"更重要

苗木图像的关键结构是冠层轮廓与苗干,高斯滤波会把细枝和窄冠一起糊掉。实践中优先用双边滤波:cv2.bilateralFilter(L, d=7, sigmaColor=50, sigmaSpace=50)。d 为邻域直径,sigmaColor 控制颜色相似度容忍度,sigmaSpace 控制空间距离权重。若图像噪声以椒盐为主(少见),可先用 3×3 中值滤波;若为压缩块效应,可考虑非局部均值 fastNlMeansDenoising,但耗时明显上升,需按批量规模权衡。

四、自适应光照校正:用"大核背景"估计光照场

光照不均的本质是低频亮度偏移。做法是用一个远大于树冠尺寸的高斯核估计背景光照场,再从原图中减去:

bg = cv2.GaussianBlur(Lf, (0, 0), sigmaX=61)   # 大核背景估计
Lc = Lf - bg + 128.0                            # 平移到中灰
Lc = np.clip(Lc, 0, 255).astype(np.uint8)

sigmaX 的选取是关键:应大于最大树冠半径,使背景估计只保留缓变光照而"看不见"树冠。若 sigmaX 偏小,树冠会被当作光照背景减掉,冠层中心变暗、边缘发亮,形成"光晕"伪影。判断方法很直接:校正后观察同一树冠内部亮度是否均匀,若出现明显亮环,说明核太小,应增大 sigmaX。对逆光场景,也可用形态学闭运算(大结构元)估计背景,效果类似但边缘更硬。

五、CLAHE:局部对比度,但要防噪声放大

全局直方图均衡对光照不均图像往往无效,CLAHE 将图像分块(如 8×8)分别均衡并限制对比度增幅,再用双线性插值消除块间边界。调用方式:

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
L_eq = clahe.apply(Lc)

clipLimit 是核心参数:值越大对比度越强,但噪声与纹理也被同步放大,裸土颗粒可能被误判为小苗。建议从 2.0 起步,在验证集上观察冠层与背景的灰度分离度(如 Otsu 阈值两侧的类间方差)再微调。tileGridSize 过小会产生块状伪影,8×8 在 1000~2000 像素级图像上通常较稳。

六、锐化:放在最后,且要克制

推荐非锐化掩模(USM):cv2.addWeighted(L_eq, 1.0 + amount, cv2.GaussianBlur(L_eq, (0,0), 1.0), -amount, 0),amount 取 0.8~1.5。锐化会放大残余噪声,因此必须放在去噪与 CLAHE 之后;若锐化后出现明显白边,应降低 amount 或增大模糊 sigma。锐化完成后将 L 与 a、b 合并并转回 BGR:cv2.cvtColor(cv2.merge([L_sharp, a, b]), cv2.COLOR_LAB2BGR)。

七、数据增强流水线:只对训练集随机

增强的目的是让模型见过更多光照与姿态变化,而不是改变标签语义。对苗木计数任务,水平/垂直翻转、±15° 旋转、±10% 亮度抖动、随机裁剪是安全操作;缩放与透视变换会改变苗木像素尺寸与密度标签的对应关系,若使用必须同步变换标注框或点标注。增强应封装为独立函数,仅在训练 DataLoader 中启用,验证与推理走确定性路径。可用如下结构组织:

def preprocess(img, augment=False):
    lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
    L, a, b = cv2.split(lab)
    Lf = L.astype(np.float32)
    Lf = cv2.bilateralFilter(Lf, 7, 50, 50)
    bg = cv2.GaussianBlur(Lf, (0, 0), 61)
    Lc = np.clip(Lf - bg + 128, 0, 255).astype(np.uint8)
    Lc = cv2.createCLAHE(2.0, (8, 8)).apply(Lc)
    Ls = cv2.addWeighted(Lc, 2.2, cv2.GaussianBlur(Lc, (0,0), 1.0), -1.2, 0)
    out = cv2.cvtColor(cv2.merge([Ls, a, b]), cv2.COLOR_LAB2BGR)
    if augment:
        out = random_flip_rotate_brightness(out)
    return out
调试建议:把同一张图的六个阶段(原图、去噪、光照校正、CLAHE、锐化、增强后)拼成一张对比图逐格观察。重点看三处——树冠内部是否均匀、冠层与裸土边界是否清晰、裸土颗粒是否被过度放大。三者同时满足,才说明参数组合可用。
常见误区:把 CLAHE 与锐化反复叠加"求清晰",结果噪声被逐级放大,分割时裸土碎斑被计为苗木,密度被系统性高估。另一个误区是训练集做了增强、验证集也顺手做了增强,导致验证指标虚高且与上线表现脱节。

整条流水线的验收标准应落到下游任务上:在固定验证集上比较预处理前后的分割 IoU 或计数误差,而不是凭肉眼"更清楚"。若预处理后计数误差未下降甚至上升,优先回查光照校正核尺寸与 CLAHE 的 clipLimit,这两个参数对冠层可分性影响最大。