qt项目-《图像标注软件》源码阅读笔记-Shape类绘图及其子类

发布时间:2023年12月23日

目录

1. Shape 概览?

2. Shape 基类

2.1 字段

2.2 方法

2.3 嵌套类型

3. Shape2D?2d形状纯虚基类

3.1 字段

3.2 方法

4. Shape3D?3d形状纯虚基类

5. Shape2D子类

5.1 Rectangle 矩形类


1. Shape 概览?

功能:Shape类及其子类负责形状的绘制及形状的存储。Shape有7个子类。

  1. Brush代表画刷形状,用于分割标注;
  2. Rectangle代表矩形形状;
  3. Polygons代表多边形形状;
  4. Circle代表圆形形状;
  5. Curve代表平滑曲线形状;
  6. Rectangle3D代表3d长方体形状;
  7. Brush3D代表3d画刷形状,用于3d分割标注。

2. Shape 基类

2.1 字段

  1. color:标注颜色;
  2. isFill:是否被选中,选中时为true,并填充标注形状的内部,便于用户交互;
  3. isHide:是否隐藏标注;
  4. isHover:是否悬浮,若鼠标悬浮在标注内部,则变为true,填充内部颜色,便于用户交互;
  5. label:标注对应的标签文字;
  6. type:标注类型,枚举类型,各种形状(Brush,Rectangle,Polygons,...);

2.2 方法

  1. Shape(Type t):构造函数,初始化标注类型,即标注的形状;
  2. virtual ~Shape()=0;纯虚函数,使其成为抽象类。

2.3 嵌套类型

enum Type{Brush,Rectangle,Polygons,Circle,Curve,Rectangle3D,Brush3D};

// 子类可以通过继承 My::Shape 类并在构造函数中初始化 type 成员来使用这个枚举类型。

嵌套类型有很多种,包括内部类、嵌套结构、嵌套枚举等。嵌套类型提供了一种将相关的类型组织在一起并隐藏其实现细节的方式。这里使用的是枚举类型的嵌套。

作用和优点

  • 清晰的类型定义, 通过将这些类型放入枚举中,代码提供了一种清晰的、可读性强的方式来表示标注形状的类型。这样的设计使得在代码中使用这些类型更为直观,提高了代码的可维护性。
  • 扩展性,如果需要添加新的标注形状,只需在 enum Type 中添加新的成员。这样的设计使得系统更具有扩展性,而无需修改大量现有代码。
#ifndef SHAPE_H
#define SHAPE_H
#include<QString>
#include<QColor>

#include"Namespace.h"  // 头文件内声明了Shape, Brush,Rectangle,Polygons,...等类名

/// \brief 所有标注形状的基类
///
/// 负责形状的绘制及形状的存储
class My::Shape{
public:

    /// \brief 标注形状的类型
    ///
    /// Brush代表画刷形状,用于分割标注,
    /// Rectangle代表矩形形状,Polygons代表多边形形状,Circle代表圆形形状,Curve代表平滑曲线形状
    /// Rectangle3D代表3d长方体形状,Brush3D代表3d画刷形状,用于3d分割标注
    enum Type{Brush,Rectangle,Polygons,Circle,Curve,Rectangle3D,Brush3D};

    /// \brief 标注类型
    const Type type;

    /// \brief 标注对应的标签文字
    QString label;

    /// \brief 默认标注颜色
    QColor color=QColor(100,255,0,100);

    /// \brief 是否填充内部
    ///
    /// 当被选中时,isFill会变为true,填充标注形状的内部,便于用户交互
    bool isFill=false;

    /// \brief 是否隐藏标注
    bool isHide=false;

    /// \brief 是否悬浮
    ///
    /// 当前鼠标是否悬浮在标注内部,若悬浮在内部,则变为true,填充内部颜色,便于用户交互
    bool isHover=false;

    Shape(Type t);

    virtual ~Shape()=0;
};
#endif // SHAPE_H

