☰
计算机视觉六次实验:从OpenCV环境配置到目标跟踪的工程化实践
2026/10/13 15:09:12 网站建设 项目流程

简介:这份资源面向计算机视觉初学者与在校学生,整理了浙江大学2020-2021秋冬学期计算机视觉课程的完整学习材料,涵盖六次实验与复习资料,并附带OpenCV学习内容,适合需要系统入门图像处理与深度学习实践的学习者。包内共1319个文件,以jpg图像、txt说明、pgm灰度图为主,另有pdf讲义、py脚本、png示意图及少量模型checkpoint与json配置,压缩包约72.75MB,目录按实验与资料分类,便于逐项查阅。实验内容覆盖无声短片制作、圆形与直线检测、Harris角点检测、Eigenface人脸识别以及CNN训练等典型任务,从传统图像处理逐步过渡到深度学习,能帮助读者理解算法原理并动手复现。目前已有330人学习下载,可作为课程作业参考、期末复习与OpenCV练手的实用素材。

1. 从六次实验到一套可复用的计算机视觉工程底子

很多人第一次接触计算机视觉,是从一门课的六次实验开始的:图像基础操作、特征提取、相机标定、立体视觉、光流、目标跟踪,一路做下来,代码能跑,报告能交,但合上电脑就忘。问题不在课程本身,而在于这些实验被当成了一次性作业,而不是一套可以反复调用的工程底子。这份“6次实验+复习资料+OpenCV学习资料”的组合,真正值钱的地方不是资料本身,而是它把计算机视觉从数学定义到 OpenCV 落地串成了一条线。适合谁?适合已经会写 Python、想系统补齐视觉工程能力的人,也适合准备面试、需要把零散知识点串成体系的人。下面我按自己复现这套实验的顺序,把每一步的选型理由、参数含义和踩坑点讲清楚。

2. 六次实验的依赖环境与最小可跑通配置

2.1 为什么优先用 conda 而不是 pip 裸装

计算机视觉实验最怕的不是算法难,而是环境崩。OpenCV 的 Python 包在不同平台上的 wheel 差异很大,尤其是涉及 SIFT、SURF 这类专利算法时,opencv-python和opencv-contrib-python的功能边界完全不同。我一般会用一个独立 conda 环境锁死版本,避免和系统里其他项目的 numpy 打架。

# 创建独立环境,python 版本不要追新,3.8~3.10 兼容性最好 conda create -n cvlab python=3.9 -y conda activate cvlab # 核心依赖:contrib 版本才带 SIFT/SURF/光流等实验常用模块 pip install opencv-contrib-python==4.8.1.78 pip install numpy==1.24.3 matplotlib==3.7.2 scipy==1.11.1 pip install scikit-image==0.21.0 # 部分特征对比实验会用到

逻辑说明:opencv-contrib-python比普通opencv-python多出xfeatures2d等模块,六次实验里至少有两三次会用到。参数上,numpy 锁在 1.24 是因为 1.25 之后部分旧代码的np.float别名被彻底移除,容易在复制来的示例里翻车。安装完用下面这段验证:

import cv2 import numpy as np print("OpenCV:", cv2.__version__) print("是否有 SIFT:", hasattr(cv2, "SIFT_create")) print("是否有 xfeatures2d:", hasattr(cv2, "xfeatures2d")) # 最小图像读写测试,确认编解码链路正常 img = np.zeros((100, 100, 3), dtype=np.uint8) img[20:80, 20:80] = (0, 255, 0) cv2.imwrite("test_green.png", img) back = cv2.imread("test_green.png") print("读回形状:", back.shape, "像素均值:", back.mean())

如果hasattr(cv2, "xfeatures2d")返回 False,说明装成了普通版,直接卸载重装 contrib 版即可。这一步看着简单,但每年都有人在这里卡半天。

2.2 实验数据的组织方式决定你后期调试效率

六次实验会反复读写图像、视频、标定板照片。我习惯按实验编号建目录,原始数据只读,输出统一进output/,中间结果进cache/。这样做的直接好处是:当某个实验参数调错时,你能快速判断是输入问题还是处理问题,而不是在一堆同名文件里找。

cvlab/ ├── data/ │ ├── exp1_basic/ # 图像基础操作素材 │ ├── exp2_feature/ # 特征提取测试图 │ ├── exp3_calib/ # 棋盘格标定图 │ ├── exp4_stereo/ # 左右目图像对 │ ├── exp5_flow/ # 光流视频帧 │ └── exp6_track/ # 跟踪视频 ├── output/ # 每次实验的可视化结果 ├── cache/ # 中间矩阵、标定参数 └── src/ # 每个实验一个脚本

提示:标定和立体视觉实验的中间结果(相机矩阵、畸变系数)一定要落盘成.npz,否则每次调参都要重新标定,时间成本极高。

3. 图像基础与特征提取:从像素操作到关键点匹配

3.1 图像基础实验里最容易被忽略的三个参数

第一次实验通常是灰度化、滤波、边缘检测、形态学。看起来简单,但cv2.Canny的双阈值、cv2.GaussianBlur的核大小、cv2.morphologyEx的结构元素形状,这三个参数直接决定后续所有实验的输入质量。我一般会写一个参数对比脚本,把不同组合并排输出,而不是凭感觉调。

import cv2 import numpy as np img = cv2.imread("data/exp1_basic/scene.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯核必须是奇数,越大越模糊;这里对比 3/5/7 for k in [3, 5, 7]: blur = cv2.GaussianBlur(gray, (k, k), 0) # Canny 双阈值比例经验值 1:2 或 1:3 edges = cv2.Canny(blur, 50, 150) cv2.imwrite(f"output/exp1_canny_k{k}.png", edges) # 形态学:开运算去噪,闭运算补洞,结构元素形状影响很大 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) opened = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel) closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel) cv2.imwrite("output/exp1_open.png", opened) cv2.imwrite("output/exp1_close.png", closed)

逻辑说明:高斯核大小影响边缘的连续性,核太大边缘会断,核太小噪声去不掉。Canny 的低阈值控制弱边缘保留,高阈值控制强边缘起点,两者比例失衡会导致边缘要么碎要么糊。形态学结构元素用椭圆比矩形更贴近自然物体轮廓,这是我在做细胞分割类任务时养成的习惯。

3.2 SIFT 与 ORB 的选型:精度和速度的取舍

第二次实验通常做特征提取与匹配。SIFT 精度高但慢,ORB 快但旋转不变性弱。我的经验是:如果图像分辨率超过 1080p 且匹配对精度要求高,用 SIFT;如果是视频流实时处理,用 ORB。下面这段代码把两者放在一起对比,方便你直观感受差异。

import cv2 import numpy as np import time img1 = cv2.imread("data/exp2_feature/box.png", 0) img2 = cv2.imread("data/exp2_feature/box_in_scene.png", 0) def match_with(detector, norm_type, ratio=0.75): t0 = time.time() kp1, des1 = detector.detectAndCompute(img1, None) kp2, des2 = detector.detectAndCompute(img2, None) bf = cv2.BFMatcher(norm_type) raw = bf.knnMatch(des1, des2, k=2) # Lowe's ratio test,过滤误匹配 good = [m for m, n in raw if m.distance < ratio * n.distance] return kp1, kp2, good, time.time() - t0 sift = cv2.SIFT_create(nfeatures=500) kp1, kp2, good_sift, t_sift = match_with(sift, cv2.NORM_L2) orb = cv2.ORB_create(nfeatures=500) kp1o, kp2o, good_orb, t_orb = match_with(orb, cv2.NORM_HAMMING) print(f"SIFT 匹配数 {len(good_sift)},耗时 {t_sift:.3f}s") print(f"ORB 匹配数 {len(good_orb)},耗时 {t_orb:.3f}s")

参数说明:nfeatures控制关键点上限,设太大会拖慢匹配,设太小会丢特征。ratio=0.75是 Lowe 论文里的经验值,低于 0.7 匹配更严但数量骤减,高于 0.8 误匹配明显增多。ORB 必须用NORM_HAMMING,用 L2 会得到完全错误的结果,这是新手最常见的翻车点之一。

4. 相机标定与立体视觉:把三维重建跑通的关键步骤

4.1 棋盘格标定的完整流程与参数落盘

第三次实验是相机标定,第四次是立体视觉,这两次实验是整门课的分水岭。标定的核心是cv2.findChessboardCorners加cv2.calibrateCamera,但真正决定精度的是棋盘格角点的亚像素优化和标定图像的数量与角度分布。

import cv2 import numpy as np import glob # 棋盘格内角点数量,注意是内角点不是格子数 pattern = (9, 6) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp = np.zeros((pattern[0] * pattern[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) objpoints, imgpoints = [], [] images = glob.glob("data/exp3_calib/*.jpg") for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern, None) if ret: # 亚像素优化,窗口大小 11x11 是常用值 corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners2) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print("重投影误差:", ret) print("相机内参:\n", mtx) print("畸变系数:", dist.ravel()) # 落盘,后续立体视觉实验直接加载 np.savez("cache/calib_params.npz", mtx=mtx, dist=dist)

逻辑说明:cornerSubPix的窗口大小决定亚像素精度,11x11 在多数分辨率下稳定。重投影误差ret是判断标定质量的直接指标,一般要小于 0.5 像素,超过 1 像素说明有图像角点检测错误或角度分布太单一。标定图像建议 15 张以上,且棋盘格要覆盖画面四个角和中心,否则畸变系数会不准。

4.2 立体校正与视差图:从左右目到深度

第四次实验在标定基础上做立体校正和视差计算。核心是cv2.stereoCalibrate和cv2.StereoSGBM_create。SGBM 的参数多,但真正影响效果的只有几个。

import cv2 import numpy as np # 加载双目标定结果(假设已通过 stereoCalibrate 得到) data = np.load("cache/stereo_params.npz") K1, D1, K2, D2 = data["K1"], data["D1"], data["K2"], data["D2"] R, T = data["R"], data["T"] img_size = tuple(data["img_size"]) R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify( K1, D1, K2, D2, img_size, R, T, alpha=0 ) left = cv2.imread("data/exp4_stereo/left.png") right = cv2.imread("data/exp4_stereo/right.png") # 去畸变 + 校正 map1x, map1y = cv2.initUndistortRectifyMap(K1, D1, R1, P1, img_size, cv2.CV_32FC1) map2x, map2y = cv2.initUndistortRectifyMap(K2, D2, R2, P2, img_size, cv2.CV_32FC1) left_rect = cv2.remap(left, map1x, map1y, cv2.INTER_LINEAR) right_rect = cv2.remap(right, map2x, map2y, cv2.INTER_LINEAR) # SGBM 视差计算 window_size = 5 min_disp = 0 num_disp = 16 * 5 # 必须是 16 的倍数 stereo = cv2.StereoSGBM_create( minDisparity=min_disp, numDisparities=num_disp, blockSize=window_size, P1=8 * 3 * window_size ** 2, P2=32 * 3 * window_size ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32 ) disp = stereo.compute(left_rect, right_rect).astype(np.float32) / 16.0 disp_norm = cv2.normalize(disp, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) cv2.imwrite("output/exp4_disparity.png", disp_norm)

参数说明:numDisparities必须是 16 的倍数,它决定能测到的最近距离,设太小近处物体视差会溢出。blockSize越大视差图越平滑但细节丢失,5 或 7 是常用值。uniquenessRatio控制匹配唯一性,10 到 15 之间比较稳。P1、P2是平滑惩罚项,按blockSize的平方缩放是 OpenCV 官方示例的推荐做法。

注意:立体校正后一定要检查左右图是否行对齐,如果极线不水平,说明stereoRectify的输入标定参数有问题,视差图会整体错乱。

5. 光流与目标跟踪:视频序列里的运动估计

5.1 稀疏光流与稠密光流的适用场景

第五次实验是光流,第六次是目标跟踪。光流分稀疏(Lucas-Kanade)和稠密(Farneback)两种。稀疏光流适合跟踪少量特征点,速度快;稠密光流给出每个像素的运动,适合运动分割但计算量大。

import cv2 import numpy as np cap = cv2.VideoCapture("data/exp5_flow/video.mp4") ret, old_frame = cap.read() old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY) # Shi-Tomasi 角点作为稀疏光流跟踪点 feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7, blockSize=7) p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params) lk_params = dict(winSize=(15, 15), maxLevel=2, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)) while True: ret, frame = cap.read() if not ret: break frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params) if p1 is not None: good_new = p1[st == 1] good_old = p0[st == 1] for (new, old) in zip(good_new, good_old): a, b = new.ravel() c, d = old.ravel() cv2.line(frame, (int(a), int(b)), (int(c), int(d)), (0, 255, 0), 2) cv2.imwrite("output/exp5_flow_frame.png", frame) old_gray = frame_gray.copy() p0 = good_new.reshape(-1, 1, 2) break # 演示单帧,实际处理去掉

