OpenCV图像变换实战:放缩、平移、旋转、仿射与透视变换详解
1. 图像变换到底在解决什么问题很多人刚接触图像处理时觉得放缩、平移、旋转这些操作不就是把图片拉一拉、转一转吗能有多复杂。但真正上手做过项目的人都知道图像变换是整个计算机视觉 pipeline 里最基础也最容易翻车的一环。你拿到的原始图像可能角度是歪的、尺寸不统一、目标物体位置偏得离谱甚至因为拍摄角度导致形状畸变。这些问题不解决后面的特征提取、目标检测、图像拼接全都白搭。图像变换的核心价值就一句话把图像映射到一个我们需要的坐标空间里去。不管是把 4000×3000 的原图压到 640×640 喂给模型还是把倾斜拍摄的文档矫正成正视图本质上都是在做坐标映射。区别只在于映射的复杂度不同——放缩和平移是线性变换旋转也是线性变换仿射变换是线性变换加平移透视变换则是非线性映射。我见过不少新手在这一步踩坑比如用错了插值方式导致图像模糊或者变换后坐标对不上导致标注框全偏了。这些问题看起来小但在实际项目里排查起来非常费时间。所以这篇文章我打算把放缩、平移、旋转、仿射变换、透视变换这五种最常见的操作从头到尾捋一遍包括背后的数学原理、OpenCV 里的实现方式、参数怎么选、坑在哪里。适合有一定 Python 基础、正在做图像处理相关项目的朋友参考也适合刚入门计算机视觉想打牢基础的同学。2. 五种变换的核心原理与方案选型2.1 放缩变换最基础但参数最讲究放缩变换说白了就是改变图像的尺寸。你给一个缩放因子图像就按比例放大或缩小。数学上非常简单dst(x, y) src(x * sx, y * sy)其中 sx 和 sy 分别是水平和垂直方向的缩放比例。当 sx sy 时是等比例缩放不等时就是非等比例缩放会导致图像拉伸变形。OpenCV 里用cv2.resize()实现核心参数就三个输入图像、目标尺寸或缩放因子、插值方法。很多人不注意插值方法的选择觉得默认的就行但实际上不同场景下插值方式对结果影响很大。插值方法适用场景特点INTER_NEAREST实时性要求极高的场景速度最快但会产生锯齿INTER_LINEAR放大图像默认速度快效果均衡INTER_CUBIC放大图像质量优先速度慢边缘更平滑INTER_AREA缩小图像缩小效果最好避免摩尔纹INTER_LANCZOS4高质量放大速度最慢质量最高我个人的经验是缩小图像一定用 INTER_AREA放大图像用 INTER_LINEAR 或 INTER_CUBIC。这个选择背后的逻辑是缩小图像时如果还用线性插值会丢失大量像素信息导致混叠而 INTER_AREA 本质上是做区域平均能更好地保留缩小后的整体信息。还有一个容易忽略的点cv2.resize()的dsize参数是(width, height)的顺序而图像的 shape 是(height, width, channels)。这个顺序搞反了是新手最常见的错误之一图像会变成转置的样子。2.2 平移变换矩阵形式才是正确打开方式平移就是把图像沿 x 轴和 y 轴移动一定距离。直观理解很简单但这里我要强调一个关键点平移虽然看起来简单但它是理解仿射变换的入口。如果用矩阵形式表达平移变换可以写成[1 0 tx] [x] [0 1 ty] × [y] [0 0 1 ] [1]这就是一个 3×3 的变换矩阵其中 tx 和 ty 分别是 x 和 y 方向的平移量。为什么用 3×3 而不是 2×2因为 2×2 矩阵只能表达线性变换旋转、缩放、错切无法表达平移。引入齐次坐标后平移也变成了矩阵乘法这样所有变换都可以统一用矩阵乘法来表示和组合。OpenCV 里用cv2.warpAffine()实现平移需要构造一个 2×3 的变换矩阵import cv2 import numpy as np M np.float32([[1, 0, tx], [0, 1, ty]]) dst cv2.warpAffine(img, M, (width, height))注意warpAffine的第三个参数是输出图像的尺寸(width, height)不是输入图像的尺寸。如果你平移后想保持原图大小就传原图的宽高如果想看到平移后多出来的区域就需要把输出尺寸调大。实操心得平移后超出原图边界的部分默认会被裁掉warpAffine的borderMode参数可以控制边界填充方式。默认是BORDER_CONSTANT黑色填充也可以设为BORDER_REPLICATE复制边缘像素或BORDER_REFLECT镜像反射。2.3 旋转变换旋转中心决定一切旋转是让图像绕某个点转动指定角度。这里最关键的问题是绕哪个点转OpenCV 的cv2.getRotationMatrix2D()需要三个参数旋转中心(cx, cy)、旋转角度angle、缩放因子scale。旋转矩阵的构造过程是这样的M [α β (1-α)·cx - β·cy] [-β α β·cx (1-α)·cy]其中α scale · cos(angle)β scale · sin(angle)。角度为正时是逆时针旋转单位为度。这个旋转矩阵是 2×3 的可以直接传给cv2.warpAffine()。很多人会犯一个错误旋转中心设成了(0, 0)结果图像转到了画布外面。正确的做法通常是设成图像中心(width/2, height/2)。但即使设成中心旋转后图像的四个角也可能超出原画布范围被裁掉。解决办法是计算旋转后图像的新边界扩大输出画布h, w img.shape[:2] center (w // 2, h // 2) M cv2.getRotationMatrix2D(center, 45, 1.0) # 计算旋转后图像的新尺寸 cos np.abs(M[0, 0]) sin np.abs(M[0, 1]) new_w int(h * sin w * cos) new_h int(h * cos w * sin) # 调整旋转矩阵的平移分量 M[0, 2] (new_w / 2) - center[0] M[1, 2] (new_h / 2) - center[1] dst cv2.warpAffine(img, M, (new_w, new_h))这段代码是我在实际项目里反复用到的模板建议直接收藏。它的逻辑是先算出旋转后图像的包围盒尺寸然后把旋转中心平移到新画布的中心这样旋转后的图像就能完整显示。2.4 仿射变换三点定乾坤仿射变换是平移、旋转、缩放、错切的组合它保持平行线变换后仍然平行但不保持角度和长度不变。仿射变换有 6 个自由度对应 2×3 变换矩阵的 6 个参数。在 OpenCV 里获取仿射变换矩阵有两种常用方式方式一三点法。你只需要在原图上找三个点再指定它们在目标图上的对应位置cv2.getAffineTransform()就能自动算出变换矩阵。这三个点不能共线。pts_src np.float32([[50, 50], [200, 50], [50, 200]]) pts_dst np.float32([[10, 100], [200, 50], [100, 250]]) M cv2.getAffineTransform(pts_src, pts_dst) dst cv2.warpAffine(img, M, (width, height))方式二手动构造。如果你明确知道要做什么变换比如先旋转再平移可以自己构造矩阵然后相乘。三点法的优势在于直观——你不需要理解矩阵的每个参数是什么意思只需要告诉算法“这个点应该跑到那个位置去”。这在做图像对齐、人脸对齐等任务时特别方便。注意事项三点法对点的精度非常敏感。如果三个点的位置有偏差变换结果会明显扭曲。在实际项目中这三个点通常来自特征点检测或手动标注建议用亚像素级别的精度。2.5 透视变换四点映射解决畸变问题透视变换也叫投影变换它比仿射变换更灵活能处理“近大远小”的透视畸变。仿射变换保持平行线平行但透视变换不保持——这正是它能矫正倾斜拍摄文档的原因。透视变换需要 4 个对应点变换矩阵是 3×3 的有 8 个自由度因为齐次坐标下尺度等价9 个参数中有一个是冗余的。pts_src np.float32([[56, 65], [368, 52], [28, 387], [389, 390]]) pts_dst np.float32([[0, 0], [300, 0], [0, 300], [300, 300]]) M cv2.getPerspectiveTransform(pts_src, pts_dst) dst cv2.warpPerspective(img, M, (300, 300))cv2.getPerspectiveTransform()内部通过解线性方程组来求解变换矩阵。4 个点提供了 8 个方程正好解出 8 个未知数。透视变换最典型的应用就是文档矫正你拍了一张倾斜的纸质文档照片找到文档四个角在原图中的坐标然后指定它们映射到一个矩形的四个角就能把文档“拉平”。变换类型自由度需要点数保持平行线典型应用放缩21缩放因子是图像尺寸统一平移21平移量是位置调整旋转31中心角度是角度矫正仿射63是人脸对齐、图像配准透视84否文档矫正、AR 平面映射3. 完整实操流程与关键代码实现3.1 环境准备与基础工具函数我平时做图像变换相关的开发环境基本就是 Python OpenCV NumPy 三件套。安装很简单pip install opencv-python numpy如果你需要处理视频流或者用到一些 contrib 模块可以装opencv-contrib-python。版本方面OpenCV 4.x 和 3.x 在变换 API 上基本一致不用太纠结。在开始写具体变换之前我习惯先封装几个工具函数后面反复调用会方便很多。比如一个用来显示图像对比的函数import cv2 import numpy as np def show_comparison(original, transformed, title1Original, title2Transformed): 并排显示原图和变换后的图像 h1, w1 original.shape[:2] h2, w2 transformed.shape[:2] # 统一高度 target_h max(h1, h2) def resize_to_height(img, h): ratio h / img.shape[0] return cv2.resize(img, (int(img.shape[1] * ratio), h)) img1 resize_to_height(original, target_h) img2 resize_to_height(transformed, target_h) combined np.hstack([img1, img2]) cv2.imshow(f{title1} vs {title2}, combined) cv2.waitKey(0) cv2.destroyAllWindows()这个函数看起来简单但在调试变换效果时非常实用。并排对比能让你一眼看出变换前后的差异比单独看两张图效率高得多。3.2 放缩变换的完整实现与参数计算假设你有一张 1920×1080 的图片需要缩放到 640×640 输入给模型。这里有个关键问题直接缩放到 640×640 会导致图像变形因为原始宽高比是 16:9目标宽高比是 1:1。正确的做法是保持宽高比缩放然后填充黑边letterboxdef letterbox_resize(img, target_size(640, 640), color(0, 0, 0)): 保持宽高比的缩放 填充 h, w img.shape[:2] target_w, target_h target_size scale min(target_w / w, target_h / h) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) # 创建目标画布并居中放置 canvas np.full((target_h, target_w, 3), color, dtypenp.uint8) x_offset (target_w - new_w) // 2 y_offset (target_h - new_h) // 2 canvas[y_offset:y_offsetnew_h, x_offset:x_offsetnew_w] resized return canvas, scale, (x_offset, y_offset)这个 letterbox 函数在目标检测项目里几乎是标配。返回的 scale 和 offset 后面可以用来把检测框的坐标映射回原图。缩放因子的计算逻辑是scale min(target_w / w, target_h / h)取较小值保证图像能完整放入目标区域。如果取较大值图像会超出目标区域被裁掉。实操心得缩小图像时如果缩放比例小于 0.5建议分多次缩小而不是一步到位。比如要缩小到 1/4先缩到 1/2 再缩到 1/4效果会比直接缩到 1/4 好。这是因为每次缩小的采样密度更均匀能减少信息丢失。3.3 平移与旋转的组合操作实际项目中平移和旋转经常需要组合使用。比如数据增强时你可能想同时做小幅度的平移和旋转。这时候有两种做法做法一分别调用两次 warpAffine。简单直接但两次插值会累积误差。做法二矩阵相乘一次变换。把平移矩阵和旋转矩阵相乘得到一个组合矩阵只做一次插值。def get_rotation_matrix(center, angle, scale1.0): 获取旋转矩阵3x3齐次形式 rad np.deg2rad(angle) cos_a, sin_a np.cos(rad), np.sin(rad) cx, cy center M np.array([ [scale * cos_a, scale * -sin_a, (1 - scale * cos_a) * cx scale * sin_a * cy], [scale * sin_a, scale * cos_a, (1 - scale * cos_a) * cy - scale * sin_a * cx], [0, 0, 1] ]) return M def get_translation_matrix(tx, ty): 获取平移矩阵3x3齐次形式 return np.array([ [1, 0, tx], [0, 1, ty], [0, 0, 1] ]) # 组合先旋转再平移 M_rot get_rotation_matrix((w/2, h/2), 30) M_trans get_translation_matrix(50, 30) M_combined M_trans M_rot # 矩阵乘法 # 取前两行传给warpAffine dst cv2.warpAffine(img, M_combined[:2], (w, h))注意矩阵乘法的顺序M_trans M_rot表示先应用旋转再应用平移。因为矩阵乘法是从右往左作用的这个顺序不能搞反。3.4 仿射变换在图像对齐中的实战仿射变换最实用的场景之一是图像对齐。假设你有两张同一场景但拍摄角度略有不同的照片想把他们对齐。思路是找三对匹配点算仿射矩阵然后变换。def align_images_affine(img_src, img_dst, pts_src, pts_dst): 基于三对点做仿射对齐 M cv2.getAffineTransform( np.float32(pts_src), np.float32(pts_dst) ) h, w img_dst.shape[:2] aligned cv2.warpAffine(img_src, M, (w, h)) return aligned, M三对点的选择很关键。理想情况下这三个点应该尽量分散在图像的不同区域形成一个较大的三角形。如果三个点挤在一起变换矩阵的数值稳定性会很差微小的点位误差会被放大。在实际操作中我通常用 ORB 或 SIFT 特征点检测来自动找匹配点然后用 RANSAC 剔除误匹配最后取三个内点来算仿射矩阵。这样比手动选点靠谱得多。3.5 透视变换矫正倾斜文档的完整流程文档矫正是我做过最多的透视变换应用整个流程可以拆成四步第一步找到文档的四个角点。可以用边缘检测 轮廓查找也可以手动标注。自动检测的代码大致是这样def find_document_corners(img): 自动检测文档四个角点 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) edged cv2.Canny(blurred, 75, 200) contours, _ cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours sorted(contours, keycv2.contourArea, reverseTrue) for c in contours: peri cv2.arcLength(c, True) approx cv2.approxPolyDP(c, 0.02 * peri, True) if len(approx) 4: return approx.reshape(4, 2) return None第二步对角点排序。检测出来的四个角点顺序是乱的需要按左上、右上、右下、左下排序def order_points(pts): 按左上、右上、右下、左下排序 rect np.zeros((4, 2), dtypenp.float32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] # 左上xy最小 rect[2] pts[np.argmax(s)] # 右下xy最大 diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] # 右上x-y最小 rect[3] pts[np.argmax(diff)] # 左下x-y最大 return rect排序的逻辑很巧妙左上角的 xy 最小右下角的 xy 最大右上角的 x-y 最小因为 x 大 y 小左下角的 x-y 最大。第三步计算目标矩形的宽高。用两点间距离公式def calculate_target_size(rect): 计算矫正后的目标尺寸 (tl, tr, br, bl) rect width_a np.linalg.norm(br - bl) width_b np.linalg.norm(tr - tl) max_width max(int(width_a), int(width_b)) height_a np.linalg.norm(tr - br) height_b np.linalg.norm(tl - bl) max_height max(int(height_a), int(height_b)) return max_width, max_height第四步执行透视变换def perspective_correct(img, src_pts): 完整的文档矫正流程 rect order_points(src_pts) max_w, max_h calculate_target_size(rect) dst_pts np.float32([ [0, 0], [max_w - 1, 0], [max_w - 1, max_h - 1], [0, max_h - 1] ]) M cv2.getPerspectiveTransform(rect, dst_pts) warped cv2.warpPerspective(img, M, (max_w, max_h)) return warped这套流程我用了很多次效果稳定。关键点在于角点检测的准确性——如果 Canny 边缘检测的参数不合适可能找不到四个角点或者找到的角点位置有偏差。4. 常见问题与排查技巧实录4.1 变换后图像出现黑边或内容丢失这是最常见的问题几乎每个人都会遇到。根本原因是变换后的像素坐标超出了输出图像的边界。排查思路先检查输出尺寸是否足够大。对于旋转用前面提到的包围盒计算方法对于平移把输出尺寸设为(w abs(tx), h abs(ty))对于透视变换确保目标尺寸覆盖了所有变换后的点。另一个隐藏原因是变换矩阵的平移分量没有正确设置。比如旋转时只设置了旋转部分没有把旋转中心平移到新画布中心图像就会偏到一边。避坑技巧在调试阶段把warpAffine或warpPerspective的borderMode设为BORDER_REPLICATE这样超出边界的部分会用边缘像素填充你能清楚地看到哪些区域是变换后新增的哪些是原始内容。4.2 插值方式选错导致图像质量下降我见过最典型的案例是把一张 4000×3000 的图缩到 200×150 做缩略图用了默认的INTER_LINEAR结果缩略图里全是摩尔纹细节完全糊成一团。换成INTER_AREA之后立刻清晰了。选择原则缩小图像INTER_AREA是唯一正确选择放大图像INTER_LINEAR速度优先或INTER_CUBIC质量优先保持尺寸不变只做旋转/平移INTER_LINEAR足够需要极致质量且不在乎速度INTER_LANCZOS44.3 坐标映射对不上导致标注框偏移做目标检测时你对图像做了变换但标注框的坐标没有同步变换结果框全偏了。这个问题在数据增强里特别常见。解决方案对标注框的坐标也应用同样的变换矩阵。对于仿射变换框的四个角分别做矩阵乘法即可。对于透视变换需要把坐标转成齐次坐标乘以 3×3 矩阵后再除以第三维def transform_points(pts, M): 对点集应用透视变换矩阵 pts np.array(pts, dtypenp.float32) ones np.ones((pts.shape[0], 1), dtypenp.float32) pts_homo np.hstack([pts, ones]) # Nx3 transformed pts_homo M.T # Nx3 transformed transformed[:, :2] / transformed[:, 2:3] # 归一化 return transformed注意透视变换后需要除以第三维做归一化这是仿射变换不需要的步骤。很多人在这里漏掉了除法导致坐标完全错误。4.4 旋转后图像中心偏移用cv2.getRotationMatrix2D()旋转图像后发现图像的中心位置变了。这是因为旋转矩阵是绕指定中心旋转的但旋转后的图像在新画布中的位置需要额外调整。快速修复方法旋转后重新计算图像的有效区域然后做一次平移把内容居中。或者直接用前面 3.3 节里的包围盒方法一步到位。4.5 透视变换后图像比例失真做文档矫正时矫正后的文档被拉长或压扁了。原因是目标矩形的宽高比和实际文档的宽高比不一致。解决办法如果你知道文档的实际宽高比比如 A4 纸是 210:297就按这个比例设置目标尺寸。如果不知道就用源图像中对应边的长度来估算——这也是前面calculate_target_size函数在做的事情。问题现象可能原因排查方法解决方案图像有黑边输出尺寸不够检查输出尺寸和变换后边界扩大输出画布图像模糊插值方式不当确认缩小还是放大缩小用 INTER_AREA标注框偏移坐标未同步变换对比变换前后坐标对标注点应用同一矩阵旋转后偏移中心未重定位检查旋转矩阵平移分量调整平移分量透视后失真目标宽高比错误对比源边和目标边比例按实际比例设置目标尺寸边缘锯齿插值方式过于简单检查是否用了 NEAREST换用 LINEAR 或 CUBIC4.6 性能优化大批量图像变换怎么加速如果你需要对几千张图像做同样的变换逐张调用 OpenCV 函数会比较慢。几个优化方向第一能用 NumPy 做的操作不要用 OpenCV。比如纯平移直接用数组切片比warpAffine快得多def fast_translate(img, tx, ty): 基于数组切片的快速平移 h, w img.shape[:2] dst np.zeros_like(img) src_x1, src_x2 max(0, -tx), min(w, w - tx) src_y1, src_y2 max(0, -ty), min(h, h - ty) dst_x1, dst_x2 max(0, tx), min(w, w tx) dst_y1, dst_y2 max(0, ty), min(h, h ty) dst[dst_y1:dst_y2, dst_x1:dst_x2] img[src_y1:src_y2, src_x1:src_x2] return dst第二固定变换矩阵时预计算。如果多张图用同一个变换矩阵矩阵只需要算一次。第三用多进程并行。OpenCV 的很多函数内部已经用了多线程但在处理大量图像时用multiprocessing.Pool在图像级别并行效果更好。实操心得cv2.setNumThreads(0)可以关闭 OpenCV 的内部多线程在配合多进程使用时避免线程竞争整体吞吐量反而更高。这个技巧我在处理十万级图像数据集时验证过速度提升明显。4.7 变换矩阵的逆变换与坐标回溯实际项目中经常需要把变换后的坐标映射回原图。比如你在缩放后的图像上检测到了目标想知道它在原图上的位置。这时候就需要变换矩阵的逆矩阵。def invert_affine(M): 求仿射变换矩阵的逆 # 对于2x3矩阵补成3x3再求逆 M_3x3 np.vstack([M, [0, 0, 1]]) M_inv np.linalg.inv(M_3x3) return M_inv[:2] def invert_perspective(M): 求透视变换矩阵的逆 return np.linalg.inv(M)仿射变换的逆矩阵仍然是仿射变换透视变换的逆矩阵仍然是透视变换。这个性质保证了坐标可以在两个空间之间双向映射。需要注意的是逆变换的插值误差会累积。如果你做了变换再逆变换图像会有轻微的质量损失。所以在精度要求高的场景下尽量保存原始坐标而不是依赖逆变换恢复。5. 几个容易被忽略的细节5.1 图像坐标系的方向问题OpenCV 的图像坐标系原点是左上角x 轴向右y 轴向下。这和数学课本里的笛卡尔坐标系原点在左下角y 轴向上是反的。这个差异导致旋转角度的方向也和我们直觉相反——在 OpenCV 里正角度是逆时针旋转但因为 y 轴向下视觉上看起来是顺时针。我刚开始做旋转的时候设了 30 度结果图像往反方向转了排查了半天才发现是坐标系的问题。记住这个规则能省很多时间。5.2 变换矩阵的数据类型cv2.warpAffine()和cv2.warpPerspective()要求变换矩阵是np.float32类型。如果你用np.float64构造矩阵OpenCV 会报类型错误。这个错误信息不太直观新手容易卡住。# 错误写法 M np.array([[1, 0, 50], [0, 1, 30]]) # 默认float64 # 正确写法 M np.float32([[1, 0, 50], [0, 1, 30]])5.3 大角度旋转的信息损失旋转 90 度、180 度、270 度这些特殊角度其实不需要用warpAffine做插值。直接用np.rot90()或者转置加翻转速度快且无信息损失。只有任意角度的旋转才需要插值。# 90度旋转无信息损失 rotated_90 np.rot90(img, k1) # 逆时针90度 rotated_180 np.rot90(img, k2) rotated_270 np.rot90(img, k3)5.4 透视变换的点顺序必须一致cv2.getPerspectiveTransform()要求源点和目标点的顺序严格对应。如果源点按左上、右上、右下、左下排列目标点也必须按同样的顺序。顺序错了变换结果会完全乱掉。我建议在代码里加一个断言或者注释明确标注点的顺序避免后续维护时搞混。5.5 变换后的图像尺寸计算透视变换后目标尺寸的选择直接影响输出质量。如果目标尺寸设得太小图像会被压缩丢失细节设得太大会有大量空白区域。一个实用的经验法则是目标宽度取源四边形对边长度的最大值目标高度同理。这样能保证变换后的图像分辨率不低于原始信息量。6. 写在最后图像变换这个东西看起来是基础操作但真正做好做精并不容易。我自己在项目里踩过的坑包括但不限于插值方式选错导致模型精度下降、坐标变换漏了归一化导致标注框全偏、旋转后图像被裁掉一半、透视变换点顺序搞反导致输出完全错误。每一个坑都花了不少时间排查。如果你刚开始接触这些内容我的建议是先把放缩和平移吃透理解齐次坐标和变换矩阵的概念然后再去搞旋转和仿射。透视变换放在最后因为它涉及的数学稍微多一点但有了前面的基础理解起来也不会太难。代码方面OpenCV 的文档其实写得挺清楚的但有些细节需要自己动手试才能体会到。比如warpAffine的borderMode参数文档里就一句话但实际用起来不同模式的效果差异很大。多写多调手感自然就来了。另外如果你在做深度学习相关的项目图像变换的坐标同步问题一定要重视。我见过太多因为数据增强时标注框没跟着变换导致训练效果差的案例。这个问题的隐蔽性很强因为图像看起来是对的只有标注框偏了不仔细看根本发现不了。