天天看點

一個簡單粗暴的人臉認證标注工具的實作

一個使用QT編寫的簡單人臉認證标注工具。這是标注工具!沒有人臉認證的算法。

小喵的唠叨話:話說最近小喵也要開始寫論文了,想了兩周還是沒有頭緒,不知道該寫些什麼。恰好又被配置設定了一點标注資料的工作,于是乎想寫點代碼,休閑一下。結果也就是這篇部落格。對了,小喵對GUI程式設計一竅不通,隻知道Windows有MFC,Mac上的不知道。。。恰好聽說過QT,而且知道這個界面庫是跨平台的,也就選用了這個工具了。

本文系原創,轉載請注明出處~

小喵的部落格:http://www.miaoerduo.com

部落格原文:http://www.miaoerduo.com/qt/一個簡單粗暴的人臉認證标注工具的實作.html

那麼現在開始和小喵一起瞎貓似的捯饬QT吧~

先看一眼效果圖:

是不是乍一看還挺炫酷。功能上也還好,至少簡單的标注工作都能完成了。那麼讓我們來一步一步的完成這個工具吧。

一、功能需求

這個程式主要的功能是完成一個人臉認證的标注工具。

具體來說,就是給定很多對人臉的圖檔,要标注一下這一對是不是同一個人。同時,每一對的圖檔的人臉一張是生活照,一張是證件照,需要同時标注出哪張是證件照,那張是生活照。照片都是經過檢測和對齊的,這個工具隻需要完成簡單的顯示、标注、儲存記錄的工作就可以。

當然考慮到有時候需要标注的list可能很大,可以加入跳轉的功能。标注結果都儲存在記憶體,使用者可以随時更改,點選儲存,則寫入硬碟。

二、資料結構

那麼是不是現在就可以動手寫代碼了呢?當然不是!

小喵寫這個軟體一共用了3天的時間,第一天完成了一個超簡單demo程式,熟悉了一下QT的事件添加,路徑選擇和顯示圖檔的幾個功能。之後又仔細的思考了一下各種資料的結構,才動手做了這一版工具。沒有一個清晰的資料的概念,會造成許多的無用功。是以,大家在寫程式的時候,要在準備階段多花一點時間來思考,畢竟寫代碼才是最簡單的事情不是嗎?

  1. 輸入資料格式:因為小喵的工作環境下,大家都對linux有一些了解,是以可以自行生成好圖檔的路徑的list,這裡統一要求,list必須是偶數行(2n行),代表n對,相鄰的圖檔為一對。
  2. 标注資料存儲:考慮到我們不僅需要标注是不是一對,還得标注哪張是證件照,是以不妨直接在讀資料的時候就分成兩份,這樣就用兩個std::vector<std::string>來存儲就行了。
  3. 标注過程的狀态:我們需要知道标注過程中的那些資訊呢?主要應該有:總資料量,目前已标注的對數。
  4. 标注結果:每一對都有一組對應地 結果,考慮到有4中情況:未标注,不确定,不比對,比對這四種,我們定義一個枚舉的狀态表enum AnnoState就好。之後用一個std::vector<enum AnnoState>來存儲标注結果。

三、界面制作

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,