pyqt QGraphicsView和QLabel 实现boundingbox标注框的三种方式对比

目录

  • 三种实现方式效果对比及原理概述
    • 使用QLabel实现
    • 使用QGraphicsView实现可调整的Bbox
    • QGraphicsView改进
  • 代码部分
    • QLabel实现代码
    • 改进的GraphicsView代码
    • QGraphicsView改进版的完整代码

因为需要写一个标注软件,手动标注部分关键就是画标注框。

三种实现方式效果对比及原理概述

使用QLabel实现

最开始尝试用QLabel实现,主要原理是重写鼠标点击和松开事件、重写PaintEvent()。效果丝滑,优点是简单好实现,逻辑清晰好管理。缺点是大小没法调整,如果画错了或者画歪了,只能删掉重画:
pyqt QGraphicsView和QLabel 实现boundingbox标注框的三种方式对比_第1张图片
(标注管理功能不属于本文讨论内容,在此不详述。)

使用QGraphicsView实现可调整的Bbox

在QLabel上画的东西是在画板上的,也就是基本不能手动改大小。因此我又尝试用QGraphicsView类,在这个类中的图元是可以移动位置的,基本思想是在鼠标点击和松开处画两个可选中、可移动的点/圈,然后以这两个点作为左上/右下角坐标,得到Bbox框。
实现效果贴在下面,代码之前整理在这个博客,就不在这里详述了。
pyqt QGraphicsView和QLabel 实现boundingbox标注框的三种方式对比_第2张图片

QGraphicsView改进

但是上个方法有个缺点:在鼠标点击点移动的时候,边框不会跟着实时移动。然后我又参考了这篇优秀的博客,对标注框功能做了改进:pyqt QGraphicsView和QLabel 实现boundingbox标注框的三种方式对比_第3张图片
可以看到在调整BoundingBox的时候,边框的线可以跟着移动。

:关于那个高亮红框的功能:显然还有一点bug:在调整右下角坐标的时候可以跟着调,但是在调整左上角时会错。这个bug还没de,但是不影响标记框的代码。(这个高亮功能我是用QGraphicsView和QLabel重叠起来做的,Bbox是画在上层的QGraphicsView上的,红色高亮框是画在下层的QLabel上的。这部分代码不是本文讨论的内容。)

代码部分

QLabel实现代码

继承QLabel(如果不这样做,把图放在QLabel上,标注框会在图片下面,类似下图效果)
pyqt QGraphicsView和QLabel 实现boundingbox标注框的三种方式对比_第4张图片
PicLabel类,继承QLabel类,__init__()方法中

class PicLabel(QLabel):

    def __init__(self, parent=None):
        super().__init__()
        # 初始化鼠标的位置/bbox位置
        self.flag = False
        self.x1 = 0  # 左上角坐标
        self.y1 = 0  
        self.x2 = 0  # 右下角坐标
        self.y2 = 0
        self.x2_realtime = 0  # 鼠标当前位置的坐标
        self.y2_realtime = 0

        self.bboxPointList = []  # 用来存放bbox左上和右下坐标及label,每个元素以(x1,y1,x2,y2,text)的形式储存
        self.labelList = []  # 存放label,会展示在旁边的listview中。
        self.defaultLabelId = 0

        self.drawLabelFlag = -1  # 是否加了一个框,因为弹出的输入label名字的对话框,可以点取消而不画Bbox

然后重写PicLabel类的鼠标点击方法和释放方法,重点是存下坐标,用来之后画框。

    def mousePressEvent(self,event):
        self.flag = True
        self.x1 = event.x()
        self.y1 = event.y()

    def mouseReleaseEvent(self, event):
        self.flag = False
        self.x2 = event.x()
        self.y2 = event.y()
        self.x2_realtime = self.x1
        self.y2_realtime = self.y1  # 这样就不用画出实时框了
        text, ok = QInputDialog().getText(QWidget(), '添加Label', '输入label:')
        if ok and text:
            text = self.getSpecialLabel(text)  # 这个函数是为了标签名不重复
            self.savebbox(self.x1, self.y1, self.x2, self.y2, text)
            self.labelList.append(text)
            self.drawLabelFlag *= -1  # 将标记变为正,表示画了
        elif ok:
            self.defaultLabelId += 1
            defaultLabel = 'label' + str(self.defaultLabelId)
            self.savebbox(self.x1, self.y1, self.x2, self.y2, defaultLabel)  # 这个函数在下面有解释
            self.labelList.append(defaultLabel)
            self.drawLabelFlag *= -1
        # event.ignore()  # 将信号同时发给父部件。这句话是方便标签管理用的,和画label无关

mouseReleaseEvent(self, event) 方法里面,有几个方法说明一下。

首先, getSpecialLabel 方法,这是为了每个标签不重复。(这个功能也是为了标签管理方便弄的,如果没这个需求,可以不要这个方法)

    def getSpecialLabel(self, text):
        # 获得不重名的label
        index = 0
        text_new = text
        for label in self.labelList:
            if text == label.split(' ')[0]:
                index += 1
                text_new = text + ' ' + str(index)
        return text_new

其次,savebbox(),将框的位置保存在list中:

    def savebbox(self, x1, y1, x2, y2, text):
        bbox = (x1, y1, x2, y2, text)  # 两个点的坐标以一个元组的形式储存,最后一个元素是label
        self.bboxPointList.append(bbox)

