news 2026/4/3 6:27:09

YOLO11仿射变换逆矩阵,框坐标还原方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLO11仿射变换逆矩阵,框坐标还原方法

YOLO11仿射变换逆矩阵,框坐标还原方法

在YOLO11的实际部署中,一个常被忽略却至关重要的环节是:如何将模型输出的归一化预测框准确映射回原始图像坐标系。很多开发者在Python端调用Ultralytics官方API时感受不到这个问题——因为ops.scale_boxes已自动完成坐标还原;但一旦进入C++、TensorRT或自定义推理引擎开发阶段,这个“黑盒”就会打开,暴露出底层仿射变换与逆变换的核心逻辑。

本文不讲原理推导,不堆数学公式,只聚焦一个工程问题:当你用cv2.warpAffine做预处理时,怎么用cv2.invertAffineTransform得到的逆矩阵,把模型输出的(cx, cy, w, h)精准还原到原图?为什么直接套用会出错?哪些维度要单独处理?边界值怎么防越界?

全文基于YOLO11真实代码和实测数据展开,所有代码均可直接复用,所有结论均经bus.jpg、zidane.jpg等标准测试图验证。

1. 为什么必须理解逆变换?——从两个预处理说起

YOLO11支持两种主流预处理方式:LetterBox(官方默认)和warpAffine(高性能部署首选)。它们看似都做“缩放+填充”,但数学本质完全不同,直接影响后处理逻辑。

1.1 LetterBox:可逆但非线性,靠scale_boxes硬解

LetterBox先按长边缩放到640,再居中填充灰条,其变换不是严格仿射变换——因为缩放比例在x/y方向可能不同(如1080×810→640×480),且填充位置固定。Ultralytics内部用ops.scale_boxes实现还原:

# ultralytics/utils/ops.py def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None): # img1_shape: 预处理后尺寸 (640, 480) # img0_shape: 原图尺寸 (1080, 810) # ratio_pad: (scale, (dw, dh)) 如 (0.7407, (0, 0)) gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # 0.7407 pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # (0, 0) boxes[..., [0, 2]] -= pad[0] # x padding boxes[..., [1, 3]] -= pad[1] # y padding boxes[..., :4] /= gain clip_boxes(boxes, img0_shape) return boxes

它依赖ratio_pad元组,而非单一矩阵。优点是精度高、无畸变;缺点是逻辑分散,难以移植到C++。

1.2 warpAffine:严格仿射,矩阵可逆,部署友好

warpAffine则采用统一缩放+平移,全程由一个2×3矩阵M描述:

scale = min(640 / w, 640 / h) # 统一缩放因子 ox = (640 - scale * w) / 2 # x方向平移量 oy = (640 - scale * h) / 2 # y方向平移量 M = np.array([[scale, 0, ox], [0, scale, oy]], dtype=np.float32)

对任意原图点(x, y),变换后坐标为: $$ \begin{bmatrix}x'\y'\end{bmatrix} = \begin{bmatrix}scale & 0\0 & scale\end{bmatrix} \begin{bmatrix}x\y\end{bmatrix} + \begin{bmatrix}ox\oy\end{bmatrix} $$

这正是标准仿射变换:dst = M @ [x, y, 1].T。它的逆变换IM = cv2.invertAffineTransform(M)同样是一个2×3矩阵,可直接用于坐标还原。

关键结论:warpAffine预处理下,所有坐标还原必须通过逆矩阵IM完成,且必须区分“点坐标”和“框坐标”的处理方式。

2. 逆矩阵IM到底是什么?——拆解cv2.invertAffineTransform输出

很多开发者误以为IM可以直接乘以[x, y, 1]得到原坐标。这是错误的。我们用一张1080×810的bus.jpg实测:

import cv2 import numpy as np img = cv2.imread("ultralytics/assets/bus.jpg") # (1080, 810, 3) h, w = img.shape[:2] scale = min(640 / w, 640 / h) # 640/810 ≈ 0.7901 ox = (640 - scale * w) / 2 # ≈ 0 oy = (640 - scale * h) / 2 # ≈ 59.26 M = np.array([[scale, 0, ox], [0, scale, oy]], dtype=np.float32) IM = cv2.invertAffineTransform(M) print("M =\n", M) print("IM =\n", IM)

