做CV项目调模型之前先得看数据而数据进模型之前那套CV常用图像处理方法往往比网络结构更能决定最终效果。很多人训练一个检测模型数据没洗干净、尺寸没统一、光照又不一致后面怎么调参都像在沙子上盖楼。这篇文章不堆公式而是把实际项目里最常用的几个动作串起来读图、色彩转换、滤波去噪、增强、几何校正、二值化和形态学操作整理成一套可以直接拿走的预处理思路。1. 图像处理在CV项目里到底是什么角色1.1 先统一认知图像处理是模型的“喂饭”环节很多人一上来就盯着模型结构觉得卷积、注意力、损失函数才是核心。但真实项目里模型吃进去的是什么和模型本身一样重要。我见过不少团队数据采集得很猛结果没做色彩空间统一、没有去噪、没有校正畸变训练时一张图亮一张图暗不同设备拍出来的颜色偏得离谱。最后模型在验证集上忽高忽低很难说清是模型问题还是数据问题。图像处理在CV项目里解决的就是这类脏活把原始图像整理成模型能稳定消化的格式。比如把不同分辨率统一到固定尺寸把偏色图像拉回正常观感把光照不均的区域做对比度校正把倾斜的文档板正。这些工作不产生新的语义信息但能让模型把精力放在特征上而不是被无关的光照和噪声干扰。对刚接触CV的朋友来说先把这块打扎实后面做分类、检测、分割都会顺很多。1.2 我习惯按数据流的顺序来拆解我把常用图像处理按一条数据流排列读入图像做色彩空间转换然后改尺寸和裁剪如果图像有噪声就去滤波如果对比度不够就做增强和边缘提取如果目标位置不正就做几何变换如果要把目标和背景分开就做阈值分割和形态学处理。每一步对应着不同的问题而不是把所有函数都堆一遍。这样的顺序也符合调试习惯。比如检测模型漏检先看是不是图像太暗再去找是否尺寸变形最后才怀疑模型参数。你按数据流一步步排查很快能定位是预处理哪个环节出了问题。下面的内容全是围绕这条线展开的每个方法我都会给出选型和调参建议也会提一些文档里不会明说的坑。2. 读图与色彩空间最容易翻车的第一步2.1 BGR还是RGB这个坑我踩了不止一次用OpenCV读图第一步就要记住cv2.imread读进来的图像通道顺序是BGR不是RGB。这个细节如果没处理好图像用matplotlib一显示红色和蓝色直接对调整个画面看起来就像蒙了一层怪异的滤镜。更麻烦的是如果直接把这种BGR图像送到按RGB顺序训练的模型里推理结果会明显不对而很多人第一反应是模型坏了其实只是喂错了通道顺序。正确方式很简单import cv2 img cv2.imread(demo.jpg) # 读进来是BGR rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转成RGB gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转灰度 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 转HSV灰度图适合绝大多数形状和纹理分析。HSV则适合按颜色做筛选比如从画面中找出红色区域。这里有个小知识OpenCV里的HSVH通道范围是0到180S和V是0到255和很多图像处理软件里H的范围不完全一样。你要提取红色在HSV空间里需要同时考虑两个区间因为红色在色相环上横跨0附近一般写两个范围再用cv2.inRange合并。色彩空间转换看似基础可是很容易被忽略。我建议所有图像处理流程的第一步都明确当前图像是什么通道顺序模型期望用什么通道顺序。如果中间有多个处理步骤也要保证每次转换都不丢失编码范围尤其是uint8和float32互换时别把归一化后的图像再当0到255的图去处理否则颜色全发灰。2.2 resize和裁剪尺寸统一时那些隐藏细节分类、检测、分割任务基本都要求输入尺寸固定所以resize几乎是绕不开的。但resize不是随便填一个尺寸那么简朴。小型化图像时我建议用INTER_AREA它可以减少锯齿和摩尔纹放大图像时INTER_LINEAR是默认选择速度和质量比较平衡追求更高画质可以换INTER_CUBIC但相应更慢。resized cv2.resize(img, (224, 224), interpolationcv2.INTER_AREA)保存宽高比是另一个关键点。直接把一个长方形图拉伸成正方形物体会被拉扁拉长模型学到的形状特征就是错的。正确做法是等比缩放后再填充到目标尺寸或者直接做中心裁剪。填充时一般用灰色128作为背景很多检测框架里的letterbox操作就是这个原理。如果你要保留目标框坐标必须记录缩放比例和填充偏移量推理时再映射回原图否则框和物体对不上。裁剪时还有一个新手高频错误用切片方式img[y:yh, x:xw]时行对应的是y列对应的是x。你先写行再写列和坐标系里先x后y的习惯正好相反。比如要裁左上角100个像素宽、80个像素高正确写法是crop img[0:80, 0:100]不是img[0:100, 0:80]。这类问题看起来小一旦批量生成训练数据错误就会被放大非常耽误时间。3. 滤波去噪降噪不是越狠越好3.1 高斯滤波为什么比均值滤波更常用噪声是图像处理里的常客尤其是低光照场景暗部全是颗粒感。滤波的思路就是让每个像素跟周围像素融合消除孤立噪点。均值滤波最简单直接把周围一圈像素取平均但代价是边缘也被糊掉了。高斯滤波在平均的基础上引入了权重离当前像素越近的点权重越大更符合自然图像的局部相关性所以边缘保留效果通常比均值滤波好。OpenCV里写起来很直接gaussian cv2.GaussianBlur(img, (5, 5), 0.8)核大小一般是奇数比如3、5、7。核越大平滑范围越广图像越模糊。还有一个小参数sigma可以在传0时让OpenCV自动根据核大小计算。实际项目里核大小设为5通常够用sigma设0.8到1.2之间问题不大。如果是为深度学习做预处理千万不要用太大的核否则纹理都被磨平了模型反而学不到细节。滤波的一个常见误区是把它当作万能药。有的图像本身很干净滤波只会增加计算量甚至让边缘发虚。正确做法是先看直方图和局部方差确认噪声确实明显再上滤波。噪声分很多种高斯噪声用高斯滤波合适椒盐噪声就得换中值滤波这一点很多人没区分。3.2 中值滤波和双边滤波各自救什么场景中值滤波把窗口内所有像素排序后取中间值对付椒盐噪声特别有效。图像上那种黑白颗粒点均值滤波和高斯滤波会把污染扩散到周围中值滤波却能把孤立点直接去掉。对文字、二维码这类高对比图像中值滤波是一个很稳的预处理手段。双边滤波则是“既能降噪又能保边”的代表。它的权重由两部分组成位置近邻程度和像素值相似度。像素值差距大的地方即使空间距离很近也不会参与平均所以边缘能保留下来暗部的颗粒又能被磨掉。OpenCV参数一般这样写result cv2.bilateralFilter(img, d9, sigmaColor75, sigmaSpace75)d是窗口直径sigmaColor是颜色域的判定范围sigmaSpace是空间域范围。如果图像像素范围是0到255sigmaColor和sigmaSpace设75附近比较常用。一个深刻教训双边滤波效果不错但速度慢尤其是大窗口下实时摄像头处理时不能每帧都用必须做性能评估。还有一类很实用的操作是拉普拉斯锐化但它和滤波方向相反。如果图像看起来发闷、边缘不够清晰可以把原图和拉普拉斯结果叠加。要注意锐化过度会出现白色光晕尤其是目标边缘带高亮时。我的习惯是先小幅调比如权重0.5再一点点往上加每次都要用真实测试集中的图像检查不能在调参时只盯一张样例图。4. 增强与边缘让模型看到该看的东西4.1 直方图均衡化和CLAHE的取舍对比度不足在监控、文档扫描、工业视觉里太常见了。一张曝光不足的图直方图会集中在低灰度段看不出暗部层次。直方图均衡化的思路是把灰度分布拉平让暗部细节变得更清楚。OpenCV里对灰度图一行代码就能做equ cv2.equalizeHist(gray)但全局均衡化有个让人头痛的副作用它会把背景噪声一起放大。如果图像有大片暗部处理后噪声颗粒会变明显而且不同区域明暗差异很大时暗区被强行提亮效果很生硬。后来我基本改用CLAHE也就是限制对比度自适应直方图均衡化。它先把图像分成小块在每个块内做均衡化同时限制对比度放大幅度避免噪声被过度增强。clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) enhanced clahe.apply(gray)clipLimit是限制对比度的阈值一般2.0到3.0太大会失去“限制”的意义tileGridSize决定分块大小8乘8是常见起点。若是彩色图像最简单的方法是转LAB空间只对L通道做CLAHE再把三个通道合并回去。这样能增强亮度细节又不破坏色彩关系。有人直接在RGB三通道上分别做CLAHE出来的颜色会明显失真这个坑建议避开。4.2 Canny边缘检测的参数到底怎么调边缘检测是把图像转换成线条特征的重要方法。Sobel算子分x和y方向求梯度适合计算局部边缘强度Laplacian对噪声极其敏感边缘图容易花掉更适合检测角点之类的突变。实际用得最多的还是Canny它通过非极大值抑制把边缘压成细线再用双阈值判断真假边缘。OpenCV用法如下edges cv2.Canny(gray, threshold150, threshold2150, apertureSize3, L2gradientTrue)threshold1是最小阈值低于它的点直接丢弃threshold2是最大阈值高于它的点被确定为强边缘。正常情况下两条阈值按1比2或1比3设置比较合理。threshold2设得太低噪声都会变成边缘threshold1设得太高边缘会断裂成一截一截。实际调参时我会先固定threshold2为150再去看threshold1在30到80之间的效果。Canny内部自带高斯滤波步骤所以如果你在调用Canny前已经做了很强的模糊反而会让弱边缘消失。对于边缘断裂问题一个技巧是在Canny之后做一次轻微的膨胀让边缘线条连起来再做形态学细化。如果是文字识别场景Canny常被用来提取字符轮廓配合透视变换就可以做文档校正这个流程后面会提到。5. 几何变换把不正的图像拉回来5.1 仿射变换和透视变换别混用现实拍摄的图片经常是歪的。拍文档、拍屏幕、拍柜子编号免不了有透视形变。仿射变换保持平行线和平行关系不变可以做平移、旋转、缩放和错切透视变换则允许更自由的四点映射能把一个四边形区域拉成一个矩形。两者不要混用不然会出现拉不回来的奇怪变形。仿射变换至少需要3组对应点OpenCV里用cv2.getAffineTransform计算变换矩阵然后用cv2.warpAffine做映射。透视变换至少需要4组点比如检测到文档四个角后把四个点映射到标准矩形的四个顶点pts_src np.float32([[x1, y1], [x2, y2], [x3, y3], [x4, y4]]) pts_dst np.float32([[0, 0], [width, 0], [width, height], [0, height]]) M cv2.getPerspectiveTransform(pts_src, pts_dst) result cv2.warpPerspective(img, M, (width, height))这里有个常见问题坐标点必须用float32否则计算时会因为整数截断产生偏差。输出尺寸要按目标矩形宽高设置如果设小了结果会被压缩。透视变换后图像边缘会有黑色或空白区域通常用cv2.warpPerspective的borderMode和borderValue参数填充我一般用cv2.BORDER_REPLICATE复制边界像素视觉上不会露黑边。5.2 旋转位移时的坐标系和补边问题图像旋转在数据增强里经常用到。OpenCV提供cv2.getRotationMatrix2D(center, angle, scale)生成旋转矩阵再交给warpAffine执行。要注意angle是角度制正数表示逆时针旋转但图像坐标系y轴向下所以直观上的方向可能和数学坐标相反。另外旋转后会损失四个角的图像区域一定要选合适的边界填充策略。M cv2.getRotationMatrix2D((w // 2, h // 2), 15, 1.0) rotated cv2.warpAffine(img, M, (w, h), borderModecv2.BORDER_REPLICATE)三种边界模式各有用途BORDER_CONSTANT用固定颜色填充适合数据增强时不想引入边缘伪影的情况BORDER_REPLICATE复制最外侧像素适合真实感较强的图像BORDER_WRAP会把另一边图像接过来会有明显拼接感一般少用。目标检测做旋转增强时还要同步把标注框旋转这一步很多开源代码写得比较绕但不处理就会产生大量错误标签。我的做法是统一封装一个函数输入原图和标注输出旋转后的图和旋转后的标注永远不要让标注和图像脱节。6. 形态学操作与阈值分割玩转二值图6.1 腐蚀膨胀与开闭运算的直觉形态学操作主要作用于二值图但也可以对灰度图使用。核心是结构元素在图像上扫描。腐蚀的作用是“收缩亮区”小亮点和小连桥会被消掉膨胀的作用是“扩张亮区”空洞和缺口会被补上。对于二值图中文字边缘的毛刺先用腐蚀把细毛刺磨掉再用膨胀恢复文字主体这就是开运算。反过来先膨胀再腐蚀就是闭运算适合填充文字内部的小孔洞。kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)结构元素形状有矩形、椭圆、十字三种。矩形结构元素最常用椭圆对边缘更平滑十字适合处理对角线方向的细线。核大小不建议一上来就设很大从3乘3开始试不够再扩大。迭代次数也是类似逻辑迭代太多会把目标直接消掉。很多初学者忽略的是形态学处理前必须确保二值图质量够好如果阈值分割阶段就一团糟开闭运算只会把错误也放大。6.2 OTSU与自适应阈值光照不均时选谁二值化就是把灰度图变成黑白图。最简单的是固定阈值比如大于127为白色。但光照变化复杂的图固定阈值基本不靠谱。OTSU可以根据直方图自动寻找一个全局最优阈值适合背景和前景灰度差别比较明显的图。OpenCV里设置阈值0配合cv2.THRESH_OTSU函数会自动计算_, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)但OTSU毕竟是全局阈值遇到半边亮半边暗的扫描件仍然会丢失局部信息。自适应阈值则每个像素都参考邻域计算阈值能更好地处理光照不均adaptive cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)blockSize必须是奇数表示邻域大小一般从11开始C是最终减去的常数C越大判为白色的条件越严格。自适应阈值的问题是背景纹理如果太丰富会产生很多噪点配合开运算可以先去除小噪点。如果图像受阴影干扰特别严重也可以考虑用形态学顶帽变换MORPH_TOPHAT来校正背景然后再做阈值分割。这个方法很多人没试过但对文档类图片非常管用。7. 一套可复用的预处理流程和问题速查7.1 一个综合预处理的OpenCV示例下面给一个我自己常用的预处理函数模板它把读图、通道转换、CLAHE、尺寸缩放和归一化组合在一起训练和推理都可以复用。注意顺序很重要先做几何和色彩处理再做增强最后归一化。import cv2 import numpy as np def preprocess_image(path, target_size(416, 416)): img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 缩放到目标尺寸保持宽高比并用灰色填充 h, w img.shape[:2] tw, th target_size scale min(tw / w, th / h) nw, nh int(w * scale), int(h * scale) resized cv2.resize(img, (nw, nh), interpolationcv2.INTER_AREA) canvas np.full((th, tw, 3), 128, dtypenp.uint8) canvas[(th - nh) // 2:(th - nh) // 2 nh, (tw - nw) // 2:(tw - nw) // 2 nw] resized # 对亮度做CLAHE增强 lab cv2.cvtColor(canvas, cv2.COLOR_RGB2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) l2 clahe.apply(l) enhanced cv2.merge([l2, a, b]) enhanced cv2.cvtColor(enhanced, cv2.COLOR_LAB2RGB) # 归一化到[0, 1] result enhanced.astype(np.float32) / 255.0 return result, scale, (tw - nw) // 2, (th - nh) // 2这里的scale和填充偏移量返回是为了在推理时把模型输出的框映射回原图。如果不需要坐标映射可以忽略返回值。很多人容易在这里把图像直接归一化后喂给框架却忘了模型可能要求先做标准化比如用ImageNet均值方差那就要继续对每个通道做处理不要半路停住。7.2 高频问题速查我整理了一张表实际项目里有一批问题反复出现我把最常见的现象、原因和处理方式整理成一张表方便你排查。现象可能原因处理建议显示图像颜色蓝红偏转OpenCV读图为BGR显示时要求RGB用cvtColor切换到正确通道顺序resize后检测框位置错乱没有保存缩放比例和填充偏移记录scale、pad并反向映射坐标图像过暗全局均衡后噪声明显全局直方图均衡放大了暗部噪声改用CLAHE并限制clipLimitCanny边缘断成一截一截minVal和maxVal比例不合适降低minVal或提高maxVal保留更连续的边缘阈值分割在阴影区域大面积失真光照不均全局阈值无法适应改成自适应阈值或先做顶帽变换二值图里充满白色小噪点背景纹理被误判为前景阈值后做开运算用小核去掉孤立点文字二值化后内部有孔洞笔画内部对比度不足闭运算补洞再用中值滤波消除边缘毛刺旋转增强后的目标框明显偏离目标只旋转了图像没有旋转标注框把坐标同乘旋转矩阵再根据新尺寸调整这张表其实覆盖了80%的早期问题。如果遇到表格里没有的现象我建议你按预处理顺序逐步可视化把读图、色彩转换、滤波、增强、几何变换、阈值分割的中间结果都保存下来一眼就能定位是哪一步出的问题。不要直接对着最终结果猜。7.3 我最后想说的一个习惯很多人在网上找了一堆图像处理函数却从来没有系统看一遍效果。说实话图像处理是“手感”很重的活同样的参数在清晰文档和夜间监控上完全不同。我自己现在拿到一张新图会先把灰度直方图调出来再依次看滤波后的细节、Canny边缘和阈值图把所有中间结果铺在眼前心里才有数。曾经有一次为了省时间跳过了CLAHE结果模型在强光影下频繁丢失浅色目标后来只是补上这一小步误检率明显下降。预处理没有银弹但你把每个方法用到什么效果摸索一遍后面的项目就会节省大量调参时间。
阅读完成 · 觉得有帮助?