基于Qt Quick的图像标注与标注数据管理工具
基于Qt Quick的图像标注与标注数据管理工具
-
- 一、背景介绍
-
- 1、为什么需要这个工具?
- 2、为什么选择Qt Quick?
- 二、效果图展示
- 三、功能介绍
-
- 1、 **图像加载与显示**
- 2、 **绘图工具系统**
- 3、 **标注编辑与管理**
- 4、 **数据持久化**
- 5、 **用户界面特色**
- 6、 **快捷键支持**
- 四、代码
一、背景介绍
1、为什么需要这个工具?
- 自动驾驶:在激光雷达投影的2D图像上标注道路元素(车道线、交通标志、行人等)
- 数据标准化:将标注结果保存为JSON格式,便于不同系统间的数据交换
2、为什么选择Qt Quick?
- 跨平台:一次开发,可在Windows、Linux、macOS上运行
- 性能优异:基于OpenGL的硬件加速渲染
- 开发效率高:声明式QML语言简化UI开发
二、效果图展示
三、功能介绍
在加载的图片上绘制多种几何形状(多段线、矩形、多边形),并将标注数据保存为JSON格式。
1、 图像加载与显示
- 支持多种图片格式(JPG、JPEG、PNG、BMP、TIFF)
- 自动适应显示区域大小
- 智能坐标缩放,保持原始图片坐标与显示坐标的映射关系
2、 绘图工具系统
- 多段线绘制:连续点击创建折线,双击完成绘制
- 矩形绘制:点击两点自动生成完整矩形
- 多边形绘制:创建封闭多边形,自动闭合形状
- 实时预览绘制过程
- 随机颜色分配,便于区分不同标注
3、 标注编辑与管理
- 控制点编辑:选中标注后可拖动控制点调整形状
- 删除功能:支持删除单个选中标注或清空所有标注
- 选择状态可视化:选中标注高亮显示
4、 数据持久化
- JSON导出:保存标注数据,包含原始图片坐标信息
- JSON导入:加载之前保存的标注数据
- 数据预览:在保存前预览生成的JSON数据
- 支持复制到剪贴板或保存到文件
5、 用户界面特色
- 现代化深色主题设计
- 直观的工具按钮组
- 实时状态提示和操作说明
- 响应式布局,适应不同窗口大小
6、 快捷键支持
Ctrl+O
:打开图片Ctrl+S
:保存标注数据Ctrl+L
:加载标注数据Delete
:删除选中标注Esc
:取消当前操作或清除选择
四、代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtQuick.WindowWindow {id: windowwidth: 1200height: 800visible: true// 绘图模式枚举property int drawMode: 0readonly property int modeNone: 0readonly property int modePolyline: 1readonly property int modeRectangle: 2readonly property int modePolygon: 3// 颜色主题readonly property color primaryColor: "#2c3e50"readonly property color secondaryColor: "#3498db"readonly property color accentColor: "#e74c3c"readonly property color backgroundColor: "#ecf0f1"readonly property color textColor: "#2c3e50"readonly property color borderColor: "#bdc3c7"// 当前选中的shape和控制点索引property var selectedShape: nullproperty int selectedPointIndex: -1// 存储原始图片坐标和显示坐标的比例property real scaleX: 1.0property real scaleY: 1.0property real lastPaintedWidth: 0property real lastPaintedHeight: 0// 主布局ColumnLayout {anchors.fill: parentanchors.margins: 12spacing: 12// 控制栏Rectangle {id: controlBarLayout.fillWidth: trueheight: 70color: primaryColorradius: 8RowLayout {anchors.fill: parentanchors.margins: 12spacing: 8// 文件操作按钮组ColumnLayout {spacing: 4Layout.alignment: Qt.AlignTopButton {text: "📁 加载图片"Layout.fillWidth: trueonClicked: fileDialog.open()background: Rectangle {color: secondaryColorradius: 4}contentItem: Text {text: parent.textcolor: "white"font.bold: truehorizontalAlignment: Text.AlignHCenter}}RowLayout {spacing: 4Button {text: "💾 保存"Layout.fillWidth: trueonClicked: canvas.saveToJson()background: Rectangle {color: "#27ae60"radius: 4}contentItem: Text {text: parent.textcolor: "white"font.bold: truehorizontalAlignment: Text.AlignHCenter}}Button {text: "📂 加载"Layout.fillWidth: trueonClicked: loadJsonDialog.open()background: Rectangle {color: "#f39c12"radius: 4}contentItem: Text {text: parent.textcolor: "white"font.bold: truehorizontalAlignment: Text.AlignHCenter}}}}// 模式选择按钮组ColumnLayout {spacing: 4Layout.alignment: Qt.AlignTopLabel {text: "绘图工具:"color: "white"font.bold: true}ButtonGroup {id: modeGroup}RowLayout {spacing: 4Repeater {model: [{"text": "✏️ 多段线","mode": modePolyline}, {"text": "⬜ 矩形","mode": modeRectangle}, {"text": "🔺 多边形","mode": modePolygon}]Button {text: modelData.textcheckable: trueButtonGroup.group: modeGroupLayout.fillWidth: trueonClicked: {window.drawMode = modelData.modecanvas.resetCurrentDrawing()clearSelection()}background: Rectangle {color: checked ? accentColor : "#34495e"radius: 4border.width: checked ? 2 : 0border.color: "white"}contentItem: Text {text: parent.textcolor: "white"font.bold: truehorizontalAlignment: Text.AlignHCenter}}}}}// 操作按钮组ColumnLayout {spacing: 4Layout.alignment: Qt.AlignTopLabel {text: "操作:"color: "white"font.bold: true}Button {text: "🗑️ 清除所有"Layout.fillWidth: trueonClicked: {canvas.resetCanvas()clearSelection()}background: Rectangle {color: "#e74c3c"radius: 4}contentItem: Text {text: parent.textcolor: "white"font.bold: truehorizontalAlignment: Text.AlignHCenter}}}// 状态信息Rectangle {Layout.fillWidth: trueLayout.fillHeight: truecolor: "transparent"border.color: borderColorborder.width: 1radius: 6Label {anchors.fill: parentanchors.margins: 8text: getModeDescription()color: "white"wrapMode: Text.WordWrapfont.pointSize: 10}}}}// 图片显示和绘制区域Rectangle {id: contentAreaLayout.fillWidth: trueLayout.fillHeight: truecolor: "#2c3e50"radius: 8clip: true// 图片显示区域Item {id: imageContaineranchors.fill: parentanchors.margins: 8Image {id: backgroundImageasynchronous: truecache: falsefillMode: Image.Stretchanchors.fill: parent// 图片加载完成后的处理onStatusChanged: {if (status === Image.Ready) {canvas.resetCanvas()updateScaleFactors()console.log("图片尺寸:", sourceSize.width, "x",sourceSize.height)console.log("图片显示尺寸:", paintedWidth, "x",paintedHeight)console.log("图片位置:", x, y)}}// 图片显示尺寸变化时重新计算坐标onPaintedWidthChanged: {if (status === Image.Ready) {updateScaleFactors()canvas.updateShapeCoordinates()}}onPaintedHeightChanged: {if (status === Image.Ready) {updateScaleFactors()canvas.updateShapeCoordinates()}}}// 绘制画布 - 直接覆盖在图片上Canvas {id: canvas// 直接使用图片的显示区域anchors.fill: backgroundImageproperty var currentShape: nullproperty var shapes: []property bool drawing: falseproperty point lastPoint: Qt.point(0, 0)property int rectanglePointCount: 0// 存储原始坐标(相对于原始图片尺寸)property var originalShapes: []// 更新所有形状的坐标(当图片显示尺寸变化时)function updateShapeCoordinates() {if (!backgroundImage.sourceSize.width|| !backgroundImage.sourceSize.height) {return}var newScaleX = backgroundImage.paintedWidth/ backgroundImage.sourceSize.widthvar newScaleY = backgroundImage.paintedHeight