输出:

M = [[0.79012346 0. 0. ] [0. 0.79012346 59.259259]] IM = [[1.265625 0. -0. ] [0. 1.265625 -74.999999]]

注意:IM的第三列是[-0, -74.999999],不是[0, -oy/scale]。这是因为cv2.invertAffineTransform求解的是: $$ \text{dst} = M \cdot \begin{bmatrix}x\y\1\end{bmatrix}, \quad \text{src} = IM \cdot \begin{bmatrix}x'\y'\1\end{bmatrix} $$

所以IM的平移项是-M^{-1} @ [ox, oy].T,即[-ox/scale, -oy/scale]。本例中oy/scale ≈ 75,验证无误。

核心认知:IM是一个完整的逆变换矩阵,使用时必须按[x', y', 1]格式输入,不能只取前两列。

3. 框坐标还原的三大陷阱与正确写法

YOLO11模型输出的是归一化后的(cx, cy, w, h),单位是预处理后图像的像素(640×640)。还原时需分两步:
① 将(cx, cy)、(cx±w/2, cy±h/2)四点转为640×640坐标系下的绝对像素;
② 用IM将这些像素点映射回原图。

但直接套用会导致三类典型错误:

3.1 陷阱一:混淆“点”与“框”,对(cx, cy)直接乘IM

错误写法:

# ❌ 错误!cx, cy是中心点,但IM要求输入[x, y, 1],且需先转为像素坐标 cx_px = cx * 640 cy_px = cy * 640 orig_cx, orig_cy = IM @ np.array([cx_px, cy_px, 1]) # 维度不匹配!

正确做法:所有点坐标必须先转为640×640下的整数像素,再补1维。

# 正确:先归一化→像素→补维→矩阵乘 cx_px = int(cx * 640) # 例如 cx=0.5 → 320 cy_px = int(cy * 640) # 例如 cy=0.3 → 192 point = np.array([cx_px, cy_px, 1], dtype=np.float32) orig_point = IM @ point # 输出 [orig_x, orig_y] orig_cx, orig_cy = int(orig_point[0]), int(orig_point[1])

3.2 陷阱二:对宽高w/h直接乘IM——宽高不是点!

错误写法:

# ❌ 错误!宽高是长度,不是坐标,不能直接用IM变换 w_px = int(w * 640) h_px = int(h * 640) orig_w, orig_h = IM @ np.array([w_px, h_px, 0]) # 第三维度应为0?错!

真相:宽高是向量,其变换只与缩放有关,与平移无关。因为M是相似变换(uniform scale + translation),所以: $$ \text{orig_w} = \frac{w_{px}}{\text{scale}}, \quad \text{orig_h} = \frac{h_{px}}{\text{scale}} $$

scale = IM[0,0] = IM[1,1](因M是对角阵)。所以:

# 正确:宽高仅受缩放影响,直接除以scale scale_inv = IM[0, 0] # 或 IM[1, 1],二者相等 w_px = w * 640 h_px = h * 640 orig_w = int(w_px / scale_inv) # = w_px * (1/scale) = w * 640 / scale orig_h = int(h_px / scale_inv)

3.3 陷阱三:四角点还原后未裁剪,导致坐标越界

当原图长宽比与640×640差异大时(如手机竖屏图),还原后的框可能部分落在图像外。若直接cv2.rectangle会报错或显示异常。

正确做法:还原后立即裁剪到[0, orig_w)×[0, orig_h)范围内。

# 正确:四角点还原 + 裁剪 left_px = int((cx - w/2) * 640) top_px = int((cy - h/2) * 640) right_px = int((cx + w/2) * 640) bottom_px = int((cy + h/2) * 640) # 还原四点 pts = np.array([ [left_px, top_px, 1], [right_px, top_px, 1], [right_px, bottom_px, 1], [left_px, bottom_px, 1] ], dtype=np.float32).T orig_pts = (IM @ pts).T # (4, 2) orig_pts = np.round(orig_pts).astype(int) # 裁剪到原图范围 orig_h, orig_w = img.shape[:2] orig_pts[:, 0] = np.clip(orig_pts[:, 0], 0, orig_w - 1) orig_pts[:, 1] = np.clip(orig_pts[:, 1], 0, orig_h - 1) # 取最小外接矩形作为最终框 left_final = orig_pts[:, 0].min() top_final = orig_pts[:, 1].min() right_final = orig_pts[:, 0].max() bottom_final = orig_pts[:, 1].max()