然后还想画出实时框,所以要获得每刻鼠标的位置,重写mouseMoveEvent():

    def mouseMoveEvent(self,event):
        if self.flag:
            self.x2_realtime = event.x()
            self.y2_realtime = event.y()
            self.update()

写完了这些方法,鼠标点击这个自定义Label的时候会把鼠标的坐标存起来,但是在软件界面上并不会发生什么变化,因为还没有重写paintEvent():
注:这个方法是在控件更新的时候自动调用的,所以不要在代码里显式调用它,会报错的。

    def paintEvent(self, event):
        super().paintEvent(event)
        painter = QPainter()
        painter.begin(self)
        for point in self.bboxPointList:
            rect = QRect(point[0], point[1], abs(point[0]-point[2]), abs(point[1]-point[3]))
            painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
            painter.drawRect(rect)
            painter.drawText(point[0], point[1], point[4])
        # 实时显示
        rect_realtime = QRect(self.x1, self.y1, abs(self.x1-self.x2_realtime), abs(self.y1-self.y2_realtime))
        painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine))
        painter.drawRect(rect_realtime)
        painter.end()

至此,自定义的继承QLabel的、可以画Bbox的 钮祜禄 · QLabel 就写好了。然后随便在哪个程序里把它实例化就可以用了,就像用一般的QLabel那么用,只是它有特殊的Bbox技能。

完整代码如下:(直接运行这段代码是什么都不会得到的啊!要在MainWindow之类里面实例化才行。

from PyQt5.QtCore import QRect, Qt, QObject, pyqtSignal
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtWidgets import QLabel, QInputDialog, QWidget, QTreeWidget

# class Comunicate(QObject):
#     drawLabelSignal = pyqtSignal(list)

class PicLabel(QLabel):

    def __init__(self, parent=None):
        super().__init__()
        # 初始化鼠标的位置/bbox位置
        self.flag = False
        self.x1 = 0
        self.y1 = 0
        self.x2 = 0
        self.y2 = 0
        self.x2_realtime = 0
        self.y2_realtime = 0

        self.bboxPointList = []  # 用来存放bbox左上和右下坐标及label,每个元素以(x1,y1,x2,y2,text)的形式储存
        self.labelList = []  # 存放label,会展示在旁边的listview中。现在保证了不会重名
        self.defaultLabelId = 0

        self.drawLabelFlag = -1  # 是否加了一个框,因为可以点取消而不画Bbox

    def mousePressEvent(self,event):
        self.flag = True
        self.x1 = event.x()
        self.y1 = event.y()

    def mouseReleaseEvent(self, event):
        self.flag = False
        self.x2 = event.x()
        self.y2 = event.y()
        self.x2_realtime = self.x1
        self.y2_realtime = self.y1  # 这样就不用画出实时框了
        text, ok = QInputDialog().getText(QWidget(), '添加Label', '输入label:')
        if ok and text:
            text = self.getSpecialLabel(text)
            self.savebbox(self.x1, self.y1, self.x2, self.y2, text)
            self.labelList.append(text)
            self.drawLabelFlag *= -1  # 将标记变为正,表示画了
        elif ok:
            self.defaultLabelId += 1
            defaultLabel = 'label' + str(self.defaultLabelId)
            self.savebbox(self.x1, self.y1, self.x2, self.y2, defaultLabel)
            self.labelList.append(defaultLabel)
            self.drawLabelFlag *= -1
        event.ignore()  # 将信号同时发给父部件

    def getSpecialLabel(self, text):
        # 获得不重名的label
        index = 0
        text_new = text
        for label in self.labelList:
            if text == label.split(' ')[0]:
                index += 1
                text_new = text + ' ' + str(index)
        return text_new

    def mouseMoveEvent(self,event):
        if self.flag:
            self.x2_realtime = event.x()
            self.y2_realtime = event.y()
            self.update()

    def savebbox(self, x1, y1, x2, y2, text):
        bbox = (x1, y1, x2, y2, text)  # 两个点的坐标以一个元组的形式储存,最后一个元素是label
        self.bboxPointList.append(bbox)

    def paintEvent(self, event):
        super().paintEvent(event)
        painter = QPainter()
        painter.begin(self)
        for point in self.bboxPointList:
            rect = QRect(point[0], point[1], abs(point[0]-point[2]), abs(point[1]-point[3]))
            painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
            painter.drawRect(rect)
            painter.drawText(point[0], point[1], point[4])
        # 实时显示
        rect_realtime = QRect(self.x1, self.y1, abs(self.x1-self.x2_realtime), abs(self.y1-self.y2_realtime))
        painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine))
        painter.drawRect(rect_realtime)
        painter.end()

改进的GraphicsView代码

这部分精华在这篇优秀的博客里(建议好好读一下,详细学习GraphicsView,读到后面edge的部分,再看下面的内容),我主要对里面的edge做了改写,关键是对path做了改写
关键的代码贴在这里(如果认真读了上面的,应该能很快看懂下面的。看不懂也没关系,而且如果懒得读那个博客也没关系,后文会对整个代码做解释):

