简介

作者是一名QT初学者,为检验学习成果及完成毕业设计,在张老师和学姐的指导下,开发了这个标注工具。CSDN上很多文章对我的学习提供了极大的帮助,分享这篇文章给需要的人一起学习进步~

废话不多说,先看看效果:

开发环境

Windows10、Qt5.13.2(编译器用的是MinGW64_bit)、OpenCV4.1

开发过程

环境配置

首先,安装Qt Creator,在Qt里引入OpenCV库,需要使用CMake对库进行编译,相关环境配置具体参考了这两篇文章:

win10下Qt5.12.3配置OpenCV4.5.3

opencv编译

编译过程需要注意版本问题,版本过高编译容易出错,一些常见的错误在参考文章结尾有提到。另外在编译过程中需要下载一些文件,最好挂个梯子,不然需要自己单独去下载。

项目文件结构

aboutdialog:点击帮助->关于弹出的对话框,用于简单介绍使用方法

mainwindow:程序主窗口,用于响应主窗口的点击事件及图像数据处理

mygraphicsview:显示图像的控件,用于处理用户与图像的交互事件

selectmergemapdialog:点击拆分合并->合并后弹出的对话框,用于选择需要合并的图像

Resources目前只存放了程序的图标

操作按键说明

按住鼠标右键拖动将轨迹上的点标注

按住shift键右键拖动把轨迹上的点取消标注

按住alt键右键拖曳把区域内的点取消标注

按住ctrl键右键拖曳把区域以外的点取消标注

双击左键图像复位

部分核心代码

mainwindow部分

在初始界面显示“把图片拖到此处打开”,涉及重叠控件的布局问题

//显示“把图片拖到此处打开”
QFont font("楷体",20,QFont::Bold);
welcome_label->setFont(font);
welcome_label->setText("把图片拖到此处打开");
welcome_label->setAlignment(Qt::AlignCenter);
welcome_label->setStyleSheet("color:gray;");
welcome_label->resize(260,30);
welcome_label->setGeometry(this->width()/2-welcome_label->width()/2,this->height()/2-welcome_label->height()/2,welcome_label->width(),welcome_label->height());
//将m_layout装进graphicsView,然后把welcome_label放进m_layout,设置居中对齐
m_layout = new QHBoxLayout(ui->graphicsView);
m_layout->addWidget(welcome_label);
m_layout->setAlignment(welcome_label, Qt::AlignCenter);

保存当前显示的图像,文件名设置为系统时间,如:20230323_113726.png

