《计算机视觉:模型、学习和推理》第 15 章-变换模型

📅 发布时间:2026/7/17 6:17:24 👁️ 浏览次数:
《计算机视觉:模型、学习和推理》第 15 章-变换模型
目录二维变换模型核心重点2.1 各类变换模型对比2.2 完整代码实现 效果对比2.3 代码说明变换模型中的学习参数求解3.1 学习欧氏参数代码实现3.2 代码说明变换的鲁棒学习RANSAC4.1 RANSAC 完整代码对比普通方法4.2 效果说明实战应用视觉全景拼接投影变换 单应性5.1 全景拼接完整代码5.2 代码说明讨论与习题6.1 讨论6.2 习题总结前言大家好今天我们深入讲解《计算机视觉模型、学习和推理》中第 15 章-变换模型的核心内容。变换模型是计算机视觉的基础从简单的图像旋转、缩放到复杂的增强现实、全景拼接都离不开它。本文会用通俗易懂的语言拆解核心概念搭配可直接运行的完整 Python 代码和直观的效果对比图让你真正理解并能动手实践。二维变换模型核心重点二维变换模型是对图像像素坐标的 “空间重塑”我们可以把它理解成给图像做不同程度的 “变形手术”从最简单的 “平移旋转” 到复杂的 “透视扭曲”。2.1 各类变换模型对比变换类型核心特点通俗理解自由度典型应用欧氏变换只平移、旋转形状 / 大小 / 比例都不变像推箱子3图像对齐、目标跟踪相似变换欧氏变换 均匀缩放像给图片按比例放大 / 缩小4尺度不变特征匹配仿射变换相似变换 剪切 / 非均匀缩放像捏扁一张纸6图像矫正、数字几何处理投影变换仿射变换 透视效果像从斜角看正方形变梯形8全景拼接、相机透视矫正2.2 完整代码实现 效果对比下面给出完整代码实现 4 种核心变换并在同窗口展示原始图 4 种变换后的效果对比适配 Mac 系统 Matplotlib 中文显示。import cv2 import numpy as np import matplotlib.pyplot as plt # Mac系统Matplotlib中文显示配置 plt.rcParams[font.sans-serif] [Arial Unicode MS, DejaVu Sans] plt.rcParams[axes.unicode_minus] False plt.rcParams[font.family] Arial Unicode MS plt.rcParams[axes.facecolor] white # 1. 读取并预处理图像 # 读取测试图像建议用自己的图片替换路径即可 img cv2.imread(test_img.jpg) # OpenCV默认BGR格式转换为RGB适配Matplotlib显示 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) height, width img.shape[:2] # 取图像中心作为变换的基准点 center (width // 2, height // 2) # 2. 定义各类变换矩阵 # 2.1 欧氏变换旋转45度平移50像素 # 旋转矩阵45度弧度制 rot_mat cv2.getRotationMatrix2D(center, 45, 1.0) # 平移矩阵x50, y50 rot_mat[0, 2] 50 rot_mat[1, 2] 50 # 执行欧氏变换 euclidean_img cv2.warpAffine(img, rot_mat, (width, height), borderValue(255, 255, 255)) # 2.2 相似变换旋转45度缩放0.7倍 similarity_mat cv2.getRotationMatrix2D(center, 45, 0.7) similarity_img cv2.warpAffine(img, similarity_mat, (width, height), borderValue(255, 255, 255)) # 2.3 仿射变换三点映射原始点→变换点 # 原始三个点左上角、右上角、左下角 src_pts np.float32([[0, 0], [width-1, 0], [0, height-1]]) # 变换后的三个点模拟剪切效果 dst_pts np.float32([[0, 0], [width-1, 50], [50, height-1]]) affine_mat cv2.getAffineTransform(src_pts, dst_pts) affine_img cv2.warpAffine(img, affine_mat, (width, height), borderValue(255, 255, 255)) # 2.4 投影变换四点映射模拟透视效果 # 原始四个点四个角 src_pts_persp np.float32([[0, 0], [width-1, 0], [0, height-1], [width-1, height-1]]) # 变换后的四个点右下角偏移制造透视 dst_pts_persp np.float32([[0, 0], [width-1, 0], [50, height-1], [width-50, height-1]]) perspective_mat cv2.getPerspectiveTransform(src_pts_persp, dst_pts_persp) perspective_img cv2.warpPerspective(img, perspective_mat, (width, height), borderValue(255, 255, 255)) # 3. 可视化效果对比 # 创建2行3列的子图显示原始图4种变换图 fig, axes plt.subplots(2, 3, figsize(18, 12)) # 隐藏多余的子图 axes[1, 2].axis(off) # 绘制原始图 axes[0, 0].imshow(img) axes[0, 0].set_title(原始图像, fontsize14, fontweightbold) axes[0, 0].axis(off) # 绘制欧氏变换图 axes[0, 1].imshow(euclidean_img) axes[0, 1].set_title(欧氏变换旋转平移, fontsize14, fontweightbold) axes[0, 1].axis(off) # 绘制相似变换图 axes[0, 2].imshow(similarity_img) axes[0, 2].set_title(相似变换旋转缩放, fontsize14, fontweightbold) axes[0, 2].axis(off) # 绘制仿射变换图 axes[1, 0].imshow(affine_img) axes[1, 0].set_title(仿射变换剪切缩放, fontsize14, fontweightbold) axes[1, 0].axis(off) # 绘制投影变换图 axes[1, 1].imshow(perspective_img) axes[1, 1].set_title(投影变换透视效果, fontsize14, fontweightbold) axes[1, 1].axis(off) # 总标题 fig.suptitle(二维变换模型效果对比, fontsize18, fontweightbold, y0.95) # 显示图像 plt.tight_layout() plt.show() # 4. 增加不确定性模拟噪声 def add_transform_noise(img, transform_mat, noise_std2.0, transform_typeaffine): 给变换结果增加高斯噪声模拟实际场景的不确定性 :param img: 原始图像 :param transform_mat: 变换矩阵 :param noise_std: 噪声标准差越大噪声越多 :param transform_type: 变换类型affine/perspective :return: 带噪声的变换图像 height, width img.shape[:2] # 执行基础变换 if transform_type affine: transformed cv2.warpAffine(img, transform_mat, (width, height), borderValue(255, 255, 255)) else: transformed cv2.warpPerspective(img, transform_mat, (width, height), borderValue(255, 255, 255)) # 添加高斯噪声 noise np.random.normal(0, noise_std, transformed.shape).astype(np.float32) noisy_img transformed.astype(np.float32) noise # 限制像素值在0-255之间 noisy_img np.clip(noisy_img, 0, 255).astype(np.uint8) return noisy_img # 给仿射变换添加噪声 noisy_affine_img add_transform_noise(img, affine_mat, noise_std5.0) # 可视化噪声效果对比 fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 6)) ax1.imshow(affine_img) ax1.set_title(无噪声仿射变换, fontsize14, fontweightbold) ax1.axis(off) ax2.imshow(noisy_affine_img) ax2.set_title(带噪声仿射变换模拟不确定性, fontsize14, fontweightbold) ax2.axis(off) plt.tight_layout() plt.show()2.3 代码说明1.环境依赖运行前需安装依赖pip install opencv-python numpy matplotlib2.图像路径替换test_img.jpg为你的测试图像路径建议用长宽适中的图片效果更明显3.核心函数cv2.getRotationMatrix2D生成旋转欧氏 / 相似矩阵cv2.getAffineTransform通过三点映射生成仿射矩阵cv2.getPerspectiveTransform通过四点映射生成投影矩阵add_transform_noise模拟实际场景的变换不确定性噪声。变换模型中的学习参数求解“学习”本质是根据已知的“原始点 - 变换点”对应关系求解变换矩阵的参数。我们以最常用的 “欧氏参数学习” 为例给出完整代码。3.1 学习欧氏参数代码实现import cv2 import numpy as np import matplotlib.pyplot as plt # Mac中文配置同上 plt.rcParams[font.sans-serif] [Arial Unicode MS, DejaVu Sans] plt.rcParams[axes.unicode_minus] False plt.rcParams[font.family] Arial Unicode MS plt.rcParams[axes.facecolor] white # 1. 生成模拟的对应点对原始点→欧氏变换后的点 np.random.seed(42) # 固定随机种子保证结果可复现 num_pts 20 # 20个对应点 # 原始随机点 src_pts np.random.randint(0, 300, (num_pts, 2)).astype(np.float32) # 手动构造欧氏变换旋转30度 平移(50, 30) angle 30 tx, ty 50, 30 # 旋转矩阵 rot_mat np.array([[np.cos(np.radians(angle)), -np.sin(np.radians(angle))], [np.sin(np.radians(angle)), np.cos(np.radians(angle))]]) # 变换后的点原始点×旋转矩阵 平移 dst_pts np.dot(src_pts, rot_mat.T) np.array([tx, ty]) # 2. 学习求解欧氏变换参数 # 使用cv2.estimateAffinePartial2D专门求解相似/欧氏变换参数设置为纯欧氏 euclidean_mat, inliers cv2.estimateAffinePartial2D(src_pts, dst_pts, methodcv2.RANSAC) # 提取求解出的参数 rot_angle np.degrees(np.arctan2(euclidean_mat[1, 0], euclidean_mat[0, 0])) # 旋转角度 trans_x euclidean_mat[0, 2] # x平移 trans_y euclidean_mat[1, 2] # y平移 # 3. 用求解出的矩阵还原变换 recovered_pts cv2.transform(src_pts.reshape(-1, 1, 2), euclidean_mat).reshape(-1, 2) # 4. 可视化对比 fig, ax plt.subplots(1, 1, figsize(10, 8)) # 绘制原始点 ax.scatter(src_pts[:, 0], src_pts[:, 1], cblue, label原始点, s50) # 绘制真实变换点 ax.scatter(dst_pts[:, 0], dst_pts[:, 1], cred, label真实变换点, s50) # 绘制求解后还原的点 ax.scatter(recovered_pts[:, 0], recovered_pts[:, 1], cgreen, marker*, label学习后还原的点, s100) ax.set_title(f欧氏参数学习结果\n求解角度{rot_angle:.1f}°真实30°平移({trans_x:.1f}, {trans_y:.1f})真实50,30, fontsize12, fontweightbold) ax.legend(fontsize10) ax.axis(equal) # 等比例显示避免变形 plt.show() # 输出求解的变换矩阵 print(求解出的欧氏变换矩阵) print(euclidean_mat)3.2 代码说明cv2.estimateAffinePartial2DOpenCV 中专门用于求解相似 / 欧氏变换的函数能自动从对应点对中学习参数我们先手动构造 “原始点→欧氏变换点” 的对应关系再用函数反推参数验证 “学习” 的效果结果会显示求解的角度 / 平移与真实值几乎一致证明参数学习的有效性。变换的鲁棒学习RANSAC实际场景中点对匹配会有“外点”错误匹配普通方法会被外点带偏而RANSAC随机抽样一致性是解决这个问题的 “神器”—— 可以理解为从一堆数据中筛选出 “靠谱” 的内点用内点求解参数忽略不靠谱的外点。4.1 RANSAC 完整代码对比普通方法import cv2 import numpy as np import matplotlib.pyplot as plt # Mac中文配置 plt.rcParams[font.sans-serif] [Arial Unicode MS, DejaVu Sans] plt.rcParams[axes.unicode_minus] False plt.rcParams[font.family] Arial Unicode MS plt.rcParams[axes.facecolor] white # 1. 生成带外点的对应点对 np.random.seed(42) num_inliers 20 # 20个内点正确匹配 num_outliers 8 # 8个外点错误匹配 # 生成内点欧氏变换 src_inliers np.random.randint(0, 300, (num_inliers, 2)).astype(np.float32) angle 30 tx, ty 50, 30 rot_mat np.array([[np.cos(np.radians(angle)), -np.sin(np.radians(angle))], [np.sin(np.radians(angle)), np.cos(np.radians(angle))]]) dst_inliers np.dot(src_inliers, rot_mat.T) np.array([tx, ty]) # 生成外点随机错误匹配 src_outliers np.random.randint(0, 300, (num_outliers, 2)).astype(np.float32) dst_outliers np.random.randint(0, 300, (num_outliers, 2)).astype(np.float32) # 合并内点和外点 src_pts np.vstack([src_inliers, src_outliers]) dst_pts np.vstack([dst_inliers, dst_outliers]) # 2. 两种方法求解参数 # 方法1普通最小二乘无鲁棒性会被外点干扰 ls_mat, _ cv2.estimateAffinePartial2D(src_pts, dst_pts, methodcv2.LMEDS) # LMEDS近似普通LS # 方法2RANSAC鲁棒方法抗外点 ransac_mat, inliers_mask cv2.estimateAffinePartial2D(src_pts, dst_pts, methodcv2.RANSAC, ransacReprojThreshold5.0) # 关键修复将inliers_mask从三维压缩为一维 inliers_mask inliers_mask.ravel() # 新增这一行把(28,1,1)转为(28,) # 3. 还原变换结果 ls_recovered cv2.transform(src_pts.reshape(-1, 1, 2), ls_mat).reshape(-1, 2) ransac_recovered cv2.transform(src_pts.reshape(-1, 1, 2), ransac_mat).reshape(-1, 2) # 4. 可视化对比 fig, (ax1, ax2) plt.subplots(1, 2, figsize(16, 7)) # 普通方法结果 ax1.scatter(src_pts[:, 0], src_pts[:, 1], cblue, label原始点, s40) ax1.scatter(dst_pts[:, 0], dst_pts[:, 1], cred, label真实点含外点, s40) ax1.scatter(ls_recovered[:, 0], ls_recovered[:, 1], cgreen, marker*, label普通方法还原, s80) ax1.set_title(普通最小二乘被外点干扰, fontsize14, fontweightbold) ax1.legend() ax1.axis(equal) # RANSAC方法结果修复后索引正常 ax2.scatter(src_pts[:, 0], src_pts[:, 1], cblue, label原始点, s40) ax2.scatter(dst_pts[inliers_mask1, 0], dst_pts[inliers_mask1, 1], cred, label内点正确匹配, s40) ax2.scatter(dst_pts[inliers_mask0, 0], dst_pts[inliers_mask0, 1], cgray, label外点错误匹配, s40, alpha0.5) ax2.scatter(ransac_recovered[:, 0], ransac_recovered[:, 1], cgreen, marker*, labelRANSAC还原, s80) ax2.set_title(RANSAC鲁棒学习忽略外点, fontsize14, fontweightbold) ax2.legend() ax2.axis(equal) plt.tight_layout() plt.show() # 输出内点数量 print(fRANSAC筛选出的内点数量{np.sum(inliers_mask)} / {len(src_pts)})4.2 效果说明普通最小二乘会把外点也纳入计算还原结果严重偏离真实值RANSAC能自动识别并忽略外点仅用内点求解参数还原结果几乎和真实内点一致ransacReprojThreshold核心参数阈值越小筛选的内点越严格。实战应用视觉全景拼接投影变换 单应性全景拼接是变换模型的经典应用核心是通过单应性矩阵投影变换的一种拼接多张图像。5.1 全景拼接完整代码import cv2 import numpy as np import matplotlib.pyplot as plt # Mac中文配置 plt.rcParams[font.sans-serif] [Arial Unicode MS, DejaVu Sans] plt.rcParams[axes.unicode_minus] False plt.rcParams[font.family] Arial Unicode MS plt.rcParams[axes.facecolor] white def stitch_images(img1, img2): 拼接两张图像基于SIFT特征匹配RANSAC求解单应性矩阵 :param img1: 左图 :param img2: 右图 :return: 拼接后的全景图 # 1. 提取SIFT特征 sift cv2.SIFT_create() kp1, des1 sift.detectAndCompute(img1, None) kp2, des2 sift.detectAndCompute(img2, None) # 2. 特征匹配FLANN匹配器 FLANN_INDEX_KDTREE 1 index_params dict(algorithmFLANN_INDEX_KDTREE, trees5) search_params dict(checks50) flann cv2.FlannBasedMatcher(index_params, search_params) matches flann.knnMatch(des1, des2, k2) # 3. 筛选优质匹配Lowes准则 good_matches [] for m, n in matches: if m.distance 0.7 * n.distance: good_matches.append(m) # 4. 求解单应性矩阵RANSAC 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(dst_pts, src_pts, cv2.RANSAC, 5.0) # 5. 拼接图像 # 获取拼接后的图像尺寸 height max(img1.shape[0], img2.shape[0]) width img1.shape[1] img2.shape[1] # 对右图做投影变换对齐左图 warped_img2 cv2.warpPerspective(img2, H, (width, height)) # 将左图放入拼接画布 warped_img2[0:img1.shape[0], 0:img1.shape[1]] img1 # 6. 裁剪黑边可选 gray cv2.cvtColor(warped_img2, cv2.COLOR_RGB2GRAY) _, thresh cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) x, y, w, h cv2.boundingRect(contours[0]) stitched_img warped_img2[y:yh, x:xw] return stitched_img, good_matches, kp1, kp2 # 主程序 if __name__ __main__: # 读取两张待拼接的图像建议用连续拍摄的两张图 img1 cv2.imread(panorama1.jpg) img2 cv2.imread(panorama2.jpg) img1 cv2.cvtColor(img1, cv2.COLOR_BGR2RGB) img2 cv2.cvtColor(img2, cv2.COLOR_BGR2RGB) # 拼接图像 stitched_img, matches, kp1, kp2 stitch_images(img1, img2) # 可视化结果 fig, (ax1, ax2, ax3) plt.subplots(1, 3, figsize(20, 8)) # 显示左图 ax1.imshow(img1) ax1.set_title(拼接左图, fontsize14, fontweightbold) ax1.axis(off) # 显示右图 ax2.imshow(img2) ax2.set_title(拼接右图, fontsize14, fontweightbold) ax2.axis(off) # 显示拼接结果 ax3.imshow(stitched_img) ax3.set_title(全景拼接结果, fontsize14, fontweightbold) ax3.axis(off) # 可视化特征匹配可选 match_img cv2.drawMatches(img1, kp1, img2, kp2, matches[:50], None, flagscv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) fig2, ax4 plt.subplots(1, 1, figsize(20, 8)) ax4.imshow(cv2.cvtColor(match_img, cv2.COLOR_BGR2RGB)) ax4.set_title(SIFT特征匹配结果前50个优质匹配, fontsize14, fontweightbold) ax4.axis(off) plt.show()5.2 代码说明特征提取用 SIFT 提取图像的关键特征点不变特征特征匹配用 FLANN 快速匹配两张图的特征点单应性求解用 RANSAC 从匹配点中求解投影变换矩阵单应性图像拼接将右图做投影变换后与左图拼接最终裁剪黑边得到全景图。讨论与习题6.1 讨论1.为什么投影变换的自由度是 8而仿射变换是 6提示投影矩阵有 9 个元素但齐次坐标下比例无关故自由度 8仿射矩阵最后一行固定为 [0,0,1]故自由度 62.RANSAC 的核心步骤是什么为什么它能抵抗外点干扰提示随机抽样→模型拟合→内点计数→迭代最优3.单应性矩阵适用于什么场景如果是非平面场景单应性还适用吗6.2 习题修改二维变换代码实现 “绕任意点旋转 任意缩放” 的相似变换并对比效果给 RANSAC 代码增加不同比例的外点如 20%、50%观察内点筛选效果的变化尝试用 3 张图像实现全景拼接提示先拼接前两张再和第三张拼接。总结1.二维变换模型按复杂度分为欧氏平移旋转→相似 缩放→仿射 剪切→投影 透视自由度依次增加2.变换参数的“学习”本质是从对应点对中求解变换矩阵RANSAC 是鲁棒学习的核心方法能有效过滤外点3.变换模型的经典应用包括图像矫正、全景拼接、增强现实等核心是通过单应性 / 仿射矩阵实现图像对齐。希望本文能帮助你理解变换模型的核心内容所有代码均可直接运行建议动手修改参数直观感受不同变换的效果如果有问题欢迎在评论区交流