# 关键改动部分
    def calc_path(self):  # 计算线条的路径
        path = QPainterPath(QPointF(self.pos_src[0], self.pos_src[1]))  # 起点
        path.lineTo(self.pos_dst[0], self.pos_src[1])
        path.lineTo(self.pos_dst[0], self.pos_dst[1])
        path.moveTo(self.pos_src[0], self.pos_src[1])
        path.lineTo(self.pos_src[0], self.pos_dst[1])
        path.lineTo(self.pos_dst[0], self.pos_dst[1])

        font = QFont("Helvetica [Cronyx]", 12)
        path.addText(self.pos_src[0], self.pos_src[1], font, self.edge_wrap.labelText)
        self.information['coordinates'] = str([self.pos_src[0], self.pos_src[1], self.pos_dst[0], self.pos_dst[1]])
        self.information['class'] = self.edge_wrap.labelText
        return path

主要思路还是左上和右下画两个圈,然后用这两个圈的坐标画一个框。
先来处理Scene,也就是所有图元安放的地方。
GraphicScene 继承 QGraphicsScene,重点是把场景中的图元(包括左上右下的圈和框(四条边))都存进list,方便管理。

class GraphicScene(QGraphicsScene):

    def __init__(self, parent=None):
        super().__init__(parent)

        # settings
        self.grid_size = 20
        self.grid_squares = 5

        # self._color_background = QColor('#393939')
        self._color_background = Qt.transparent  # 因为后面的设计中想要两个部件重叠,所以让这个背景为透明,没有这个需求的,用上面那句设定自己喜欢的背景颜色就好
        self._color_light = QColor('#2f2f2f')
        self._color_dark = QColor('#292929')

        self._pen_light = QPen(self._color_light)
        self._pen_light.setWidth(1)
        self._pen_dark = QPen(self._color_dark)
        self._pen_dark.setWidth(2)

        self.setBackgroundBrush(self._color_background)
        self.setSceneRect(0, 0, 500, 500)

        self.nodes = []  # 储存图元
        self.edges = []  # 储存连线

        self.real_x = 50

    def add_node(self, node):  # 这个函数可以改成传两个参数node1node2,弄成一组加进self.nodes里
        self.nodes.append(node)
        self.addItem(node)

    def remove_node(self, node):
        self.nodes.remove(node)
        for edge in self.edges:
            if edge.edge_wrap.start_item is node or edge.edge_wrap.end_item is node:
                self.remove_edge(edge)
        self.removeItem(node)

    def add_edge(self, edge):
        self.edges.append(edge)
        self.addItem(edge)

    def remove_edge(self, edge):
        self.edges.remove(edge)
        self.removeItem(edge)

我们先来处理圈,圈继承QGraphicsEllipseItem类,重点是要重写mouseMoveEvent方法,让圈被点击的时候线能跟着移动。

class GraphicItem(QGraphicsEllipseItem):

    def __init__(self, parent=None):
        super().__init__(parent)
        pen = QPen()
        pen.setColor(Qt.red)
        pen.setWidth(2.0)
        self.setPen(pen)
        self.pix = self.setRect(0, 0, 10, 10)
        self.width = 10
        self.height = 10
        self.setFlag(QGraphicsItem.ItemIsSelectable)
        self.setFlag(QGraphicsItem.ItemIsMovable)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        # update selected node and its edge
        # 如果图元被选中,就更新连线,这里更新的是所有。可以优化,只更新连接在图元上的。
        if self.isSelected():
            for gr_edge in self.scene().edges:
                gr_edge.edge_wrap.update_positions()

上面这个gr_edge是什么嘞?就是框四周的边,是继承QGraphicsPathItem的类的对象,这个类下面说。
另外self.scene().edges中,scene()可以获得当前图元所在的scene,而我们刚刚已经在自定义的继承QGraphicsScene的GraphicScene类中,把所有的图元(包括圈和框(框由四条线组成))存在list里面了,self.scene().edges就可以获得当前scene中所有的框(边)
然后处理边:

class Edge:
    '''
    线条的包装类
    '''

    def __init__(self, scene, start_item, end_item, labelText=''):
        super().__init__()
        # 参数分别为场景、开始图元、结束图元
        self.scene = scene
        self.start_item = start_item
        self.end_item = end_item
        self.labelText = labelText

        # 线条图形在此创建
        self.gr_edge = GraphicEdge(self)
        # add edge on graphic scene  一旦创建就添加进scene
        self.scene.add_edge(self.gr_edge)

        if self.start_item is not None:
            self.update_positions()

    def store(self):
        self.scene.add_edge(self.gr_edge)

    def update_positions(self):
        patch = self.start_item.width / 2  # 想让线条从图元的中心位置开始,让他们都加上偏移
        src_pos = self.start_item.pos()
        self.gr_edge.set_src(src_pos.x()+patch, src_pos.y()+patch)
        if self.end_item is not None:
            end_pos = self.end_item.pos()
            self.gr_edge.set_dst(end_pos.x()+patch, end_pos.y()+patch)
        else:
            self.gr_edge.set_dst(src_pos.x()+patch, src_pos.y()+patch)
        self.gr_edge.update()

    def remove_from_current_items(self):
        self.end_item = None
        self.start_item = None

    def remove(self):
        self.remove_from_current_items()
        self.scene.remove_edge(self.gr_edge)
        self.gr_edge = None
