程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 一個簡單粗暴的人臉認證標注工具的實現,人臉標注

一個簡單粗暴的人臉認證標注工具的實現,人臉標注

編輯:C++入門知識

一個簡單粗暴的人臉認證標注工具的實現,人臉標注


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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved