十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Qt的深度学习图像标注工具开源代码解析

基于Qt的深度学习图像标注工具开源代码解析 简介这是一款面向计算机视觉研究者与深度学习开发者的Qt跨平台图像标注工具开源项目专为解决模型训练前的高质量数据标注难题而设计适用于目标检测、实例分割等任务的数据预处理环节。资源包共61个文件包含13个核心CPP源码、12个H头文件构成完整Qt应用架构4个QRC资源文件管理界面图标如open.png、zoomIn.png、polygon.png等以及XML/JSON多格式标注读写模块XmlWriteRead.cpp、JsonWriteRead.h、XmlPolygonWriteRead.cpp等整体仅171KB轻量易部署。已有1744人学习下载代码结构清晰、模块职责分明——主窗口、文件列表、标注框管理、多边形编辑、OpenCV图像处理等功能均独立封装附带APPIcon图标与Pro工程配置支持二次开发与格式扩展。 做深度学习项目的人估计都有过这种经历模型选型、训练调参都折腾完了回头一看数据标注才是真正卡脖子的地方。尤其做目标检测、分割这类监督学习任务几千张图等着标框用LabelImg这类老牌工具又得开Python环境格式转换还要写脚本标注效率低不说团队协作起来更是麻烦。我自己之前带项目时就踩过不少坑后来索性基于Qt把标注工具重写了一遍代码开源出去没想到在社区里反响还不错。这次分享的就是这个项目的完整拆解基于Qt的深度学习图像标签工具开源代码。它会详细讲清楚这个工具的设计思路、核心交互实现、数据格式对接以及从环境搭建到实际标注的全流程。不管是刚入门深度学习、需要自己准备数据集的同学还是做算法工程化、需要给团队搭建标注流程的开发者都可以直接照着思路落地。1. 项目整体设计与思路拆解1.1 为什么是Qt而不是纯Web方案很多人第一反应是现在标注工具不是都用Web吗LabelStudio、CVAT都是Web端的为什么还要用Qt重新做一套我个人的体会是Qt在图像标注这个特定场景下有几个Web方案比不了的优势。第一个是原生性能。图像标注要频繁处理大图缩放、拖拽、画框重绘这些操作Qt的QGraphicsView框架是C底层绘制GraphicItem挂在场景里平移缩放都是显卡加速级别的流畅感。Web端用Canvas实现标注图元一多就容易卡顿尤其遇到超大病理切片或者遥感影像浏览器内存直接爆掉。我之前测过一张2万像素级别的长图QGraphicsView场景下缩放依然跟手这是Web方案很难做到的。第二个是离线可用性。深度学习的数据准备经常在本地服务器、离线工控机上做Web方案要么搭一套后端服务要么绕开浏览器安全策略配置成本很高。Qt是原生桌面应用编译完一个exe或者二进制文件扔过去就能跑不依赖Node、Python运行时摄像头工控机、PACS内网环境都能直接用。第三个是深度定制能力。标注工具不是画个框那么简单实际项目中你可能要加半自动辅助标注、键盘快捷键流水线操作、特定标注协议校验、甚至对接Halcon的算法结果做后处理。Qt的信号槽机制和插件化架构让这些定制变得很自然不像Web前端改起来那么零散。当然Qt方案也有缺点跨平台分发要针对Windows、Linux分别编译UI布局调试比Web要麻烦一些。但这些成本相对于标注流程的长期收益来说是完全可以接受的。1.2 工具的核心应用场景定位在设计这个工具之前我梳理了深度学习图像标注的几大典型场景确定了这个开源工具要覆盖的核心场景目标检测数据准备最常见场景标注车辆、行人、缺陷、农作物等目标格式要支持Pascal VOC和YOLO。语义分割辅助标注业界大多做多边形标注但考虑到多边形标起来太慢项目里做了“超级像素预标注手动修正”的简化流程。医学影像基础标注很多医学项目是从PNG截图开始的需要工具能直接处理这类图像并输出灰阶友好的标签。摄像头视频帧标注做安防、行为识别时要从视频流里抽帧标注工具在Qt侧做了定时抽帧模块的接口预留。工业质检对接Halcon等视觉库做二次确认时Qt侧的标注结果可以导出为调试数据。因为这个开源版本定位是“深度学习图像标签工具”我把重点放在了通用图像标注流程上但架构上做了接口抽象所以后续可以针对具体场景扩展。1.3 技术选型和开源方案的整体考量技术栈上采用了标准组合C Qt Widgets CMake核心UI基于QGraphicsView/QGraphicsScene构建数据存储采用JSON格式导出支持VOC/COCO/YOLO三种主流格式。为什么不用QMLQML做动效和现代化界面确实漂亮但标注工具需要的密集鼠标交互、视图坐标变换、大量图元管理在C侧实现更直接。QML和C混用会增加一层类型转换成本维护起来也麻烦所以这个项目选择了纯Widgets方案界面差点意思但胜在稳定。CMake是必须的Qt官方也在推CMake而非qmake尤其是你有跨平台编译需求的时候。项目里用CMake管理Qt5或者Qt6的依赖平台差异用预处理器宏处理。整个项目结构是这样qt-image-label/ ├── CMakeLists.txt ├── README.md ├── src/ │ ├── main.cpp │ ├── core/ │ │ ├── labelmanager.h/cpp │ │ ├── annotationitem.h/cpp │ │ └── projectmodel.h/cpp │ ├── io/ │ │ ├── imageio.h/cpp │ │ └── formatexporter.h/cpp │ ├── ui/ │ │ ├── mainwindow.h/cpp │ │ ├── graphicsview.h/cpp │ │ └── labelpanel.h/cpp │ └── utils/ │ └── globalconfig.h/cpp ├── data/ └── examples/核心思路是把“界面”和“数据”解耦。GraphicsView只负责渲染和交互annotationitem只代表一个标注框/多边形LabelManager统一管理所有标注数据ProjectModel负责序列化和恢复。这样后续不管是加QSS皮肤还是扩展新标注协议都不用动底层。2. 核心模块解析与交互实操要点2.1 QGraphicsView体系下的标注交互实现标注工具的本质是让用户在图像上精确定位并画图元。这里QGraphicsView的坐标系设计帮了大忙。QGraphicsScene是图像显示坐标系QGraphicsView是视图坐标系。常规做法是加载图像后用scene.addPixmap(...)把图像放在scene的(0,0)位置image item的rect就是图像的实际像素尺寸。标注框的几何坐标天然就是图像像素坐标导出的时候完全不用换算。这个设计比直接在QWidet上绘制再手动映射坐标要省心太多。鼠标交互的核心代码集中在GraphicsView里我做了三个关键设计第一滚轮缩放以鼠标为中心。QGraphicsView默认缩放是以视图中心为锚点但标注习惯是鼠标指哪放大哪。用setTransformationAnchor(QGraphicsView::AnchorUnderMouse)可以一行搞定缩放的时候保持鼠标下的点不动。第二缩放级别自适应。图像缩到很小或者放得特别大时要限制范围不然用户容易“跟丢”。我在wheelEvent里做了限制缩放到原图的0.05倍到50倍之间超出就忽略这次滚轮事件。这样用户体验好很多不会出现标注框缩没了的情况。第三空格切换平移模式。这个完全对标PS按住空格临时切换到手型工具松开恢复标注模式。实现方式是重写keyPressEvent和keyReleaseEvent动态设置setDragMode(QGraphicsView::ScrollHandDrag)简单但实用。实际画框流程是鼠标按下时记录起始点在scene坐标创建临时RectItem拖动过程中更新rect松开时如果宽高都大于阈值比如5像素就正式加入标注框列表并绑定当前选中的标签类别如果太小判定为误触直接删除。这里有个坑要重点说一定要使用scene坐标而不是视图坐标。因为视图像素会随缩放变化如果用mapToScene把视图坐标映射到scene坐标才能保证标注框的数值不随界面缩放变化。我在初版代码里直接在mousePressEvent里取event-pos()结果缩放之后标注框就跑偏排查了很久才意识到要mapToScene(event-pos())。2.2 标签类别管理与颜色映射策略深度学习标注里标签不只是个字符串它跟颜色、快捷键、统计信息都相关。我在LabelManager里做了两套映射。第一套是类别到颜色。每个类别分配一个固定QColor同一类别所有标注框都用这个颜色不同类别颜色尽量区分开。颜色生成策略用了简单的哈希索引方式colors[hash(classname)%colors.size()]好处是重开项目加载旧标注时颜色依然稳定不会因为类别顺序变化导致颜色漂移。第二套是类别到快捷键。数字键0-9直接绑定前10个类别选中某个类别后连续标注动作完全不需要碰鼠标。这个对效率提升特别明显标车标人这种重复劳动熟练之后可以做到一秒一个框。颜色适配还有一个细节图像亮度不同纯色框在浅色背景下看不清。我给每条边做了一层半透明白色描边再叠加彩色主边视觉上任何背景都能看清。具体的GraphicsItem绘制代码大概是void AnnotationItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { painter-setPen(QPen(Qt::white, 4)); painter-drawRect(boundingRect()); painter-setPen(QPen(color_, 2)); painter-drawRect(boundingRect()); }先画白色粗边打底再画彩色细边叠上去相当于描了个边。这个技巧在图像对比度高的时候差距不明显但遇到卫星遥感那种暗背景、亮背景交替出现时就很关键。2.3 标注数据的存储与状态恢复标注工具业务里最怕的就是标到一半闪退结果全白干。所以我设计了一个自动保存机制每次标注操作完成后防抖500ms写入JSON文件到项目目录。JSON字段设计遵循最小化原则{ version: 1.0, image_path: images/00001.jpg, image_width: 1920, image_height: 1080, annotations: [ { id: a1b2c3, type: rect, class: car, points: [100, 120, 340, 280], difficult: false } ], label_colors: { car: #ff0000, person: #00ff00 } }image_width和image_height必须存加载时如果发现原图尺寸和标注尺寸不一致直接提示“标注坐标可能错位”这比等到训练时发现坐标对不上要节约几小时排查时间。difficult字段是VOC格式里继承过来的标注那些严重遮挡、截断的难例训练时可以选择性忽略。因为整个标注过程在内存里维护的是一个QListAnnotationItem*撤销操作如果用自己实现的状态栈性能会很差。Qt其实提供了QUndoStack框架配合QUndoCommand实现“画框”“删除框”“修改类别”等操作的Command类可以原生支持CtrlZ/CtrlY。我在项目里实现了AddShapeCommand和RemoveShapeCommand效果很理想。2.4 图像加载性能优化的三个关键细节图像加载不能直接用QPixmap从路径load因为一张DSLR拍出的5000万像素RAW转出来的JPEGQPixmap直接加载会占掉几百MB显存标注时还容易崩。我做了几层优化第一统一用QImageReader加载设置setAutoTransform(true)解决手机照片Exif旋转问题。很多工具不处理Exif标注出的框全歪了这个问题隐藏得很深。第二针对超大图做降采样预览。加载时先读出图片尺寸如果尺寸超过屏幕合理范围比如最长边超过3000像素就先缩放到2000像素以内再显示。标注坐标保存时记录一个scale_factor导出时再把坐标映射回原图像素。这个细节在医学影像、工程图纸场景下是保命级的。第三使用QPixmapCache缓存同一批图像的缩略图批量翻图标注时前一张图不用反复从磁盘解码。注意降采样预览模式下坐标映射一定要用“等比缩放”的计算方式不能直接按宽度比例算。长宽比不一致会导致标注框错位。我在代码里用统一的displayScale displayWidth / originalWidth高度方向也用这个scale保证等比例。3. 实操过程与核心环节详细实现3.1 环境准备Qt开发环境搭建如果你从零开始第一步是把Qt装好。我个人推荐直接从官方下载安装程序选择“Qt 5.15.2 LTS”或者“Qt 6.5 LTS”的Desktop开发组件。以Windows为例组件里必须勾选MSVC 2019 64-bit或MinGW 8.1 64-bit二选一推荐MSVC并搭配VS2019/2022Qt Charts做统计图表用CMake和Ninja构建系统Qt Designer拖拽生成UI属于Qt Creator自带组件这里有个经验不要把MinGW和MSVC混编两者ABI不兼容CMake编译的时候容易报一堆链接错误。另外Qt版本选5.15还是6.x看你要不要用较老的第三方库Halcon的Qt集成通常要求64位比较老的环境建议5.15新项目直接上6.5没问题CMake配置差异不大。CMakeLists.txt里最核心的部分是这样cmake_minimum_required(VERSION 3.20) project(QtImageLabel) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 REQUIRED COMPONENTS Widgets Gui) qt5_standard_project_setup() add_executable(QtImageLabel src/main.cpp src/ui/mainwindow.cpp src/ui/graphicsview.cpp src/core/labelmanager.cpp src/core/annotationitem.cpp src/io/formatexporter.cpp ) target_link_libraries(QtImageLabel PRIVATE Qt5::Widgets Qt5::Gui)CMAKE_AUTOMOC这个开关很重要Qt的信号槽需要moc预处理不开的话编译会直接报“undefined reference to vtable”。3.2 标注工具从启动到加载图像的项目流程项目启动后主流程分三步第一步初始化主窗口。通过setCentralWidget把GraphicsView塞进去左侧放QListWidget显示标注列表右侧放QTreeWidget作为类别树底部是状态栏显示当前鼠标坐标和缩放比例。第二步加载图像。用户选择文件或文件夹后工具读取所有图片路径维护一个图像列表并把当前图像交给ImageView组件。核心代码大概是这样void MainWindow::loadImage(const QString path) { QImageReader reader(path); reader.setAutoTransform(true); QImage image reader.read(); if (image.isNull()) { QMessageBox::warning(this, load failed, reader.errorString()); return; } scene_-clear(); QGraphicsPixmapItem *pixmapItem scene_-addPixmap(QPixmap::fromImage(image)); pixmapItem-setTransformationMode(Qt::SmoothTransformation); view_-setSceneRect(QRectF(0, 0, image.width(), image.height())); currentImagePath_ path; }第三步自动检查是否存在同名JSON标注文件。有就加载没有就用默认空标注。标注列表、类别树同步更新。3.3 画框、移动与编辑的完整交互流程标注操作的核心逻辑我拆成三个流程画框流程确保当前选中了某个类别然后左键拖拽。mousePressEvent记录起点mouseMoveEvent实时画临时矩形mouseReleaseEvent判断矩形最小尺寸调用LabelManager::addAnnotation加入标注列表同时刷新右侧列表显示。移动流程点击选中某个标注框后Box的边缘会出现控制点拖拽控制点可以调整大小。QGraphicsRectItem自带的setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemSendsGeometryChanges)能解决大部分交互。但这里有个细节Item的移动位置默认是相对它父item的坐标系。由于我们的图像是一个PixmapItem而标注框是独立的Item需要把parent设置成pixmapItem这样移动标注框时坐标才会跟随图像的坐标空间。编辑流程双击某个标注框弹出属性编辑对话框可以改类别、难易标记、微调坐标。右键弹出上下文菜单支持删除、复制、置顶等操作。3.4 数据的导出VOC、COCO、YOLO三格式转换标完数据之后最愁人的就是格式转换。很多人的痛苦是标的时候用ToolA训练的时候框架只认ToolB的格式脚本写起来费劲还不一定对。我在IO模块里直接集成三种格式的导出器。Pascal VOC格式目录结构是JPEGImages/存放图像Annotations/存放同名的XML。每个目标对应一个object节点包括name、difficult、bndbox坐标xmin, ymin, xmax, ymax。如果类别是圆形或者多边形VOC格式用polygon来扩。COCO格式一个JSON文件包含images、annotations、categories三个数组。categories的id从1开始递增annotations里的segmentation是坐标数组bbox是[x, y, width, height]这个和VOC一个最大的坑是COCO的bbox用的是矩形左上角和宽高VOC用的是左上角和右下角转换时千万别忘记把xmax换成widthxmax-xmin很多人训练时发现box错位九成是这里出的问题。YOLO格式每个图像对应一个同名的txt文件每行是class_id x_center y_center width height坐标全部是归一化到0-1的值。这里有个更隐蔽的坑YOLO的归一化用的是图像原始尺寸不是显示尺寸。导出器里必须传入原始图像的宽高而且x_center计算时是(xmin xmax) / 2 / image_width。中心点的纵坐标同理。如果是polygon类型的标注YOLO不支持需要先转成外接矩形或者分割点集。为了方便调参我还在页面右上角加了“导出配置”区域让用户选择当前项目是检测任务还是分割任务这样导出器知道该走矩形路径还是多边形路径。3.5 开源代码的结构与二次开发指南这部分写清楚了代码目录里每个文件夹的用途方便你拿到开源代码后快速定位修改点。core/核心类LabelManager是标注数据的唯一入口AnnotationItem是图元类ProjectModel负责写JSON。io/图像读写、格式导出、批量重命名。ui/所有窗口和视图类MainWindow主窗口GraphicsView交互核心LabelPanel右侧标签列表。utils/全局配置、编码转换、文件路径工具函数。如果你要做二次开发比如增加一个新标注协议旋转框、关键点、多边形核心改动点是这三个在AnnotationItem里增加对应的图元类型。在LabelManager里增加数据模型字段。在Formatexporter里增加新的导出器类。这里我特别建议不要把所有逻辑都堆在MainWindow里。第一次开发时为了省事我把画框逻辑也写进了窗口类结果后面加功能越改越乱后来花了一周时间重构。现在每个类只做一件事单测也容易写长期维护收益远大于前期多写的接口代码。4. 常见问题与排查技巧实录4.1 图像加载失败或图片黑屏现象QImage加载JPEG时失败返回null或者加载成功但QPixmap显示纯黑块。排查思路先确认文件路径是否包含中文。Qt在Windows上对中文路径的兼容性不差但某些老版本或者特定编码的第三方库有坑最简单的办法是加载前统一走一遍QDir::toNativeSeparators并把路径转换为QString::fromLocal8Bit。确认图片是否是渐进式JPEG。某些工艺相机生成渐进式JPEGQImageReader默认可能不支持。读入前设置reader.setDecideFormatFromContent(true)。确认内存充足。超大的PSD或者TIFFQImageReader如果返回失败多半是内存不足试试降采样加载。4.2 画框坐标错位、框与图像不符这是标注工具最高频的Bug。多数情况下是因为你在拖拽画框时没有把鼠标视图坐标转换为scene坐标。我建议所有鼠标事件处理里统一用view-mapToScene(event-pos())取坐标千万不要直接用event-pos()。另一种情况是图片缩放后你没有同步更新标注框的位置。如果你用降采样预览了超大图标注框的坐标是在预览尺寸下的导出时一定要按比例映射回原始尺寸。映射公式// originalWidth / displayWidth 是缩放系数 int originalX round(annotatedX * scaleX); int originalY round(annotatedY * scaleY);如果忘记这一步你会发现标注框导出的坐标比实际位置小或者大一圈直接影响模型训练效果。4.3 撤销/重做失效有时候你会发现CtrlZ没反应。排查逻辑依次是确认QUndoStack是否和QUndoView绑定。确认画框操作是否通过QUndoCommand的子类来执行而不是直接调scene-addItem()。确认快捷键冲突。有些默认QAction占用了CtrlZ如果有冲突需要手动调整Shortcut。4.4 导出JSON/COCO时UTF-8乱码在Windows控制台下Qt控制台默认编码是GBKJSON文件默认应该是UTF-8。导出JSON时必须显式指定编码QFile file(path); file.open(QIODevice::WriteOnly); QTextStream out(file); out.setCodec(UTF-8); out doc.toJson();不然你用记事本打开没问题但Python的json.load就会报编码错误非常痛苦。提示导出文件时尽量使用系统默认编码之外的UTF-8不仅是JSONCSV和TXT都建议统一不然跨平台协作就会出现乱码。4.5 批量处理图像时程序卡死如果一次性加载几百张图并且在主线程里逐张读取界面必然卡顿。解决办法是使用QtConcurrent::run或者QRunnableQThreadPool做后台加载QtConcurrent::run([this]() { QImage image loadFromDisk(path); QMetaObject::invokeMethod(this, onImageLoaded, Qt::QueuedConnection, QVariant::fromValue(image)); });这里有个必须注意的事项后台线程不能直接操作QGraphicsScene或QGraphicsItemUI对象只能在主线程里操作。所以要拿到线程加载的QImage结果后用信号槽切回主线程再更新场景。5. 进阶扩展方向从基础标注到生产级工具5.1 医学影像标注的工程化延展医疗方向的PACS图像存储与通信系统往往需要标注工具支持DICOM格式这个和普通自然图像差别很大。DICOM图像有窗宽窗位概念直接转PNG会丢掉很多信息。Qt里可以集成DCMTK库做DICOM解析显示时先做灰度映射并用QGraphicsPixmapItem或QGraphicsView渲染。报告书写功能也不是特别复杂在标注工具右侧增加一个QTextEdit基于当前选中的图像、标注类别、测量值自动生成一段结构化报告草稿医生可以一键修改。这类工具在医院影像科落地时很受欢迎因为能省掉一半报告录入时间。5.2 摄像头设备运维与视频帧标注做安防或制造场景时摄像头会持续产生视频流运维工程师有时需要从视频里快速抽帧做缺陷标注。基于Qt的标注工具可以扩展一个“实时抽帧”模块通过QSerialPort或SDK读取摄像头状态按固定时间间隔抓帧到本地标注完成后导出为训练集。这里的热词里有“qserialport类”确实Qt的串口编程在工业场景很常用。标注工具读取摄像头设备状态如果发现设备离线可以自动截断抽帧流并告警这个能力对工业质检、设备巡检场景非常实用。Qt本身自带SerialPort模块二次开发时只要加一个CameraMonitor类负责读取设备心跳包和状态帧即可。5.3 用Halcon做辅助标注与二次质检Halcon是工业视觉里常用的图像处理库深度学习训练样本标注前经常先用传统算子做一次预分割。Qt可以通过C接口直接调用Halcon的HObject、HImage对象把Halcon的亚像素轮廓结果转成标注工具里的多边形。操作路径是在Qt里加载图像把图像数据转成HObject。调用Halcon的threshold、connection、select_shape等算子得到候选区域。再把候选区域的轮廓转换成QPolygonF作为初始标注人工修正后导出。这套流程对小缺陷检测特别有效因为人的手动画框很难和真实边缘吻合而传统视觉算子的边缘检测结果往往已经八九不离十。5.4 算法预标注与主动学习闭环更进阶的方向是接入一个推理服务做“预标注”。标注工具把未标注图像发给一个初步训练好的模型模型返回预测框标注人员只需确认或修正而不是从零画框。这样可以大幅提高标注速度尤其是对训练集充足、模型已有一定精度的场景。Qt侧实现时用HTTP请求或者本地进程通信比如零拷贝共享内存对接推理引擎相对简单。推荐用QNetworkAccessManager发JSON把预测框坐标解析后映射成AnnotationItem。配合主动学习策略模型预测置信度较低的图像优先进入标注队列这样能用更少的标注数据获得更好的模型效果这已经是不少团队行之有效的效率方案。我在开源代码里预留了PredictionClient接口就是方便做这层扩展。5.5 标注统计报表与训练集质量监控标注质量直接影响模型上限工具侧加一个“统计面板”很有必要实时展示不同类别的标注数量、每张图的平均标注数、类别分布热力图、标注员工作量排行。Qt Charts模块在这里派上用场。我实现了一个简单的QChart折线图横轴是图像编号纵轴是每张图的标注框数量。如果某一类标注数量骤降多半是标注员疲劳或者标注标准理解偏差在项目复盘时能快速定位问题。我通常在标注团队上线前要求先用这个工具标注一小批测试图统计面板里核对类别人数和目标尺寸分布确认符合预期后再全量投入标注。这一步能省下不少返工成本。整个项目从最初的一个画框demo慢慢迭代成现在这个开源版本中间踩的坑比预想的多。如果你打算在团队里引入这套工具我最想强调的一点是先明确你的数据导出格式和标注规范再动手改代码事半功倍。编码过程中的细节问题欢迎按开源仓库里的README找到项目地址提issue一起交流。本文还有配套的精品资源点击获取
返回列表