class GraphicEdge(QGraphicsPathItem):

    def __init__(self, edge_wrap, parent=None):
        super().__init__(parent)
        self.edge_wrap = edge_wrap
        print(self.edge_wrap)
        self.width = 2.0
        self.pos_src = [0, 0]  # 线条起始坐标
        self.pos_dst = [0, 0]  # 线条结束坐标

        self._pen = QPen(QColor("#000"))  # 画线条的笔
        self._pen.setWidthF(self.width)

        self._pen_dragging = QPen(QColor("#000"))  # 画拖拽线条的笔
        self._pen_dragging.setStyle(Qt.DashDotLine)
        self._pen_dragging.setWidthF(self.width)

        self._mark_pen = QPen(Qt.green)
        self._mark_pen.setWidthF(self.width)
        self._mark_brush = QBrush()
        self._mark_brush.setColor(Qt.green)
        self._mark_brush.setStyle(Qt.SolidPattern)

        # self.setFlag(QGraphicsItem.ItemIsSelectable)
        self.setZValue(-1)  # 让线条出现在所有图元的最下层

        # 标注信息
        self.information = {'coordinates':'', 'class':'', 'name':'', 'scale':'', 'owner':'', 'saliency':''}

    def set_src(self, x, y):
        self.pos_src = [x, y]

    def set_dst(self, x, y):
        self.pos_dst = [x, y]

    def calc_path(self):  # 计算线条的路径
        path = QPainterPath(QPointF(self.pos_src[0], self.pos_src[1]))  # 起点
        path.lineTo(self.pos_dst[0], self.pos_src[1])
        path.lineTo(self.pos_dst[0], self.pos_dst[1])
        path.moveTo(self.pos_src[0], self.pos_src[1])
        path.lineTo(self.pos_src[0], self.pos_dst[1])
        path.lineTo(self.pos_dst[0], self.pos_dst[1])

        font = QFont("Helvetica [Cronyx]", 12)
        path.addText(self.pos_src[0], self.pos_src[1], font, self.edge_wrap.labelText)
        self.information['coordinates'] = str([self.pos_src[0], self.pos_src[1], self.pos_dst[0], self.pos_dst[1]])
        self.information['class'] = self.edge_wrap.labelText
        return path

    def boundingRect(self):
        return self.shape().boundingRect()

    def shape(self):
        return self.calc_path()

    def paint(self, painter, graphics_item, widget=None):
        self.setPath(self.calc_path())
        path = self.path()
        if self.edge_wrap.end_item is None:
            # 包装类中存储了线条开始和结束位置的图元
            # 刚开始拖拽线条时,并没有结束位置的图元,所以是None
            # 这个线条画的是拖拽路径,点线
            painter.setPen(self._pen_dragging)
            painter.drawPath(path)
        else:
            painter.setPen(self._pen)
            painter.drawPath(path)

关键部分就是 calc_path(self) ,在原博客中是一条起始点到结束点的线,在这里把它改成一个框,加上一个label。
最后写一个类继承QGraphicsView,重点是:

  • 重写鼠标点击和松开的方法,在鼠标松开的时候弹出对话框,问要不要画Bbox以及输入label
