当前位置: 首页 > news >正文

QT5图形视图框架实战:手把手教你实现图片标注工具(附完整源码)

QT5图形视图框架实战:从零构建工业级图片标注工具

在计算机视觉和医学影像分析领域,图片标注工具是算法工程师和标注人员最亲密的"工作伙伴"。一个响应迅速、操作顺滑的标注工具,能显著提升标注效率和数据质量。本文将带您深入QT5的图形视图框架(Graphics View Framework),从架构设计到性能优化,逐步构建一个支持多形状标注、撤销重做、快捷键操作等工业级功能的完整解决方案。

1. 环境搭建与基础架构设计

1.1 项目初始化与类结构

我们采用现代C++和QT5的最新特性来构建项目。首先创建基于CMake的工程结构:

mkdir ImageAnnotator && cd ImageAnnotator touch CMakeLists.txt main.cpp AnnotationTool.h AnnotationTool.cpp

对应的CMake配置应包含QT5核心模块和图形视图组件:

cmake_minimum_required(VERSION 3.16) project(ImageAnnotator) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) find_package(Qt5 REQUIRED COMPONENTS Widgets) add_executable(ImageAnnotator main.cpp AnnotationTool.h AnnotationTool.cpp ) target_link_libraries(ImageAnnotator Qt5::Widgets )

1.2 核心类职责划分

我们采用MVC变体模式设计主要组件:

类名职责继承关系
AnnotationView视图渲染与用户交互QGraphicsView
AnnotationScene标注项管理与事件分发QGraphicsScene
RectAnnotation矩形标注项QGraphicsRectItem
PolygonAnnotation多边形标注项QGraphicsPathItem

基础类的头文件框架如下:

// AnnotationTool.h #pragma once #include <QGraphicsView> #include <QGraphicsScene> #include <QGraphicsRectItem> class AnnotationView : public QGraphicsView { Q_OBJECT public: explicit AnnotationView(QWidget *parent = nullptr); protected: void wheelEvent(QWheelEvent *event) override; void mousePressEvent(QMouseEvent *event) override; // ... 其他事件处理 }; class AnnotationScene : public QGraphicsScene { Q_OBJECT public: explicit AnnotationScene(QObject *parent = nullptr); private: QGraphicsPixmapItem *m_backgroundItem; };

2. 核心标注功能实现

2.1 智能标注项创建机制

在AnnotationScene中实现标注项的智能创建逻辑:

void AnnotationScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event->button() == Qt::LeftButton) { switch (m_currentTool) { case ToolType::Rectangle: createRectAnnotation(event->scenePos()); break; case ToolType::Polygon: if (!m_currentPolygon) { startPolygonAnnotation(event->scenePos()); } else { addPolygonPoint(event->scenePos()); } break; // 其他工具类型... } } QGraphicsScene::mousePressEvent(event); } void AnnotationScene::createRectAnnotation(const QPointF &startPos) { auto *rectItem = new RectAnnotation(startPos); rectItem->setPen(QPen(Qt::red, 2)); addItem(rectItem); m_activeAnnotation = rectItem; emit annotationCreated(rectItem); }

2.2 高级交互功能实现

2.2.1 标注项编辑功能

为标注项添加控制点实现尺寸调整:

// RectAnnotation.cpp void RectAnnotation::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { QGraphicsRectItem::paint(painter, option, widget); // 绘制控制点 painter->setBrush(Qt::white); const QRectF rect = this->rect(); const qreal handleSize = 6.0; QVector<QPointF> handles = { rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft() }; for (const auto &point : handles) { painter->drawEllipse(point, handleSize, handleSize); } }
2.2.2 智能吸附功能

实现标注项对齐时的磁吸效果:

bool AnnotationScene::snapToExisting(QPointF &pos, qreal threshold) const { for (auto *item : items()) { if (auto *annotation = dynamic_cast<AbstractAnnotation*>(item)) { QRectF boundingRect = annotation->boundingRect(); boundingRect.moveTo(annotation->pos()); // 检查8个关键点 QVector<QPointF> keyPoints = { boundingRect.topLeft(), boundingRect.topRight(), boundingRect.bottomRight(), boundingRect.bottomLeft(), boundingRect.center(), QPointF(boundingRect.left(), boundingRect.center().y()), // 其他中点... }; for (const auto &point : keyPoints) { if (QLineF(pos, point).length() < threshold) { pos = point; return true; } } } } return false; }

3. 工程化功能扩展

3.1 撤销/重做栈实现

集成QUndoStack构建完整的撤销系统:

class AddAnnotationCommand : public QUndoCommand { public: AddAnnotationCommand(AnnotationScene *scene, AbstractAnnotation *item, QUndoCommand *parent = nullptr) : QUndoCommand("Add Annotation", parent), m_scene(scene), m_item(item) {} void undo() override { m_scene->removeItem(m_item); m_scene->update(); } void redo() override { m_scene->addItem(m_item); m_scene->update(); } private: AnnotationScene *m_scene; AbstractAnnotation *m_item; }; // 使用示例 void AnnotationScene::addAnnotationWithUndo(AbstractAnnotation *item) { auto *cmd = new AddAnnotationCommand(this, item); m_undoStack->push(cmd); }

3.2 标注数据序列化

实现JSON格式的标注数据导出:

QJsonArray AnnotationScene::exportToJson() const { QJsonArray annotations; for (auto *item : items()) { if (auto *annotation = dynamic_cast<AbstractAnnotation*>(item)) { QJsonObject obj; obj["type"] = annotation->typeString(); obj["points"] = annotation->pointsToJson(); obj["attributes"] = annotation->attributesToJson(); annotations.append(obj); } } return annotations; } // RectAnnotation的实现 QJsonArray RectAnnotation::pointsToJson() const { QJsonArray points; QRectF rect = this->rect(); points.append(QJsonObject{ {"x", rect.x()}, {"y", rect.y()}, {"width", rect.width()}, {"height", rect.height()} }); return points; }

4. 性能优化实战

4.1 延迟渲染技术

对于大型图像(如病理切片),采用瓦片渲染策略:

void AnnotationView::drawBackground(QPainter *painter, const QRectF &rect) { if (m_useTiledRendering) { QRect viewportRect = viewport()->rect(); QRectF visibleSceneRect = mapToScene(viewportRect).boundingRect(); // 计算需要渲染的瓦片范围 int tileSize = 1024; int xStart = floor(visibleSceneRect.left() / tileSize) * tileSize; int yStart = floor(visibleSceneRect.top() / tileSize) * tileSize; for (int x = xStart; x < visibleSceneRect.right(); x += tileSize) { for (int y = yStart; y < visibleSceneRect.bottom(); y += tileSize) { QRectF tileRect(x, y, tileSize, tileSize); if (rect.intersects(tileRect)) { renderTile(painter, tileRect); } } } } else { QGraphicsView::drawBackground(painter, rect); } }

4.2 内存管理优化

实现标注项的LRU缓存机制:

class AnnotationCache { public: void insert(AbstractAnnotation *item) { if (m_cache.size() >= m_maxSize) { evictOldest(); } m_cache[item->id()] = {item, QDateTime::currentDateTime()}; } AbstractAnnotation* get(const QString &id) { if (m_cache.contains(id)) { m_cache[id].lastAccessed = QDateTime::currentDateTime(); return m_cache[id].item; } return nullptr; } private: struct CacheEntry { AbstractAnnotation *item; QDateTime lastAccessed; }; void evictOldest() { auto oldest = std::min_element(m_cache.begin(), m_cache.end(), [](const auto &a, const auto &b) { return a.second.lastAccessed < b.second.lastAccessed; }); if (oldest != m_cache.end()) { delete oldest->second.item; m_cache.erase(oldest); } } QHash<QString, CacheEntry> m_cache; size_t m_maxSize = 1000; };

5. 工业级功能增强

5.1 多格式导出引擎

class ExportEngine : public QObject { Q_OBJECT public: enum Format { JSON, XML, COCO, PascalVOC, YOLO }; bool exportToFile(Format format, const QString &filename, const QList<AbstractAnnotation*> &annotations) { switch (format) { case JSON: return exportToJson(filename, annotations); case COCO: return exportToCoco(filename, annotations); // 其他格式... } } private: bool exportToJson(const QString &filename, const QList<AbstractAnnotation*> &annotations) { QJsonArray array; for (auto *anno : annotations) { array.append(anno->toJson()); } QFile file(filename); if (!file.open(QIODevice::WriteOnly)) { return false; } file.write(QJsonDocument(array).toJson()); return true; } // 其他导出实现... };

5.2 插件系统架构

设计可扩展的插件接口:

class AnnotationPluginInterface { public: virtual ~AnnotationPluginInterface() = default; virtual QString pluginName() const = 0; virtual QIcon pluginIcon() const = 0; virtual QWidget* createToolWidget() = 0; virtual void applyToScene(AnnotationScene *scene) = 0; }; // 示例:AI辅助标注插件 class AIAssistPlugin : public QObject, public AnnotationPluginInterface { Q_OBJECT Q_INTERFACES(AnnotationPluginInterface) public: QString pluginName() const override { return "AI Assist"; } QIcon pluginIcon() const override { return QIcon(":/icons/ai.png"); } QWidget* createToolWidget() override { auto *widget = new QWidget; // 构建UI... return widget; } void applyToScene(AnnotationScene *scene) override { connect(scene, &AnnotationScene::imageLoaded, this, &AIAssistPlugin::onImageLoaded); } private slots: void onImageLoaded(const QImage &image) { // 调用AI模型进行预标注... } };

在开发过程中,处理超大图像时发现QGraphicsPixmapItem直接加载整张图片会导致内存暴涨。解决方案是采用分块加载策略,仅加载当前视图可见区域的图像数据,配合后台线程预加载周边区域。这种技术使工具能够流畅处理超过1GB的医学影像文件。

http://www.cnnetsun.cn/news/1948599.html

相关文章:

  • 保姆级教程:用ESP32-P4和ST7703屏打造24fps视频轮播器(附完整代码和FFmpeg转换命令)
  • 【实践】Android Telephony 双卡适配:从SubscriptionManager到TelephonyManager的正确获取与使用
  • 从数据到文本:解锁Matlab char函数的四大核心应用场景
  • 【限时解密】头部AI IDE厂商不愿公开的上下文理解黑盒:基于237万行生产代码训练的Context Embedding微调范式
  • 实战避坑指南:在ant-design-vue的a-table中集成vue-draggable-resizable实现列宽拖拽
  • 【综合题-知识点】系统工程与信息技术基础
  • Beego ORM避坑指南:从数据库设计到高效查询
  • KICAD6.0拼版神器KIKIT插件安装全攻略:从命令行配置到路径设置
  • 3ds Max 中 Gamma 与 LUT 首选项:从模型导出失败到色彩管理核心解析
  • Winhance终极指南:如何轻松优化Windows系统的完整教程
  • 3个核心功能揭秘:如何用AI智能移除图像中的任何对象
  • 告别性能瓶颈:在PyQt5中用QAbstractItemModel自定义Model优化大型QTreeView数据加载
  • 南京邮电大学电装实习2023:从零到一构建智能终端与网络互联实战
  • S/4 HANA里给MARC表加字段报错?别慌,手把手教你用EXTEND VIEW搞定
  • Qt5.14.2 MinGW静态编译实战:从环境搭建到独立可执行文件生成
  • 你的数据适合灰色预测吗?用MATLAB的级比与光滑比检验来把第一道关
  • 告别‘滋滋声’!Android录音降噪实战:手把手教你集成WebRTC NS模块(附完整代码)
  • 终极Visual C++运行库AIO管理指南:从基础部署到高级定制
  • OpenCV性能跃迁:从SIMD指令集到MIPP跨平台优化的实战指南
  • 收藏备用|35岁程序员不被淘汰!AI转型实操指南(小白/资深都能学)
  • 【51单片机】【Proteus仿真】从零构建带安全策略的智能密码锁系统
  • 微软旧版Exchange与Skype for Business延长安全更新服务
  • ROS2 rcl软件框架深度剖析:从接口抽象到通信实现的桥梁
  • OpenClaw怎么部署?2026年4月腾讯云8分钟保姆级本地教程及百炼Coding Plan流程
  • 5分钟快速上手:ncmdumpGUI让你彻底摆脱网易云音乐NCM格式限制
  • 如何在Windows系统上完美解锁苹果触控板的完整功能:mac-precision-touchpad驱动终极指南
  • 易语言MySQL数据库操作实战:从连接到增删改查
  • 你还在用短代码思维调用大模型?长代码生成的黄金分割阈值(173行)、临界衰减曲线与实时校验嵌入式模板
  • 手把手教你用JavaScript+Web Audio API打造自己的在线FFT分析仪
  • 2026届毕业生推荐的降AI率工具实测分析