在计算机视觉和图像处理领域,ROI(Region of Interest)标注是基础且关键的功能需求。传统方案往往依赖OpenCV等专业库,但当我们只需要基础的标注功能时,纯Qt实现反而能带来更轻量、更可控的解决方案。
最近我开发了一个基于Qt Graphics View框架的ROI标注工具,核心特点包括:
这个工具已在Qt5.6.1到Qt5.15.1多个版本测试通过,特别适合以下场景:
选择Qt Graphics View框架而非QPainter直接绘制,主要基于以下考量:
核心类关系如下:
cpp复制class ImageView : public QGraphicsView // 负责显示和交互
class ImageScene : public QGraphicsScene // 管理所有图形项
class ROIItem : public QGraphicsPolygonItem // 自定义图形项基类
图像显示的核心挑战在于同时支持:
关键实现代码:
cpp复制// 滚轮缩放
void ImageView::wheelEvent(QWheelEvent* event) {
const qreal factor = event->angleDelta().y() > 0 ? 1.2 : 1/1.2;
scale(factor, factor);
}
// 窗口自适应
void ImageView::resizeEvent(QResizeEvent* event) {
fitInView(sceneRect(), Qt::KeepAspectRatio);
}
// 初始化设置
ImageView::ImageView(QWidget *parent) : QGraphicsView(parent) {
setRenderHint(QPainter::SmoothPixmapTransform); // 抗锯齿
setDragMode(QGraphicsView::RubberBandDrag); // 框选模式
}
关键技巧:在构造函数中设置SmoothPixmapTransform可显著提升缩放质量,但会轻微增加CPU负载。对性能敏感的场景可考虑动态切换。
cpp复制// 普通矩形
QRectF rect(startPoint, endPoint);
QGraphicsRectItem *item = scene()->addRect(rect.normalized());
// 旋转矩形
QTransform transform;
transform.rotate(angle, originPoint.x(), originPoint.y());
QPolygonF rotatedRect = transform.mapToPolygon(baseRect.toRect());
QGraphicsPolygonItem *item = scene()->addPolygon(rotatedRect);
cpp复制// 单个圆形
qreal radius = QLineF(center, edgePoint).length();
scene()->addEllipse(center.x()-radius, center.y()-radius,
radius*2, radius*2);
// 动态同心圆
void ImageView::mouseMoveEvent(QMouseEvent* event) {
if(mode == CONCENTRIC_CIRCLE) {
qreal radius = QLineF(centerPoint_, event->pos()).length();
outerCircle->setRect(centerPoint_.x()-radius,
centerPoint_.y()-radius,
radius*2, radius*2);
innerCircle->setRect(centerPoint_.x()-radius/2,
centerPoint_.y()-radius/2,
radius, radius);
}
}
cpp复制QPainterPath createTickMark(const QPointF& pos, qreal angle) {
QPainterPath path;
QPointF offset(5 * sin(angle), 5 * cos(angle)); // 刻度线垂直方向
path.moveTo(pos - offset);
path.lineTo(pos + offset);
return path;
}
// 构建带刻度的直线
QPainterPath path;
path.moveTo(startPoint);
path.lineTo(endPoint);
for(int i=0; i<=10; ++i) {
QPointF pos = startPoint + (endPoint - startPoint)*i/10;
path.addPath(createTickMark(pos, lineAngle));
}
cpp复制// 点击添加顶点
void ImageView::mousePressEvent(QMouseEvent* event) {
if(mode == POLYGON) {
polygonPoints << mapToScene(event->pos());
if(!polygonItem) {
polygonItem = scene()->addPolygon(QPolygonF());
} else {
polygonItem->setPolygon(QPolygonF(polygonPoints));
}
}
}
// 右键完成绘制
void ImageView::mouseDoubleClickEvent(QMouseEvent* event) {
if(mode == POLYGON && event->button() == Qt::RightButton) {
if(polygonPoints.size() > 2) {
emit polygonCompleted(QPolygonF(polygonPoints));
}
resetPolygon();
}
}
图形项管理优化:
QGraphicsItemGroup管理同类项ItemDoesntPropagateOpacityToChildrenscene()->blockSignals(true)内存泄漏防护:
cpp复制void ImageScene::clearAllROI() {
QList<QGraphicsItem*> itemsToRemove;
foreach(QGraphicsItem* item, items()) {
if(item != m_bgItem && item->zValue() >=0)
itemsToRemove.append(item);
}
qDeleteAll(itemsToRemove); // 安全删除
}
针对不同Qt版本的适配策略:
坐标系统差异:
qreal替代int存储坐标值渲染差异处理:
cpp复制#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
setCacheMode(QGraphicsView::CacheBackground);
#else
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
#endif
angleDelta()和delta()QTouchEvent是否存在继承QGraphicsItem实现高级效果:
cpp复制class CustomROIItem : public QGraphicsItem {
public:
QRectF boundingRect() const override {
return shape().boundingRect();
}
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override {
// 实现渐变填充
QLinearGradient grad(rect().topLeft(), rect().bottomRight());
grad.setColorAt(0, Qt::blue);
grad.setColorAt(1, Qt::transparent);
painter->setBrush(grad);
// 添加阴影效果
painter->setPen(QPen(Qt::black, 2));
painter->drawPolygon(shape());
}
};
实现ROI数据的保存与加载:
cpp复制// 保存为JSON
QJsonArray saveROIs() {
QJsonArray array;
foreach(QGraphicsItem *item, items()) {
if(ROIItem *roi = dynamic_cast<ROIItem*>(item)) {
array.append(roi->toJson());
}
}
return array;
}
// 从JSON加载
void loadROIs(const QJsonArray &array) {
foreach(const QJsonValue &val, array) {
ROIItem *item = new ROIItem;
item->fromJson(val.toObject());
scene()->addItem(item);
}
}
交互设计黄金法则:
调试技巧:
cpp复制// 在场景中添加坐标指示器
void addDebugOverlay() {
QGraphicsSimpleTextItem *coordText = addSimpleText("");
connect(this, &ImageView::mouseMoved, [=](const QPointF &pos){
coordText->setText(QString("(%1, %2)").arg(pos.x()).arg(pos.y()));
coordText->setPos(pos + QPointF(10,10));
});
}
QElapsedTimer测量关键操作耗时QGraphicsItem::boundingRect()检查项是否过大QGraphicsView::render()检查离屏渲染问题这个项目最让我满意的不是功能实现本身,而是验证了Qt Graphics框架的强大潜力。通过合理的设计,2000行代码也能实现专业级的标注工具。特别是在处理旋转矩形碰撞检测时,发现QGraphicsPolygonItem自带的contains()方法比手动数学计算更可靠,这提醒我们:有时候相信框架比重新造轮子更明智。