class GraphicView(QGraphicsView):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.gr_scene = GraphicScene()
        self.parent = parent

        self.edge_enable = False
        self.drag_edge = None

        self.real_x = 0  # 暂时没用上的东西
        self.real_y = 0

        self.x1 = 0  # 记录左上角点位置
        self.y1 = 0
        self.x2 = 0  # 记录右下角点位置
        self.y2 = 0
        self.x1_view = 0  # 记录view坐标系下左上角位置
        self.y1_view = 0
        self.x2_view = 0
        self.y2_view = 0
        self.mousePressItem = False  # 当前是否点击了某个item, 如果点了,把item本身附上去
        self.drawLabelFlag = -1  # 是否加了一个框,因为可以点取消而不画Bbox
        self.bboxPointList = []  # 用来存放bbox左上和右下坐标及label,每个元素以[x1,y1,x2,y2,text]的形式储存
        self.labelList = []  # 存放label,会展示在旁边的listview中。单独放为了保证不重名
        self.defaultLabelId = 0

        self.bboxList = []  # 存放图元对象和对应的label,方便删除管理, 每个对象都是[item1, item2, edge_item]

        self.init_ui()

    def init_ui(self):
        self.setScene(self.gr_scene)
        self.setRenderHints(QPainter.Antialiasing |
                            QPainter.HighQualityAntialiasing |
                            QPainter.TextAntialiasing |
                            QPainter.SmoothPixmapTransform |
                            QPainter.LosslessImageRendering)
        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setTransformationAnchor(self.AnchorUnderMouse)
        self.setDragMode(self.RubberBandDrag)

    def mousePressEvent(self, event):
        # 转换坐标系
        pt = self.mapToScene(event.pos())
        self.x1 = pt.x()
        self.y1 = pt.y()
        self.x1_view = event.x()
        self.y1_view = event.y()
        print('上层graphic: view-', event.pos(), '  scene-', pt)

        item = self.get_item_at_click(event)
        if item:
            self.mousePressItem = item

        if event.button() == Qt.RightButton:
            if isinstance(item, GraphicItem):
                self.gr_scene.remove_node(item)
        elif self.edge_enable:
            if isinstance(item, GraphicItem):
                # 确认起点是图元后,开始拖拽
                self.edge_drag_start(item)
        else:
            super().mousePressEvent(event)  # 如果写到最开头,则线条拖拽功能会不起作用
            print('原来如此')
        event.ignore()

    def get_item_at_click(self, event):
        """ Return the object that clicked on. """
        pos = event.pos()
        item = self.itemAt(pos)
        return item

    def get_items_at_rubber(self):
        """ Get group select items. """
        area = self.rubberBandRect()
        return self.items(area)

    def mouseMoveEvent(self, event):
        # 实时更新线条
        pos = event.pos()
        # self.real_x = event.x()
        # self.real_y = event.y()
        # print(self.real_x)
        # if self.edge_enable and self.drag_edge is not None:
        #     sc_pos = self.mapToScene(pos)
        #     self.drag_edge.gr_edge.set_dst(sc_pos.x(), sc_pos.y())
        #     self.drag_edge.gr_edge.update()

        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        self.mousePressItem = False
        pt = self.mapToScene(event.pos())
        self.x2 = pt.x()
        self.y2 = pt.y()
        self.x2_view = event.x()
        self.y2_view = event.y()

        if self.edge_enable:
            # 拖拽结束后,关闭此功能
            self.edge_enable = False
            item = self.get_item_at_click(event)
            # 终点图元不能是起点图元,即无环图
            if isinstance(item, GraphicItem) and item is not self.drag_start_item:
                self.edge_drag_end(item)
            else:
                self.drag_edge.remove()
                self.drag_edge = None
        else:
            super().mouseReleaseEvent(event)
            item = self.get_item_at_click(event)  # 获得当前点击的item对象
            if not item:  # 如果不是点击item,则生成一个新的Bbox
                text, ok = QInputDialog().getText(QWidget(), '添加Label', '输入label:')
                if ok and text:
                    text = self.getSpecialLabel(text)
                    # 实际上存进去的是view坐标系下的坐标
                    self.savebbox(self.x1_view, self.y1_view, self.x2_view, self.y2_view, text)
                    self.labelList.append(text)
                    self.drawBbox(text)
                    self.drawLabelFlag *= -1  # 将标记变为正,表示画了
                elif ok:
                    self.defaultLabelId += 1
                    defaultLabel = 'label' + str(self.defaultLabelId)
                    self.savebbox(self.x1_view, self.y1_view, self.x2_view, self.y2_view, defaultLabel)
                    self.labelList.append(defaultLabel)
                    self.drawBbox(defaultLabel)
                    self.drawLabelFlag *= -1
            else:  # 如果点击了item,说明想拖动item
                print('点击item拖动,更新BboxPointList')
                print('更新前bboxPointList:', self.bboxPointList)
                index, position = self.findBboxItemIndexFromItem(item)
                label_text = self.bboxList[index][2].gr_edge.information['class']
                index_in_bboxPointList = self.findBboxFromLabel(label_text)
                if position == 1 :
                    self.bboxPointList[index_in_bboxPointList][0] = self.x2_view
                    self.bboxPointList[index_in_bboxPointList][1] = self.y2_view
                else:
                    self.bboxPointList[index_in_bboxPointList][2] = self.x2_view
                    self.bboxPointList[index_in_bboxPointList][3] = self.y2_view
                print('更新后bboxPointList:', self.bboxPointList)
            event.ignore()  # 将信号同时发给父部件

    def drawBbox(self, label_text):
        item1 = GraphicItem()
        item1.setPos(self.x1, self.y1)
        self.gr_scene.add_node(item1)

        item2 = GraphicItem()
        item2.setPos(self.x2, self.y2)
        self.gr_scene.add_node(item2)

        edge_item = Edge(self.gr_scene, item1, item2, label_text)  # 这里原来是self.drag_edge,我给删了

        self.bboxList.append([item1, item2, edge_item])

        print(self.bboxPointList)

    def savebbox(self, x1, y1, x2, y2, text):
        bbox = [x1, y1, x2, y2, text]  # 两个点的坐标以一个元组的形式储存,最后一个元素是label
        self.bboxPointList.append(bbox)

    def getSpecialLabel(self, text):
        # 获得不重名的label
        index = 0
        text_new = text
        for label in self.labelList:
            if text == label.split(' ')[0]:
                index += 1
                text_new = text + ' ' + str(index)
        return text_new

    def edge_drag_start(self, item):
        self.drag_start_item = item  # 拖拽开始时的图元,此属性可以不在__init__中声明
        # 开始拖拽线条,注意到拖拽终点为None
        self.drag_edge = Edge(self.gr_scene, self.drag_start_item, None)

    def edge_drag_end(self, item):
        new_edge = Edge(self.gr_scene, self.drag_start_item, item)
        self.drag_edge.remove()  # 删除拖拽时画的线
        self.drag_edge = None
        new_edge.store()  # 保存最终产生的连接线

    def findBboxFromLabel(self, label):
        '''
        根据label的内容找到self.bboxPointList的index
        '''
        for i,b in enumerate(self.bboxPointList):
            if b[4] == label:
                return i

    def findBboxItemIndexFromLabel(self, label_text):
        '''
        根据label的内容找到self.bboxList的index
        '''
        for i,b in enumerate(self.bboxList):
            edge_item = b[2]
            text = edge_item.labelText
            if text == label_text:
                return i

    def findBboxItemIndexFromItem(self, item):
        # 根据左上角或右下角的item找到此Bbox在数组中的位置
        for i,b in enumerate(self.bboxList):
            if b[0] == item:
                return i, 1  # 第二个参数1代表点击的是左上点
            elif b[1] == item:
                return i, 2  # 第二个参数2代表点击的是右下点
            else:
                return -1, -1  # 表示没找着

    def removeBbox(self, index):
        item1, item2, edge_item = self.bboxList[index]
        self.gr_scene.remove_node(item1)
        self.gr_scene.remove_node(item2)
        # self.gr_scene.remove_edge(edge_item)
        del self.bboxList[index]

