作者是一名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键右键拖曳把区域以外的点取消标注
双击左键图像复位
在初始界面显示“把图片拖到此处打开”,涉及重叠控件的布局问题
//显示“把图片拖到此处打开”
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()<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(x,y)>ui->sliderForBound_2->value()){
dst.at(x,y)=255;
}
else{
dst.at(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(i,j)=f_complex_c2.at(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 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(j,i)=mark_color.blue();//B通道
channels.at(1).at(j,i)=mark_color.green();//G通道
channels.at(2).at(j,i)=mark_color.red();//R通道
}
}
}
merge(channels,merge_mat);
ui->graphicsView->setPixmap(matToImage(merge_mat));
右键改变标注状态的实现。
有两种思路:
获取当前显示的图像,拆分为3通道,对点击处的像素设置成紫色(标注)或恢复原来的RGB值(取消标注),重新合并3通道后显示;
对边缘检测结果图进行操作,将点击处的像素值取反(0为未标记状态,1为标记),再与合并对象进行合并,最后显示。
第二种方法比较容易实现,实现过程:
//将点击处的像素值取反
edge_img.setPixel(x,y,0x00FFFFFF ^ edge_img.pixel(x,y));
//将base_img和边缘检测图合并为merge_mat
Mat merge_mat;
vector 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(j,i)=mark_color.blue();//B通道
channels.at(1).at(j,i)=mark_color.green();//G通道
channels.at(2).at(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 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(j,i)=mark_color.blue();//B通道
channels.at(1).at(j,i)=mark_color.green();//G通道
channels.at(2).at(j,i)=mark_color.red();//R通道
}
}
}
merge(channels,merge_mat);
ui->graphicsView->setPixmap(matToImage(merge_mat));
该部分用于处理用户与图像的交互事件,当捕捉到用户操作后,释放信号交由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=0 && x=0 &&ybutton()==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教程