ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenCV图像拼接实战:Stitcher自动与手动方案详解

OpenCV图像拼接实战:Stitcher自动与手动方案详解 1. 图片拼接到底在拼什么从两张照片到一张全景的底层逻辑很多人第一次听到“图片拼接”脑子里浮现的是Photoshop里那种手动拖拽对齐的操作。但OpenCV里的拼接完全是另一回事——它是让算法自动找到两张照片之间的空间变换关系然后把它们无缝地融合成一张更宽视野的图像。这件事听起来简单做起来坑不少。我拿最常见的场景举例你站在山顶手机横着拍了一张左边的风景又拍了一张右边的风景两张照片有重叠区域。人眼能轻松脑补出全景但计算机不行它需要解决三个核心问题。第一找到两张图中相同的特征点比如同一块岩石、同一棵树的角点第二根据这些匹配点计算出一个变换矩阵描述第二张图相对于第一张图该怎么旋转、平移、缩放第三把变换后的图像叠加在一起处理重叠区域的亮度和颜色差异让接缝看不出来。OpenCV的Stitcher类把这三步全部封装好了你只需要调用stitch()方法传进去一个图像列表它就能返回拼接结果。但封装好不代表没有坑恰恰相反因为内部步骤多、参数敏感实际使用中经常遇到拼接失败、黑边、畸变、接缝明显等问题。这篇文章我会把Stitcher的工作流程拆开讲清楚再补充手动拼接的方案作为备选最后把我在实际项目中踩过的坑和排查方法一并整理出来。适合谁看如果你已经会用OpenCV读写图片、做基本的图像处理但还没碰过拼接模块这篇能帮你快速上手。如果你已经试过Stitcher但总是失败这篇能帮你找到原因。如果你需要更精细的控制手动拼接部分会给你完整的替代方案。2. Stitcher模块的整体设计与方案选型2.1 为什么OpenCV要把拼接封装成一个类图像拼接在计算机视觉里算是一个经典问题但它的实现链路很长。从特征检测、特征匹配、变换估计、图像变换、接缝估计到多频段融合每一步都有多种算法可选。如果让每个开发者自己从头搭光是选特征检测器就要纠结半天——SIFT还是ORBSURF还是AKAZE匹配完了用RANSAC还是LMEDS变换模型用单应性矩阵还是仿射变换OpenCV的Stitcher类本质上是一个预配置的流水线。它内部默认使用ORB特征检测器免费且速度不错用BFMatcher做暴力匹配用RANSAC估计单应性矩阵然后用warper做透视变换最后用MultiBandBlender做多频段融合。这套默认配置在大多数场景下能跑出不错的结果省去了大量调参时间。但默认配置不是万能的。比如ORB在纹理较少的场景比如纯色墙面、天空占比大的风景照下特征点数量不够匹配就会失败。再比如MultiBandBlender虽然融合效果好但计算量大处理高分辨率图像时速度会明显下降。所以理解内部流程才能在默认配置失效时知道该调哪里。2.2 自动拼接和手动拼接的取舍Stitcher类提供的是全自动拼接你给图它出结果。但全自动意味着你失去了对中间过程的控制。如果拼接结果不理想你很难知道是哪一步出了问题。手动拼接则是把每一步拆开自己控制特征检测器、匹配器、变换估计方法和融合方式。这样做的好处是灵活坏处是代码量大、调参复杂。我的建议是先用Stitcher试如果能满足需求就直接用如果失败或者效果差再切到手动方案定位问题。下面这张表对比了两种方案的核心差异对比维度Stitcher自动拼接手动拼接代码量5行以内100行以上可控性低只能调少量参数高每步都可替换适用场景标准全景拼接、快速验证特殊场景、需要精细控制调试难度失败时难以定位原因可逐步检查中间结果速度默认配置中等可根据需求优化2.3 Stitcher内部流水线的六个阶段理解Stitcher的内部流程是排查问题的前提。它的工作流程大致分为六个阶段特征检测在每张图上找关键点默认用ORB。关键点的数量和分布直接决定了后续匹配的质量。特征匹配把不同图上的关键点配对默认用暴力匹配加交叉验证。匹配对的数量和质量决定了变换估计的准确性。变换估计根据匹配点计算单应性矩阵默认用RANSAC剔除误匹配。这一步决定了图像对齐的精度。图像变换把第二张图按照单应性矩阵做透视变换使其与第一张图对齐。接缝估计在重叠区域找一条最优的拼接缝让接缝经过颜色差异最小的区域。图像融合在接缝附近做多频段融合消除亮度差异和鬼影。这六个阶段中前三个阶段决定了“能不能拼”后三个阶段决定了“拼得好不好”。大多数拼接失败都发生在前三个阶段而拼接效果差则通常是后三个阶段的问题。3. 核心细节解析与实操要点3.1 特征检测为什么ORB是默认选择什么时候该换ORBOriented FAST and Rotated BRIEF是OpenCV默认的特征检测器原因是它免费、速度快、旋转不变性不错。SIFT和SURF虽然匹配质量更高但SIFT计算量大SURF在某些版本中受专利限制。ORB在大多数日常场景下够用尤其是纹理丰富的照片。但ORB有两个明显短板。第一它对尺度变化的鲁棒性不如SIFT。如果你的两张照片拍摄距离差异很大ORB可能匹配不上。第二它在纹理少的区域提取不到足够的关键点。比如拍一面白墙ORB可能只找到几个点匹配就无从谈起。判断是否需要换检测器的方法很简单在拼接前先单独跑一遍特征检测看看每张图上的关键点数量。如果少于100个基本可以确定匹配会失败。这时候可以换SIFT试试虽然慢一些但关键点数量会明显增加。import cv2 # 用ORB检测关键点 orb cv2.ORB_create(nfeatures2000) img1 cv2.imread(left.jpg) img2 cv2.imread(right.jpg) kp1, des1 orb.detectAndCompute(img1, None) kp2, des2 orb.detectAndCompute(img2, None) print(f图1关键点数量: {len(kp1)}) print(f图2关键点数量: {len(kp2)}) # 如果数量太少换SIFT if len(kp1) 100 or len(kp2) 100: sift cv2.SIFT_create() kp1, des1 sift.detectAndCompute(img1, None) kp2, des2 sift.detectAndCompute(img2, None) print(fSIFT图1关键点数量: {len(kp1)}) print(fSIFT图2关键点数量: {len(kp2)})注意nfeatures参数控制ORB最多提取多少个关键点默认是500。对于高分辨率图像建议调到2000以上否则关键点分布会过于稀疏。3.2 特征匹配暴力匹配和FLANN怎么选特征匹配的任务是把图1的关键点描述子和图2的关键点描述子配对。OpenCV提供两种匹配器BFMatcher暴力匹配和FlannBasedMatcher近似最近邻匹配。暴力匹配的思路很直接对图1的每个描述子计算它与图2所有描述子的距离取最近的那个作为匹配。它的优点是结果精确缺点是当描述子数量大时速度慢。FLANN用KD树或LSH做近似搜索速度快但可能漏掉一些正确匹配。对于拼接任务我通常用暴力匹配加交叉验证。交叉验证的意思是图1的点A匹配到图2的点B同时图2的点B也匹配到图1的点A这样的匹配才算数。这样做能过滤掉大量误匹配提高后续变换估计的准确性。# 暴力匹配 交叉验证 bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) matches bf.match(des1, des2) # 按距离排序距离越小匹配越好 matches sorted(matches, keylambda x: x.distance) # 取前20%作为优质匹配 good_matches matches[:int(len(matches) * 0.2)] print(f总匹配数: {len(matches)}优质匹配数: {len(good_matches)})提示NORM_HAMMING适用于ORB和BRIEF这类二进制描述子SIFT和SURF用NORM_L2。用错范数会导致匹配结果完全错误。3.3 变换估计单应性矩阵和RANSAC的配合有了匹配点对之后下一步是计算单应性矩阵。单应性矩阵是一个3x3的矩阵描述了两个平面之间的透视变换关系。理论上四个匹配点对就能解出一个单应性矩阵但实际匹配中总会有误匹配所以需要用RANSAC随机采样一致性来剔除 outliers。RANSAC的工作方式是随机选4个匹配点对计算单应性矩阵然后看有多少其他匹配点对符合这个矩阵符合的数量叫内点。重复多次取内点最多的那个矩阵作为最终结果。reprojThreshold参数控制“符合”的标准默认是3.0像素。这个值越小对内点的要求越严格剔除的误匹配越多但也可能把正确匹配误杀。# 提取匹配点的坐标 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) # 用RANSAC计算单应性矩阵 H, mask cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 3.0) # mask标记了哪些点是内点 inliers mask.ravel().sum() print(f内点数量: {inliers} / {len(good_matches)})如果内点比例低于30%说明匹配质量很差拼接大概率会失败。这时候需要回到特征检测阶段换检测器或者调整参数。3.4 图像融合多频段融合为什么比简单叠加好图像变换完成后两张图已经对齐了但直接叠加会在重叠区域出现明显的亮度差异和接缝。简单的方法是做线性渐变让重叠区域的像素按距离加权平均。但这种方法在纹理复杂的区域会产生鬼影。MultiBandBlender用的是拉普拉斯金字塔融合它把图像分解成不同频率的层低频层亮度用宽范围的渐变融合高频层纹理用窄范围的融合。这样既能消除亮度差异又能保留纹理细节。代价是计算量大处理一张4000x3000的图可能需要几秒钟。# Stitcher默认使用MultiBandBlender stitcher cv2.Stitcher_create(cv2.Stitcher_PANORAMA) status, result stitcher.stitch([img1, img2]) if status cv2.Stitcher_OK: cv2.imwrite(panorama.jpg, result) else: print(f拼接失败错误码: {status})Stitcher_PANORAMA模式假设相机是水平旋转拍摄的适合全景场景。如果是平面拼接比如扫描文档应该用Stitcher_SCANS模式。4. 完整实操流程从两张照片到一张全景4.1 环境准备和图像读取先确认OpenCV版本。Stitcher类在opencv-contrib-python包里普通的opencv-python不包含。安装命令是pip install opencv-contrib-python。如果你用的是conda可以conda install -c conda-forge opencv。import cv2 import numpy as np print(cv2.__version__) # 建议4.x以上 # 读取图像注意路径不要有中文 img1 cv2.imread(left.jpg) img2 cv2.imread(right.jpg) if img1 is None or img2 is None: raise FileNotFoundError(图像读取失败检查路径) print(f图1尺寸: {img1.shape}) print(f图2尺寸: {img2.shape})注意OpenCV的imread不支持中文路径。如果路径有中文先用np.fromfile读成数组再用cv2.imdecode解码。4.2 用Stitcher做快速拼接最简单的用法就是三行代码stitcher cv2.Stitcher_create() status, panorama stitcher.stitch([img1, img2]) if status cv2.Stitcher_OK: cv2.imwrite(result.jpg, panorama) print(拼接成功) elif status cv2.Stitcher_ERR_NEED_MORE_IMGS: print(需要更多图像或重叠区域不足) elif status cv2.Stitcher_ERR_HOMOGRAPHY_EST_FAIL: print(单应性矩阵估计失败特征匹配质量差) elif status cv2.Stitcher_ERR_CAMERA_PARAMS_ADJUST_FAIL: print(相机参数调整失败)stitch()返回的status是一个枚举值不同的错误码对应不同阶段的失败。ERR_NEED_MORE_IMGS通常意味着重叠区域太小或者特征点太少。ERR_HOMOGRAPHY_EST_FAIL说明匹配点对不足以计算可靠的变换矩阵。4.3 调整Stitcher参数提升成功率Stitcher提供了一些可调参数通过setXXX方法设置stitcher cv2.Stitcher_create(cv2.Stitcher_PANORAMA) # 设置特征检测器为SIFT stitcher.setFeaturesFinder(cv2.SIFT_create()) # 设置匹配方式为FLANN stitcher.setFeaturesMatcher(cv2.FlannBasedMatcher()) # 设置接缝估计方法 stitcher.setSeamFinder(cv2.detail_SeamFinder_createDefault(cv2.detail_SeamFinder_VORONOI_SEAM)) # 设置融合器 stitcher.setBlender(cv2.detail_Blender_createDefault(cv2.detail_Blender_MULTI_BAND)) # 设置置信度阈值 stitcher.setPanoConfidenceThresh(0.5) status, panorama stitcher.stitch([img1, img2])setPanoConfidenceThresh控制拼接结果的置信度阈值默认是1.0。调低到0.5可以让算法在匹配质量稍差时也尝试拼接但可能产生畸变。4.4 手动拼接的完整实现当Stitcher失败时手动拼接能帮你定位问题。下面是完整的实现import cv2 import numpy as np def manual_stitch(img1, img2): # 第一步特征检测 sift cv2.SIFT_create(nfeatures3000) kp1, des1 sift.detectAndCompute(img1, None) kp2, des2 sift.detectAndCompute(img2, None) if des1 is None or des2 is None: return None, 特征检测失败 # 第二步特征匹配 bf cv2.BFMatcher() raw_matches bf.knnMatch(des1, des2, k2) # Lowes ratio test good [] for m, n in raw_matches: if m.distance 0.75 * n.distance: good.append(m) if len(good) 10: return None, f匹配点不足: {len(good)} # 第三步变换估计 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) H, mask cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) if H is None: return None, 单应性矩阵计算失败 inlier_ratio mask.sum() / len(good) if inlier_ratio 0.3: return None, f内点比例过低: {inlier_ratio:.2f} # 第四步图像变换 h1, w1 img1.shape[:2] h2, w2 img2.shape[:2] # 计算变换后的画布大小 corners1 np.float32([[0,0], [0,h1], [w1,h1], [w1,0]]).reshape(-1,1,2) corners2 np.float32([[0,0], [0,h2], [w2,h2], [w2,0]]).reshape(-1,1,2) warped_corners2 cv2.perspectiveTransform(corners2, H) all_corners np.concatenate((corners1, warped_corners2), axis0) x_min, y_min np.int32(all_corners.min(axis0).ravel() - 0.5) x_max, y_max np.int32(all_corners.max(axis0).ravel() 0.5) translation [-x_min, -y_min] T np.array([[1, 0, translation[0]], [0, 1, translation[1]], [0, 0, 1]]) # 变换两张图到同一坐标系 result cv2.warpPerspective(img1, T H, (x_max - x_min, y_max - y_min)) result[translation[1]:h2translation[1], translation[0]:w2translation[0]] img2 return result, 成功 result, msg manual_stitch(img1, img2) if result is not None: cv2.imwrite(manual_result.jpg, result) print(msg) else: print(f失败: {msg})这段代码的关键在于Lowes ratio test和画布大小的计算。ratio test用最近邻距离和次近邻距离的比值来过滤误匹配比值小于0.75的才保留。画布大小的计算需要把两张图的四个角都变换到同一坐标系然后取最小外接矩形。4.5 处理拼接后的黑边手动拼接的结果通常会有黑边因为变换后的画布比实际内容大。处理方法有两种一是裁剪掉黑色区域二是用cv2.copyMakeBorder做填充。def crop_black_border(img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return img # 找最大轮廓 max_contour max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(max_contour) return img[y:yh, x:xw] cropped crop_black_border(result) cv2.imwrite(cropped_result.jpg, cropped)提示如果黑边不是纯黑比如有噪声可以把阈值从1调到10或20。5. 常见问题与排查技巧实录5.1 拼接失败的五大原因和排查顺序拼接失败时不要盲目调参按下面的顺序排查排查顺序检查项判断方法解决方法1图像是否有重叠区域肉眼观察两张图是否有相同内容重新拍摄保证30%以上重叠2特征点数量是否足够打印len(kp1)和len(kp2)换SIFT或调大nfeatures3匹配点对是否足够打印len(good_matches)调低ratio test阈值或换匹配器4内点比例是否正常打印inlier_ratio调大RANSAC阈值或换检测器5单应性矩阵是否合理检查H矩阵的数值范围检查匹配点坐标是否正确我遇到最多的情况是第2步——特征点不够。尤其是用手机拍的照片如果光线暗或者纹理少ORB可能只找到几十个点。这时候换SIFT通常能解决问题代价是速度慢一些。5.2 拼接结果有鬼影或重影怎么办鬼影通常出现在重叠区域有运动物体的情况。比如拍全景时有人走过两张照片里人的位置不同融合时就会产生半透明的重影。MultiBandBlender对鬼影有一定的抑制作用但不能完全消除。解决方法有三个。第一拍摄时避免运动物体这是最根本的。第二用Stitcher的setSeamFinder方法把接缝估计方法换成DP_SEAM它会在重叠区域找一条能量最低的路径作为接缝能避开运动物体。第三如果鬼影不严重可以在融合后用cv2.inpaint做局部修复。# 换用DP接缝估计 stitcher.setSeamFinder(cv2.detail_SeamFinder_createDefault(cv2.detail_SeamFinder_DP_SEAM))5.3 拼接后图像畸变严重的原因畸变通常是因为单应性矩阵估计不准确或者拍摄时相机有俯仰角变化。Stitcher_PANORAMA模式假设相机只做水平旋转如果拍摄时相机上下移动了拼接结果就会扭曲。判断方法看拼接后的图像是否在边缘出现明显的拉伸或压缩。如果是说明变换模型不对。解决方法是用Stitcher_SCANS模式它假设图像是平面扫描不做透视变换只做仿射变换。或者手动拼接时用cv2.estimateAffinePartial2D代替findHomography。5.4 处理高分辨率图像时的内存和速度问题高分辨率图像比如4000x3000以上拼接时MultiBandBlender会消耗大量内存。我试过用8GB内存的机器拼接两张6000x4000的图直接爆内存。解决方法有两个。第一先降采样再拼接拼完再放大。但这样会损失细节。第二用Stitcher的setBlender方法换成FeatherBlender它比MultiBandBlender快很多内存占用也小代价是融合效果稍差。# 换用FeatherBlender stitcher.setBlender(cv2.detail_Blender_createDefault(cv2.detail_Blender_FEATHER))提示如果图像超过2000万像素建议先用cv2.resize降到1000万像素左右再拼接拼完如果需要高分辨率可以用超分辨率模型放大。5.5 常见错误码速查表错误码含义最可能的原因优先尝试的解决Stitcher_OK成功--ERR_NEED_MORE_IMGS需要更多图像重叠区域不足或特征点太少换SIFT检查重叠ERR_HOMOGRAPHY_EST_FAIL单应性估计失败匹配点对不足或误匹配太多调ratio test换匹配器ERR_CAMERA_PARAMS_ADJUST_FAIL相机参数调整失败图像畸变严重或内参不一致先做畸变校正6. 进阶技巧多图拼接和性能优化6.1 三张以上图像的拼接策略Stitcher支持传入多张图像但图像数量越多失败概率越高。因为多图拼接需要估计所有图像之间的相对变换误差会累积。我的经验是如果超过5张图先用两两拼接的方式生成中间结果再把中间结果拼在一起。# 多图拼接 images [cv2.imread(fimg{i}.jpg) for i in range(1, 6)] stitcher cv2.Stitcher_create() status, panorama stitcher.stitch(images) if status cv2.Stitcher_OK: cv2.imwrite(multi_panorama.jpg, panorama)如果多图拼接失败可以尝试分组拼接先拼前3张再拼后2张最后把两个结果拼在一起。这样每次只处理少量图像成功率更高。6.2 用GPU加速拼接OpenCV的拼接模块默认用CPU但特征检测和匹配可以用GPU加速。如果你有NVIDIA显卡并且编译了CUDA版本的OpenCV可以用cv2.cuda模块加速SIFT和BFMatcher。不过CUDA版本的OpenCV编译比较麻烦我一般只在处理大量图像时才折腾这个。更实际的优化是调整图像尺寸。把输入图像的长边缩放到1000像素左右拼接速度能提升5到10倍而结果质量在大多数场景下没有明显下降。6.3 拼接结果的自动裁剪和校正拼接完成后结果通常不是完美的矩形。可以用crop_black_border函数裁剪也可以用透视变换把结果校正成矩形。校正的方法是找到拼接结果的最大内接矩形然后做透视变换。def crop_to_rectangle(img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY) # 找最大内接矩形 contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) max_contour max(contours, keycv2.contourArea) # 用多边形逼近 epsilon 0.02 * cv2.arcLength(max_contour, True) approx cv2.approxPolyDP(max_contour, epsilon, True) if len(approx) 4: # 已经是四边形直接透视变换 pts approx.reshape(4, 2).astype(np.float32) # 排序点左上、右上、右下、左下 rect np.zeros((4, 2), dtypenp.float32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] rect[3] pts[np.argmax(diff)] # 计算目标尺寸 width int(max(np.linalg.norm(rect[0]-rect[1]), np.linalg.norm(rect[2]-rect[3]))) height int(max(np.linalg.norm(rect[0]-rect[3]), np.linalg.norm(rect[1]-rect[2]))) dst np.array([[0,0], [width-1,0], [width-1,height-1], [0,height-1]], dtypenp.float32) M cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(img, M, (width, height)) return img这段代码先找最大轮廓再用多边形逼近判断是否是四边形。如果是就做透视变换校正。如果不是说明拼接结果形状不规则只能做矩形裁剪。6.4 批量拼接的工程化建议如果你需要批量处理大量图像对建议把拼接逻辑封装成函数加上异常处理和日志记录。我通常会把失败的原因和中间结果比如匹配点数量、内点比例记录下来方便后续分析。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) def batch_stitch(image_pairs, output_dir): stitcher cv2.Stitcher_create() results [] for i, (img1_path, img2_path) in enumerate(image_pairs): try: img1 cv2.imread(img1_path) img2 cv2.imread(img2_path) if img1 is None or img2 is None: logging.error(fPair {i}: 图像读取失败) results.append((i, False, 读取失败)) continue status, panorama stitcher.stitch([img1, img2]) if status cv2.Stitcher_OK: output_path f{output_dir}/panorama_{i}.jpg cv2.imwrite(output_path, panorama) logging.info(fPair {i}: 拼接成功 - {output_path}) results.append((i, True, output_path)) else: logging.warning(fPair {i}: 拼接失败错误码 {status}) results.append((i, False, f错误码 {status})) except Exception as e: logging.error(fPair {i}: 异常 {str(e)}) results.append((i, False, str(e))) return results批量处理时要注意Stitcher对象可以复用但每次stitch()调用后内部状态会重置所以不用担心状态污染。7. 我在实际项目中的几个体会拼接这件事参数调优是一方面前期拍摄质量才是决定性的。我处理过一批手机拍摄的全景素材失败率超过50%后来发现主要问题是拍摄时曝光不一致——两张照片一张亮一张暗融合时怎么调都有色差。后来要求拍摄时锁定曝光和对焦失败率直接降到10%以下。另一个体会是不要迷信全自动。Stitcher在标准场景下确实好用但遇到特殊场景比如显微图像拼接、卫星图像拼接手动方案才是正解。手动方案虽然代码多但每一步都可控出了问题能定位到具体环节。最后分享一个小技巧如果拼接结果的重叠区域有轻微错位可以在融合前用cv2.findTransformECC做一次精细配准它能基于像素亮度做亚像素级的对齐对提升拼接质量有明显帮助。这个函数在cv2的video模块里用法是传入两张灰度图和一个初始变换矩阵返回优化后的矩阵。
返回列表