(这里面有些没被调用的查找index的方法,是用来管理标签的,可以忽略。)

QGraphicsView改进版的完整代码

全部代码为:
(这部分代码也是要在某个主程序里面用,直接运行是没有东西的!)
(主程序用另一个python文件装,附在最后面)

import math

from PyQt5.QtWidgets import QGraphicsView, QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsScene, \
    QInputDialog, QWidget, QGraphicsTextItem, QGraphicsRectItem
from PyQt5.QtCore import Qt, QPointF, QLine
from PyQt5.QtGui import QPainter, QPen, QColor, QBrush, QPainterPath, QFont


class GraphicView(QGraphicsView):

    def __init__(self, parent=None):
        super().__init__(parent)

        self.gr_scene = GraphicScene()
        self.parent = parent

        self.edge_enable = False
        self.drag_edge = None

        self.real_x = 0  # 暂时没用上的东西
        self.real_y = 0

        self.x1 = 0  # 记录左上角点位置
        self.y1 = 0
        self.x2 = 0  # 记录右下角点位置
        self.y2 = 0
        self.x1_view = 0  # 记录view坐标系下左上角位置
        self.y1_view = 0
        self.x2_view = 0
        self.y2_view = 0
        self.mousePressItem = False  # 当前是否点击了某个item, 如果点了,把item本身附上去
        self.drawLabelFlag = -1  # 是否加了一个框,因为可以点取消而不画Bbox
        self.bboxPointList = []  # 用来存放bbox左上和右下坐标及label,每个元素以[x1,y1,x2,y2,text]的形式储存
        self.labelList = []  # 存放label,会展示在旁边的listview中。单独放为了保证不重名
        self.defaultLabelId = 0

        self.bboxList = []  # 存放图元对象和对应的label,方便删除管理, 每个对象都是[item1, item2, edge_item]

        self.init_ui()

    def init_ui(self):
        self.setScene(self.gr_scene)
        self.setRenderHints(QPainter.Antialiasing |
                            QPainter.HighQualityAntialiasing |
                            QPainter.TextAntialiasing |
                            QPainter.SmoothPixmapTransform |
                            QPainter.LosslessImageRendering)
        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setTransformationAnchor(self.AnchorUnderMouse)
        self.setDragMode(self.RubberBandDrag)

    def mousePressEvent(self, event):
        # 转换坐标系
        pt = self.mapToScene(event.pos())
        self.x1 = pt.x()
        self.y1 = pt.y()
        self.x1_view = event.x()
        self.y1_view = event.y()
        print('上层graphic: view-', event.pos(), '  scene-', pt)

        item = self.get_item_at_click(event)
        if item:
            self.mousePressItem = item

        if event.button() == Qt.RightButton:
            if isinstance(item, GraphicItem):
                self.gr_scene.remove_node(item)
        elif self.edge_enable:
            if isinstance(item, GraphicItem):
                # 确认起点是图元后,开始拖拽
                self.edge_drag_start(item)
        else:
            super().mousePressEvent(event)  # 如果写到最开头,则线条拖拽功能会不起作用
            print('原来如此')
        event.ignore()

    def get_item_at_click(self, event):
        """ Return the object that clicked on. """
        pos = event.pos()
        item = self.itemAt(pos)
        return item

    def get_items_at_rubber(self):
        """ Get group select items. """
        area = self.rubberBandRect()
        return self.items(area)

    def mouseMoveEvent(self, event):
        # 实时更新线条
        pos = event.pos()
        # self.real_x = event.x()
        # self.real_y = event.y()
        # print(self.real_x)
        # if self.edge_enable and self.drag_edge is not None:
        #     sc_pos = self.mapToScene(pos)
        #     self.drag_edge.gr_edge.set_dst(sc_pos.x(), sc_pos.y())
        #     self.drag_edge.gr_edge.update()

        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        self.mousePressItem = False
        pt = self.mapToScene(event.pos())
        self.x2 = pt.x()
        self.y2 = pt.y()
        self.x2_view = event.x()
        self.y2_view = event.y()

        if self.edge_enable:
            # 拖拽结束后,关闭此功能
            self.edge_enable = False
            item = self.get_item_at_click(event)
            # 终点图元不能是起点图元,即无环图
            if isinstance(item, GraphicItem) and item is not self.drag_start_item:
                self.edge_drag_end(item)
            else:
                self.drag_edge.remove()
                self.drag_edge = None
        else:
            super().mouseReleaseEvent(event)
            item = self.get_item_at_click(event)  # 获得当前点击的item对象
            if not item:  # 如果不是点击item,则生成一个新的Bbox
                text, ok = QInputDialog().getText(QWidget(), '添加Label', '输入label:')
                if ok and text:
                    text = self.getSpecialLabel(text)
                    # 实际上存进去的是view坐标系下的坐标
                    self.savebbox(self.x1_view, self.y1_view, self.x2_view, self.y2_view, text)
                    self.labelList.append(text)
                    self.drawBbox(text)
                    self.drawLabelFlag *= -1  # 将标记变为正,表示画了
                elif ok:
                    self.defaultLabelId += 1
                    defaultLabel = 'label' + str(self.defaultLabelId)
                    self.savebbox(self.x1_view, self.y1_view, self.x2_view, self.y2_view, defaultLabel)
                    self.labelList.append(defaultLabel)
                    self.drawBbox(defaultLabel)
                    self.drawLabelFlag *= -1
            else:  # 如果点击了item,说明想拖动item
                print('点击item拖动,更新BboxPointList')
                print('更新前bboxPointList:', self.bboxPointList)
                index, position = self.findBboxItemIndexFromItem(item)
                label_text = self.bboxList[index][2].gr_edge.information['class']
                index_in_bboxPointList = self.findBboxFromLabel(label_text)
                if position == 1 :
                    self.bboxPointList[index_in_bboxPointList][0] = self.x2_view
                    self.bboxPointList[index_in_bboxPointList][1] = self.y2_view
                else:
                    self.bboxPointList[index_in_bboxPointList][2] = self.x2_view
                    self.bboxPointList[index_in_bboxPointList][3] = self.y2_view
                print('更新后bboxPointList:', self.bboxPointList)
            event.ignore()  # 将信号同时发给父部件

    def drawBbox(self, label_text):
        item1 = GraphicItem()
        item1.setPos(self.x1, self.y1)
        self.gr_scene.add_node(item1)

        item2 = GraphicItem()
        item2.setPos(self.x2, self.y2)
        self.gr_scene.add_node(item2)

        edge_item = Edge(self.gr_scene, item1, item2, label_text)  # 这里原来是self.drag_edge,我给删了

        self.bboxList.append([item1, item2, edge_item])

        print(self.bboxPointList)

    def savebbox(self, x1, y1, x2, y2, text):
        bbox = [x1, y1, x2, y2, text]  # 两个点的坐标以一个元组的形式储存,最后一个元素是label
        self.bboxPointList.append(bbox)

    def getSpecialLabel(self, text):
        # 获得不重名的label
        index = 0
        text_new = text
        for label in self.labelList:
            if text == label.split(' ')[0]:
                index += 1
                text_new = text + ' ' + str(index)
        return text_new

    def edge_drag_start(self, item):
        self.drag_start_item = item  # 拖拽开始时的图元,此属性可以不在__init__中声明
        # 开始拖拽线条,注意到拖拽终点为None
        self.drag_edge = Edge(self.gr_scene, self.drag_start_item, None)

    def edge_drag_end(self, item):
        new_edge = Edge(self.gr_scene, self.drag_start_item, item)
        self.drag_edge.remove()  # 删除拖拽时画的线
        self.drag_edge = None
        new_edge.store()  # 保存最终产生的连接线

    def findBboxFromLabel(self, label):
        '''
        根据label的内容找到self.bboxPointList的index
        '''
        for i,b in enumerate(self.bboxPointList):
            if b[4] == label:
                return i

    def findBboxItemIndexFromLabel(self, label_text):
        '''
        根据label的内容找到self.bboxList的index
        '''
        for i,b in enumerate(self.bboxList):
            edge_item = b[2]
            text = edge_item.labelText
            if text == label_text:
                return i

    def findBboxItemIndexFromItem(self, item):
        # 根据左上角或右下角的item找到此Bbox在数组中的位置
        for i,b in enumerate(self.bboxList):
            if b[0] == item:
                return i, 1  # 第二个参数1代表点击的是左上点
            elif b[1] == item:
                return i, 2  # 第二个参数2代表点击的是右下点
            else:
                return -1, -1  # 表示没找着

    def removeBbox(self, index):
        item1, item2, edge_item = self.bboxList[index]
        self.gr_scene.remove_node(item1)
        self.gr_scene.remove_node(item2)
        # self.gr_scene.remove_edge(edge_item)
        del self.bboxList[index]