//设置保存路径
QString path=QCoreApplication::applicationDirPath();
path.append("/");
path.append(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"));
path.append(".png");
//qDebug()<<path;
QString get_save_path=QFileDialog::getSaveFileName(this,"保存当前显示图像",path);//执行保存
if(ui->graphicsView->getPixmap().save(get_save_path)){QMessageBox::information(this,"提示","保存成功");
}

点击边缘检测->canny,对目标图像进行canny边缘检测。程序中设置了四个图像缓存,分别用于存储原图、变换图、滤波图、边缘检测图,依次命名为origin_img,transform_img,filted_img,edge_img,在进行任何图像处理前需要选择目标图像。

//对图像进行边缘检测并将结果显示到graphicsView中
Mat src,t,dst;
//选择图像来源,优先次序为filted_img,transform_img,origin_img
if(!filted_img.isNull()){t=fromImage(filted_img);t.copyTo(src);
}
else if(!transform_img.isNull()){t=fromImage(transform_img);t.copyTo(src);
}
else{t=fromImage(origin_img);t.copyTo(src);
}
//将目标图像转换成8位单通道灰度图
if(src.type()!=CV_8UC1){src.convertTo(src,CV_8UC1);
}Canny(src,dst,ui->sliderForThreshold1->value(),ui->sliderForThreshold2->value());
QImage img=matToImage(dst);
//保存图像到缓存,注意要用深拷贝
edge_img=img.copy(0,0,img.width(),img.height());

对目标图像进行sobel边缘检测。

//先计算xy方向上的边缘检测图
Mat sobel_x,sobel_y;
Sobel(src,sobel_x,CV_64F,1,0);
Sobel(src,sobel_y,CV_64F,0,1);
convertScaleAbs(sobel_x,sobel_x);
convertScaleAbs(sobel_y,sobel_y);
//两者加权平均
addWeighted(sobel_x,0.5,sobel_y,0.5,0,dst);
//将得到的检测结果dst根据阈值进行两级化,高于阈值的像素值置为255,低于的置为0
for (int x = 0; x < dst.rows; ++x) {for (int y = 0; y < dst.cols; ++y) {if(dst.at<uchar>(x,y)>ui->sliderForBound_2->value()){dst.at<uchar>(x,y)=255;}else{dst.at<uchar>(x,y)=0;}}
}
QImage img=matToImage(dst);
//保存图像到缓存,注意要用深拷贝
edge_img=img.copy(0,0,img.width(),img.height());

对目标图像进行巴特沃斯高通滤波,算法使用C++和OpenCV实现:

Mat src,dst;
//高通滤波,增强边缘
src.convertTo(src,CV_32FC1);
Mat f_complex_c2;
//傅里叶变换
dft(src,f_complex_c2,DFT_COMPLEX_OUTPUT);//将f_complex_c2低频区域的值归零,保留高频区域的值
//计算滤波半径,图像中心位置
int radius=f_complex_c2.cols>f_complex_c2.rows?(f_complex_c2.rows/2)*(ui->lcdHighpassRadius->value()/100.0):(f_complex_c2.cols/2)*(ui->lcdHighpassRadius->value()/100.0);
int cx=f_complex_c2.cols/2;
int cy=f_complex_c2.rows/2;
//将低频移至中心
Mat temp;
//这里用的是浅拷贝,对part图像的交换操作将影响f_complex_c2
Mat part1(f_complex_c2,Rect(0,0,cx,cy));
Mat part2(f_complex_c2,Rect(cx,0,cx,cy));
Mat part3(f_complex_c2,Rect(0,cy,cx,cy));
Mat part4(f_complex_c2,Rect(cx,cy,cx,cy));
part1.copyTo(temp);
part4.copyTo(part1);
temp.copyTo(part4);part2.copyTo(temp);
part3.copyTo(part2);
temp.copyTo(part3);
//巴特沃斯高通滤波
for (int i = 0; i < f_complex_c2.rows; ++i) {for (int j = 0; j < f_complex_c2.cols; ++j) {f_complex_c2.at<Vec2f>(i,j)=f_complex_c2.at<Vec2f>(i,j)*(1.0-(1.0 / (1.0 + pow(sqrt(pow(i - cy, 2.0) + pow(j - cx, 2.0)) / radius, 4.0))));}
}
//将低频中心移回原来位置
Mat temp_;
//这里用的是浅拷贝,对part图像的交换操作将影响f_complex_c2
Mat part1_(f_complex_c2,Rect(0,0,cx,cy));
Mat part2_(f_complex_c2,Rect(cx,0,cx,cy));
Mat part3_(f_complex_c2,Rect(0,cy,cx,cy));
Mat part4_(f_complex_c2,Rect(cx,cy,cx,cy));
part1_.copyTo(temp_);
part4_.copyTo(part1_);
temp_.copyTo(part4_);part2_.copyTo(temp_);
part3_.copyTo(part2_);
temp_.copyTo(part3_);
//傅里叶逆变换,只取实部
Mat f_real_c1;
dft(f_complex_c2,f_real_c1,DFT_REAL_OUTPUT + DFT_SCALE + DFT_INVERSE);
f_real_c1.convertTo(dst,CV_8UC1);QImage img=matToImage(dst);
//保存图像到缓存,注意要用深拷贝
filted_img=img.copy(0,0,img.width(),img.height());

log变换。

//对图像做新的log变换
Mat src,dst;
src=fromImage(origin_img);
src.convertTo(src, CV_32FC1);  //转化为32位浮点型
src = src*value + 1;           //计算 r*v+1
log(src, src);                 //计算log(1+r*v),底数为e
src=src/log(value);//底数换成v
//归一化处理
normalize(src, dst, 0, 255, NORM_MINMAX,CV_8UC1);
//保存图像到缓存,注意要用深拷贝
QImage img=matToImage(dst);
transform_img=img.copy(0,0,img.width(),img.height());

图像合并。base_img来源于“选择合并对象”对话框的选择结果。标注颜色存储在mark_color变量中,类型为QColor,改变标注颜色即改变该变量的值,默认为紫色。

//将base_img和边缘检测图合并为merge_mat
Mat merge_mat;
vector<Mat> channels;
//将base_img转为4通道,并分离到channels
split(fromImage(base_img.convertToFormat(QImage::Format_ARGB32)),channels);
int x=base_img.width();
int y=base_img.height();
//在base_img的基础上对检测到的边缘标注紫色,紫色ARGB值为(255,255,0,255)
//注意mat和qimage的坐标关系,刚好相反
for (int i = 0; i < x; ++i) {for (int j = 0; j < y; ++j) {if(edge_img.pixel(i,j)==0xFFFFFFFF){channels.at(0).at<uchar>(j,i)=mark_color.blue();//B通道channels.at(1).at<uchar>(j,i)=mark_color.green();//G通道channels.at(2).at<uchar>(j,i)=mark_color.red();//R通道}}
}
merge(channels,merge_mat);
ui->graphicsView->setPixmap(matToImage(merge_mat));

右键改变标注状态的实现。

有两种思路:

  1. 获取当前显示的图像,拆分为3通道,对点击处的像素设置成紫色(标注)或恢复原来的RGB值(取消标注),重新合并3通道后显示;

  1. 对边缘检测结果图进行操作,将点击处的像素值取反(0为未标记状态,1为标记),再与合并对象进行合并,最后显示。

第二种方法比较容易实现,实现过程:

//将点击处的像素值取反
edge_img.setPixel(x,y,0x00FFFFFF ^ edge_img.pixel(x,y));//将base_img和边缘检测图合并为merge_mat
Mat merge_mat;
vector<Mat> channels;
//将base_img转为4通道,并分离到channels
split(fromImage(base_img.convertToFormat(QImage::Format_ARGB32)),channels);
int x=base_img.width();
int y=base_img.height();
//在base_img的基础上对检测到的边缘标注紫色,紫色ARGB值为(255,255,0,255)
//注意mat和qimage的坐标关系,刚好相反
for (int i = 0; i < x; ++i) {for (int j = 0; j < y; ++j) {if(edge_img.pixel(i,j)==0xFFFFFFFF){channels.at(0).at<uchar>(j,i)=mark_color.blue();//B通道channels.at(1).at<uchar>(j,i)=mark_color.green();//G通道channels.at(2).at<uchar>(j,i)=mark_color.red();//R通道}}
}
merge(channels,merge_mat);
ui->graphicsView->setPixmap(matToImage(merge_mat));

按住shift键和右键移动鼠标,对轨迹附近的点取消标注。按住alt键右键拖曳,将矩形区域内的点取消标记也是这个道理。

//对(x,y)八邻域的像素取消标记
for (int i = x-1; i <= x+1; ++i) {for (int j = y-1; j <= y+1; ++j) {edge_img.setPixel(i,j,0xFF000000);}
}//修改边缘检测图edge_img,将区域内的像素全部置黑,即取消标记
for (int x = lx; x <= rx; ++x) {for (int y = ty; y <= by; ++y) {edge_img.setPixel(x,y,0xFF000000);}
}//将base_img和边缘检测图合并为merge_mat
Mat merge_mat;
vector<Mat> channels;
//将base_img转为4通道,并分离到channels
split(fromImage(base_img.convertToFormat(QImage::Format_ARGB32)),channels);
int x=base_img.width();
int y=base_img.height();
//在base_img的基础上对检测到的边缘标注紫色,紫色ARGB值为(255,255,0,255)
//注意mat和qimage的坐标关系,刚好相反
for (int i = 0; i < x; ++i) {for (int j = 0; j < y; ++j) {if(edge_img.pixel(i,j)==0xFFFFFFFF){channels.at(0).at<uchar>(j,i)=mark_color.blue();//B通道channels.at(1).at<uchar>(j,i)=mark_color.green();//G通道channels.at(2).at<uchar>(j,i)=mark_color.red();//R通道}}
}
merge(channels,merge_mat);
ui->graphicsView->setPixmap(matToImage(merge_mat));
mygraphicsview部分

该部分用于处理用户与图像的交互事件,当捕捉到用户操作后,释放信号交由mainwindow处理

拖动图片到窗口打开需要重写dragEnterEvent和dropEvent事件

void MyGraphicsView::dragEnterEvent(QDragEnterEvent *event)
{//如果拖进窗口的文件类型是png、jpg、bng,接受这类文件if(!event->mimeData()->urls()[0].fileName().right(3).compare("png")||!event->mimeData()->urls()[0].fileName().right(3).compare("jpg")||!event->mimeData()->urls()[0].fileName().right(3).compare("bng")){event->accept();}else{event->ignore();//否则不接受鼠标事件}QGraphicsView::dragEnterEvent(event);
}void MyGraphicsView::dropEvent(QDropEvent *event){//从event中获取文件路径const QMimeData *data=event->mimeData();//向主窗口传递信号QString file_name=data->urls()[0].toLocalFile();emit dragFile(file_name);QGraphicsView::dropEvent(event);
}

滚动滑轮进行缩放,重写wheelEvent事件

void MyGraphicsView::zoom(qreal factor)
{//防止缩得太小或放得太大qreal t = transform().scale(factor, factor).mapRect(QRectF(0, 0, 1, 1)).width();if (t < 0.07 || t > 100)return ;scale(factor, factor);
}//当滑轮滚动时触发该函数,进行图像缩放
void MyGraphicsView::wheelEvent(QWheelEvent *event)
{//当滑轮滚动时,获取其滚动量QPoint amount=event->angleDelta();//正值表示放大,负值表示缩小amount.y()>0?zoom(1.1):zoom(0.9);
}

还有重写mousePressEvent、mouseMoveEvent、mouseReleaseEvent事件以实现各种快捷键操作

//当鼠标按下时触发该函数
void MyGraphicsView::mousePressEvent(QMouseEvent *event)
{//如果按下左键,选中标记置true,同时记录按下位置if(event->button()==Qt::LeftButton){isSelected=true;currentPoint=event->globalPos();}//如果按下shift键后按下右键,并且当前图像经过边缘检测处理,可能做的是将鼠标划过的点取消标注的操作else if(event->modifiers() == Qt::ShiftModifier && event->button()==Qt::RightButton && isProcessed){remove_points=true;//标记右键被按下this->rightbuttonIsPressed=true;}//如果按下alt键后按下右键,并且当前图像经过边缘检测处理,可能做的是将拖曳区域内的点取消标注的操作else if(event->modifiers() == Qt::AltModifier && event->button()==Qt::RightButton && isProcessed){delete_points=true;//标记右键被按下this->rightbuttonIsPressed=true;//记录右键按下位置this->start=mapToScene(event->pos());}//如果按下ctrl键后按下右键,并且当前图像经过边缘检测处理,可能做的是保留拖曳区域内点的操作else if(event->modifiers() == Qt::ControlModifier && event->button()==Qt::RightButton && isProcessed){reserve_points=true;//标记右键被按下this->rightbuttonIsPressed=true;//记录右键按下位置this->start=mapToScene(event->pos());}//如果按下右键,并且当前图像经过边缘检测处理else if(event->button()==Qt::RightButton && isProcessed){//标记右键被按下this->rightbuttonIsPressed=true;//记录右键按下位置this->start=mapToScene(event->pos());}QGraphicsView::mousePressEvent(event);
}//当鼠标移动时触发该函数
void MyGraphicsView::mouseMoveEvent(QMouseEvent *event)
{//当鼠标左键在按住状态下移动时,计算光标偏移量(这里不能用event->button()==Qt::LeftButton)if(isSelected){QPoint offset=event->globalPos()-currentPoint;currentPoint=event->globalPos();//移动窗口实现图片拖动效果,但拖动图像时会出现图像偏移的情况,有时又正常,一直想不明白原因,这个地方有待研究改进int x=(width()-1)/2-offset.x();int y=(height()-1)/2-offset.y();centerOn(mapToScene(x,y));}QPointF p=mapToScene(event->pos());int x=p.x();int y=p.y();//如果鼠标在显示图像内,释放信号,传递坐标if(!pixmapItem->pixmap().isNull()){int width=pixmapItem->pixmap().width();int height=pixmapItem->pixmap().height();//不能用0<=x<widthif(x>=0 && x<width && y>=0 &&y<height){emit mouseMove(x,y);//同时当鼠标右键在按住状态下移动时,对轨迹上的点进行标注if(rightbuttonIsPressed&&!delete_points&&!reserve_points&&!remove_points){emit mouseMoveWithRightButton(x,y);}//当鼠标右键和shift被按下,鼠标移动过程中对轨迹上的点取消标注else if(remove_points){emit mouseMoveWithRightButtonAndShift(x,y);}}}QGraphicsView::mouseMoveEvent(event);
}//当鼠标松开时,选中标记置false
void MyGraphicsView::mouseReleaseEvent(QMouseEvent *event)
{if(event->button()==Qt::LeftButton){isSelected=false;}//如果松开的是右键,并且当前图像经过边缘检测处理else if(event->button()==Qt::RightButton && isProcessed){//右键松开this->rightbuttonIsPressed=false;//记录右键松开的位置QPointF end=mapToScene(event->pos());//两者做差QPointF offset=end-start;//如果做的是拖曳消除区域点的操作if(qAbs(offset.x())>=1||qAbs(offset.y())>=1){//获取区域左上角和右下角的坐标int larger_x,smaller_x,larger_y,smaller_y;end.x()>start.x()?larger_x=end.x():larger_x=start.x();end.x()>start.x()?smaller_x=start.x():smaller_x=end.x();end.y()>start.y()?larger_y=end.y():larger_y=start.y();end.y()>start.y()?smaller_y=start.y():smaller_y=end.y();//将区域约束到图像内if(smaller_x<0)smaller_x=0;if(smaller_x>pixmapItem->pixmap().width()-1)smaller_x=pixmapItem->pixmap().width()-1;if(larger_x<0)larger_x=0;if(larger_x>pixmapItem->pixmap().width()-1)larger_x=pixmapItem->pixmap().width()-1;if(smaller_y<0)smaller_y=0;if(smaller_y>pixmapItem->pixmap().height()-1)smaller_y=pixmapItem->pixmap().height()-1;if(larger_y<0)larger_y=0;if(larger_y>pixmapItem->pixmap().height()-1)larger_y=pixmapItem->pixmap().height()-1;//释放信号交由主窗口处理if(reserve_points){reserve_points=false;emit rightButtonDragCtrl(smaller_x,smaller_y,larger_x,larger_y);}else if(delete_points){delete_points=false;emit rightButtonDrag(smaller_x,smaller_y,larger_x,larger_y);}else if(remove_points){remove_points=false;}}//否则做的是标注点的操作else{//释放信号交由主窗口处理emit rightButtonClick(end.x(),end.y());}}QGraphicsView::mouseReleaseEvent(event);
}

存在问题

鼠标左键拖动图像时会出现图像偏移现象,即鼠标指针没有固定到图像的某点上,但在某些情况下又是正常的,具体效果如下:

异常(拖动前后指针不在同一点)

正常(拖动前后指针在同一点)

拖动效果在mouseMoveEvent中实现,在代码中也有相应注释,欢迎各位大佬指正

上述问题已得到解决。

github地址:https://github.com/FonlinGH/MarkupTool,包含源代码及可执行程序

第一次写博客,有不恰当的地方还请谅解,仅用作学习记录。

参考文章链接:

win10下Qt5.12.3配置OpenCV4.5.3

qt5配置msvc2017

opencv编译

Opencv图像增强算法实现

OpenCV像增强之对数变换log

OPENCV Mat的数据类型

QGraphicsView图形视图框架使用(一)坐标变换

QGraphicsView教程

QT+OpenCV实现一个标注工具(图像处理、边缘检测)相关推荐

  1. 用Qt自制视频数据标注工具

    编写该工具的意义 视频数据主要用于多目标追踪中,与目标检测不同,用于训练追踪模型的数据最重要的是目标的ID号(若某一帧中某目标被标为1号,在所有的视频帧中都应为1号),目前现成的数据集有行人追踪的MO ...

  2. 我的Qt作品(15)使用Qt+OpenCV实现一个卡尺测量工具,具备找线和找圆的功能

    一.卡尺原理 一句话总结:卡尺的原理就是找N个小矩形ROI里面的灰度值突变的地方. 即:遍历每个小矩形ROI,分别找到1个点,这个点是灰度突变的峰值.然后把这N个点拟合成直线或者圆.所以ROI的位置摆 ...

  3. 【无标题】用Qt+OpenCV做一个图像处理软件(灰度化)

    初入坑,有问题请指正. 首先,点击菜单栏插入选项,从文件目录中选取一张图片 . 附代码 void MainWindow::on_charu_triggered() {QString filename= ...

  4. 使用QT简单实现一个画图工具

    mainwindow.h文件 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QA ...

  5. 用Qt编写视频追踪数据标注工具

    文章目录 编写该工具的意义 使用步骤 选择图片所在的目录 标注目标ID 保存标注结果 编写该工具的意义 视频数据主要用于多目标追踪中,与目标检测不同,用于训练追踪模型的数据最重要的是目标的ID号(若某 ...

  6. 一个便捷的免费图像标注工具AutoSeg

    请不要使用中文路径,路径中不可以有空格等无效字符,否则无法正常使用. 最新版2019-09-25 版本更新信息请参考: AutoSeg版本更新信息_tanmx219的博客-CSDN博客 正文 由于经常 ...

  7. QT+opencv学习笔记(5)——霍夫直线检测、圆检测及椭圆检测

    开发环境为:win10+QT5.8+opencv3.2 Hough变换是图像处理中从图像中识别几何形状的基本方法之一,应用很广泛.最基本的Hough变换是从黑白图像中检测直线,还可以经过改进检测圆.椭 ...

  8. 使用标注工具Labelme和PaddleSeg实现铁路的图像分割

    一.前言 该项目是用标注工具Labelme和PaddleSeg实现卫星遥感图像的铁路分割. Labelme是一个标注工具,可以基本实现深度学习的标注任务,让我们标注自己的数据集,不过麻烦的一点就是要自 ...

  9. UI设计图的标注工具大比拼

    UI设计图标注工具大比拼 作为一名在乎设计的前端程序,电脑里备着诸如PS啊AI啊Sketch之类的是一件很平常的事情,方便与设计稿进行对比啊(方便偷偷修改下设计稿,对个像素改个尺寸什么的,哈哈- -哈 ...

最新文章

  1. 新元素之video,audio,meter,datalist,keygen,output
  2. hive脚本执行方式
  3. linux服务器登录时慢出现卡顿
  4. 后Kubernetes时代的微服务
  5. Compass样式重置
  6. hisicv200 exfat支持(转)
  7. 多密钥ssh-key生成与管理
  8. python list函数使用总结_python——list总结
  9. RT-Thread 4.0 + STM32F407 学习笔记1
  10. html mysql 数据列表_html的列表加载数据库
  11. 查找项目里面资源文件报空指针的解决办法
  12. [导入]+ADO.NET读书笔记系列 一+
  13. XSS绕过与防御总结
  14. IE浏览器卡死的问题
  15. dspbios设计指南_视频广告设计者指南
  16. TypeError Class constructor ExampleService cannot be invoked withou ‘new‘ at
  17. UML相关工具一览(2018年5月更新)
  18. 四平方数和定理(leetcode 279 python)
  19. 《和平精英》×开心麻花电影《独行月球》的讨巧互利联动
  20. lds天线技术流程图_什么是LDS天线技术

热门文章

  1. excel保存列名不变,将好几列数据按顺序整合到一列
  2. 网页设计学习笔记——关于网页、样式文件的命名规范
  3. css设置ie浏览器的样式,IE浏览器常见的9个css Bug以及解决办法
  4. PHP代码中的情话,[php发送短信验证码代码]php发送短信验证码
  5. Vue之组件间的双向绑定
  6. 军用emm管控平台_深信服企业移动管理EMM
  7. android notification设置不同字体颜色,Android Notification自定义通知样式你要知道的事...
  8. java.security.AccessControlException: access denied 错误的解决方法
  9. python-异常处理、元类
  10. 摘自《读者》的哲理短句——生活篇