目录
功能:Shape类及其子类负责形状的绘制及形状的存储。Shape有7个子类。
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
,你可以将所有与项目有关的类都放在同一个命名空间下,使代码更有条理。points:?存储标注形状的像素点位
#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
#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
每个子类都有3个相同的方法(继承自Shape2D基类):
字段:
继承的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