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

简介

作者是一名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处理

拖动图片到窗口打开需要重写dragEnterEventdropEvent事件

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);
}

还有重写mousePressEventmouseMoveEventmouseReleaseEvent事件以实现各种快捷键操作

//当鼠标按下时触发该函数
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<width
        if(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教程

  • 6
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值