小喵的唠叨話:話說最近小喵也要開始寫論文了,想了兩周還是沒有頭緒,不知道該寫些什麼。恰好又被分配了一點標注數據的工作,於是乎想寫點代碼,休閒一下。結果也就是這篇博客。對了,小喵對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,