class GraphicScene(QGraphicsScene):

    def __init__(self, parent=None):
        super().__init__(parent)

        # settings
        self.grid_size = 20
        self.grid_squares = 5

        # self._color_background = QColor('#393939')
        self._color_background = Qt.transparent
        self._color_light = QColor('#2f2f2f')
        self._color_dark = QColor('#292929')

        self._pen_light = QPen(self._color_light)
        self._pen_light.setWidth(1)
        self._pen_dark = QPen(self._color_dark)
        self._pen_dark.setWidth(2)

        self.setBackgroundBrush(self._color_background)
        self.setSceneRect(0, 0, 500, 500)

        self.nodes = []  # 储存图元
        self.edges = []  # 储存连线

        self.real_x = 50

    def add_node(self, node):  # 这个函数可以改成传两个参数node1node2,弄成一组加进self.nodes里
        self.nodes.append(node)
        self.addItem(node)

    def remove_node(self, node):
        self.nodes.remove(node)
        for edge in self.edges:
            if edge.edge_wrap.start_item is node or edge.edge_wrap.end_item is node:
                self.remove_edge(edge)
        self.removeItem(node)

    def add_edge(self, edge):
        self.edges.append(edge)
        self.addItem(edge)

    def remove_edge(self, edge):
        self.edges.remove(edge)
        self.removeItem(edge)

