一個使用QT編寫的簡單人臉認證标注工具。這是标注工具!沒有人臉認證的算法。
小喵的唠叨話:話說最近小喵也要開始寫論文了,想了兩周還是沒有頭緒,不知道該寫些什麼。恰好又被配置設定了一點标注資料的工作,于是乎想寫點代碼,休閑一下。結果也就是這篇部落格。對了,小喵對GUI程式設計一竅不通,隻知道Windows有MFC,Mac上的不知道。。。恰好聽說過QT,而且知道這個界面庫是跨平台的,也就選用了這個工具了。
本文系原創,轉載請注明出處~
小喵的部落格:http://www.miaoerduo.com
部落格原文:http://www.miaoerduo.com/qt/一個簡單粗暴的人臉認證标注工具的實作.html
那麼現在開始和小喵一起瞎貓似的捯饬QT吧~
先看一眼效果圖:
是不是乍一看還挺炫酷。功能上也還好,至少簡單的标注工作都能完成了。那麼讓我們來一步一步的完成這個工具吧。
一、功能需求
這個程式主要的功能是完成一個人臉認證的标注工具。
具體來說,就是給定很多對人臉的圖檔,要标注一下這一對是不是同一個人。同時,每一對的圖檔的人臉一張是生活照,一張是證件照,需要同時标注出哪張是證件照,那張是生活照。照片都是經過檢測和對齊的,這個工具隻需要完成簡單的顯示、标注、儲存記錄的工作就可以。
當然考慮到有時候需要标注的list可能很大,可以加入跳轉的功能。标注結果都儲存在記憶體,使用者可以随時更改,點選儲存,則寫入硬碟。
二、資料結構
那麼是不是現在就可以動手寫代碼了呢?當然不是!
小喵寫這個軟體一共用了3天的時間,第一天完成了一個超簡單demo程式,熟悉了一下QT的事件添加,路徑選擇和顯示圖檔的幾個功能。之後又仔細的思考了一下各種資料的結構,才動手做了這一版工具。沒有一個清晰的資料的概念,會造成許多的無用功。是以,大家在寫程式的時候,要在準備階段多花一點時間來思考,畢竟寫代碼才是最簡單的事情不是嗎?
- 輸入資料格式:因為小喵的工作環境下,大家都對linux有一些了解,是以可以自行生成好圖檔的路徑的list,這裡統一要求,list必須是偶數行(2n行),代表n對,相鄰的圖檔為一對。
- 标注資料存儲:考慮到我們不僅需要标注是不是一對,還得标注哪張是證件照,是以不妨直接在讀資料的時候就分成兩份,這樣就用兩個std::vector<std::string>來存儲就行了。
- 标注過程的狀态:我們需要知道标注過程中的那些資訊呢?主要應該有:總資料量,目前已标注的對數。
- 标注結果:每一對都有一組對應地 結果,考慮到有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,