You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

加载背景图时Bounding Box Y坐标计算错误及图像缩放修复求助

修复PyQt6加载外部图像时的Bounding Box坐标与缩放问题

核心问题拆解

  • 外部图像加载时未按等比例缩放适配控件,导致显示尺寸与原图比例失衡,后续坐标映射完全错误
  • PyQt控件坐标系Y轴向下,而OpenCV图像坐标系Y轴向上,两者未做转换,导致Y坐标颠倒
  • 拖拽绘制的边界框直接使用控件坐标,未映射回原图实际尺寸,保存后在OpenCV中位置偏差

修复方案与代码修改

1. 图像加载与等比例缩放处理

加载外部图像时,计算控件与原图的比例,按最小比例缩放,同时计算图像在控件中的居中偏移量,保存这些参数用于后续坐标转换。

2. 坐标映射逻辑

将控件上的鼠标坐标,通过缩放比例和偏移量转换为原图坐标,同时在保存时翻转Y轴适配OpenCV的坐标系。

3. 修改后的完整代码

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QFileDialog
from PyQt6.QtGui import QPainter, QPen, QImage, QPixmap
from PyQt6.QtCore import Qt, QRectF
import cv2
import numpy as np

class BBoxDrawer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Bounding Box Drawer")
        self.setGeometry(100, 100, 800, 600)
        
        self.canvas = CanvasWidget(self)
        self.setCentralWidget(self.canvas)
        
        self.canvas.setFocusPolicy(Qt.FocusPolicy.StrongFocus)

class CanvasWidget(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.bbox_mode = False
        self.start_point = None
        self.current_rect = None
        self.bboxes = []
        self.bg_image = None
        self.scaled_image = None
        self.scale_ratio = 1.0
        self.image_offset = (0, 0)  # 图像在控件中的偏移量

    def keyPressEvent(self, event):
        if event.key() == Qt.Key.Key_B:
            self.bbox_mode = not self.bbox_mode
            if not self.bbox_mode:
                self.current_rect = None
                self.update()

    def mousePressEvent(self, event):
        if self.bbox_mode and self.bg_image is not None:
            img_pos = self.widget_to_image_pos(event.pos())
            self.start_point = img_pos
            self.current_rect = QRectF(img_pos.x(), img_pos.y(), 0, 0)

    def mouseMoveEvent(self, event):
        if self.bbox_mode and self.start_point is not None and self.bg_image is not None:
            img_pos = self.widget_to_image_pos(event.pos())
            self.current_rect = QRectF(
                min(self.start_point.x(), img_pos.x()),
                min(self.start_point.y(), img_pos.y()),
                abs(self.start_point.x() - img_pos.x()),
                abs(self.start_point.y() - img_pos.y())
            )
            self.update()

    def mouseReleaseEvent(self, event):
        if self.bbox_mode and self.current_rect is not None:
            if self.current_rect.width() > 5 and self.current_rect.height() > 5:
                x1 = int(self.current_rect.left())
                y1 = int(self.current_rect.top())
                x2 = int(self.current_rect.right())
                y2 = int(self.current_rect.bottom())
                self.bboxes.append((x1, y1, x2, y2))
            self.current_rect = None
            self.start_point = None
            self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.fillRect(self.rect(), Qt.GlobalColor.white)

        if self.scaled_image is not None:
            painter.drawPixmap(
                self.image_offset[0], self.image_offset[1],
                self.scaled_image.width(), self.scaled_image.height(),
                self.scaled_image
            )

        if self.current_rect is not None and self.bg_image is not None:
            widget_rect = self.image_to_widget_rect(self.current_rect)
            pen = QPen(Qt.GlobalColor.red, 2, Qt.PenStyle.DashLine)
            painter.setPen(pen)
            painter.drawRect(widget_rect)

        for bbox in self.bboxes:
            x1, y1, x2, y2 = bbox
            img_rect = QRectF(x1, y1, x2 - x1, y2 - y1)
            widget_rect = self.image_to_widget_rect(img_rect)
            pen = QPen(Qt.GlobalColor.blue, 2)
            painter.setPen(pen)
            painter.drawRect(widget_rect)

    def load_background(self, file_path):
        self.bg_image = QImage(file_path)
        if self.bg_image.isNull():
            return
        
        widget_width = self.width()
        widget_height = self.height()
        img_width = self.bg_image.width()
        img_height = self.bg_image.height()

        # 计算等比例缩放比例
        scale_width = widget_width / img_width
        scale_height = widget_height / img_height
        self.scale_ratio = min(scale_width, scale_height)

        # 生成缩放后的图像
        scaled_width = int(img_width * self.scale_ratio)
        scaled_height = int(img_height * self.scale_ratio)
        self.scaled_image = QPixmap.fromImage(self.bg_image.scaled(
            scaled_width, scaled_height,
            Qt.AspectRatioMode.KeepAspectRatio,
            Qt.TransformationMode.SmoothTransformation
        ))

        # 计算居中偏移量
        self.image_offset = (
            (widget_width - scaled_width) // 2,
            (widget_height - scaled_height) // 2
        )
        self.update()

    def widget_to_image_pos(self, widget_pos):
        # 控件坐标转原图坐标
        x = (widget_pos.x() - self.image_offset[0]) / self.scale_ratio
        y = (widget_pos.y() - self.image_offset[1]) / self.scale_ratio
        return QRectF(x, y, 0, 0).topLeft()

    def image_to_widget_rect(self, image_rect):
        # 原图坐标转控件坐标
        x = self.image_offset[0] + image_rect.left() * self.scale_ratio
        y = self.image_offset[1] + image_rect.top() * self.scale_ratio
        width = image_rect.width() * self.scale_ratio
        height = image_rect.height() * self.scale_ratio
        return QRectF(x, y, width, height)

    def save_bboxes(self, file_path):
        if not self.bboxes or self.bg_image is None:
            return
        
        img = cv2.imread(file_path)
        img_height = img.shape[0]

        for bbox in self.bboxes:
            x1, y1, x2, y2 = bbox
            # 转换PyQt的顶向下Y坐标为OpenCV的底向上Y坐标
            y1_cv = img_height - y1
            y2_cv = img_height - y2
            cv2.rectangle(img, (x1, min(y1_cv, y2_cv)), (x2, max(y1_cv, y2_cv)), (0, 255, 0), 2)
        
        cv2.imwrite("bbox_visualization.jpg", img)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = BBoxDrawer()
    window.show()

    file_path, _ = QFileDialog.getOpenFileName(window, "Open Image", "", "Image Files (*.png *.jpg *.jpeg)")
    if file_path:
        window.canvas.load_background(file_path)
        # 绘制完成后调用保存方法
        # window.canvas.save_bboxes(file_path)
    
    sys.exit(app.exec())

关键修改说明

  • 等比例缩放:使用KeepAspectRatio模式保持图像比例,避免拉伸变形
  • 坐标映射:新增双向坐标转换方法,确保绘制的边界框坐标与原图尺寸匹配
  • Y轴适配:保存时翻转Y轴,对齐OpenCV的坐标系逻辑
  • 居中显示:计算图像在控件中的偏移量,保证图像居中,坐标转换更准确

验证方法

  1. 加载外部图像,按B键进入绘制模式拖拽绘制边界框
  2. 调用save_bboxes方法生成可视化结果
  3. 打开bbox_visualization.jpg,边界框应准确覆盖目标区域

内容的提问来源于stack exchange,提问作者KansaiRobot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 15:23:19