4. 完整可运行的还原函数(Python版)

综合以上分析,给出一个零依赖、开箱即用的restore_box函数:

import numpy as np import cv2 def restore_box(cx, cy, w, h, IM, orig_shape): """ 将YOLO11输出的归一化框(cx,cy,w,h)还原到原图坐标 Args: cx, cy, w, h (float): 归一化中心坐标和宽高(0~1) IM (np.ndarray): 2x3逆仿射变换矩阵,来自cv2.invertAffineTransform(M) orig_shape (tuple): 原图尺寸 (h, w) Returns: tuple: (left, top, right, bottom) 像素坐标,已裁剪 """ h_orig, w_orig = orig_shape # Step 1: 转为640x640下的像素坐标 cx_px = cx * 640 cy_px = cy * 640 w_px = w * 640 h_px = h * 640 # Step 2: 计算四角点(左上、右上、右下、左下) left_px = cx_px - w_px / 2 top_px = cy_px - h_px / 2 right_px = cx_px + w_px / 2 bottom_px = cy_px + h_px / 2 # Step 3: 构造齐次坐标并批量还原 pts = np.array([ [left_px, top_px, 1], [right_px, top_px, 1], [right_px, bottom_px, 1], [left_px, bottom_px, 1] ], dtype=np.float32).T orig_pts = (IM @ pts).T # (4, 2) # Step 4: 裁剪并取外接矩形 orig_pts = np.round(orig_pts).astype(int) orig_pts[:, 0] = np.clip(orig_pts[:, 0], 0, w_orig - 1) orig_pts[:, 1] = np.clip(orig_pts[:, 1], 0, h_orig - 1) left = int(orig_pts[:, 0].min()) top = int(orig_pts[:, 1].min()) right = int(orig_pts[:, 0].max()) bottom = int(orig_pts[:, 1].max()) return left, top, right, bottom # 使用示例 if __name__ == "__main__": img = cv2.imread("ultralytics/assets/bus.jpg") h_orig, w_orig = img.shape[:2] # 模拟一个YOLO11输出的框(归一化) cx, cy, w, h = 0.52, 0.48, 0.35, 0.28 # 构造M和IM(同前文) scale = min(640 / w_orig, 640 / h_orig) ox = (640 - scale * w_orig) / 2 oy = (640 - scale * h_orig) / 2 M = np.array([[scale, 0, ox], [0, scale, oy]], dtype=np.float32) IM = cv2.invertAffineTransform(M) # 还原 left, top, right, bottom = restore_box(cx, cy, w, h, IM, (h_orig, w_orig)) print(f"还原框: ({left}, {top}) -> ({right}, {bottom})") # 绘制验证 cv2.rectangle(img, (left, top), (right, bottom), (0, 255, 0), 2) cv2.imwrite("restored_box.jpg", img) print("还原框已保存至 restored_box.jpg")

该函数已在bus.jpg(1080×810)、zidane.jpg(1080×720)上100%验证通过,还原误差≤1像素。

5. C++ CUDA中的高效实现要点

在tensorRT_Pro等C++部署框架中,restore_box需在GPU上并行执行。核心优化点有三:

5.1 避免逐点矩阵乘,用标量运算替代

CUDA kernel中不应调用IM @ [x,y,1]。因IM是稀疏矩阵(仅对角+平移),可展开为:

// 已知 IM = [[s, 0, tx], [0, s, ty]], s = 1/scale float s = IM[0]; // IM[0] = IM[4] = scale_inv float tx = IM[2]; // IM[2] = -ox * s float ty = IM[5]; // IM[5] = -oy * s // 还原单点 float orig_x = s * x + tx; float orig_y = s * y + ty;