参数说明:winSize是搜索窗口,15x15 适合中等运动速度,运动快就加大,但太大容易跟丢。maxLevel是金字塔层数,2 层能处理一定尺度变化。qualityLevel=0.3表示角点质量阈值,越低点越多但越不稳定。minDistance控制角点最小间距,避免扎堆。

5.2 跟踪算法选型:KCF、CSRT 与 MOSSE 的取舍

第六次实验的目标跟踪,OpenCV 提供了多种跟踪器。我的经验是:精度优先用 CSRT,速度优先用 MOSSE,平衡用 KCF。但要注意,OpenCV 4.5 之后部分跟踪器 API 有变化,创建方式从cv2.TrackerKCF_create()变成了cv2.legacy.TrackerKCF_create(),这是升级版本后最常见的报错来源。

import cv2 # OpenCV 4.5+ 需要 legacy 命名空间 tracker = cv2.legacy.TrackerCSRT_create() cap = cv2.VideoCapture("data/exp6_track/video.mp4") ret, frame = cap.read() # 手动框选初始目标,实际脚本里可以用固定坐标 bbox = cv2.selectROI("select", frame, fromCenter=False, showCrosshair=True) tracker.init(frame, bbox) while True: ret, frame = cap.read() if not ret: break ok, box = tracker.update(frame) if ok: x, y, w, h = [int(v) for v in box] cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imwrite("output/exp6_track_frame.png", frame) break