使用 #include "Namespace.h" 的形式是为了引入命名空间 My 中的其他类。这样做的主要优点包括:

  • 代码组织: 使用命名空间可以将相关的类、函数、变量等组织在一起,提高了代码的可读性和可维护性。通过引入命名空间 My,你可以将所有与项目有关的类都放在同一个命名空间下,使代码更有条理。
  • 避免命名冲突: 命名空间提供了一种防止命名冲突的机制。如果在项目中有多个独立开发的部分,使用命名空间可以避免不同部分定义相同名称的类或函数时的冲突问题。

3. Shape2D?2d形状纯虚基类

3.1 字段

points:?存储标注形状的像素点位

3.2 方法

  • draw: 绘制标注形状,虚函数;
  • isInShape: 判断鼠标是否在标注形状内部,虚函数;
  • offset:偏移标注形状,虚函数;
#ifndef SHAPE2D_H
#define SHAPE2D_H
#include<QVector>
#include<QPoint>
#include<QLabel>
#include<opencv2/opencv.hpp>
#include"Shape.h"

/// \brief 2d标注形状的基类
class My::Shape2D:public My::Shape{
public:

    /// \brief 存储标注形状的像素点位
    QVector<QPointF> points;
    Shape2D(My::Shape::Type t):Shape(t){}

    /// \brief 绘制标注形状虚函数
    virtual void draw(QWidget* w);

    /// \brief 判断鼠标是否在标注形状内部虚函数
    virtual bool isInShape(QPointF p,QWidget* w);

    /// \brief 偏移标注形状虚函数
    virtual void offset(float xOffset,float yOffset);
    virtual ~Shape2D()=0;
};

#endif // SHAPE_H


4. Shape3D?3d形状纯虚基类

#ifndef SHAPE3D_H
#define SHAPE3D_H
#include<QVector>
#include<opencv2/opencv.hpp>
#include"Shape.h"

/// \brief 3d标注形状的基类
class My::Shape3D:public My::Shape{
public:

    /// \brief 存储像素点位
    QVector<cv::Point3f> points;
public:
    Shape3D(Type t);
    virtual ~Shape3D()=0;
};

#endif // SHAPE3D_H

5. Shape2D子类

每个子类都有3个相同的方法(继承自Shape2D基类):

  • draw: 绘制标注形状,虚函数;
  • isInShape: 判断鼠标是否在标注形状内部,虚函数;
  • offset:偏移标注形状,虚函数;

5.1 Rectangle 矩形类

字段:

  • width:?存储矩形的宽,相对Widget宽度的百分比;
  • height:存储矩形的高,也是相对值;

继承的points存放的x, y也是相对Widget百分比值。

方法:

Rectangle: 默认构造函数,在头文件做好了实现,用于构造自己和父类。

// Rectangle()默认构造函数,调用基类构造函数,传递Type,
// 用于初始化 Rectangle 类的基类 Shape2D 的成员。
Rectangle():Shape2D(Shape2D::Rectangle){} 
#ifndef RECTANGLE_H
#define RECTANGLE_H
#include"Shape2D.h"

/// \brief 代表矩形形状,继承Shape2D类
class My::Rectangle:public My::Shape2D{
public:
    
    /// \brief 存储矩形的宽
    float width;

    /// \brief 存储矩形的高
    float height;

    Rectangle():Shape2D(Shape2D::Rectangle){} // Rectangle()默认构造函数,调用基类构造函数,传递Type,用于初始化 Rectangle 类的基类 Shape2D 的成员。

    /// \brief 绘制形状函数
    virtual void draw(QWidget* w);

    /// \brief 判断是否在形状内函数
    virtual bool isInShape(QPointF p,QWidget* w);

    /// \brief 偏移形状函数
    virtual void offset(float xOffset,float yOffset);
};

#endif // RECTANGLE_H
#include<QPainter>
#include"Rectangle.h"
#include<QDebug>
#include<math.h>


