简介:这份资源是浙江大学2020-2021秋冬学期计算机视觉课程的配套资料合集,面向正在学习计算机视觉、需要动手完成课程实验的高校学生与自学者。内容覆盖从基础图像处理到深度学习的完整链路,包含六次实验:制作无声小短片视频、圆形与直线检测、Harris角点检测、Eigenface人脸检测以及CNN实现,并附有复习资料与OpenCV学习材料,适合按课程节奏逐项实践或作为期末复习参考。压缩包共1319个文件,以436个jpg图像、419个txt文本、400个pgm灰度图为主,另含14个pdf讲义、7个py脚本、7个png图示及若干模型checkpoint与json配置,整体约72.75MB,目录按实验与资料分块组织,便于检索。目前已有330人学习下载。读者可借此获得六次实验的完整素材与代码框架,理解角点检测、人脸识别与卷积网络等算法的实现细节,并借助OpenCV资料与复习文档查漏补缺,适合作为课程作业与考前梳理的实用参考。
1. 从六份实验报告说起:这套计算机视觉资料到底能帮你省下多少试错时间
如果你正在自学计算机视觉,或者带本科毕设、准备课程大作业,大概率经历过这样的场景:OpenCV 的 API 翻来覆去就那几个,但真到写实验报告时,连一个像样的图像滤波对比图都凑不齐;想复现一个特征匹配流程,结果卡在cv2.findHomography的参数上,一调就是一下午。这套来自某高校 2020-2021 秋冬学期的计算机视觉课程资料,包含 6 次完整实验、配套复习资料和一份 OpenCV 学习资料,恰好能填上这个缺口。它不是那种只给结论的课件,而是把每次实验的代码、数据、报告模板都摆出来了,适合想按部就班走一遍经典 CV 流程的人。我翻完这六次实验的目录后,第一反应是:如果当年我学 SIFT 的时候有这套东西,至少能少熬三个晚上。
2. 六次实验的骨架拆解:从像素操作到特征匹配的递进路线
2.1 实验一与实验二:图像基础操作与空域滤波
前两次实验是整个课程的入口,重点落在像素级操作和空域滤波上。实验一通常要求读取图像、分离通道、做灰度化和直方图均衡化,实验二则进入卷积核的世界,要求手写均值滤波、高斯滤波和中值滤波,并与 OpenCV 内置函数做对比。这套资料里最值钱的部分是它给出了完整的对比脚本,而不是只丢一个cv2.GaussianBlur就完事。
我一般会建议按这个顺序跑一遍:
import cv2 import numpy as np import matplotlib.pyplot as plt # 读取图像并转为灰度图 img = cv2.imread('lena.jpg', cv2.IMREAD_GRAYSCALE) # 手动实现均值滤波(3x3) kernel_mean = np.ones((3, 3), np.float32) / 9 manual_mean = cv2.filter2D(img, -1, kernel_mean) # OpenCV 内置均值滤波 builtin_mean = cv2.blur(img, (3, 3)) # 计算两种方式的差异 diff = cv2.absdiff(manual_mean, builtin_mean) print(f'最大差异值: {diff.max()}') # 正常应为 0 或极小值 # 并排显示 plt.subplot(131), plt.imshow(img, cmap='gray'), plt.title('Original') plt.subplot(132), plt.imshow(manual_mean, cmap='gray'), plt.title('Manual Mean') plt.subplot(133), plt.imshow(builtin_mean, cmap='gray'), plt.title('Builtin Mean') plt.show()这段代码的逻辑很直白:用filter2D手动卷积一个 3x3 的均值核,再和cv2.blur的结果做逐像素差值。如果差值不为零,说明边界处理方式不同——OpenCV 默认对边界做反射填充,而手动卷积如果不指定borderType,默认是常量填充。这个坑我在第一次跑的时候就踩了,输出图像边缘有一圈暗带,后来把borderType=cv2.BORDER_REFLECT加上才对齐。参数上,ddepth=-1表示输出图像深度与输入一致,如果输入是uint8,卷积结果会被截断到 0-255,做对比时最好先转成float32再算差值。
实验二还会涉及高斯核的生成。资料里给了一个手动计算高斯核的函数,用np.exp和归一化实现,和cv2.getGaussianKernel的结果做对比。这里的关键参数是sigma,它控制核的权重分布:sigma 越大,中心像素的权重被摊得越平,滤波效果越接近均值滤波。我通常会把 sigma 设成 1.0 到 2.0 之间,再大就糊得看不清细节了。
2.2 实验三与实验四:频域变换与边缘检测
实验三进入频域,要求实现傅里叶变换、频谱图绘制和低通/高通滤波。实验四则是边缘检测,包括 Sobel、Laplacian 和 Canny 算子。这两次实验的难度明显上一个台阶,因为频域的概念对初学者不太友好,而 Canny 的双阈值又特别依赖经验。
频域实验的核心代码通常长这样:
import cv2 import numpy as np img = cv2.imread('lena.jpg', cv2.IMREAD_GRAYSCALE) f = np.fft.fft2(img) fshift = np.fft.fftshift(f) # 构建理想低通滤波器 rows, cols = img.shape crow, ccol = rows // 2, cols // 2 mask = np.zeros((rows, cols), np.uint8) D0 = 30 # 截止频率 for i in range(rows): for j in range(cols): if np.sqrt((i - crow)**2 + (j - ccol)**2) <= D0: mask[i, j] = 1 fshift_filtered = fshift * mask f_ishift = np.fft.ifftshift(fshift_filtered) img_back = np.fft.ifft2(f_ishift) img_back = np.abs(img_back) # 显示频谱和滤波结果 magnitude_spectrum = 20 * np.log(np.abs(fshift) + 1)这里有几个参数需要留意。D0是截止频率,设得太小,图像会糊成一片;设得太大,等于没滤波。我一般从 30 开始试,根据图像尺寸调整。另外,np.log里的+1是为了避免对零取对数,这个细节资料里没写,但实际跑的时候不加会报 warning。频域滤波的坑在于:理想低通滤波器会产生振铃效应,因为它的频率响应是矩形窗,对应的空域是 sinc 函数。如果想减轻振铃,可以换成巴特沃斯或高斯滤波器,资料里也给了对应的实现。
边缘检测部分,Canny 的双阈值是重灾区。资料里建议的低高阈值比是 1:2 或 1:3,但实际图像千差万别。我通常先用cv2.Canny的默认阈值跑一遍,看边缘图的效果,再根据漏检和误检情况调整。如果边缘断断续续,说明低阈值太高;如果背景噪声全被检出来,说明高阈值太低。Sobel 算子则要注意ksize参数,取 3 时是标准的 3x3 核,取 -1 时用 Scharr 核,后者对细边缘更敏感。
2.3 实验五与实验六:特征提取与图像拼接
最后两次实验是整套资料的高潮,涉及 Harris 角点检测、SIFT 特征提取、特征匹配和单应性矩阵估计。实验五通常要求实现 Harris 角点检测,实验六则是用 SIFT + FLANN 做图像拼接。这部分代码量最大,也最容易出问题。
一个典型的 SIFT 匹配和拼接流程如下:
import cv2 import numpy as np # 读取两张待拼接图像 img1 = cv2.imread('left.jpg') img2 = cv2.imread('right.jpg') # 创建 SIFT 检测器 sift = cv2.SIFT_create() # 检测关键点和计算描述子 kp1, des1 = sift.detectAndCompute(img1, None) kp2, des2 = sift.detectAndCompute(img2, None) # FLANN 匹配器参数 FLANN_INDEX_KDTREE = 1 index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5) search_params = dict(checks=50) flann = cv2.FlannBasedMatcher(index_params, search_params) # KNN 匹配 matches = flann.knnMatch(des1, des2, k=2) # Lowe's ratio test 筛选优质匹配 good_matches = [] for m, n in matches: if m.distance < 0.7 * n.distance: good_matches.append(m) # 至少需要 4 对匹配点计算单应性矩阵 if len(good_matches) > 4: src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) # 用 H 矩阵做透视变换 result = cv2.warpPerspective(img1, H, (img1.shape[1] + img2.shape[1], img1.shape[0])) result[0:img2.shape[0], 0:img2.shape[1]] = img2这段代码里,ratio test的 0.7 是个经验值,资料里也是这么写的。如果匹配点太少,可以放宽到 0.75 或 0.8,但误匹配会增多。findHomography的 RANSAC 阈值设为 5.0,单位是像素,意思是重投影误差超过 5 个像素的点会被当作外点。这个值不能太小,否则内点不够,矩阵估计不稳定;也不能太大,否则外点混进来,拼接结果会扭曲。我踩过的坑是:两张图像如果重叠区域太小,SIFT 匹配点不够,findHomography会返回 None,后面warpPerspective直接报错。所以实际跑之前,最好先检查len(good_matches)是否大于 10。
3. OpenCV 学习资料怎么用:别把它当字典翻
3.1 资料的组织逻辑与检索方式
这份 OpenCV 学习资料不是那种按 API 字母顺序排列的手册,而是按功能模块组织的:图像读写、颜色空间转换、几何变换、形态学操作、直方图、轮廓、视频处理等。每个模块下有示例代码和参数说明。我一般不会从头读到尾,而是把它当“配方集”用——遇到具体任务时,先翻到对应模块,看示例代码能不能直接改。
比如要做形态学开运算去噪,资料里会给出cv2.morphologyEx的完整调用和结构元素的创建方式。但要注意,资料里的结构元素是用cv2.getStructuringElement生成的,如果你手写一个np.ones((3,3))传进去,效果可能不一样,因为getStructuringElement生成的矩形核默认锚点在中心,而手写的核在filter2D里锚点也是中心,但在morphologyEx里行为有差异。这个细节资料里没展开,但实际写代码时如果发现腐蚀膨胀结果偏移,大概率是锚点问题。
3.2 把学习资料和六次实验对照着看
单独看 OpenCV 学习资料容易忘,因为缺乏任务驱动。我的习惯是:每做一次实验,就翻到学习资料里对应的章节,把示例代码和实验代码做 diff。比如实验二做高斯滤波,学习资料里可能只给了cv2.GaussianBlur的调用,但实验里要求手动实现,这时候对照着看,就能理解sigma参数在内部是怎么影响核权重的。
另外,学习资料里的视频处理部分和六次实验没有直接重叠,但如果你想把实验扩展到视频流,比如做实时边缘检测,那部分代码可以直接拿来改。我试过把 Canny 检测套到摄像头帧上,延迟大概在 30ms 左右,对于 640x480 的分辨率来说够用了。参数上,视频处理要注意cv2.VideoCapture的缓冲区和帧率设置,默认的CAP_PROP_BUFFERSIZE是 0,如果处理速度跟不上,帧会堆积,导致延迟越来越大。
4. 避坑与排查:六次实验里最容易翻车的五个地方
4.1 现象:SIFT 创建失败,报错AttributeError: module 'cv2' has no attribute 'SIFT_create'
原因:OpenCV 版本问题。从 4.4.0 开始,SIFT 专利到期,被移回主模块,但如果你装的是更早的版本,或者装的是opencv-python-headless,可能没有这个函数。另外,某些国内镜像源里的包版本较旧,也会导致这个问题。
解决:先查版本print(cv2.__version__),如果低于 4.4.0,升级到 4.5 以上。如果用的是opencv-contrib-python,确认安装的是完整版而不是 headless 版。升级命令:pip install --upgrade opencv-python opencv-contrib-python。
4.2 现象:频域滤波后图像全黑或全白
原因:傅里叶变换后的直流分量在fftshift之后位于中心,如果滤波器把中心也滤掉了,图像均值就没了,结果要么全黑要么全白。另外,np.fft.ifft2的结果是复数,直接imshow会显示异常。
解决:检查滤波器的中心位置是否保留。低通滤波器的中心应该是 1,高通滤波器的中心应该是 0。显示之前用np.abs()取模,再归一化到 0-255。如果还是全黑,检查D0是不是设得太小,把大部分频率都滤掉了。
4.3 现象:Canny 边缘检测结果断断续续,或者背景噪声全被检出来
原因:双阈值设置不当。低阈值太高,弱边缘被丢弃;高阈值太低,噪声被当成边缘。另外,如果输入图像没有做高斯模糊,噪声会直接进入梯度计算,导致大量伪边缘。
解决:先用cv2.GaussianBlur做 3x3 或 5x5 的平滑,再跑 Canny。阈值方面,我一般用np.median算图像梯度中值,然后设低阈值为中值的 0.66 倍,高阈值为中值的 1.33 倍。这个比例不是固定的,但比瞎猜靠谱。
4.4 现象:findHomography返回 None,或者拼接结果严重扭曲
原因:匹配点太少,或者匹配点里外点太多。SIFT 在纹理贫乏的区域(比如白墙、天空)提取不到足够的关键点,导致good_matches数量不够。另外,如果两张图像有重复纹理(比如窗户、瓷砖),FLANN 会匹配到错误的位置,RANSAC 也救不回来。
解决:先打印len(good_matches),如果小于 10,考虑换用 ORB 或 AKAZE 特征,它们对纹理要求低一些。如果匹配点够但结果扭曲,把 RANSAC 阈值从 5.0 降到 3.0,或者用cv2.findHomography的method=cv2.LMEDS替代 RANSAC,后者对高斯噪声更鲁棒。
4.5 现象:手动卷积和 OpenCV 内置函数结果不一致
原因:边界填充方式不同。filter2D默认的borderType是BORDER_DEFAULT,实际是反射填充,而手动实现时如果没指定,默认是常量填充(补零)。另外,如果输入是uint8,卷积结果会截断,导致负值变 0,正值超过 255 变 255。
解决:在filter2D里显式指定borderType=cv2.BORDER_REFLECT,并且把输入转成float32再做卷积。对比时用cv2.absdiff算差值,如果最大差值在 1e-3 量级,说明只是浮点误差,可以接受。
5. 进阶技巧:用这套资料做一次完整的图像拼接验证
5.1 从六次实验里抽出一个可复现的验证流程
如果你想验证自己是否真的吃透了这套资料,我建议做一次端到端的图像拼接:从实验一读取图像开始,到实验六的 SIFT 匹配和单应性变换结束,中间穿插实验三的频域滤波做预处理。具体步骤是:先对两张待拼接图像做傅里叶变换,用高通滤波增强边缘,再跑 SIFT 匹配,最后用warpPerspective拼接。这个流程能同时检验频域、特征和几何变换三个模块的理解。
验证时,我一般会记录几个指标:匹配点数量、RANSAC 内点率、拼接后的重叠区域宽度。匹配点数量在 50 到 200 之间比较理想,内点率高于 60% 说明匹配质量不错,重叠区域宽度至少要有图像宽度的 20%,否则拼接缝会很明显。如果内点率低于 40%,我会回头检查频域滤波是不是把太多细节滤掉了,或者 SIFT 的contrastThreshold设得太高。
5.2 参数调优的边界与经验值
这套资料里的参数大多是默认值,但实际用的时候需要根据图像调整。我整理了一个常用参数的边界表:
| 参数 | 常用范围 | 作用 | 调参方向 |
|---|---|---|---|
| SIFT contrastThreshold | 0.01 - 0.04 | 过滤低对比度关键点 | 值越大,关键点越少 |
| Lowe's ratio | 0.6 - 0.8 | 筛选优质匹配 | 值越小,匹配越严格 |
| RANSAC 重投影阈值 | 3.0 - 8.0 | 判断内点 | 值越小,内点要求越高 |
| Canny 低阈值 | 50 - 150 | 弱边缘检测 | 值越低,边缘越多 |
| 高斯核 sigma | 0.8 - 2.0 | 平滑程度 | 值越大,越模糊 |
这张表不是金科玉律,但能帮你快速定位调参方向。比如拼接结果扭曲,先看 RANSAC 阈值是不是太大;匹配点太少,先看 ratio 是不是太严。
5.3 一个我反复踩过的坑
最后说一个血泪经验:这套资料里的实验图像是固定的,但如果你换成自己的照片,SIFT 匹配可能会因为 EXIF 方向信息而失败。手机拍的照片通常带旋转标记,OpenCV 读取时不会自动旋转,导致两张图的方向不一致,匹配点全乱。我后来养成了一个习惯:读图之后先跑一遍cv2.rotate根据 EXIF 校正,或者直接用PIL的ImageOps.exif_transpose处理后再转成 OpenCV 格式。从那以后,我每次做图像拼接之前,都强制走一遍方向检查,再也没出现过匹配点对不上的玄学问题。希望这套资料能帮你把计算机视觉的基础流程跑通,少走点我当年走过的弯路。
本文还有配套的精品资源,点击获取