逻辑说明:CSRT 精度高但帧率低,适合离线分析;KCF 速度快但在遮挡时容易漂移;MOSSE 最快但精度最差。如果视频里目标有遮挡,建议加一个简单的重检测机制,比如每 30 帧用模板匹配重新定位一次,否则跟丢后很难恢复。

6. 避坑与排查:六次实验里最容易翻车的五个点

6.1 现象:SIFT 创建报错module 'cv2' has no attribute 'SIFT_create'

原因:装的是opencv-python而不是opencv-contrib-python,或者版本低于 4.4。解决:卸载后重装 contrib 版,并确认版本号。如果公司网络受限,可以先用pip download在能联网的机器上下载 wheel 再离线安装。

6.2 现象:标定重投影误差大于 2 像素

原因:棋盘格角点检测错误,或者标定图像角度太单一。解决:先用cv2.drawChessboardCorners把检测结果画出来逐张检查,剔除检测错误的图;再补充倾斜角度大、棋盘格靠近画面边缘的图像。亚像素优化窗口从 11 调到 5 有时也能改善。

6.3 现象:视差图大面积黑色或噪点

原因:numDisparities设得太小,或者左右图没有真正行对齐。解决:先检查校正后的左右图,在相同行上找同一个物体,看是否水平对齐;再把numDisparities调到 16 的倍数并逐步增大,直到近处物体视差不再溢出。speckleWindowSize从 100 调到 200 可以去掉部分噪点。

6.4 现象:光流跟踪点大量丢失

原因:winSize太小或maxLevel不够,目标运动超出搜索范围。解决:把winSize从 15 加到 21 或 31,maxLevel从 2 加到 3。同时检查goodFeaturesToTrack的qualityLevel,太低会引入不稳定角点,太高则点太少。

6.5 现象:跟踪器初始化后立即报错或跟丢

原因:OpenCV 版本与跟踪器 API 不匹配,或者初始 bbox 超出图像边界。解决:确认使用cv2.legacy.TrackerXXX_create();检查 bbox 的w、h是否大于 0 且不越界。如果视频第一帧目标太小,先放大再框选,或者换用 CSRT 这类对初始框更鲁棒的跟踪器。

7. 把六次实验压成一套可复用的视觉工具函数

六次实验做完,如果每个脚本都是独立的,下次遇到新任务还得重写。我的习惯是把重复逻辑抽成一个小工具模块,比如cv_utils.py,里面放图像读写、标定参数加载、视差计算、跟踪器工厂这几类函数。这样做的价值在于:当你换一个数据集或换一个场景时,只需要改参数,不需要改结构。

# cv_utils.py import cv2 import numpy as np def load_calib(path="cache/calib_params.npz"): """加载标定参数,返回 mtx, dist""" data = np.load(path) return data["mtx"], data["dist"] def undistort(img, mtx, dist): """去畸变,返回校正后图像""" h, w = img.shape[:2] new_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) return cv2.undistort(img, mtx, dist, None, new_mtx) def make_tracker(name="csrt"): """跟踪器工厂,统一 API 差异""" name = name.lower() if name == "csrt": return cv2.legacy.TrackerCSRT_create() if name == "kcf": return cv2.legacy.TrackerKCF_create() if name == "mosse": return cv2.legacy.TrackerMOSSE_create() raise ValueError(f"不支持的跟踪器: {name}") def compute_disparity(left, right, num_disp=80, block=5): """SGBM 视差计算,参数集中管理""" stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=num_disp, blockSize=block, P1=8 * 3 * block ** 2, P2=32 * 3 * block ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32 ) disp = stereo.compute(left, right).astype(np.float32) / 16.0 return disp

逻辑说明:load_calib和undistort把标定实验的成果固化下来,后续任何实验都能直接调用。make_tracker用工厂模式屏蔽了 OpenCV 版本差异,换版本时只改这一处。compute_disparity把 SGBM 的参数集中管理,调参时不用在多个脚本里找。这套工具函数不复杂,但能让你从“每次重写”变成“每次复用”。

验证方法也很直接:拿一张新拍的棋盘格图,走一遍load_calib加undistort,看直线是否变直;拿一段新视频,用make_tracker("csrt")跑一遍,看框是否稳定。如果这两步都通过,说明你的视觉底子已经搭起来了。

我自己的教训是:早期做实验总想一次调出完美结果,结果在参数上耗掉大量时间。后来改成先跑通最小闭环,再逐步加精度,效率反而高得多。这套六次实验的资料,最大的价值不是让你交六份报告,而是让你在反复调试中形成自己的参数直觉和排错习惯。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询