5.2 宽高还原用乘法代替除法

orig_w = w_px * sorig_w = w_px / scale更快,且s可作为kernel参数传入。

5.3 四角点还原用向量化指令

对每个预测框,用float4一次加载四个点,用__fmadd_rn融合乘加:

// CUDA伪代码 float4 corners = make_float4(left, top, right, top); // 简化示意 float4 orig_x = fmaf(corners.x, s, tx); // s*left + tx float4 orig_y = fmaf(corners.y, s, ty); // s*top + ty // ... 同理处理其他两点

这些优化使单帧8400个框的还原耗时从3.2ms降至0.8ms(Tesla V100)。

6. 常见问题速查表

问题现象根本原因解决方案
还原后框整体偏右下IM计算错误,或M构造时ox/oy符号反了检查M[0,2]M[1,2]是否为正,IM第三列应为负
框被严重拉伸误将w/h当作点坐标用IM变换宽高必须用scale_inv缩放,不可用矩阵乘
部分框消失或错位未对还原坐标裁剪,导致负值或超界还原后必须clip(x, 0, w-1)clip(y, 0, h-1)
多尺度图效果不一致IM未随每张图动态重算每张图预处理后立即调用cv2.invertAffineTransform(M),缓存IM
Python还原准,C++不准C++中float精度不足或未roundC++中还原后强制lrintf()取整,避免浮点累积误差

获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/28 10:23:27

智能茅台预约自动化工具:3步实现效率提升300%的预约解决方案

智能茅台预约自动化工具:3步实现效率提升300%的预约解决方案 【免费下载链接】campus-imaotai i茅台app自动预约,每日自动预约,支持docker一键部署 项目地址: https://gitcode.com/GitHub_Trending/ca/campus-imaotai 你是否每天定好闹…

作者头像 李华
网站建设 2026/3/26 23:50:12

5大突破!iOS激活锁终极解决方案:从原理到实践的完全攻略

5大突破!iOS激活锁终极解决方案:从原理到实践的完全攻略 【免费下载链接】applera1n icloud bypass for ios 15-16 项目地址: https://gitcode.com/gh_mirrors/ap/applera1n 你的iOS设备遭遇激活锁困境?探索专业绕过技术的可能性 当你…

作者头像 李华
网站建设 2026/4/3 6:24:35

Qwen2.5省钱部署方案:按需GPU计费成本降低60%

Qwen2.5省钱部署方案:按需GPU计费成本降低60% 1. 为什么小模型也能撑起日常AI工作流? 你是不是也遇到过这样的情况:想跑一个轻量级大模型做内部知识问答、自动写周报、或者给产品文档生成摘要,结果一查报价——单卡A100月租3000…

作者头像 李华
网站建设 2026/3/24 10:17:23

embeddinggemma-300m企业实操:Ollama构建私有化文档语义检索系统

embeddinggemma-300m企业实操:Ollama构建私有化文档语义检索系统 在企业知识管理场景中,员工常面临“明明文档存在却找不到”的困境——传统关键词搜索无法理解“客户投诉处理流程”和“售后问题应对规范”其实是同一类内容。这时候,语义检索…

作者头像 李华
网站建设 2026/3/27 13:25:03

VibeThinker-1.5B在Electron中的集成,打造桌面智能工具

VibeThinker-1.5B在Electron中的集成,打造桌面智能工具 你是否曾想过:一个能在笔记本电脑上安静运行、不联网、不上传数据,却能实时解出LeetCode中等难度算法题、推导微积分步骤、甚至帮你写出可执行JavaScript验证函数的AI助手——它不该只…

作者头像 李华
网站建设 2026/3/25 14:24:13

Face3D.ai Pro保姆级入门:照片转4K级UV贴图全流程

Face3D.ai Pro保姆级入门:照片转4K级UV贴图全流程 关键词:Face3D.ai Pro, 3D人脸重建, UV贴图生成, ResNet50面部拓扑, 4K纹理, 单图3D建模, Gradio应用, ModelScope模型 摘要:本文以零基础用户视角,手把手带你完成从一张普通正面…

作者头像 李华