簡介
作者是一名QT初學(xué)者,為檢驗學(xué)習(xí)成果及完成畢業(yè)設(shè)計,在張老師和學(xué)姐的指導(dǎo)下,開發(fā)了這個標(biāo)注工具。CSDN上很多文章對我的學(xué)習(xí)提供了極大的幫助,分享這篇文章給需要的人一起學(xué)習(xí)進(jìn)步~
廢話不多說,先看看效果:

開發(fā)環(huán)境
Windows10、Qt5.13.2(編譯器用的是MinGW64_bit)、OpenCV4.1
開發(fā)過程
環(huán)境配置
首先,安裝Qt Creator,在Qt里引入OpenCV庫,需要使用CMake對庫進(jìn)行編譯,相關(guān)環(huán)境配置具體參考了這兩篇文章:
win10下Qt5.12.3配置OpenCV4.5.3
opencv編譯
編譯過程需要注意版本問題,版本過高編譯容易出錯,一些常見的錯誤在參考文章結(jié)尾有提到。另外在編譯過程中需要下載一些文件,最好掛個梯子,不然需要自己單獨去下載。
項目文件結(jié)構(gòu)

aboutdialog:點擊幫助->關(guān)于彈出的對話框,用于簡單介紹使用方法
mainwindow:程序主窗口,用于響應(yīng)主窗口的點擊事件及圖像數(shù)據(jù)處理
mygraphicsview:顯示圖像的控件,用于處理用戶與圖像的交互事件
selectmergemapdialog:點擊拆分合并->合并后彈出的對話框,用于選擇需要合并的圖像
Resources目前只存放了程序的圖標(biāo)
操作按鍵說明
按住鼠標(biāo)右鍵拖動將軌跡上的點標(biāo)注
按住shift鍵右鍵拖動把軌跡上的點取消標(biāo)注
按住alt鍵右鍵拖曳把區(qū)域內(nèi)的點取消標(biāo)注
按住ctrl鍵右鍵拖曳把區(qū)域以外的點取消標(biāo)注
雙擊左鍵圖像復(fù)位
部分核心代碼
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裝進(jìn)graphicsView,然后把welcome_label放進(jìn)m_layout,設(shè)置居中對齊
m_layout = new QHBoxLayout(ui->graphicsView);
m_layout->addWidget(welcome_label);
m_layout->setAlignment(welcome_label, Qt::AlignCenter);
保存當(dāng)前顯示的圖像,文件名設(shè)置為系統(tǒng)時間,如:20230323_113726.png
//設(shè)置保存路徑
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,"保存當(dāng)前顯示圖像",path);
//執(zhí)行保存
if(ui->graphicsView->getPixmap().save(get_save_path)){
QMessageBox::information(this,"提示","保存成功");
}
點擊邊緣檢測->canny,對目標(biāo)圖像進(jìn)行canny邊緣檢測。程序中設(shè)置了四個圖像緩存,分別用于存儲原圖、變換圖、濾波圖、邊緣檢測圖,依次命名為origin_img,transform_img,filted_img,edge_img,在進(jìn)行任何圖像處理前需要選擇目標(biāo)圖像。
//對圖像進(jìn)行邊緣檢測并將結(jié)果顯示到graphicsView中
Mat src,t,dst;
//選擇圖像來源,優(yōu)先次序為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);
}
//將目標(biāo)圖像轉(zhuǎn)換成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());
對目標(biāo)圖像進(jìn)行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);
//兩者加權(quán)平均
addWeighted(sobel_x,0.5,sobel_y,0.5,0,dst);
//將得到的檢測結(jié)果dst根據(jù)閾值進(jìn)行兩級化,高于閾值的像素值置為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());
對目標(biāo)圖像進(jìn)行巴特沃斯高通濾波,算法使用C++和OpenCV實現(xiàn):
Mat src,dst;
//高通濾波,增強(qiáng)邊緣
src.convertTo(src,CV_32FC1);
Mat f_complex_c2;
//傅里葉變換
dft(src,f_complex_c2,DFT_COMPLEX_OUTPUT);
//將f_complex_c2低頻區(qū)域的值歸零,保留高頻區(qū)域的值
//計算濾波半徑,圖像中心位置
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); //轉(zhuǎn)化為32位浮點型
src = src*value + 1; //計算 r*v+1
log(src, src); //計算log(1+r*v),底數(shù)為e
src=src/log(value);//底數(shù)換成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來源于“選擇合并對象”對話框的選擇結(jié)果。標(biāo)注顏色存儲在mark_color變量中,類型為QColor,改變標(biāo)注顏色即改變該變量的值,默認(rèn)為紫色。
//將base_img和邊緣檢測圖合并為merge_mat
Mat merge_mat;
vector<Mat> channels;
//將base_img轉(zhuǎn)為4通道,并分離到channels
split(fromImage(base_img.convertToFormat(QImage::Format_ARGB32)),channels);
int x=base_img.width();
int y=base_img.height();
//在base_img的基礎(chǔ)上對檢測到的邊緣標(biāo)注紫色,紫色ARGB值為(255,255,0,255)
//注意mat和qimage的坐標(biāo)關(guān)系,剛好相反
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));
右鍵改變標(biāo)注狀態(tài)的實現(xiàn)。
有兩種思路:
獲取當(dāng)前顯示的圖像,拆分為3通道,對點擊處的像素設(shè)置成紫色(標(biāo)注)或恢復(fù)原來的RGB值(取消標(biāo)注),重新合并3通道后顯示;
對邊緣檢測結(jié)果圖進(jìn)行操作,將點擊處的像素值取反(0為未標(biāo)記狀態(tài),1為標(biāo)記),再與合并對象進(jìn)行合并,最后顯示。
第二種方法比較容易實現(xiàn),實現(xiàn)過程:
//將點擊處的像素值取反
edge_img.setPixel(x,y,0x00FFFFFF ^ edge_img.pixel(x,y));
//將base_img和邊緣檢測圖合并為merge_mat
Mat merge_mat;
vector<Mat> channels;
//將base_img轉(zhuǎn)為4通道,并分離到channels
split(fromImage(base_img.convertToFormat(QImage::Format_ARGB32)),channels);
int x=base_img.width();
int y=base_img.height();
//在base_img的基礎(chǔ)上對檢測到的邊緣標(biāo)注紫色,紫色ARGB值為(255,255,0,255)
//注意mat和qimage的坐標(biāo)關(guān)系,剛好相反
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鍵和右鍵移動鼠標(biāo),對軌跡附近的點取消標(biāo)注。按住alt鍵右鍵拖曳,將矩形區(qū)域內(nèi)的點取消標(biāo)記也是這個道理。
//對(x,y)八鄰域的像素取消標(biāo)記
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,將區(qū)域內(nèi)的像素全部置黑,即取消標(biāo)記
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轉(zhuǎn)為4通道,并分離到channels
split(fromImage(base_img.convertToFormat(QImage::Format_ARGB32)),channels);
int x=base_img.width();
int y=base_img.height();
//在base_img的基礎(chǔ)上對檢測到的邊緣標(biāo)注紫色,紫色ARGB值為(255,255,0,255)
//注意mat和qimage的坐標(biāo)關(guān)系,剛好相反
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部分
該部分用于處理用戶與圖像的交互事件,當(dāng)捕捉到用戶操作后,釋放信號交由mainwindow處理
拖動圖片到窗口打開需要重寫dragEnterEvent和dropEvent事件
void MyGraphicsView::dragEnterEvent(QDragEnterEvent *event)
{
//如果拖進(jìn)窗口的文件類型是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();//否則不接受鼠標(biāo)事件
}
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);
}
滾動滑輪進(jìn)行縮放,重寫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);
}
//當(dāng)滑輪滾動時觸發(fā)該函數(shù),進(jìn)行圖像縮放
void MyGraphicsView::wheelEvent(QWheelEvent *event)
{
//當(dāng)滑輪滾動時,獲取其滾動量
QPoint amount=event->angleDelta();
//正值表示放大,負(fù)值表示縮小
amount.y()>0?zoom(1.1):zoom(0.9);
}
還有重寫mousePressEvent、mouseMoveEvent、mouseReleaseEvent事件以實現(xiàn)各種快捷鍵操作
//當(dāng)鼠標(biāo)按下時觸發(fā)該函數(shù)
void MyGraphicsView::mousePressEvent(QMouseEvent *event)
{
//如果按下左鍵,選中標(biāo)記置true,同時記錄按下位置
if(event->button()==Qt::LeftButton){
isSelected=true;
currentPoint=event->globalPos();
}
//如果按下shift鍵后按下右鍵,并且當(dāng)前圖像經(jīng)過邊緣檢測處理,可能做的是將鼠標(biāo)劃過的點取消標(biāo)注的操作
else if(event->modifiers() == Qt::ShiftModifier && event->button()==Qt::RightButton && isProcessed){
remove_points=true;
//標(biāo)記右鍵被按下
this->rightbuttonIsPressed=true;
}
//如果按下alt鍵后按下右鍵,并且當(dāng)前圖像經(jīng)過邊緣檢測處理,可能做的是將拖曳區(qū)域內(nèi)的點取消標(biāo)注的操作
else if(event->modifiers() == Qt::AltModifier && event->button()==Qt::RightButton && isProcessed){
delete_points=true;
//標(biāo)記右鍵被按下
this->rightbuttonIsPressed=true;
//記錄右鍵按下位置
this->start=mapToScene(event->pos());
}
//如果按下ctrl鍵后按下右鍵,并且當(dāng)前圖像經(jīng)過邊緣檢測處理,可能做的是保留拖曳區(qū)域內(nèi)點的操作
else if(event->modifiers() == Qt::ControlModifier && event->button()==Qt::RightButton && isProcessed){
reserve_points=true;
//標(biāo)記右鍵被按下
this->rightbuttonIsPressed=true;
//記錄右鍵按下位置
this->start=mapToScene(event->pos());
}
//如果按下右鍵,并且當(dāng)前圖像經(jīng)過邊緣檢測處理
else if(event->button()==Qt::RightButton && isProcessed){
//標(biāo)記右鍵被按下
this->rightbuttonIsPressed=true;
//記錄右鍵按下位置
this->start=mapToScene(event->pos());
}
QGraphicsView::mousePressEvent(event);
}
//當(dāng)鼠標(biāo)移動時觸發(fā)該函數(shù)
void MyGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
//當(dāng)鼠標(biāo)左鍵在按住狀態(tài)下移動時,計算光標(biāo)偏移量(這里不能用event->button()==Qt::LeftButton)
if(isSelected){
QPoint offset=event->globalPos()-currentPoint;
currentPoint=event->globalPos();
//移動窗口實現(xiàn)圖片拖動效果,但拖動圖像時會出現(xiàn)圖像偏移的情況,有時又正常,一直想不明白原因,這個地方有待研究改進(jìn)
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();
//如果鼠標(biāo)在顯示圖像內(nèi),釋放信號,傳遞坐標(biāo)
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);
//同時當(dāng)鼠標(biāo)右鍵在按住狀態(tài)下移動時,對軌跡上的點進(jìn)行標(biāo)注
if(rightbuttonIsPressed&&!delete_points&&!reserve_points&&!remove_points){
emit mouseMoveWithRightButton(x,y);
}
//當(dāng)鼠標(biāo)右鍵和shift被按下,鼠標(biāo)移動過程中對軌跡上的點取消標(biāo)注
else if(remove_points){
emit mouseMoveWithRightButtonAndShift(x,y);
}
}
}
QGraphicsView::mouseMoveEvent(event);
}
//當(dāng)鼠標(biāo)松開時,選中標(biāo)記置false
void MyGraphicsView::mouseReleaseEvent(QMouseEvent *event)
{
if(event->button()==Qt::LeftButton){
isSelected=false;
}
//如果松開的是右鍵,并且當(dāng)前圖像經(jīng)過邊緣檢測處理
else if(event->button()==Qt::RightButton && isProcessed){
//右鍵松開
this->rightbuttonIsPressed=false;
//記錄右鍵松開的位置
QPointF end=mapToScene(event->pos());
//兩者做差
QPointF offset=end-start;
//如果做的是拖曳消除區(qū)域點的操作
if(qAbs(offset.x())>=1||qAbs(offset.y())>=1){
//獲取區(qū)域左上角和右下角的坐標(biāo)
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();
//將區(qū)域約束到圖像內(nèi)
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;
}
}
//否則做的是標(biāo)注點的操作
else{
//釋放信號交由主窗口處理
emit rightButtonClick(end.x(),end.y());
}
}
QGraphicsView::mouseReleaseEvent(event);
}
存在問題
鼠標(biāo)左鍵拖動圖像時會出現(xiàn)圖像偏移現(xiàn)象,即鼠標(biāo)指針沒有固定到圖像的某點上,但在某些情況下又是正常的,具體效果如下:
異常(拖動前后指針不在同一點)

正常(拖動前后指針在同一點)

拖動效果在mouseMoveEvent中實現(xiàn),在代碼中也有相應(yīng)注釋,歡迎各位大佬指正
上述問題已得到解決。
github地址:https://github.com/FonlinGH/MarkupTool,包含源代碼及可執(zhí)行程序
第一次寫博客,有不恰當(dāng)?shù)牡胤竭€請諒解,僅用作學(xué)習(xí)記錄。
參考文章鏈接:
win10下Qt5.12.3配置OpenCV4.5.3
qt5配置msvc2017
opencv編譯
Opencv圖像增強(qiáng)算法實現(xiàn)
OpenCV像增強(qiáng)之對數(shù)變換log
OPENCV Mat的數(shù)據(jù)類型
QGraphicsView圖形視圖框架使用(一)坐標(biāo)變換文章來源:http://www.zghlxwxcb.cn/news/detail-678252.html
QGraphicsView教程文章來源地址http://www.zghlxwxcb.cn/news/detail-678252.html
到了這里,關(guān)于QT+OpenCV實現(xiàn)一個標(biāo)注工具(圖像處理、邊緣檢測)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!