class GraphicItem(QGraphicsEllipseItem):

    def __init__(self, parent=None):
        super().__init__(parent)
        pen = QPen()
        pen.setColor(Qt.red)
        pen.setWidth(2.0)
        self.setPen(pen)
        self.pix = self.setRect(0, 0, 10, 10)
        self.width = 10
        self.height = 10
        self.setFlag(QGraphicsItem.ItemIsSelectable)
        self.setFlag(QGraphicsItem.ItemIsMovable)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        # update selected node and its edge
        # 如果图元被选中,就更新连线,这里更新的是所有。可以优化,只更新连接在图元上的。
        if self.isSelected():
            for gr_edge in self.scene().edges:
                gr_edge.edge_wrap.update_positions()

class Edge:
    '''
    线条的包装类
    '''

    def __init__(self, scene, start_item, end_item, labelText=''):
        super().__init__()
        # 参数分别为场景、开始图元、结束图元
        self.scene = scene
        self.start_item = start_item
        self.end_item = end_item
        self.labelText = labelText

        # 线条图形在此创建
        self.gr_edge = GraphicEdge(self)
        # add edge on graphic scene  一旦创建就添加进scene
        self.scene.add_edge(self.gr_edge)

        if self.start_item is not None:
            self.update_positions()

    def store(self):
        self.scene.add_edge(self.gr_edge)

    def update_positions(self):
        patch = self.start_item.width / 2  # 想让线条从图元的中心位置开始,让他们都加上偏移
        src_pos = self.start_item.pos()
        self.gr_edge.set_src(src_pos.x()+patch, src_pos.y()+patch)
        if self.end_item is not None:
            end_pos = self.end_item.pos()
            self.gr_edge.set_dst(end_pos.x()+patch, end_pos.y()+patch)
        else:
            self.gr_edge.set_dst(src_pos.x()+patch, src_pos.y()+patch)
        self.gr_edge.update()

    def remove_from_current_items(self):
        self.end_item = None
        self.start_item = None

    def remove(self):
        self.remove_from_current_items()
        self.scene.remove_edge(self.gr_edge)
        self.gr_edge = None

class GraphicEdge(QGraphicsPathItem):

    def __init__(self, edge_wrap, parent=None):
        super().__init__(parent)
        self.edge_wrap = edge_wrap
        print(self.edge_wrap)
        self.width = 2.0
        self.pos_src = [0, 0]  # 线条起始坐标
        self.pos_dst = [0, 0]  # 线条结束坐标

        self._pen = QPen(QColor("#000"))  # 画线条的笔
        self._pen.setWidthF(self.width)

        self._pen_dragging = QPen(QColor("#000"))  # 画拖拽线条的笔
        self._pen_dragging.setStyle(Qt.DashDotLine)
        self._pen_dragging.setWidthF(self.width)

        self._mark_pen = QPen(Qt.green)
        self._mark_pen.setWidthF(self.width)
        self._mark_brush = QBrush()
        self._mark_brush.setColor(Qt.green)
        self._mark_brush.setStyle(Qt.SolidPattern)

        # self.setFlag(QGraphicsItem.ItemIsSelectable)
        self.setZValue(-1)  # 让线条出现在所有图元的最下层

        # 标注信息
        self.information = {'coordinates':'', 'class':'', 'name':'', 'scale':'', 'owner':'', 'saliency':''}

    def set_src(self, x, y):
        self.pos_src = [x, y]

    def set_dst(self, x, y):
        self.pos_dst = [x, y]

    def calc_path(self):  # 计算线条的路径
        path = QPainterPath(QPointF(self.pos_src[0], self.pos_src[1]))  # 起点
        path.lineTo(self.pos_dst[0], self.pos_src[1])
        path.lineTo(self.pos_dst[0], self.pos_dst[1])
        path.moveTo(self.pos_src[0], self.pos_src[1])
        path.lineTo(self.pos_src[0], self.pos_dst[1])
        path.lineTo(self.pos_dst[0], self.pos_dst[1])

        font = QFont("Helvetica [Cronyx]", 12)
        path.addText(self.pos_src[0], self.pos_src[1], font, self.edge_wrap.labelText)
        self.information['coordinates'] = str([self.pos_src[0], self.pos_src[1], self.pos_dst[0], self.pos_dst[1]])
        self.information['class'] = self.edge_wrap.labelText
        return path

    def boundingRect(self):
        return self.shape().boundingRect()

    def shape(self):
        return self.calc_path()

    def paint(self, painter, graphics_item, widget=None):
        self.setPath(self.calc_path())
        path = self.path()
        if self.edge_wrap.end_item is None:
            # 包装类中存储了线条开始和结束位置的图元
            # 刚开始拖拽线条时,并没有结束位置的图元,所以是None
            # 这个线条画的是拖拽路径,点线
            painter.setPen(self._pen_dragging)
            painter.drawPath(path)
        else:
            painter.setPen(self._pen)
            painter.drawPath(path)

上面所有的代码都放在一个python文件里,叫view.py。主程序放在另一个python文件里:

import sys
import cgitb

from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMainWindow

from view import GraphicView, GraphicScene  # 上面所有的代码都放在一个python文件里,叫view.py

cgitb.enable(format("text"))


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.scene = GraphicScene(self)
        self.view = GraphicView(self.scene, self)

        self.setMinimumHeight(500)
        self.setMinimumWidth(500)
        self.setCentralWidget(self.view)
        self.setWindowTitle("Graphics Demo")


def demo_run():
    app = QApplication(sys.argv)
    demo = MainWindow()
    # compatible with Mac Retina screen.
    app.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
    app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    # show up
    demo.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    demo_run()

以上。

你可能感兴趣的:(Windows开发,python,pyqt5)