简介:在图形界面开发中,交互式绘图与标注是常见的需求,而Qt提供的Graphics View框架为处理复杂图形场景和用户交互提供了优雅的解决方案。该框架通过视图、场景与图形项的三层结构,将坐标变换、碰撞检测、选中事件等底层逻辑封装起来,极大地提升了开发效率和可扩展性。坐标映射是标注工具的核心,理解视图坐标、场景坐标与图像原始坐标的转换关系,才能确保标注数据的准确性。Graphics View的技术价值在于,它将每个标注对象建模为独立的图形项,支持灵活的样式定制和交互响应,广泛应用于图片标注、地图编辑、CAD等场景。本文基于Qt 5和C++,详细讲解如何使用QGraphicsView构建一个功能完备的图片标注工具,涵盖坐标变换、图形项封装、数据序列化及常见问题排查,帮助开发者快速掌握这一实用技术。
2. 为什么选择 QGraphics View 而不是直接在 QWidget 上画
先说结论:用 QGraphics View 做图片标注工具,尤其是要做矩形框、多边形、点选这类交互式标注,几乎是首选架构。
为什么不直接在 QWidget 的 paintEvent 里画?原因很简单:可扩展性和交互能力差太多。直接绘制方案,每个标注框都要自己维护坐标、自己处理鼠标命中检测、自己管理重绘区域,标注数量一多,代码就乱成了麻。而 QGraphics View 体系下,每个标注就是一个 QGraphicsItem 对象,它自带了坐标变换、碰撞检测、选中事件、层级关系,这些能力如果全都自己造轮子,一个月都写不完。
2.1 坐标变换是标注工具的灵魂
标注工具里最容易翻车的点就是坐标变换。图片标注这个场景里有三套坐标体系:
一是视图坐标,就是 QGraphicsView 上的像素坐标,鼠标事件返回的 event->pos() 是这一套;二是场景坐标,就是 QGraphicsScene 里的坐标,所有图形项都生活在这一层;三是图片原始坐标,也就是要写进标注文件里的像素坐标。
Qt 提供了 mapToScene 和 mapFromScene 这两组方法,作用是把鼠标的视图坐标转换到场景坐标。但注意,这还不能直接存盘。因为图片被缩放、平移之后,场景坐标不等于图片坐标。正确做法是拿标注框的场景坐标去跟图片 item 的坐标做一次映射。
我给标注矩形加了 getRectInImageCoords 方法,返回的是相对图片左上角的坐标。这个值是最终要写入 JSON 文件的数据。
2.2 图形项的设计直接决定后期维护成本
标注矩形我用了 QGraphicsRectItem,但没让它直接裸奔,而是做了两层封装:第一层是 AnnotationRectItem,负责绘制样式(边的粗细、颜色、高亮效果、尺寸约束);第二层是 AnnotationData,负责纯数据(坐标、类别标签)。这样 UI 和数据的边界非常清晰。
有一个经验值得分享:一定要给 QGraphicsRectItem 开启 setFlags,否则你会发现标注框无法被鼠标选中,更别提拖动了。需要设置的标志位至少包括:ItemIsSelectable、ItemIsMovable、ItemIsFocusable。第一个管选中,第二个管拖动,第三个管接收键盘事件(比如按 Delete 键删除标注框就是靠它)。
如果加了 ItemIsFocusable 之后,鼠标滚轮缩放事件会失灵——因为焦点在 item 上,滚轮事件被 item 吃掉了。解决方式是在 MainWindow 的 eventFilter 里做处理,或者重写 item 的 wheelEvent 直接忽略掉。
2.3 标注数据的序列化要有远见
标注数据我选了 JSON 格式,没有用自定义的二进制格式,原因很简单:JSON 可读性高、调试方便、和其他工具对接也容易。Qt 自带的 QJsonDocument 完全够用,不需要引入第三方库。
标注结果的 JSON 结构我一开始就设计得很宽:
每个标注矩形包含:
- label:类别的字符串标识
- className:类别的显示名(比如"car")
- x、y、width、height:图片原始像素坐标(浮点数)
- 可选字段:confidence(置信度)、group_id(实例分组)、is_hard(困难样本标记)
预留这些字段的代价几乎为零,但等你要做二次开发时,就知道"当初多写了一个字段"有多香了。
3. 代码实现:环境配置、函数设计、关键代码
这一节直接把代码逻辑拆开揉碎了讲。环境是 Windows 11 + Qt 5.15.2 + MinGW 64 位,编译器用 MinGW 而不是 MSVC,原因是 Qt 开源自带的套件就是 MinGW,省去单独装 Visual Studio 的麻烦。如果你手头是 Qt 6.x,代码基本兼容,个别 API 有小改动。
工程文件用 qmake,完整 .pro 文件如下:
这段配置里 include 和 DEPENDPATH 是指向当前目录,add 了 widgets 模块。整个工程的类只有 5 个,没有用 model/view 那一套重型架构,因为标注工具的复杂度用不着上 MVC。
3.1 MainWindow 的主界面布局
主界面的布局逻辑很简单:菜单栏 + 中央 QGraphicsView + 状态栏 + 右侧浮动面板。
右侧面板包含:
- 类别管理区:可以添加、编辑类别(类别的颜色自动分配)
- 标注列表区:显示当前图片的所有标注框,选中哪一项,对应框体高亮
- 保存区:导出当前图片的标注 JSON
3.2 图片加载和缩放
图片加载后会把 QPixmap 塞进一个 QGraphicsPixmapItem,然后 setSceneRect 让场景和图片尺寸保持一致。这部分有一个坑需要专门说:
图片加载后的显示模式选择"适应窗口"而不是"原始大小",第一张图看起来是铺满的,但一旦你缩放了图片,点击"自适应窗口"时,应该调用 view->fitInView(pixmapItem, Qt::KeepAspectRatio),而不是手动去 setSceneRect。
fitInView 的第二个参数选项,KeepAspectRatio 是等比缩放,IgnoreAspectRatio 会拉伸图片导致变形,标注工具的标注框坐标是基于图片的,所以必须保持等比,这一点不能偷懒。
状态栏显示的缩放比例,我是从 view->transform().m11() 拿到的,因为做了等比缩放,m11 和 m22 相等,直接取一个就行。
3.3 标注矩形绘制的完整流程
鼠标画矩形的完整逻辑:
- 按下鼠标左键时,如果当前处于"画框模式",记录场景坐标作为起点,并创建一个宽度和高度为 0 的 QGraphicsRectItem
- 拖动过程中,实时更新矩形的右下角坐标
- 松开鼠标时,如果矩形的宽高小于 10 像素,直接删除该 item,认为是误操作
这个防误操作设置太重要了。我最初做完功能自测时,经常因为鼠标单击导致生成了 1 像素宽高的矩形框,后来加了 10 像素的最小限制,这类问题再也没出现过。
3.4 标注数据导入导出
导出逻辑把每个 QGraphicsRectItem 的 AnnotationData 序列化到 JSON。这里需要注意,保存时标注坐标必须取图片原始坐标,而不能直接存场景坐标。转换公式很简单:
得到一个相对图片的坐标:imgX = rect.x() - imageItem->pos().x(); imgY = rect.y() - imageItem->pos().y(); 宽度和高度直接取 rect 的 width 和 height。
注意:这里的坐标换算必须用 item->pos() 而不能写成 imageItem->mapFromScene(rect.topLeft()),因为 QGraphicsRectItem 的 rect() 本身就是基于 item 自身的坐标系,scence 坐标转 item 坐标用 mapFromScene 更准,但这里换算出的是相对图片左上角的偏移量,imgX 和 imgY 直接等于偏移量。
4. 核心代码全解:标注工具的骨架与五脏六腑
这一部分把上面设计思路落成代码。我分几个文件来拆解。
4.1 工程文件与入口
MainWindow 的核心信号槽用 connect 连接,代码结构上把"画框模式切换"用了一个简易状态机管理:当前模式是画矩形、选择图形还是缩放图片。
这样设计的核心好处是:以后要扩展"画多边形标注"或"画点标注",只需要新增对应的绘制函数和状态即可。
4.2 AnnotationRectItem 类的实现
AnnotationRectItem 的核心区别在于它复写了 paint() 函数。默认的 QGraphicsRectItem 的绘制只能设置统一的画笔颜色,我需要一个"选中时高亮"的效果,以及一个"边角处显示缩放控制点"的效果。
判断 item 是否被选中,在 paint 函数里使用 option->state 与 QStyle::State_Selected 的按位与操作。一旦检测到选中,画笔宽度从 2 像素加粗到 4 像素,同时画出四个角的锚点方块。
4.3 视图与事件处理
QGraphicsView 默认的事件分发在某些场景下不够用。比如滚轮缩放图片时,我希望以鼠标所在位置为中心缩放,这样更符合人的操作直觉。默认的 QGraphicsView 缩放行为是以视图中心为锚点的,需要用 setTransformationAnchor(QGraphicsView::AnchorUnderMouse)。
我在做滚动缩放时额外做了对比度限制:缩放比例 minScale=0.1,maxScale=10.0。小于 0.1 时图片占的像素太小,标注已经失去意义;大于 10.0 时像素被过度放大,边缘模糊。这个限制对标注工具的用户体验提升很大。
4.4 拖拽标注框的边界检查
图片标注时,允许用户拖动已画好的矩形框,但矩形框不能拖出图片范围。我在 AnnotationRectItem 的 itemChange 事件里捕获 ItemPositionChange 变化,一旦发现新位置越界,就将位置反馈为旧位置。这个逻辑看似简单,但写错位置会导致矩形抖动或者"拖不动"的诡异问题。
5. 环境配置与编译运行全流程
这个项目用到的依赖只有 Qt 5.15.2 的 Widgets 模块,没有任何第三方库。但是 Qt 环境本身配置也有一坑要说明。
5.1 minGW 编译器与 Qt 版本匹配
很多人下载 Qt 后,编译不过,查了半天发现是环境变量没配好。Qt 5.15 之后,官方不再单独提供离线 installer,要用在线安装器,而且 5.15 的在线安装包只提供商业或开源注册版。这一步其实很多人都卡在这里不断试错。
其实有更省心的方案:直接从 Qt 镜像站下载 Qt Online Installer,登录 Qt 账号后选择 5.15.2 版本,把 MinGW 8.1.0 64 位的那个选项勾上。不要把 Qt 的维护工具跟编译器混为一谈,MinGW 编译器是需要单独勾选的组件。
5.2 编译 .pro 文件
进入源码目录,终端执行:qmake && mingw32-make,如果没报错,直接得到可执行文件。这时候最小依赖的运行库有三四个 DLL,部署起来比较麻烦,推荐用 windeployqt 工具去扫描依赖,它会把 Qt 运行时依赖的 DLL 全部拷贝到 exe 同目录。
6. 常见问题与排查技巧实录
这个项目我在代码调试时遇到了几个典型问题,都是花了不少时间踩坑才解决的,列出来供大家参考。
| 问题现象 | 根本原因 | 解决方法 |
|---|---|---|
| 图片加载后一片空白 | QGraphicsPixmapItem 未调用 setSceneRect 或者 pixmap 为 null | 检查文件路径是否正确;用 QFile::exists 验证路径后,再 setPixmap |
| 标注矩形框画不出来 | 没有设置 item 的 flag,或者画笔颜色太浅导致看不见 | 给 item 设置 setFlags 选中和移动标志;画笔颜色用饱和度较高的值 |
| 画矩形时造成 view 整体移动 | 滚轮事件或拖拽事件被 QGraphicsView 截获 | 设置 view 的 dragMode 为 RubberBandDrag;同时确保 annotation 模式下忽略鼠标移动事件 |
| 矩形框旋转后位置错乱 | QGraphicsRectItem 的 rect() 和场景坐标混用 | 所有坐标转换统一用 mapToScene / mapFromScene,不要混用 |
| 导出 JSON 时中文乱码 | 控制台输出编码不一致 | 用 QString::toUtf8() 或 QFile 写文件时指定编码 UTF-8 |
6.1 中文路径导致图片加载失败
Windows 下 Qt 的 QFile 本身就是支持中文路径的,但如果你用了 ifstream 或者 C 标准库去读取文件,遇到中文路径就会失败。这个项目里图片加载全部走 QPixmap 和 QFile,所以没有这个问题。但如果把代码移植到其他平台或者用了第三方库,要格外注意路径编码。
6.2 图像缩略图与真实图片的比例
如果你最后把导出的标注框映射到缩略图上,必须使用同一套坐标缩放比例,不能直接拿原图坐标除以某个整数,必须精确计算 scaleX = thumbWidth / imageWidth,scaleY = thumbHeight / imageHeight,然后分别乘以 x、y、width、height,再取整。很多人在这一步偷懒导致框位偏大几个像素,但实际上像素级的偏差对模型训练影响不大,不过做可视化检查时很不美观。
7. 后续扩展方向与项目总结
这个标注工具做到"能用"很简单,做到"好用"却有不少路要走。我把整理出来的扩展方向和优先级列一下。
最值得优先做的扩展是标注类别预设管理:在实际标注时,类别通常有几十种(比如目标检测里的车辆、行人、交通灯等),每个类别的颜色应该保持固定,并且能一键切换。我在当前版本里只是把颜色随机分配,但标注工具一旦需要多人协作,就需要一个统一的类别配置文件。
其次是标注数据检查功能:标注完成后,应该提供"查看所有标注框"的列表视图,并支持一键定位到某个框、删除误标注、修改类别等操作。这个和导出 JSON 的 schema 严格对应。
再进一步是支持导入外部标注格式:比如 Pascal VOC 的 XML 格式、YOLO 的 txt 格式。这样标注工具就能无缝接入常用的训练流程。实际上如果只是给自己用,支持一种格式就够了,但如果你需要开源出去,多格式导入导出几乎是标配。
最后,还有一个我实测过的小技巧:定期自动保存。标注是个耗时活,如果程序中途崩溃,所有标注数据都会丢失。我后来给项目加了一个 QTimer 自动保存机制,每 30 秒把当前图片的标注数据写入临时备份文件,这个改动成本极低,但实用价值极大。
用 Qt 和 C++ 实现图片标注工具,本质上练习的是三件事:C++ 对象设计能力、Qt 事件系统与 View/Scene 框架的理解、以及数据格式设计能力。这个项目做完,
本文还有配套的精品资源,点击获取