
简介2019年华为杯数学建模竞赛C题视觉情报信息分析二等奖作品完整收录了获奖团队的代码与过程文件。资源面向备赛学生与建模爱好者展示从图像配准、仿射变换到单目重建、光流测距的完整技术链路对理解计算机视觉类赛题很有参考价值。压缩包共25个文件以C源码为核心配合处理前后的jpg/png图像、任务结果图以及md/pdf/doc说明文档便于对照代码与实验现象学习包体大小24.08MB目录清晰适合按任务模块检索。当前已有108人学习下载。基于本包可掌握交比模型、SIFT匹配、透视变换、光流法等方法的实际落地写法也能借鉴获奖团队在数据读取、函数拆分和结果可视化上的编码习惯是系统备赛和快速上手视觉建模任务的高质量参考。1. 华为杯 C 题二等奖代码包先看懂这个 zip 里的主链路人们下载「2019 华为杯建模竞赛C题代码及文件二等奖.zip」这类压缩包时第一反应是解压找 README其实更该先看论文里的流程图。2019 年华为杯 C 题「视觉情报信息分析」的输入是有重叠区域的实拍图像和带控制点的参考地图要输出拼接全景图、目标像素坐标和换算后的经纬度。二等奖代码包通常是同一套路几个 .py 脚本、一篇论文 PDF、若干图片文件主链路固定在 SIFT 拼接、目标检测、控制点坐标变换三段。参赛者的价值点不在跑通而在看懂每段参数和误差来源对图像配准工程师它也是一套完整的定位样例。下面按这条链路逐段拆解给出可直接运行的 python 代码和参数依据。2. C 题图像拼接代码SIFT 特征提取与 RANSAC 单应性估计2.1 为什么这里首选 SIFT 而不是 ORB华为杯 C 题的实测图像从不同角度、不同距离拍摄同一片区域在相邻两幅图里有明显的旋转和尺度差异。ORB 虽然提取速度快但它的二进制描述子对旋转比较敏感旋转超过 30 度后正确匹配率下降明显SIFT 描述子基于局部梯度直方图对旋转、尺度和光照变化都有较稳定的表现所以多数二等奖代码的拼接段都选 SIFT。需要注意版本差异OpenCV 4.4 之前 SIFT 在 opencv-contrib 的 xfeatures2d 模块里4.4 之后才进主库直接cv2.SIFT_create()就能用。复现时先打印cv2.__version__否则容易遇到module cv2 has no attribute SIFT_create这类报错。另一个常见问题是特征点数量。SIFT_create 默认只提 500 个点对一张三四千万像素的航拍全景拼接来说远远不够直接后果是匹配对数不足、单应矩阵估算失败。代码包里常见的修正就是把nfeatures调到 5000 以上并在弱纹理区域把contrastThreshold从默认 0.04 降到 0.02。这两个参数优先调比换算法快得多。2.2 两张图配准的最小示例代码下面这段是拼接部分最精简的示例代码输入两张有重叠的图输出单应矩阵 H 和筛选后的匹配点。import cv2 import numpy as np def compute_homography(img1, img2): # 提取 SIFT 特征nfeatures 调大保证后续匹配数量 sift cv2.SIFT_create(nfeatures8000) kp1, des1 sift.detectAndCompute(img1, None) kp2, des2 sift.detectAndCompute(img2, None) # KNN 匹配k2 是为下面的 Lowe 比率筛选准备 bf cv2.BFMatcher(cv2.NORM_L2) matches bf.knnMatch(des1, des2, k2) # 最近距离明显小于次近距离才认为是正确匹配 good [m for m, n in matches if m.distance 0.75 * n.distance] if len(good) 10: return None, [] 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) # RANSAC 剔除误匹配5.0 是允许的重投影误差像素 H, mask cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) return H, good逻辑说明先对两张图分别提取 SIFT 关键点和 128 维描述子再用 BFMatcher 做 KNN 匹配。每个查询点返回两个最近邻Lowe 比率测试判断最近距离是否显著小于次近距离0.75 是论文里的经验值匹配质量差时可以收紧到 0.7。findHomography 内部用 RANSAC 迭代每次随机抽 4 组点对估计 3x3 单应矩阵统计符合该矩阵的内点数5.0 表示内点允许的像素偏差。返回的 H 把 img2 坐标投影到 img1 坐标系后续所有图都以同一坐标系为基准。得到 H 后再用 warpPerspective 把 img2 变换过去。实际写代码时不能直接按原图尺寸 warp要先算四个角点变换后的包围盒否则视野外的部分会被裁掉def warp_to_base(img, H): h, w img.shape[:2] corners np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2) dst cv2.perspectiveTransform(corners, H).reshape(-1, 2) x_min, y_min dst.min(axis0).astype(int) x_max, y_max dst.max(axis0).astype(int) # 平移矩阵把变换后的负坐标归零 T np.array([[1, 0, -x_min], [0, 1, -y_min], [0, 0, 1]], dtypenp.float64) warped cv2.warpPerspective(img, T H, (x_max - x_min, y_max - y_min)) return warped, (x_min, y_min)说明四个角点乘 H 后可能出现负坐标直接 warp 会丢内容所以先把包围盒平移量构造成仿射矩阵 T用 T H 作为最终变换输出尺寸就是包围盒宽高。返回值里同时带回平移偏移量后续叠图时把基准图放到对应位置做平均融合即可。2.3 多图拼接的次序与累计误差控制C 题的图像通常有 6 到 10 张沿拍摄方向大致排成一条弧线。最常见的错误是随便选第一张做基准顺序往后拼拼到第三四张就开始变形。稳健做法是选中间一张为基准向两侧扩展每次算新图到基准图的单应矩阵而不是前后两两拼接再合。原因在于每次 warp 都是一次重采样重采样会损失精度链式拼接把误差一路放大到最远端向基准两侧扩展则把误差控制在距离基准最远的图上。另一个容易忽略的问题是重叠区域太小。拼接前先人工确认相邻图的公共区域占比小于 15% 时 SIFT 匹配点数会骤减此时应该缩小拍摄间隔而不是指望算法。若发现拼接处错位按下面这张参数表逐项排查参数默认值调参方向失效表现nfeatures500调到 5000~8000特征点不足匹配对数少contrastThreshold0.04降到 0.02~0.03草地、水面弱纹理提不出点edgeThreshold10保持默认边缘簇拥导致误匹配Lowe 比率0.75收紧到 0.7错配多H 明显偏斜RANSAC 阈值5.0降到 3.0拼接缝两侧错位3. C 题检测环节代码模板匹配与像素坐标提取3.1 模板匹配低标注成本下的首选方案拼接完成后得到一张大尺寸全景图目标数量在个位到十位级别目标外观有题面提供的样例图。这类场景不需要上深度学习模板匹配加阈值筛选是获奖代码里最常见的做法。matchTemplate 沿大图滑动与模板等大的窗口计算每个位置的相似度OpenCV 提供 TM_SQDIFF、TM_CCORR_NORMED、TM_CCOEFF_NORMED 等度量。实践中首选 TM_CCOEFF_NORMED因为它先对模板和窗口各自减去均值再算归一化相关对亮度变化不敏感TM_SQDIFF 对光照敏感同一种光照下拍的两张图还好一旦拼接段做了亮度均衡它的分数就不再稳定。注意模板匹配对旋转敏感。题目给的样例图是一种朝向但全景图里的目标可能转了角度一个模板会漏检。处理办法是准备多角度模板把样例图按 15 度步长旋转生成 24 张逐一匹配后取每个位置的最大分数。步长再细收益有限反而把匹配时间拉长几倍。3.2 检测加 NMS 去重的示例代码def iou(a, b): x1, y1 max(a[0], b[0]), max(a[1], b[1]) x2, y2 min(a[2], b[2]), min(a[3], b[3]) inter max(0, x2 - x1) * max(0, y2 - y1) area_a (a[2] - a[0]) * (a[3] - a[1]) area_b (b[2] - b[0]) * (b[3] - b[1]) return inter / (area_a area_b - inter 1e-6) def detect_targets(panorama, template, threshold0.75): h, w template.shape[:2] res cv2.matchTemplate(panorama, template, cv2.TM_CCOEFF_NORMED) # 高于阈值的所有位置同一个目标会命中一片邻域 loc np.where(res threshold) boxes [] for pt in zip(*loc[::-1]): # loc 是 (y, x)反转成 (x, y) boxes.append([pt[0], pt[1], pt[0] w, pt[1] h, res[pt[1], pt[0]]]) # 按分数降序逐个保留与已选框中不重叠的框 boxes.sort(keylambda b: b[4], reverseTrue) kept [] for b in boxes: if not any(iou(b, k) 0.3 for k in kept): kept.append(b) return kept逻辑说明res 的尺寸是 (H-h1, W-w1)np.where 拿到的是行号列号zip(*loc[::-1])把它还原成 (x, y) 像素坐标。目标在图上占上百像素峰值周围一圈分数都过阈值一个目标会命中几十个框所以必须做非极大值抑制按分数从高到低保留框与已保留框交并比超过 0.3 就丢弃。交并比阈值 0.3 对飞机、舰船这类紧致目标合适目标细长时建议降到 0.2否则相邻目标会被吞并。threshold 决定查全率和误检率的平衡。0.75 在大多数样例上表现均衡目标与模板差异大时降到 0.65 防漏检代价是建筑棱角、阴影边缘出现零星误检误检多就抬到 0.8。调阈值不是拍脑袋把每档阈值下的框数打出来看到框数随阈值变化出现明显平台期就取平台起点的值。3.3 像素坐标输出与可视化验证最终提交要求每个目标的中心像素坐标对检测框取 (x1x2)/2、(y1y2)/2 即可。输出建议用 CSV字段为 target_id、x_center、y_center、score保存时用 utf-8-sig 编码避免 Excel 中文乱码。像素坐标保留整数透视变换的精度在米级亚像素没有意义。可视化验证这一步直接拉开获奖等次差距。把检测框和中心点画到全景图上另存一张标注图漏检、误检、拼接缝切碎目标这类问题在标注图上一眼可见在 CSV 数字里完全看不出来。检查时重点看两类区域拼接重叠带的亮度突变处、图像边缘的暗角处这两处是误检高发区。检测方案适用条件主要代价模板匹配目标外观固定、数量少旋转与尺度敏感HOG SVM单一类别、样本可标需要正负样本YOLO 系列多类别、数量多训练时间长无 GPU 难跑4. C 题坐标换算代码控制点标定与透视变换参数4.1 控制点选取是这道题的拿分关键拼接和检测回答的是目标在图上哪里题目最后要求的是经纬度。从像素坐标到经纬度靠参考地图上标注的控制点每个控制点有图上像素坐标和地图经纬度两组值用多组点对求解坐标变换矩阵。控制点质量直接决定最终精度比算法选择更关键。需要先区分仿射变换和透视变换。仿射变换 6 个自由度只表达旋转、平移、缩放和剪切C 题的拍摄图像相对地图存在视角倾斜梯形区域映射到矩形时仿射变换的残差很大。透视变换即单应矩阵8 个自由度能表达完整的投影关系这正是拼接段已经在用的东西坐标换算段复用同一套数学。变换自由度适用条件残差表现仿射6近似正射、纯平面旋转缩放倾斜视角下四角误差大透视8视角倾斜、一般航拍多点最小二乘后整体均衡控制点选取原则有三条分布在目标四周而不是挤在一侧数量至少 6 个超过 4 个后才可以用最小二乘平均掉单点误差标点时像素误差控制在 5 像素内否则透视变换会把标定误差放大到地面距离。实际操作时一般先把全景图放大到能看清地图细节的倍率再逐点点击记录像素坐标并与地图经纬度一一配对顺序不能乱这是整套代码里最耗人工但最值得花时间的环节。4.2 getPerspectiveTransform 与 findHomography 的选用OpenCV 给两个入口。getPerspectiveTransform 恰好吃 4 组点对输出精确解对单点标定误差零容忍控制点多于 4 个时用 findHomographymethod0 走最小二乘能平均掉随机标定误差担心某个点标错就换 RANSAC 让它自动剔除。下面代码用 7 组控制点求解并把检测结果换算成经纬度import cv2 import numpy as np # 全景图中的控制点像素坐标顺序与 geo_pts 一一对应 pixel_pts np.array([ [120, 340], [780, 355], [150, 900], [820, 880], [450, 600], [300, 700], [620, 720] ], dtypenp.float32) # 参考地图上对应的经纬度 geo_pts np.array([ [121.4738, 31.2304], [121.4812, 31.2315], [121.4745, 31.2241], [121.4809, 31.2248], [121.4771, 31.2278], [121.4758, 31.2260], [121.4788, 31.2269] ], dtypenp.float32) # 7 组点对最小二乘求透视变换 H_geo cv2.findHomography(pixel_pts, geo_pts, method0)[0] # 目标检测得到的像素坐标形状必须是 (n,1,2) target_pixel np.array([[[520, 460]]], dtypenp.float32) target_geo cv2.perspectiveTransform(target_pixel, H_geo) print(target_geo[0][0]) # [经度, 纬度]逻辑说明findHomography 的 method0 对应最小二乘解返回的 H_geo 是 3x3 矩阵把像素齐次坐标映射到经纬度。perspectiveTransform 严格要求输入为 (n,1,2)单点也要包三层括号否则会报维度错误。这里直接用经纬度做变换题目地图覆盖范围在几公里内局部投影变形远小于控制点标定误差可以接受如果换到大范围区域应先把经纬度投影到 UTM 平面坐标再做变换避免地球曲率引入偏差。4.3 重投影误差验证矩阵质量拿到 H_geo 第一件事不是换算目标点而是验证矩阵本身准不准。把全部控制点像素坐标变换到经纬度再与标定值比较算平均误差和最大误差这组数字就是论文里坐标换算部分的精度依据。pred cv2.perspectiveTransform(pixel_pts.reshape(-1, 1, 2), H_geo).reshape(-1, 2) err np.sqrt(((pred - geo_pts) ** 2).sum(axis1)) print(平均误差(度):, err.mean()) print(最大误差(度):, err.max()) # 剔除残差最大的控制点剩下的重新拟合 bad int(err.argmax()) pixel_pts2 np.delete(pixel_pts, bad, axis0) geo_pts2 np.delete(geo_pts, bad, axis0) H_geo2 cv2.findHomography(pixel_pts2, geo_pts2, method0)[0]合格线一般取平均误差小于 0.0002 度、约 20 米最大误差小于 0.0005 度、约 50 米。超限时逐点打印误差找出最大的那个点它多半是标错的控制点删掉重拟合残差会显著下降。这一过程要保留中间结果论文里画一张误差直方图比贴一行最终坐标有说服力得多。5. 复现这份 C 题代码包的 3 个高频坑与验证技巧5.1 中文路径导致图像读不出来不管代码包来自比赛论坛还是 GitHub 下载的 zip解压后先做一件事用 unzip -l 列出内容看图片文件路径里有没有中文或空格。cv2.imread 遇到中文路径会静默返回 None不报错后面所有步骤全部失效。统一用下面这段替换 imreadunzip -l 2019 华为杯建模竞赛C题代码及文件二等奖.zipdef imread_unicode(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)5.2 多图拼接的累积漂移按中心基准向两侧拼接时全景图若出现整体弯曲或同一目标双影说明某两图的 H 估算失败。排查办法是打印每对图的内点数和 RANSAC 内点率内点数骤降的那一对就是漂移起点。临时补救是收紧那对图的 Lowe 比率到 0.65 后重拼根治方案是每张图只与基准图直接配准不做相邻链式传递虽然计算量变大但误差不再随链路放大。5.3 模板匹配在拼接缝附近的误检拼接缝两侧亮度突变matchTemplate 容易在缝附近给出高分。处理办法是在融合阶段对重叠区做 alpha 渐变平滑或在检测阶段把拼接重叠带的位置记录下来落在重叠带内的检测框直接丢弃。验证时把检测框和重投影误差叠加到同一张全景图上输出框与误差点一张图全看清。比赛中我一般把最终结果出成带经纬度标注的全景图每个目标旁写上经度、纬度评委可逐点核对这比在论文里堆表格直观得多。本文还有配套的精品资源点击获取