COMP9517图像处理核心技术与实战笔记:从滤波到分割
发布时间:2026/10/14 2:19:54 锦皓数字建站

1. 从COMP9517这门课说起图像处理到底在解决什么问题第一次翻开COMP9517的课程大纲时我脑子里冒出的第一个念头是图像处理不就是调调滤镜、磨磨皮吗真正学进去才发现这门课覆盖的东西远比想象中硬核——从像素级的数学运算到频域变换再到特征提取与图像分割每一块都在回答同一个根本问题如何让机器看懂一张图里有什么以及怎么把这张图变成更有用的形式。图像处理Image Processing的核心任务说白了就三类改善图像质量去噪、增强、复原、提取有用信息边缘、角点、纹理、特征描述子、为更高层任务做准备分割、配准、识别的前置处理。COMP9517作为一门计算机视觉方向的基础课把这三条线串得很清楚。它不会一上来就让你跑深度学习模型而是先逼你把传统方法的底层逻辑吃透——这一点我后来做项目时才体会到有多重要。这篇文章适合谁看如果你正在修COMP9517或者类似课程想找一份能帮你把零散知识点串起来的笔记那这篇内容就是为你写的。如果你已经工作但图像处理的基础是半路出家遇到去噪、边缘检测、特征匹配这些概念总觉得似懂非懂那这篇也能帮你把地基补上。我会按课程的实际推进节奏把每个技术点的原理、适用场景、实操细节和踩坑经验都讲清楚不是照搬PPT而是把我自己理解和调试过程中真正有用的东西整理出来。需要提前说明的是图像处理这个领域很大一篇文章不可能面面俱到。我选择聚焦在COMP9517课程中最核心、也最常被实际项目用到的几类技术图像增强与滤波、边缘检测、形态学操作、特征提取与匹配、图像分割。每一块我都会给出可运行的代码示例和参数调优建议你可以直接拿去改。2. 图像增强与滤波为什么你的去噪结果总是不理想2.1 空域滤波的数学本质卷积到底在干什么很多人学卷积的时候公式背得滚瓜烂熟但一到实际调参就懵。我一开始也是这样后来强迫自己用一个小例子手算了一遍才真正理解卷积核在做什么。简单说卷积就是用一个小的权重矩阵核在图像上滑动每个位置做加权求和。这个操作的物理意义取决于核的设计如果核的所有元素都是正数且和为1那它就是在做平滑均值滤波如果核中心是正的大数、周围是负数那它就是在做锐化或边缘检测。以3x3均值滤波为例核是每个元素为1/9的矩阵。把它盖在图像上某个像素及其8邻域上输出就是这9个像素的平均值。效果是噪声被抑制了但边缘也糊了。这就是为什么均值滤波在实际项目中很少单独使用——它不分青红皂白地把所有高频信息都抹掉了。高斯滤波则聪明一些核的权重按二维高斯分布生成中心权重最大越往外越小。这样在平滑的同时离中心远的像素影响更小边缘保留得比均值滤波好。在OpenCV里一行代码就能实现import cv2 import numpy as np img cv2.imread(input.jpg) # 高斯滤波核大小5x5标准差自动计算 blurred cv2.GaussianBlur(img, (5, 5), 0)这里有个细节核大小必须是奇数因为要有一个明确的中心像素。标准差设为0时OpenCV会根据核大小自动推算一个合理的值公式是sigma 0.3*((ksize-1)*0.5 - 1) 0.8。如果你手动指定sigma核大小和sigma的匹配关系会直接影响平滑程度——sigma越大权重分布越分散平滑效果越强。2.2 中值滤波椒盐噪声的克星但不是万能的椒盐噪声图像中随机出现的纯黑或纯白像素是图像处理作业里最常见的噪声类型。对付它中值滤波几乎是标准答案。原理很简单把每个像素替换成其邻域内所有像素的中值。因为椒盐噪声的像素值要么极大要么极小排序后一定排在两端取中值自然就把它们剔除了。# 中值滤波核大小必须为奇数 median cv2.medianBlur(img, 5)但中值滤波有个容易被忽略的问题当核较大时细小的纹理和尖角会被抹掉。我做过一个实验对一张包含文字的照片分别用3x3和7x7的中值滤波3x3还能保留笔画细节7x7直接把细笔画糊成了色块。所以实际使用时核大小要从3开始试够用就行不要盲目加大。另外中值滤波的计算量比高斯滤波大不少因为每个位置都要做排序。如果图像分辨率很高、实时性要求又强可以考虑自适应中值滤波——先判断当前像素是否是噪声比如是否等于邻域极值只对疑似噪声点做中值替换其余像素保持原样。OpenCV没有直接提供这个函数但自己实现也就二十行代码的事。2.3 频域滤波傅里叶变换在图像处理中的实际用途COMP9517会花不少时间讲傅里叶变换很多人学到这就开始掉队了。我的建议是先别纠结数学推导先理解它能干什么。傅里叶变换把图像从空间域像素坐标变到频域频率成分。低频对应图像中变化缓慢的区域比如大片天空、墙面高频对应变化剧烈的区域边缘、噪声、纹理。变换之后你可以直接在频域上操作想平滑就去掉高频想锐化就增强高频想去周期性噪声就在对应频率位置做陷波。# 频域低通滤波示例 dft cv2.dft(np.float32(gray), flagscv2.DFT_COMPLEX_OUTPUT) dft_shift np.fft.fftshift(dft) rows, cols gray.shape crow, ccol rows // 2, cols // 2 mask np.zeros((rows, cols, 2), np.uint8) # 理想低通滤波器半径30的圆内保留 mask[crow-30:crow30, ccol-30:ccol30] 1 fshift dft_shift * mask f_ishift np.fft.ifftshift(fshift) img_back cv2.idft(f_ishift) img_back cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])这里有个实际项目中很容易踩的坑理想低通滤波器直接截断频率会产生振铃效应——图像边缘出现波纹状伪影。原因是理想滤波器在频域是矩形窗对应到空间域是sinc函数旁瓣导致了振铃。解决办法是改用巴特沃斯滤波器或高斯滤波器它们的频率响应是平滑过渡的不会产生明显振铃。频域滤波在实际中用得最多的是去除周期性噪声。比如图像上有一层规律性的条纹干扰在频谱图上会表现为几个亮点手动把这些点抑制掉就能大幅改善图像质量。这种操作在空间域很难做到但在频域就是几行代码的事。3. 边缘检测Canny为什么能成为行业标准3.1 从Sobel到Canny边缘检测的进化逻辑边缘检测的目标是找出图像中亮度变化剧烈的像素。最朴素的想法是求梯度——梯度大的地方就是边缘。Sobel算子就是用两个3x3的核分别计算水平和垂直方向的梯度然后取模长。sobelx cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize3) sobely cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize3) magnitude np.sqrt(sobelx**2 sobely**2)但Sobel的结果问题很明显边缘粗、有噪声、阈值难调。直接对整幅图用一个阈值要么漏掉弱边缘要么引入大量噪声。Canny算法之所以成为事实上的标准是因为它用一套多阶段的流程系统性地解决了这些问题高斯滤波先去噪避免噪声被误判为边缘。计算梯度幅值和方向用Sobel算子得到每个像素的梯度。非极大值抑制沿梯度方向检查只保留局部最大值把粗边缘细化成单像素宽。双阈值检测设高阈值和低阈值高于高阈值的强边缘直接保留低于低阈值的丢弃介于两者之间的弱边缘只有连接到强边缘时才保留。edges cv2.Canny(gray, 50, 150)3.2 双阈值怎么设一个被很多人忽略的关键参数Canny的两个阈值low_threshold和high_threshold是实际使用中最需要调的部分。经验法则是高阈值:低阈值大约在2:1到3:1之间。但具体数值取决于图像内容和噪声水平。我自己的调参流程是这样的先固定比例然后从高到低扫一遍高阈值观察边缘数量和连续性。高阈值太高边缘断裂严重太低噪声全出来了。找到边缘基本连续但噪声还可接受的临界点再微调低阈值来补全弱边缘。提示OpenCV的Canny函数如果只传一个阈值它会自动把低阈值设为高阈值的一半。但自动设置不一定适合你的图像建议手动指定。还有一个容易被忽略的细节Canny对图像噪声非常敏感所以在调用Canny之前最好先做一次高斯滤波。虽然Canny内部已经包含了高斯平滑但如果你用的sigma和核大小不合适去噪效果可能不够。我的习惯是先用cv2.GaussianBlur做一次轻度平滑核大小3x3或5x5再送入Canny。3.3 边缘检测在实际项目中的局限与替代方案Canny虽然好用但它有个根本局限它检测的是亮度梯度而不是语义上的边缘。比如一张纹理丰富的草地照片Canny会检测出密密麻麻的纹理边缘但这些边缘对理解图像内容几乎没有帮助。在实际项目中如果目标是提取物体轮廓更好的做法是先做图像分割或区域生长再在分割结果上提取边界。如果目标是特征匹配那边缘检测通常不是最优选择而是应该用SIFT、ORB这类特征点检测算法。另外对于彩色图像直接转灰度再检测边缘会丢失颜色边界信息。比如红色和绿色交界处灰度值可能很接近Canny就检测不到。解决办法是在多个颜色通道上分别检测再合并结果或者使用基于颜色梯度的边缘检测方法。4. 形态学操作腐蚀膨胀不只是变胖变瘦4.1 腐蚀与膨胀的本质结构元素在图像上的逻辑运算形态学操作的核心是结构元素kernel——一个小的形状模板通常是矩形、椭圆或十字形。腐蚀和膨胀就是拿这个模板在图像上滑动做逻辑运算。腐蚀结构元素覆盖的所有像素都为1时中心像素才为1。效果是白色区域缩小黑色区域扩大。膨胀结构元素覆盖的像素中只要有一个为1中心像素就为1。效果是白色区域扩大。kernel np.ones((3, 3), np.uint8) eroded cv2.erode(binary_img, kernel, iterations1) dilated cv2.dilate(binary_img, kernel, iterations1)这两个操作看起来简单但组合起来能解决很多实际问题。开运算先腐蚀后膨胀可以去除小的白色噪点同时保持大物体尺寸基本不变。闭运算先膨胀后腐蚀可以填充物体内部的小黑洞连接断裂的区域。4.2 结构元素的选择形状和大小决定一切结构元素的形状不是随便选的。矩形核适合处理水平和垂直方向的规则结构椭圆核适合处理圆形或自然形状的物体十字形核适合保留对角线方向的连接。大小方面核越大效果越强烈但细节丢失也越严重。我的经验是先用3x3试如果效果不够再逐步加大到5x5、7x7。对于去除小噪点3x3通常就够了对于填充较大的空洞可能需要7x7甚至更大。注意形态学操作通常用于二值图像但也可以用于灰度图像。在灰度图像上腐蚀是取邻域最小值膨胀是取邻域最大值。这个特性可以用来做灰度形态学边缘检测——膨胀结果减去腐蚀结果就是边缘。4.3 形态学在文档处理中的实战应用我做过一个文档图像处理的小项目目标是从拍摄的纸质文档中提取文字区域。流程是这样的转灰度自适应二值化得到黑白图像。用开运算去除小的噪点和孤立像素。用闭运算把文字笔画连接成块方便后续检测。用膨胀把相邻的文字块合并成段落区域。这里有个关键参数闭运算的核大小要略大于文字笔画之间的间距。如果核太小笔画连不起来太大不同行的文字会被错误合并。我的做法是先测量几个典型文字区域的笔画间距然后取略大于该间距的核大小。另外对于倾斜拍摄的文档形态学操作之前最好先做倾斜校正否则结构元素的方向和文字方向不匹配效果会大打折扣。5. 特征提取与匹配SIFT、SURF、ORB该怎么选5.1 特征点检测的核心要求不变性特征点检测的目标是找到图像中独特且可重复的点。所谓可重复就是同一物体在不同视角、不同尺度、不同光照下拍摄检测到的特征点应该尽量一致。这就要求特征点具有尺度不变性、旋转不变性、光照不变性。SIFT尺度不变特征变换是这方面的经典算法。它通过高斯差分金字塔在不同尺度上检测极值点然后为每个特征点分配主方向最后生成128维的描述子。这套流程保证了SIFT对尺度、旋转、亮度变化都有很好的鲁棒性。sift cv2.SIFT_create() keypoints, descriptors sift.detectAndCompute(gray, None)SURF是SIFT的加速版用积分图和盒式滤波器近似高斯差分速度更快但精度略低。ORB则是完全不同的思路——它基于FAST角点检测和BRIEF描述子速度极快但没有尺度不变性虽然可以通过图像金字塔部分弥补。5.2 匹配策略暴力匹配 vs FLANN检测到特征点后下一步是匹配。暴力匹配Brute-Force就是计算每个描述子之间的距离找最近的。对于SIFT的128维描述子通常用欧氏距离对于ORB的二进制描述子用汉明距离。bf cv2.BFMatcher(cv2.NORM_L2, crossCheckTrue) matches bf.match(descriptors1, descriptors2) matches sorted(matches, keylambda x: x.distance)FLANN快速近似最近邻搜索库适合大规模特征匹配它用KD树或LSH做近似搜索速度快但可能漏掉一些正确匹配。实际使用中匹配结果里通常有大量错误匹配需要过滤。常用的过滤方法有两种交叉验证crossCheckTrue要求双向匹配一致和比率测试Lowes ratio test对每个匹配找最近和次近的两个如果最近距离远小于次近距离才保留。# Lowes ratio test bf cv2.BFMatcher() matches bf.knnMatch(descriptors1, descriptors2, k2) good [] for m, n in matches: if m.distance 0.75 * n.distance: good.append(m)比率测试的0.75这个阈值是Lowe论文里的推荐值但实际使用时可以根据场景调整。阈值越小匹配越严格正确率越高但数量越少。5.3 特征匹配的典型应用图像拼接与目标识别特征匹配最直接的应用是图像拼接。流程是检测两张图的特征点匹配用RANSAC估计单应性矩阵然后做透视变换和融合。if len(good) 10: src_pts np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst_pts np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) H, mask cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) result cv2.warpPerspective(img1, H, (img1.shape[1]img2.shape[1], img1.shape[0])) result[0:img2.shape[0], 0:img2.shape[1]] img2这里RANSAC的阈值5.0表示重投影误差超过5个像素的点被视为外点。这个值需要根据图像分辨率和匹配质量调整。分辨率高、匹配好的话可以设小一点2-3匹配噪声大的话要设大一点5-10。另一个应用是目标识别——在场景图中找到已知物体的位置。做法是匹配特征点后用单应性矩阵把模板图的边框投影到场景图上。但要注意如果物体有严重遮挡或视角变化太大特征匹配可能失败这时候就需要考虑基于深度学习的方法了。6. 图像分割从阈值法到区域生长6.1 全局阈值与自适应阈值什么时候该用哪个阈值分割是最简单的分割方法像素值大于阈值的归为一类小于的归为另一类。全局阈值如Otsu方法对整幅图计算一个阈值适合光照均匀、前景背景对比明显的图像。ret, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)Otsu方法的核心是最大化类间方差——遍历所有可能的阈值找到使前景和背景两类像素方差最大的那个。这个方法的优点是自动、无需手动调参缺点是假设图像是双峰的直方图有两个明显的峰。如果图像光照不均或前景背景比例悬殊Otsu效果会变差。自适应阈值则对图像每个小区域单独计算阈值适合光照不均匀的情况。adaptive cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)这里的blockSize11是邻域大小C2是从计算出的阈值中减去的常数。blockSize必须为奇数且要大于前景物体的尺寸否则会把物体内部也分割成背景。6.2 区域生长与分水岭处理粘连物体的利器当图像中的物体相互粘连时简单的阈值分割无法把它们分开。这时候分水岭算法就派上用场了。分水岭的思路是把图像看作地形图像素值看作海拔。从每个局部最小值开始注水水不断上涨不同水源相遇的地方就形成分界线。但直接对梯度图做分水岭会导致过分割——每个小纹理都会形成一个区域。解决办法是基于标记的分水岭先手动或自动确定每个物体的内部标记种子点然后从这些标记开始注水。# 距离变换 阈值找种子点 dist cv2.distanceTransform(binary, cv2.DIST_L2, 5) ret, sure_fg cv2.threshold(dist, 0.5 * dist.max(), 255, 0) sure_fg np.uint8(sure_fg) unknown cv2.subtract(sure_bg, sure_fg) # 标记 ret, markers cv2.connectedComponents(sure_fg) markers markers 1 markers[unknown 255] 0 # 分水岭 markers cv2.watershed(img, markers) img[markers -1] [0, 0, 255]这段代码里距离变换是关键——它计算每个前景像素到最近背景像素的距离距离大的地方就是物体中心。用0.5倍最大距离做阈值就能得到比较可靠的种子点。6.3 分割效果评估没有 ground truth 怎么办实际项目中很多时候没有标注好的ground truth来评估分割效果。这时候可以用一些无参考的评估指标区域一致性分割出的区域内部像素方差越小越好。区域间差异不同区域之间的均值差异越大越好。边界贴合度分割边界和实际边缘的重合程度。如果只是做定性评估我通常会把分割结果叠加在原图上用不同颜色显示不同区域然后肉眼检查是否有明显的错误分割。这个方法虽然主观但在调试阶段非常高效。另外分割参数没有万能值。同一套参数在不同图像上效果可能天差地别。我的做法是把参数暴露成变量写一个简单的脚本批量跑一批测试图然后统计哪些参数组合在多数图上表现稳定。这个过程虽然笨但比拍脑袋调参靠谱得多。7. 我在COMP9517学习过程中踩过的几个坑第一个坑是过度依赖默认参数。OpenCV的很多函数都有默认参数比如Canny的阈值、高斯滤波的核大小。刚开始我直接调用结果在某些图上效果很差还以为是算法本身的问题。后来才明白默认参数只是能跑不是跑得好。每个参数都要根据具体图像调整。第二个坑是忽略数据类型。OpenCV读入的图像默认是uint80-255但很多计算比如梯度、卷积会产生负数或超过255的值。如果不先转成float32结果会被截断导致信息丢失。我在这上面浪费过不少时间后来养成了习惯做任何数值计算前先确认数据类型必要时用astype(np.float32)转换。第三个坑是在错误的空间做处理。比如想去除周期性噪声在空间域用滤波核怎么调都效果不好后来转到频域几分钟就解决了。选择正确的处理域比调参重要得多。第四个坑是没有可视化中间结果。图像处理是一个多步骤的流程如果只在最后看结果出了问题很难定位是哪一步的错。我的建议是每一步都保存或显示中间图像这样能快速定位问题所在。8. 持续更新计划与学习建议图像处理这个领域光看理论不写代码是学不会的。我的建议是每学一个算法就找一张图跑一遍然后故意改参数观察变化。比如学高斯滤波时把sigma从0.5调到5看看平滑程度怎么变学Canny时把高低阈值分别调大调小观察边缘数量和连续性的变化。这种破坏性实验比按部就班地跑通代码收获大得多。后续我计划补充的内容包括霍夫变换直线和圆检测、模板匹配、图像金字塔、光流法。这些都是COMP9517后续会涉及、也是实际项目中常用的技术。如果你有特别想了解的部分也可以告诉我我会优先安排。最后分享一个我自己的学习习惯每学完一个模块用一句话总结它的核心思想。比如高斯滤波就是按距离加权平均Canny就是梯度非极大值抑制双阈值分水岭就是从种子点注水找分界线。这些一句话总结看似简单但能帮你快速回忆和串联知识点复习时特别有用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。