/// \brief 绘制矩形
void My::Rectangle::draw(QWidget *w){
    if(isHide)return;  // 隐藏标注了,则不会下面的绘制。
    QPainter painter(w);  // 它用于在 QWidget 上进行绘制。
    QPen pen;
    pen.setColor(color);  // 设置线颜色
    pen.setWidth(4);  // 矩形线宽,固定是4个像素。
    painter.setPen(pen);
    if(isFill || isHover)painter.setBrush(color);  // 是否被选中,或者悬浮,则设置刷子颜色。
    else painter.setBrush(Qt::NoBrush);  // 否在不使用刷子
    if(points.length()==0)return;  // 为空,则说明没有点击,则不能绘制矩形,直接返回。

    // 分别绘制不同方向的矩形。
    // 这种操作的目的通常是为了将相对于控件大小的百分比坐标转化为实际的像素坐标。x,y,width,height都是相对值。
    if(width>=0&&height>=0){  // 矩形的宽度和高度都正常,不是负数. draw(x,y,w,h)
        painter.drawRect(int(points[0].x()*w->width()), int(points[0].y()*w->height()),int(width*w->width()),int(height*w->height()));
    }
    if(width<0&&height>=0){   // 宽度为负数,points[0]点在右上 draw(x,y,w,h)
        painter.drawRect(int(points[0].x()*w->width()+width* w->width()), int(points[0].y()*w->height()),int(abs(width*w->width())),int(height*w->height()));
    }
    if(width<0&&height<0){    // 均为负数,points[0]点在右下
        painter.drawRect(int(points[0].x()*w->width()+width* w->width()),int(points[0].y()*w->height()+height*w->height()),int(abs(width*w->width())),int(abs(height*w->height())));
    }
    if(width>=0&&height<0){   // 高度为负数. points[0]点在左下
        painter.drawRect(int(points[0].x()*w->width()),int(points[0].y()*w->height()+height*w->height()),int(width*w->width()),int(abs(height*w->height())));
    }

}


/// \brief 判断是否在矩形内部
bool My::Rectangle::isInShape(QPointF p,QWidget* w){

    if(points.length()==0)return false;  // 没有点击过,不存在矩形,则返回。
    
    // 矩形的四个顶点: 左上开始,顺时针走。
    std::vector<cv::Point2f> vec;
    vec.push_back(cv::Point2f(float(points[0].x())*w->width(),float(points[0].y())*w->height()));
    vec.push_back(cv::Point2f((float(points[0].x())+width)*w->width(),float(points[0].y())*w->height()));
    vec.push_back(cv::Point2f((float(points[0].x())+width)*w->width(),(float(points[0].y())+height)*w->height()));
    vec.push_back(cv::Point2f(float(points[0].x())*w->width(),(float(points[0].y())+height)*w->height()));

    cv::Point2f cvp(float(p.x())*w->width(),float(p.y())*w->height());

    double res=cv::pointPolygonTest(vec,cvp,false);
    if(res<0)return false;
    else return true;
}


/// \brief 偏移标注
void My::Rectangle::offset(float xOffset,float yOffset){
    for(int i=0;i<points.length();i++){
        points[i].rx()+=double(xOffset);  // 它返回的是一个引用,允许我们修改这个点的 x 坐标。
        points[i].ry()+=double(yOffset);
    }
}

(1)在这段代码中,x = int(points[0].x()*w->width()),这里涉及到坐标的映射和缩放。

假设 points[0].x() 是矩形左上角点在相对于矩形区域的 x 方向上的百分比坐标(范围在 0.0 到 1.0 之间),w->width()QWidget 的宽度。通过乘以 w->width(),将相对坐标转换为绝对坐标,即在 QWidget 上的具体像素坐标。

这种操作的目的通常是为了将相对于控件大小的百分比坐标转化为实际的像素坐标。这是因为在绘制图形时,常常需要考虑到用户可能调整窗口大小,而相对于窗口大小的百分比坐标能够适应不同的窗口尺寸。

(2)if(width<0&&height>=0){ ? // 宽度为负数,points[0]点在右上

左上x =?int(points[0].x()*w->width() + width* w->width()).?

int(points[0].x()*w->width()是矩形的右上x,width* w->width()是负数宽度,相加就得到左上x.

这里有个问题就是points是在哪里赋值的?

待续。。。

其他子类类似,就不细究了。


参考:GitHub - jameslahm/labelme: A image annotation software for 2D or 3D images

文章来源:https://blog.csdn.net/jizhidexiaoming/article/details/135166674
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。