小喵的唠叨話:話說最近小喵也要開始寫論文了,想了兩周還是沒有頭緒,不知道該寫些什麼。恰好又被分配了一點標注數據的工作,於是乎想寫點代碼,休閒一下。結果也就是這篇博客。對了,小喵對GUI編程一竅不通,只知道Windows有MFC,Mac上的不知道。。。恰好聽說過QT,而且知道這個界面庫是跨平台的,也就選用了這個工具了。
本文系原創,轉載請注明出處~
小喵的博客:http://www.miaoerduo.com
博客原文:http://www.miaoerduo.com/qt/一個簡單粗暴的人臉認證標注工具的實現.html
那麼現在開始和小喵一起瞎貓似的捯饬QT吧~
先看一眼效果圖:
是不是乍一看還挺炫酷。功能上也還好,至少簡單的標注工作都能完成了。那麼讓我們來一步一步的完成這個工具吧。
這個程序主要的功能是完成一個人臉認證的標注工具。
具體來說,就是給定很多對人臉的圖片,要標注一下這一對是不是同一個人。同時,每一對的圖片的人臉一張是生活照,一張是證件照,需要同時標注出哪張是證件照,那張是生活照。照片都是經過檢測和對齊的,這個工具只需要完成簡單的顯示、標注、保存記錄的工作就可以。
當然考慮到有時候需要標注的list可能很大,可以加入跳轉的功能。標注結果都保存在內存,用戶可以隨時更改,點擊保存,則寫入硬盤。
那麼是不是現在就可以動手寫代碼了呢?當然不是!
小喵寫這個軟件一共用了3天的時間,第一天完成了一個超簡單demo程序,熟悉了一下QT的事件添加,路徑選擇和顯示圖片的幾個功能。之後又仔細的思考了一下各種數據的結構,才動手做了這一版工具。沒有一個清晰的數據的概念,會造成許多的無用功。所以,大家在寫程序的時候,要在准備階段多花一點時間來思考,畢竟寫代碼才是最簡單的事情不是嗎?
GUI程序的界面一直是個很讓人頭疼的問題,記得在本科學習Java的時候,需要自己手寫一個控件,使用new JButton()類似的方式創建按鈕,然後添加到主界面上,位置什麼的都得調用這個對象來設置,十分的繁瑣。那麼QT能不能簡化這個過程呢?答案是肯定的。
創建項目->選擇Application->Qt Widgets Application。然後項目名改成Anno Pro,其他全部默認設置,就創建好了一個項目了。這個初始的項目裡面有3個文件夾:頭文件,源文件和界面文件,以及一個.pro結尾的項目配置文件。
既然需要編輯界面,我們自然會想查看一下界面文件了,雙擊MainWindow.ui(我這裡全部都是默認的名字)。出現的是一個充滿各種控件的可視化界面編輯器。
按照我們之前的界面樣式,拖動左邊的控件,就可以完成界面的編寫了。小喵這裡只用到了幾種控件:
QPushButton:各種按鈕
QLabel:所以顯示文字和圖像的區域都是這這個控件
QFrame:一個容器,小喵用它只是為了結構上更清晰
QSlider:滑動條,小喵用的是水平滑動條
QStatusBar:狀態欄, 這應該是自帶的,如果刪掉的話,在MainWindow控件點擊右鍵就可以創建了
拖動完成後,雙擊空間,就可以給空間設置文本,同時注意給每個控件起一個好聽的名字(起名字很重要的!《代碼大全》中甚至用一章,好幾十頁的篇幅介紹如何命名)。
至於其他的控件,大家可以自行研究。反正小喵現在的道行應該才是築基。
那麼我們就愉快的完成了界面的編寫了~點擊左右下的運行圖標(三角形的那個),就可以看到自己的運行程序了!
我們先前已經分析了我們需要的數據了,這部分開始使用代碼的定義這些結構。
打開我們/*唯一的*/頭文件mianwindow.h,添加需要的變量,小喵就直接把自己的頭文件復制下來了:
1 #ifndef MAINWINDOW_H 2 #define MAINWINDOW_H 3 4 #include <QMainWindow> 5 #include <vector> 6 #include <string> 7 namespace Ui { 8 9 class MainWindow; 10 } 11 12 class MainWindow : public QMainWindow 13 { 14 enum AnnoState { 15 UNKNOWN = 0, // 未標注 16 YES = 1, // 匹配 17 NO = 2, // 不匹配 18 UNSURE = 3 // 不確定 19 }; 20 21 public: 22 explicit MainWindow(QWidget *parent = 0); 23 ~MainWindow(); 24 25 private: 26 Ui::MainWindow *ui; // 自帶的,ui界面的接口 27 std::vector<std::string> image_list_1; // 用來存放左邊的圖片的list 28 std::vector<std::string> image_list_2; // 用來存放右邊的圖片的list 29 int current_idx; // 當前圖片對的id 30 int total_pair_num; // 總共的圖片對的數目 31 std::vector< AnnoState > annotation_list; // 標注的結果 32 }; 33 34 #endif // MAINWINDOW_H
可以看出,小喵添加了一個enum的類型,用來表示標注結果的類型。雖然只有4個狀態,我們甚至可以直接約定幾個int值來表示,但相信我,為這麼4個狀態定義一個枚舉類型是完全有必要的。
之後我們所有的成員變量都是private的。具體含義,注釋中也有寫明。
下一步就是初始化了。初始化的過程當然得寫在構造函數裡,這裡,小喵在初始化的時候強迫用戶選擇一個標注的list,如果不這麼做,會有很多的意外情況。請原諒小喵的怠惰。。。
1 MainWindow::MainWindow(QWidget *parent) : 2 QMainWindow(parent), 3 ui(new Ui::MainWindow) 4 { 5 ui->setupUi(this); 6 7 // 選擇輸入文件 8 while (1) { 9 QString file_name = QFileDialog::getOpenFileName(this, "choose a file to annotate", "."); 10 if (file_name.isEmpty()) { 11 int ok = QMessageBox::information(this, "choose a file to annotate", "Don't want to work now?", QMessageBox::Ok | QMessageBox::Cancel); 12 if (ok == QMessageBox::Ok) { 13 exit(0); 14 } 15 continue; 16 } 17 std::ifstream is(file_name.toStdString()); 18 std::string image_name; 19 bool is_odd = true; 20 while (is >> image_name) { 21 if (is_odd) { 22 this->image_list_1.push_back(image_name); 23 } else { 24 this->image_list_2.push_back(image_name); 25 } 26 is_odd = !is_odd; 27 } 28 is.close(); 29 30 if (image_list_1.size() != image_list_2.size()) { 31 QMessageBox::information(this, "choose a file to annotate", "this image list is not even", QMessageBox::Ok); 32 continue; 33 } 34 if (0 == image_list_1.size()) { 35 QMessageBox::information(this, "choose a file to annotate", "this image list is empty", QMessageBox::Ok); 36 continue; 37 } 38 break; 39 } 40 41 assert(image_list_1.size() == image_list_2.size()); 42 // 初始化其他參數 43 this->total_pair_num = image_list_1.size(); 44 this->current_idx = 0; 45 std::vector<AnnoState> annotation_list(this->total_pair_num, AnnoState::UNKNOWN); 46 this->annotation_list.swap(annotation_list); 47 48 display(); 49 }
這裡用了兩個QT的組件:
QFileDialog:這個組件是一個文件對話框,其中有兩個十分有用的函數:getOpenFileName用於選擇一個文件,並返回文件名;getSaveFileName用於選擇一個文件來保存數據,並返回一個文件名。這兩個函數的參數很多,小喵只用到了前面的3個,用到的參數依次是:父組件,標題,初始目錄。其他的參數的功能,喵粉可以去官網查一下。
QMessageBox::information,這個函數的功能是顯示一個消息窗口。四個參數分別表示:父組件,標題,內容,按鈕樣式。
相信大家懂一點點C++的知識的話,很容易看懂這段代碼。
這裡就是使用了一個循環,讓用戶選擇文件,如果選擇成功了,則讀取數據到我們的list中,最終初始化了其他的參數,在調用display函數來顯示。這個display函數是我們自己編寫的,後面會說到。另外,assert函數是斷言,他保證了斷言的數據的合法性,如果不合法,程序會退出。想使用這個函數,需要包含頭文件assert.h。
小喵之前了解到,QT使用的是一種信號和槽的事件機制,是一種十分高級的機制。那麼有沒有什麼簡單的方法,為我們的每個控件綁定自己的的事件呢?
在界面編輯界面下,右擊需要添加事件的空間,然後選擇轉到槽。這時候會有很多選項,這裡直接選擇clicked就可以。然後你會發現我們的mainwindow類中,多了一個pivate slot的函數(也就是槽函數)。
我們可以給每一個需要添加事件的函數都用這種方式來綁定事件,最終頭文件中會出現這樣的聲明(函數名稱的規則是:on_控件名_信號類型):
1 private slots: 2 void on_pushButton_save_clicked(); 3 void on_pushButton_ok_clicked(); 4 void on_pushButton_no_clicked(); 5 void on_pushButton_unsure_clicked(); 6 void on_pushButton_next_clicked(); 7 void on_pushButton_prev_clicked(); 8 void on_pushButton_switch_clicked(); 9 void on_horizontalSlider_progress_sliderReleased();
在源文件中,也會生成空的函數定義。我們只需要自己完成函數定義就大功告成!
下面給出的是除了save的所有的函數的定義。
主要工作是,給每個事件編寫修改數據的代碼,而不去負責任何界面相關的部分。各個控件可以通過this->ui來設置和獲取。使用Qt Creator的時候,要充分利用智能提示。
1 /** 2 * @brief MainWindow::on_pushButton_ok_clicked 3 * 標注為"匹配" 4 */ 5 void MainWindow::on_pushButton_ok_clicked() 6 { 7 this->annotation_list[this->current_idx] = MainWindow::AnnoState::YES; 8 ++ this->current_idx; 9 display(); 10 } 11 12 /** 13 * @brief MainWindow::on_pushButton_no_clicked 14 * 標注為"不匹配" 15 */ 16 void MainWindow::on_pushButton_no_clicked() 17 { 18 this->annotation_list[this->current_idx] = MainWindow::AnnoState::NO; 19 ++ this->current_idx; 20 display(); 21 } 22 23 /** 24 * @brief MainWindow::on_pushButton_unsure_clicked 25 * 標注為"不確定" 26 */ 27 void MainWindow::on_pushButton_unsure_clicked() 28 { 29 this->annotation_list[this->current_idx] = MainWindow::AnnoState::UNSURE; 30 ++ this->current_idx; 31 display(); 32 } 33 34 /** 35 * @brief MainWindow::on_pushButton_next_clicked 36 * 移動到下一組 37 */ 38 void MainWindow::on_pushButton_next_clicked() 39 { 40 ++ this->current_idx; 41 display(); 42 } 43 44 /** 45 * @brief MainWindow::on_pushButton_prev_clicked 46 * 移動到上一組 47 */ 48 void MainWindow::on_pushButton_prev_clicked() 49 { 50 -- this->current_idx; 51 display(); 52 } 53 54 /** 55 * @brief MainWindow::on_pushButton_switch_clicked 56 * 交換兩邊的圖片 57 */ 58 void MainWindow::on_pushButton_switch_clicked() 59 { 60 std::string tmp = this->image_list_1[this->current_idx]; 61 this->image_list_1[this->current_idx] = this->image_list_2[this->current_idx]; 62 this->image_list_2[this->current_idx] = tmp; 63 display(); 64 } 65 66 /** 67 * @brief MainWindow::on_horizontalSlider_progress_sliderReleased 68 * 拖放進度條,控制進度 69 */ 70 void MainWindow::on_horizontalSlider_progress_sliderReleased() 71 { 72 int pos = this->ui->horizontalSlider_progress->value(); 73 this->current_idx = pos; 74 this->display(); 75 }
至此,我們的大體的功能邏輯就編寫完了。
那麼怎麼讓界面上顯示我們的系統狀態呢?注意到了我們上面的每一個函數都調用了display這個函數了嗎?這個函數正式負責繪制界面的功能。
部分主要介紹三個函數:
1 const std::string UNSURE_FILE = ":File/images/unsure.png"; 2 const std::string YES_FILE = ":File/images/yes.gif"; 3 const std::string NO_FILE = ":File/images/no.gif"; 4 const std::string UNKNOWN_FILE = ":File/images/unknown.png"; 5 6 /** 7 * @brief set_image 將圖像設置到label上,圖像自動根據label的大小來縮放 8 * @param label 9 * @param image 10 */ 11 void set_image(QLabel *label, const QPixmap &image) { 12 float ratio(0.); 13 ratio = 1. * label->width() / image.width(); 14 ratio = fmin( 1. * label->height() / image.height(), ratio ); 15 QPixmap m = image.scaled(static_cast<int>(image.width() * ratio), static_cast<int>(image.height() * ratio)); 16 label->setPixmap(m); 17 } 18 19 void set_image(QLabel *label, const std::string image_path) { 20 QPixmap image(image_path.c_str()); 21 set_image(label, image); 22 } 23 24 /** 25 * @brief MainWindow::display \n 26 * 根據系統中的所有的變量來設置當前界面中的各個部分的內容 27 */ 28 void MainWindow::display() { 29 30 if (this->current_idx >= this->total_pair_num) { 31 QMessageBox::information(this, "annotation over", "Congratulations! You've finished all the job! Please save your work :)", QMessageBox::Ok); 32 this->current_idx = this->total_pair_num - 1; 33 } 34 if (this->current_idx < 0) { 35 QMessageBox::information(this, "annotation warning", "You must start at 0 (not a negative position, I konw you wanna challenge this app) :)", QMessageBox::Ok); 36 this->current_idx = 0; 37 } 38 39 // 進度條 40 this->ui->horizontalSlider_progress->setRange(0, this->total_pair_num - 1); 41 this->ui->horizontalSlider_progress->setValue(this->current_idx); 42 43 // 狀態欄 44 this->ui->statusBar->showMessage(QString((std::to_string(this->current_idx + 1) + " / " + std::to_string(this->total_pair_num)).c_str())); 45 46 // 文件名 47 std::string image_name_1 = this->image_list_1[this->current_idx]; 48 std::string image_base_name_1 = image_name_1.substr(image_name_1.find_last_of("/") + 1); 49 std::string image_name_2 = this->image_list_2[this->current_idx]; 50 std::string image_base_name_2 = image_name_2.substr(image_name_2.find_last_of("/") + 1); 51 this->ui->label_image_name_1->setText(image_base_name_1.c_str()); 52 this->ui->label_image_name_2->setText(image_base_name_2.c_str()); 53 54 // 顯示圖像 55 set_image(this->ui->label_image_view_1, image_name_1); 56 set_image(this->ui->label_image_view_2, image_name_2); 57 58 // 顯示標注結果 59 std::string show_image_name = UNKNOWN_FILE; 60 switch (this->annotation_list[this->current_idx]) { 61 case AnnoState::UNKNOWN: 62 show_image_name = UNKNOWN_FILE; 63 break; 64 case AnnoState::YES: 65 show_image_name = YES_FILE; 66 break; 67 case AnnoState::NO: 68 show_image_name = NO_FILE; 69 break; 70 case AnnoState::UNSURE: 71 show_image_name = UNSURE_FILE; 72 break; 73 } 74 set_image(this->ui->label_image_compare_status, show_image_name); 75 76 }
最開始我們定義了4個圖片的路徑。這可以是絕對路徑或者相對路徑。我們這裡的路徑設置的比較奇怪,在下面我們會講到。
set_image負責將給定的圖片繪制到QLabel上,為了顯示的好看,圖像會按照QLabel的尺寸來動態的縮放。這樣就不會出現有個圖像太大或太小的情況了。
display則是負責各個區域的繪制。
還差一步是保存結果:
1 /* 2 * @brief MainWindow::on_pushButton_save_clicked \n 3 * 保存結果文件 4 */ 5 void MainWindow::on_pushButton_save_clicked() 6 { 7 QString file_name = QFileDialog::getSaveFileName(this, "choose a file to save", "."); 8 if (file_name.isEmpty()) { 9 QMessageBox::information(this, "choose a file to save", "please enter a legal file name", QMessageBox::Ok); 10 return; 11 } 12 std::ofstream os(file_name.toStdString()); 13 for (int idx = 0; idx < static_cast<int>(this->annotation_list.size()); ++ idx) { 14 os << this->image_list_1[idx] << " " << this->image_list_2[idx] << " " << this->annotation_list[idx] << "\n"; 15 } 16 os.close(); 17 QMessageBox::information(this, "save", "save result success", QMessageBox::Ok); 18 }
由於我們的程序是需要publish出去的,因此圖片文件等資源,必須包含在程序中。那麼Qt怎麼添加文件資源呢?
在項目視圖下,右鍵項目->添加新文件->Qt->Qt Resource File。就可以創建一個qrc文件了。
我這裡給這個文件取名為image。
之後,建議在項目的根目錄裡面新建一個文件夾,用來存放資源。小喵的結構是這個樣子的:
小喵的項目根目錄新建了一個文件夾images,並將圖像素材放入了這個文件夾。
之後回到QT,