
简介这套资料来自浙江大学《数字图像处理与机器视觉》课程完整收录9个实验的源代码与实验报告面向计算机、人工智能、自动化、电子信息等专业的高校学生尤其适合正在完成图像处理类课程作业或需要参照成熟实现开展实验的学习者。压缩包为zip格式整体约121.91MB内容以源代码和报告文档为主便于逐项对照学习。代码均经过运行测试报告结构完整可作为实验步骤梳理、算法理解与报告撰写的直接参考对初次接触数字图像处理实验的同学而言尤为实用。目前已有106人学习下载。下载后若在环境配置或运行中遇到问题可与作者私信沟通支持远程教学协助整体资料既可用于课程作业参考也可作为相关毕设或课设的初期演示素材。1. 九份实验作业含金量在哪一条被课程作业串起来的机器视觉流水线这9个数字图像处理实验几乎把课程主干内容全部覆盖了一遍灰度变换、直方图均衡化、空域滤波、频域分析、图像分割、形态学处理、特征点匹配最后落在一个带机器视觉味道的分类或检测任务上。对正在修这门课的学生来说它是作业对想入行视觉算法的工程师来说它是一条现成的入门路线图。我按这9个实验逐个复现过一遍发现真正花时间的不是调算法而是把参数、边界情况和报告组织清楚。这篇笔记按我的复盘顺序来写每个实验怎么拆、代码怎么写、坑在哪、报告怎么组织。后面的内容不跟着目录走跟着你实际动手时遇到问题的顺序走。2. 按模块复现9个实验图像增强、频域滤波、分割与特征匹配的核心代码这9个实验单独看是9个独立任务串起来其实是一条典型的机器视觉处理链先做图像增强再做频域或空域滤波去噪然后分割目标区域用形态学清理分割结果最后提取特征做匹配或识别。我按这条链把9个实验分成六组来写每组给可复现的代码和参数经验。2.1 灰度变换与直方图均衡化一张低对比图的救法实验1和实验2通常放在一起一个做灰度变换一个做直方图均衡化。灰度变换的核心是逐像素重映射常用的有线性拉伸、log变换、幂次变换。log变换压缩高光区动态范围幂次变换通过γ的取值控制暗部亮度γ小于1时提亮暗部γ大于1时压暗。OpenCV里cv2.convertScaleAbs配合自定义查表就可以做但作业里常要求手写映射过程。直方图均衡化是这组实验的重点光调用cv2.equalizeHist不够报告里得展示累积分布函数CDF的映射过程。下面这段代码手写均衡化适合直接嵌进实验报告import cv2 import numpy as np def my_equalize_hist(img): # 彩色图先转灰度作业里一般只要求处理灰度图 if len(img.shape) 3: img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 统计灰度直方图 hist cv2.calcHist([img], [0], None, [256], [0, 256]).ravel() # 计算累积分布函数CDF cdf hist.cumsum() # 归一化到0~255同时把最暗的像素平移到0 cdf_normalized (cdf - cdf.min()) * 255 / (cdf.max() - cdf.min()) # 用查表方式完成像素重映射 lut cdf_normalized.astype(np.uint8) return lut[img] img cv2.imread(low_contrast.jpg, cv2.IMREAD_GRAYSCALE) dst my_equalize_hist(img) cv2.imwrite(equalized.jpg, dst)这段代码的关键在于 CDF 的归一化方式。cdf - cdf.min()让原图中最暗的像素强制映射到0这样低对比图的直方图会被拉伸到整个灰度范围。如果去掉减法结果会整体偏亮。一个容易忽略的边界是当原图是纯色图时cdf.max() - cdf.min()等于0会直接除零报错具体翻车现场写到第3章。2.2 空域平滑与锐化卷积核尺寸是第一个玄学参数实验3做空域滤波平滑和锐化各写一遍。平滑最常见的是高斯滤波OpenCV里一个GaussianBlur就够但参数组合很值得在报告里做对比。核尺寸和σ必须匹配核窗口一般取约为 6σ1向上取奇数。σ0.5时用3×3核σ1时用5×5核σ2时用13×13核。如果核太小而σ太大高斯核实际上被截断了效果会变得像均值滤波。锐化则用拉普拉斯算子基本思路是原图减去拉普拉斯响应。下面这段同时展示平滑和锐化方便实验报告做对照import cv2 img cv2.imread(noisy.jpg, cv2.IMREAD_GRAYSCALE) # 三组参数对比核尺寸随sigma增大而增大 blur_1 cv2.GaussianBlur(img, (3, 3), 0.5) blur_2 cv2.GaussianBlur(img, (5, 5), 1.0) blur_3 cv2.GaussianBlur(img, (13, 13), 2.0) # 锐化原图减去拉普拉斯响应k控制锐化强度 laplacian cv2.Laplacian(blur_2, cv2.CV_64F) sharp cv2.subtract(blur_2, 0.8 * laplacian) sharp cv2.normalize(sharp, None, 0, 255, cv2.NORM_MINMAX)锐化操作里有个常见误区直接img - laplacian得到的结果经常出现大量黑色噪点因为拉普拉斯响应是浮点型且有正有负需要用cv2.subtract做饱和处理而不是numpy直接减。锐化强度k放到0.8附近比较安全超过1.2会出现明显振铃。2.3 频域滤波从空间域换到频率域看噪声实验4做频域滤波通常是傅里叶变换加高低通滤波。很多学生在这一步翻车因为原图最暗、最亮区域的频谱错位以及频谱中心化没做对。标准流程是fft2→fftshift→ 乘掩膜 →ifftshift→ifft2。掩膜必须和频谱一样大、数据类型为浮点型否则乘出来的结果会变成全黑。import numpy as np import cv2 def gaussian_lowpass(img_gray, d0): # 傅里叶变换并中心化 f np.fft.fft2(img_gray.astype(np.float32)) fshift np.fft.fftshift(f) rows, cols img_gray.shape crow, ccol rows // 2, cols // 2 # 构造高斯低通掩膜向量化代替双重for循环 y np.arange(rows).reshape(-1, 1) x np.arange(cols).reshape(1, -1) d np.sqrt((y - crow) ** 2 (x - ccol) ** 2) mask np.exp(-(d ** 2) / (2 * d0 ** 2)) # 频域乘掩膜再逆变换回空间域 filtered fshift * mask f_ishift np.fft.ifftshift(filtered) result np.fft.ifft2(f_ishift) result np.abs(result) return cv2.normalize(result, None, 0, 255, cv2.NORM_MINMAX) img cv2.imread(lena_noisy.png, cv2.IMREAD_GRAYSCALE) out gaussian_lowpass(img, d030)d0是截止频率经验上取图像短边长度的5%到10%比较合适即短边256时d0取15~30。d0太小会损失太多边缘信息图像整体发虚d0太大则起不到去噪作用。做高通滤波时用1 - mask即可但要注意高通会增强噪声实际作业里常配合拉普拉斯算子做对比。还有一点值得在报告里写频谱图要先取log再显示否则中间的低频分量会把高频细节压成一片黑。显示频谱的代码是np.log(np.abs(fshift) 1)加1防止对数零值。2.4 图像分割与边缘检测Canny双阈值怎么定实验5做图像分割重点在边缘检测。Canny算法本身包含五步高斯平滑、梯度幅值计算、非极大值抑制、双阈值检测、滞后连接。OpenCV一行就能跑但作业里要求讲清除参数来源所以双阈值的选取要单独解释。import cv2 img cv2.imread(cells.jpg, cv2.IMREAD_GRAYSCALE) # Canny之前必须做一次平滑否则阈值很难选 blurred cv2.GaussianBlur(img, (5, 5), 1.0) # 低阈值约为高阈值的一半 edges cv2.Canny(blurred, 30, 90)高阈值决定了哪些边缘是「确定边缘」低阈值决定了哪些边缘可能被保留。OpenCV的Canny内部会先算梯度幅值通过高阈值找到强边缘再从强边缘出发沿着梯度方向追踪弱边缘所以高阈值设太高会让边缘断成碎片低阈值设太高则会直接丢掉弱边缘。一个供作业参考的定阈值方式先跑一遍梯度幅值取幅值直方图前70%位置作为高阈值低阈值取高阈值的三分之一。这种方式比凭空猜30和90要显得有说服力。分割实验除了Canny通常还会要求对比Sobel和Laplacian。Sobel对噪声更敏感Laplacian对孤立噪声点会放大两级响应而Canny的抗噪性明显更好。报告里放三张对比图并解释各自对噪声的响应差异就能把这部分写得很扎实。2.5 形态学处理结构元素大小决定你是去噪还是改图实验6做形态学操作主要任务是从二值分割结果中清理噪声。开运算是先腐蚀后膨胀去掉孤立小点闭运算是先膨胀后腐蚀填补孔洞。用cv2.morphologyEx可以一步完成但结构元素的选择才是关键。import cv2 import numpy as np mask cv2.imread(segmented_mask.png, cv2.IMREAD_GRAYSCALE) _, mask cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) # 椭圆结构元素比矩形更贴近真实目标轮廓 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) opened cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 连通域分析过滤面积小于阈值的噪点 num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(opened, 8) filtered np.zeros_like(opened) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] 50: # 面积阈值 filtered[labels i] 255结构元素尺寸的经验值是目标短轴长度的五分之一到三分之一。如果结构元素比目标本身还大开运算会把目标整个腐蚀掉这是最常见的翻车方式。connectedComponentsWithStats返回的stats里第4列是面积第0、1列是外接矩形左上角坐标第2、3列是宽和高过滤小连通域时直接用面积列做判断。如果目标区域形状细长建议把面积阈值改成宽高比或外接矩形面积阈值单纯按面积过滤会把细长真目标误删。2.6 特征匹配与目标识别从SIFT到分类器的机器视觉落地实验7到实验9落在机器视觉上。实验7做特征点检测和匹配有很多选择SIFT尺度不变但速度慢ORB速度快且适合嵌入式场景。作业里一般用ORB就够它的描述子是二进制的BRIEF匹配距离用汉明距离。import cv2 img1 cv2.imread(template.jpg, cv2.IMREAD_GRAYSCALE) img2 cv2.imread(scene.jpg, cv2.IMREAD_GRAYSCALE) # ORB特征点检测nfeatures控制关键点数量 orb cv2.ORB_create(nfeatures1000) kp1, des1 orb.detectAndCompute(img1, None) kp2, des2 orb.detectAndCompute(img2, None) # crossCheckTrue保证匹配是一一对应的能去掉大量误匹配 bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) matches bf.match(des1, des2) matches sorted(matches, keylambda x: x.distance) good_matches matches[:80] # 取距离最小的前80个实验8是图像分类常见做法是提取HOG特征训练SVM或者用小规模卷积神经网络。作业场景下不需要很大的网络一个两层卷积加两个全连接层的LeNet变体在CPU上就能在几分钟内跑完MNIST。实验9是综合应用我见过最合理的安排是把前8个实验串成一个缺陷检测系统先做增强再做分割用形态学清理最后用特征匹配或小分类器判断缺陷类型。这条链路做下来机器视觉缺陷检测的完整流程就全部覆盖了。3. 实验代码避坑记录直方图过曝、频谱错位与特征误匹配的5个真实翻车现场下面的五个坑我都实际踩过顺序按出现频率排列每一条都按现象、原因、解决来讲可以直接当成排错手册来翻。3.1 直方图均衡化在纯色图上直接报错现象是当输入一张背景几乎均匀的图像时程序在cdf.max() - cdf.min()这一步抛出除法错误或者不报错但输出全黑图。原因是纯色图的直方图只有一个非零灰度级CDF的最大值和最小值相等分母为0。这个问题容易被忽略是因为实验素材都是正常的照片但报告里一旦放入人工生成的纯色测试图就会暴露。解决方法是先判断CDF的极差当极差小于一个极小阈值时直接返回原图并把这条判断逻辑作为均衡化函数的一部分写进代码里。边界处理写进实验代码后报告里顺带提一句「纯色图在直方图均衡化中无意义因此在实现中做保护处理」这比代码本身更能体现工程意识。3.2 频域滤波结果出现周期性横竖条纹现象是逆变换后的图像上有明显的网格状条纹像是叠了一层纱窗纹理。原因大概率是频域掩膜和频谱的dtype不一致或者构造掩膜时网格坐标算错导致掩膜中心不在图像中心。还有一种常见原因是做fftshift和ifftshift时用错了次数导致频谱没有真正中心化低频分量还留在四个角掩膜却在中心乘完等于把低频全滤掉了。解决方法是严格按fft2 → fftshift → 乘掩膜 → ifftshift → ifft2的顺序执行掩膜构造必须用np.float32不要用整数数组和频谱相乘。先打印掩膜的dtype和中心值再检查输出能省掉大量调试时间。3.3 Canny边缘断裂成小碎段现象是同一张图别人跑出来的边缘是连贯闭合的自己跑出来断得七零八落。绝大多数原因不是算法问题而是高阈值设得太高。高阈值偏高会把梯度幅值低于阈值的弱边缘全部丢弃而后滞连接算法又只从强边缘往外追踪断点由此产生。解决方法是先算梯度幅值直方图以积累概率70%的位置做高阈值低阈值取高阈值的1/2到1/3。这个办法比手工试参稳定很多。还有一点容易被忽略输入Canny之前必须做高斯平滑没平滑的梯度图噪声极大双阈值怎么调都调不出完整轮廓。3.4 ORB特征匹配出现大量交叉连线现象是匹配结果图上一堆线条交叉看起来像一团乱麻匹配点对根本没有空间一致性。原因是直接用bf.match做暴力匹配时没有做交叉验证或比值检测。对一个特征点描述子空间里可能有多个相似候选直接取最近距离的那个容易选中错误匹配。解决方法是把BFMatcher的crossCheck设为True要求A的最佳匹配也是B的最佳匹配时才算有效或者用knnMatch取前两个匹配检测最近距离和次近距离的比值比值小于0.75才保留。前者简单后者更精细作业报告里两个方法对比着写一段就更完整。提示RANSAC剔误匹配可以进一步解决特征匹配后的几何错误用cv2.findHomography得到的掩膜把外点滤掉剩下的匹配点对才是真正支持同一平面变换的。3.5 形态学开运算把目标物整个删掉现象是做完开运算后二值掩膜里的小目标消失不见只剩一片背景。原因是结构元素尺寸超过了目标尺寸腐蚀阶段把整个目标腐蚀为0后续膨胀也救不回来。解决方法是先用连通域分析初步统计目标的平均短轴长度再取短轴的1/5到1/3作为结构元素尺寸椭圆结构元素通常比矩形更贴近目标真实形状。写报告时把两个尺寸的开运算结果放在同一行做对比一张是目标保留完好但噪声清理不干净一张是目标消失只剩背景直观展示结构元素尺寸的边界作用。4. 实验报告怎么写对比图、量化指标与结论推导的可复用模板报告在这门课里的分量几乎和代码相当。代码跑不出结果最多扣过程分报告写得不清楚则直接扣到结论分。我看过不少同学的报告代码贴了十几页但教师看完不知道每段代码在解决什么问题、参数为什么这么取更不知道实验结论是什么。一份能拿高分的报告核心不是写得多而是每段都回答三个问题我做了什么、我为什么这么做、效果如何证明。我一般把每个实验按五个段落组织先写实验目标一句话说清这个实验要验证什么再写方法流程用文字描述从输入到输出的处理链路不贴代码然后写关键实现只贴最能体现实验要点的代码片段比如手写均衡化、频域掩膜构造、Canny双阈值选取接着放实验结果必须包含处理前对比图、处理后效果图和一个量化指标表最后写结论用一到两句话把参数变化与效果差异的关系说清楚。图表组织的标准做法是并用视觉对比图和量化指标两者缺一不可。下面这套PSNR和SSIM计算代码可以直接用在报告里import numpy as np import cv2 def calc_psnr(img1, img2): # 输入两张灰度图计算峰值信噪比 mse np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2) if mse 0: return float(inf) return 10 * np.log10(255.0 ** 2 / mse) def calc_ssim(img1, img2): # SSIM用skimage实现比手写稳定报告里直接用即可 from skimage.metrics import structural_similarity return structural_similarity(img1, img2, win_size11, data_range255) denoised cv2.GaussianBlur(img, (5, 5), 1.0) print(PSNR:, round(calc_psnr(img, denoised), 2)) print(SSIM:, round(calc_ssim(img, denoised), 4))PSNR对像素级误差敏感但对结构失真不敏感两张图像内容完全不同但只要像素误差小PSNR也能很高。SSIM则把亮度、对比度、结构三项分开比较更贴近人眼感知。写报告时两个指标一起给结论句这样写随着高斯核σ从0.5增加到2.0PSNR先从28.3升到31.1再回落到29.7SSIM从0.87升到0.93后回落到0.90说明过度平滑会同时拉低两个指标最佳参数位于σ1.0附近。这种和参数挂钩的结论比一句「效果不错」有说服力得多。实验截图的布局用一个subplot模板就能覆盖全部9个实验的需求import matplotlib.pyplot as plt fig, axes plt.subplots(2, 3, figsize(12, 8)) # 第一行放原图和中间结果 axes[0, 0].imshow(img, cmapgray) axes[0, 0].set_title(Original) axes[0, 1].imshow(blur_2, cmapgray) axes[0, 1].set_title(Gaussian sigma1.0) axes[0, 2].imshow(sharp, cmapgray) axes[0, 2].set_title(Sharpened) # 第二行放频谱和直方图 axes[1, 0].imshow(np.log(np.abs(fshift) 1), cmapgray) axes[1, 0].set_title(Spectrum) axes[1, 1].hist(img.ravel(), bins256, range(0, 256)) axes[1, 1].set_title(Histogram) axes[1, 2].axis(off) plt.tight_layout() plt.savefig(experiment_result.png, dpi150)报告里一个高频扣分点是只贴图不解释或者图和指标矛盾了也不分析。比如平滑后PSNR升高但SSIM下降说明像素误差变小了但结构细节丢了这时要主动分析原因而不是避而不谈。还有一个常见问题是结论写得像项目总结而不是实验发现。正确写法是「在不同截止频率d0下边缘保留程度与去噪效果呈负相关d030时能兼顾两者」而不是「本实验完成了频域低通滤波验证了算法有效性」这种放之四海而皆准的废话。5. 用边界数据验证整套实验从纯黑图到高噪声图的鲁棒性检查整套实验代码写完、报告也组织好之后最后一个步骤是用边界数据做验证。我自己的习惯是准备5张特殊测试图纯黑图、纯白图、半黑半白图、低对比图、高椒盐噪声图然后跑一遍全部处理链看每个环节是否报错、输出是否有意义。这个习惯救过我很多次因为作业报告里翻车最多的不是算法流程没跑通而是边界情况没处理。import cv2 import numpy as np test_images { black: np.zeros((256, 256), np.uint8), white: np.full((256, 256), 255, np.uint8), half: np.vstack([np.zeros((128, 256), np.uint8), np.full((128, 256), 255, np.uint8)]), noise: np.random.randint(0, 256, (256, 256), np.uint8) } for name, img in test_images.items(): # 直方图均衡化边界保护 hist cv2.calcHist([img], [0], None, [256], [0, 256]).ravel() cdf hist.cumsum() if cdf.max() - cdf.min() 1e-6: print(f{name}: skip equalize (flat image)) continue dst my_equalize_hist(img) print(f{name}: equalize range {dst.min()}-{dst.max()})低对比图和噪声图能验证增强和去噪模块的量化指标变化半黑半白图则能暴露直方图均衡化在双峰分布下的异常拉伸——结果往往会出现错误的伪轮廓这也是报告里值得讨论的点。习惯上我会在每个实验函数的开头加一段输入检查统计灰度范围、检查图像是否为空、确认dtype是uint8再做处理宁可多写三行防御代码也不要在报告提交前夜因为一张纯色图崩溃。每个实验写完都跑一遍这5张图发现边界问题就地修复再跑正常素材图确认效果不受影响。这个习惯坚持下来代码的鲁棒性会明显强于大多数只拿标准测试图交作业的同学。希望帮到你。本文还有配套的精品资源点击获取