SeetaFace 入門教程
0. 目錄
1. 前言
該文檔通過leanote
軟件編輯,可以去博客獲取更好的閱讀體驗,傳送門。
SeetaFace6
是中科視拓技術開發體系最新的版本,該版本為開放版,免費供大家使用。該版本包含了人臉識別、活體檢測、屬性識別、質量評估模塊。
本文檔基於SeetaFace
進行講解,同時也會給出一些通用的算法使用的結論和建議,希望能夠給使用SeetaFace
或者使用其他人臉識別產品的讀者提供幫助。
本文檔不會提及如何進行代碼編譯和調試,會對一些關鍵的可能產生問題的部分進行提醒,開發者可以根據提醒查看是否和本身遇到的問題相符合。
本文檔適用於有一定C++基礎,能夠看懂核心代碼片段,並且將代碼完成並在對應平台上編譯出可執行程序。
其他的預備知識包含OpenCV
庫的基本使用,和一些基本的圖像處理的了解。以上都是一個接口的調用,沒有使用過也不影響對文檔的理解。本書中使用的OpenCV
庫為2
或3
版本。
如果您是對C++語言本身存在疑問,而對於庫調用已經有經驗的話,本文檔可能不能直接幫助您。建議先將C++語言作為本文檔的前置課程進行學習。同時本文檔也會盡可能照顧剛剛接觸C++語言的讀者。
為了避免過長的代碼段影響大家的閱讀,因此會把一些必須展示的代碼塊當道附錄
的代碼塊
節。通過這些完整的代碼塊以便讀者可以只根據文檔陳列的內容就可以理解SeetaFace
的基本設定和原理。當然不深入這些繁雜的代碼塊,也不會完全影響理解和使用。
為了表達的簡潔,文檔中出現的代碼塊
在保留了語義一致性的基礎上,還可能經過了刪減和調整。實際實現部分的代碼,以最終下載的代碼為准。
為了照顧不同層次的理解,會對一些基本概念作出解釋,一些已經在圖像處理或者AI領域的開發者對概念熟悉的讀者可以略過相關章節,當理解有偏差時再詳細閱讀。
因為文檔中出現了很多代碼,必然要使用英文單詞。在文檔解釋中使用的是中文關鍵字。限於作者的能力,可能沒有辦法對文檔中出現的所有英文關鍵字和中文關鍵字做出一一解釋對應。好在代碼中使用的英文單詞為常用單詞,在不影響理解的前提下,其直譯的中文含義就是對應上下文中的中文關鍵字。
寫在最后,閱讀本文檔之前最好先處理好編譯的環境,遇到問題直接編譯運行。
2. 基本概念
2.1 圖像
2.1.1 結構定義
作為圖像處理的C++庫,圖像存儲是一個基本數據結構。
接口中圖像對象為SeetaImageData
。
struct SeetaImageData
{
int width; // 圖像寬度
int height; // 圖像高度
int channels; // 圖像通道
unsigned char *data; // 圖像數據
};
這里要說明的是data
的存儲格式,其存儲的是連續存放的試用8位無符號整數表示的像素值,存儲為[height, width, channels]
順序。彩色圖像時三通道以BGR
通道排列。
如下圖所示,就是展示了高4寬3的彩色圖像內存格式。
該存儲在內存中是連續存儲。因此,上圖中表示的圖像data
的存儲空間為4*3*3=36
bytes
。
提示:
BGR
是OpenCV
默認的圖像格式。在大家很多時候看到就是直接將cv::Mat
的data
直接賦值給SeetaImageData
的data
。
data
的數據類型為uint8
,數值范圍為[0, 255]
,表示了對應的灰度值由最暗到最亮。在一些用浮點數 float
表示灰度值的平台上,該范圍映射到為[0, 1]。
這里詳細描述格式是為了對接不同應用場景下的圖像庫的圖像表述格式,這里注意的兩點,1. 數據類型是uint8
表示的[0, 255]
范圍;2. 顏色通道是BGR
格式,而很多圖像庫常見的是RGB
或者RGBA
(A
為alpha
不透明度)。
提示:顏色通道會影響識別算法的精度,在不同庫的轉換時需要小心注意。
這種純C接口不利於一些資源管理的情況。SeetaFace 提供給了對應SeetaImageData
的封裝seeta::ImageData
和seeta::cv::ImageData
。
2.1.2 使用示例
以下就是使用OpenCV
加載圖像后轉換為SeetaImageData
的操作。
cv::Mat cvimage = cv::imread("1.jpg", cv::IMREAD_COLOR);
SeetaImageData simage;
simage.width = cvimage.cols;
simage.height = cvimage.rows;
simage.channels = cvimage.channels();
simage.data = cvimage.data;
注意:
simage.data
拿到的是“借來的”臨時數據,在試用的期間,必須保證cvimage
的對象不被釋放,或者形式的更新。
注意:原始圖像可能是空的,為了輸入的合法性,需要通過cvimage.empty()
方法,判斷圖像是否為空。
這里通過cv::imread
並且第二個參數設置為cv::IMREAD_COLOR
,獲取到的cv::Mat
的圖像數據是連續存儲的,這個是使用SeetaImageData
必須的。如果不確定是否是連續存儲的對象,可以調用下述代碼段進行轉換。
if (!cvimage.isContinuous()) cvimage = cvimage.clone();
當然,根據cv::Mat
和SeetaImageData
,對象的轉換可以逆向。
cv::Mat another_cvimage = cv::Mat(simage.height, simage.width, CV_8UC(simage.channels), simage.data);
seeta::ImageData
和seeta::cv::ImageData
也是基於這種基本的類型定義進行的封裝,並加進了對象聲明周期的管理。
這里展示了一種封裝:
namespace seeta
{
namespace cv
{
// using namespace ::cv;
class ImageData : public SeetaImageData {
public:
ImageData( const ::cv::Mat &mat )
: cv_mat( mat.clone() ) {
this->width = cv_mat.cols;
this->height = cv_mat.rows;
this->channels = cv_mat.channels();
this->data = cv_mat.data;
}
private:
::cv::Mat cv_mat;
};
}
}
這樣SeetaImageData
使用代碼就可以簡化為:
seeta::cv::ImageData = cv::imread("1.jpg");
因為seeta::cv::ImageData
繼承了SeetaImageData
,因此在需要傳入const SeetaImageData &
類型的地方,可以直接傳入seeta::cv::ImageData
對象。
注意:在沒有特殊說明的情況下,
SeetaFace
各個模塊的輸入圖像都必須是BGR
通道格式的彩色圖像。參考:
CStruct.h
、Struct.h
和Struct_cv.h
。
1.2 坐標系
1.2.1 結構定義
我們在表示完圖像之后,我們還需要在圖像上表示一些幾何圖形,例如矩形和點,這兩者分別用來表示人臉的位置和人臉關鍵點的坐標。
這我們采用的屏幕坐標系,也就是以左上角為坐標原點,屏幕水平向右方向為橫軸,屏幕垂直向下方向為縱軸。坐標以像素為單位。
坐標值會出現負值,並且可以為實數,分別表示超出屏幕的部分,和位於像素中間的坐標點。
有了以上的基本說明,我們就可以給出矩形、坐標點或其他的定義。
/**
* 矩形
*/
struct SeetaRect
{
int x; // 左上角點橫坐標
int y; // 左上角點縱坐標
int width; // 矩形寬度
int height; // 矩形高度
};
/**
*寬高大小
*/
struct SeetaSize
{
int width; // 寬度
int height; // 高度
};
/**
* 點坐標
*/
struct SeetaPointF
{
double x; // 橫坐標
double y; // 縱坐標
};
注意:從表達習慣上,坐標和形狀大小的表示,通常是橫坐標和寬度在前,這個和圖片表示的時候內存坐標是高度在前的表示有差異,在做坐標變換到內存偏移量的時候需要注意。
1.2.2 使用示例
這種基本數據類型的定義,在拿到以后就確定的使用方式,這里距離說明,如果拿到這種圖片,如何繪制到圖片上。
首先我們假定圖像、矩形和點的定義。
cv::Mat canvas = cv::imread("1.jpg");
SeetaRect rect = {20, 30, 40, 50};
SeetaPointF point = {40, 55};
當變量已經具有了合法含義的時候,就可以進行對應繪制。
// 繪制矩形
cv::rectangle(canvas, cv::Rect(rect.x, rect.y, rect.width, rect.height), CV_RGB(128, 128, 255), 3);
// 繪制點
cv::circle(canvas, cv::Point(point.x, point.y), 2, CV_RGB(128, 255, 128), -1);
參考:
CStruct.h
和Struct.h
。
1.3 模型設置
在算法開發包中,除了代碼庫本身以外,還有數據文件,我們通常稱之為模型。
這里給出SeetaFace
的模型設置的基本定義:
enum SeetaDevice
{
SEETA_DEVICE_AUTO = 0,
SEETA_DEVICE_CPU = 1,
SEETA_DEVICE_GPU = 2,
};
struct SeetaModelSetting
{
enum SeetaDevice device;
int id; // when device is GPU, id means GPU id
const char **model; // model string terminate with nullptr
};
這里要說明的是model
成員,是一種C風格的數組,以NULL
為結束符。一種典型的初始化為,設定模型為一個fr_model.csta
,運行在cpu:0
上。
SeetaModelSetting setting;
setting.device = SEETA_DEVICE_CPU;
setting.id = 0;
setting.model = {"fr_model.csta", NULL};
當然SeetaFace
也對應封裝了C++
版本設置方式,繼承關系為,詳細的實現見附錄
中的代碼塊
:
class seeta::ModelSetting : SeetaModelSetting;
實現代碼見Struct.h
,使用方式就變成了如下代碼段,默認在cpu:0
上運行。
seeta::ModelSetting setting;
setting.append("fr_model.csta");
有了通用模型的設置方式,一般實現時的表述就會是加載一個人臉識別模型
,模型加載的方式都是構造SeetaModelSetting
,傳入對應的算法對象。
參考:
CStruct.h
和Struct.h
。
1.4 對象生命周期
對象生命周期是C++中一個非常重要的概念,也是RAII
方法的基本依據。這里不會過多的解釋C++語言本身的特性,當然文檔的篇幅也不允許做一個完整闡述。
以人臉識別器seeta::Recognizer
為例,假設模型設置如1.3
節展示,那么構造識別器就有兩種方式:
seeta::FaceRecognizer FR(setting);
或者
seeta::FaceRecognizer *pFD = new seeta::FaceRecognizer(setting);
當然后者在pFD
不需要使用之后需要調用delete
進行資源的釋放
delete pFD;
前者方式構造FD
對象,當離開FD所在的代碼塊后(一般是最近的右花括號)后就會釋放。
上述基本的概念相信讀者能夠很簡單的理解。下面說明幾點SeetaFace
與對象聲明周期相關的特性。
1.
識別器對象不可以被復制或賦值。
seeta::FaceRecognizer FR1(setting);
seeta::FaceRecognizer FR2(setting);
seeta::FaceRecognizer FR3 = FR1; // 錯誤
FF2 = FR1; // 錯誤
因此在函數調用的時候,以值傳遞識別器對象是不被允許的。如果需要對象傳遞,需要使用對象指針。
這個特性是因為為了不暴露底層細節,保證接口的ABI兼容性,采用了impl
指針隔離實現的方式。
2.
借用
指針對象不需要釋放。
例如,人臉檢測器的檢測接口,接口聲明為:
struct SeetaFaceInfo
{
SeetaRect pos;
float score;
};
struct SeetaFaceInfoArray
{
struct SeetaFaceInfo *data;
int size;
};
SeetaFaceInfoArray FaceDetector::detect(const SeetaImageData &image) const;
其中SeetaFaceInfoArray
的data
成員就是借用
對象,不需要外部釋放。
沒有強調聲明的接口,就不需要對指針進行釋放。當然,純C接口中返回的對象指針是新對象
,所以需要對應的對象釋放函數。
3.
對象的構造和釋放會比較耗時,建議使用對象池在需要頻繁使用新對象的場景進行對象復用。
1.5 線程安全性
線程安全也是開發中需要重點關注的特性。然而,線程安全在不同的上下文解釋中總會有不同解釋。為了避免理解的偏差,這里用幾種不同的用例去解釋識別器的使用。
1.
對象可以跨線程傳遞。線程1構造的識別器,可以在線程2中調用。 2.
對象的構造可以並發構造
,即可以多個線程同時構造識別器。 3.
單個對象的接口調用不可以並發調用,即單個對象,在多個線程同時使用是被禁止的。
當然一些特殊的對象會具有更高級別的線程安全級別,例如seeta::FaceDatabase
的接口調用就可以並發調用,但是計算不會並行。
2. 人臉檢測和關鍵點定位
終於經過繁雜的基礎特性說明之后,迎來了兩個重要的識別器模塊。人臉檢測和關鍵點定位。
人臉檢測
, seeta::FaceDetector
就是輸入待檢測的圖片,輸出檢測到的每個人臉位置,用矩形表示。 關鍵點定位
,seeta::FaceLandmarker
就是輸入待檢測的圖片,和待檢測的人臉位置,輸出N
個關鍵點的坐標(圖片內)。
兩個模塊分別負責找出可以處理的人臉位置,檢測出關鍵點用於標定人臉的狀態,方便后續的人臉對齊后進行對應識別分析。
2.1 人臉檢測器
人臉檢測器的效果如圖所示:
這里給出人臉檢測器的主要接口:
namespace seeta {
class FaceDetector {
FaceDetector(const SeetaModelSetting &setting);
SeetaFaceInfoArray detect(const SeetaImageData &image) const;
std::vector<SeetaFaceInfo> detect_v2(const SeetaImageData &image) const;
void set(Property property, double value);
double get(Property property) const;
}
}
構造一個檢測器的函數參考如下:
#include <seeta/FaceDetector.h>
seeta::FaceDetector *new_fd() {
seeta::ModelSetting setting;
setting.append("face_detector.csta");
return new seeta::FaceDetector(setting);
}
有了檢測器,我們就可以對圖片檢測人臉,檢測圖片中所有人臉並打印坐標的函數參考如下:
#include <seeta/FaceDetector.h>
void detect(seeta::FaceDetector *fd, const SeetaImageData &image) {
std::vector<SeetaFaceInfo> faces = fd->detect_v2(image);
for (auto &face : faces) {
SeetaRect rect = face.pos;
std::cout << "[" << rect.x << ", " << rect.y << ", "
<< rect.width << ", " << rect.height << "]: "
<< face.score << std::endl;
}
}
這里要說明的是,一般檢測返回的所有人臉是按照置信度排序的,當應用需要獲取最大的人臉時,可以對檢測結果進行一個部分排序
獲取出最大的人臉,如下代碼排序完成后,faces[0]
就是最大人臉的位置。
std::partial_sort(faces.begin(), faces.begin() + 1, faces.end(), [](SeetaFaceInfo a, SeetaFaceInfo b) {
return a.pos.width > b.pos.width;
});
人臉檢測器可以設置一些參數,通過set
方法。可以設置的屬性有:
seeta::FaceDetector::PROPERTY_MIN_FACE_SIZE 最小人臉
seeta::FaceDetector::PROPERTY_THRESHOLD 檢測器閾值
seeta::FaceDetector::PROPERTY_MAX_IMAGE_WIDTH 可檢測的圖像最大寬度
seeta::FaceDetector::PROPERTY_MAX_IMAGE_HEIGHT 可檢測的圖像最大高度
最小人臉
是人臉檢測器常用的一個概念,默認值為20
,單位像素。它表示了在一個輸入圖片上可以檢測到的最小人臉尺度,注意這個尺度並非嚴格的像素值,例如設置最小人臉80,檢測到了寬度為75的人臉是正常的,這個值是給出檢測能力的下限。
最小人臉
和檢測器性能息息相關。主要方面是速度,使用建議上,我們建議在應用范圍內,這個值設定的越大越好。SeetaFace
采用的是BindingBox Regresion
的方式訓練的檢測器。如果最小人臉
參數設置為80的話,從檢測能力上,可以將原圖縮小的原來的1/4,這樣從計算復雜度上,能夠比最小人臉設置為20時,提速到16倍。
檢測器閾值
默認值是0.9,合理范圍為[0, 1]
。這個值一般不進行調整,除了用來處理一些極端情況。這個值設置的越小,漏檢的概率越小,同時誤檢的概率會提高;
可檢測的圖像最大寬度
和可檢測的圖像最大高度
是相關的設置,默認值都是2000
。最大高度和寬度,是算法實際檢測的高度。檢測器是支持動態輸入的,但是輸入圖像越大,計算所使用的內存越大、計算時間越長。如果不加以限制,一個超高分辨率的圖片會輕易的把內存撐爆。這里的限制就是,當輸入圖片的寬或者高超過限度之后,會自動將圖片縮小到限制的分辨率之內。
我們當然希望,一個算法在各種場景下都能夠很好的運行,但是自然規律遠遠不是一個幾兆的文件就是能夠完全解釋的。應用上總會需要取舍,也就是trade-off
。
2.2 人臉關鍵點定位器
關鍵點定位器的效果如圖所示:
關鍵定定位輸入的是原始圖片和人臉檢測結果,給出指定人臉上的關鍵點的依次坐標。
這里檢測到的5點坐標循序依次為,左眼中心、右眼中心、鼻尖、左嘴角和右嘴角。
注意這里的左右是基於圖片內容的左右,並不是圖片中人的左右,即左眼中心就是圖片中左邊的眼睛的中心。
同樣的方式,我們也可以構造關鍵點定位器:
#include <seeta/FaceLandmarker.h>
seeta::FaceLandmarker *new_fl() {
seeta::ModelSetting setting;
setting.append("face_landmarker_pts5.csta");
return new seeta::FaceLandmarker(setting);
}
根據人臉檢測關鍵點,並將坐標打印出來的代碼如下:
#include <seeta/FaceLandmarker.h>
void mark(seeta::FaceLandmarker *fl, const SeetaImageData &image, const SeetaRect &face) {
std::vector<SeetaPointF> points = fl->mark(image, face);
for (auto &point : points) {
std::cout << "[" << point.x << ", " << point.y << "]" << std::endl;
}
}
當然開放版也會有多點的模型放出來,限於篇幅不再對點的位置做過多的文字描述。
例如face_landmarker_pts68.csta
就是68個關鍵點檢測的模型。其坐標位置可以通過逐個打印出來進行區分。
這里需要強調說明一下,這里的關鍵點是指人臉上的關鍵位置的坐標,在一些表述中也將關鍵點
稱之為特征點
,但是這個和人臉識別中提取的特征概念沒有任何相關性。並不存在結論,關鍵點定位越多,人臉識別精度越高。
一般的關鍵點定位和其他的基於人臉的分析是基於5點定位的。而且算法流程確定下來之后,只能使用5點定位。5點定位是后續算法的先驗,並不能直接替換。從經驗上來說,5點定位已經足夠處理人臉識別或其他相關分析的精度需求,單純增加關鍵點個數,只是增加方法的復雜度,並不對最終結果產生直接影響。
參考:
seeta/FaceDetector.h
seeta/FaceLandmarker.h
3. 人臉特征提取和對比
這兩個重要的功能都是seeta::FaceRecognizer
模塊提供的基本功能。特征提取方式和對比是對應的。
這是人臉識別的一個基本概念,就是將待識別的人臉經過處理變成二進制數據的特征,然后基於特征表示的人臉進行相似度計算,最終與相似度閾值
對比,一般超過閾值就認為特征表示的人臉是同一個人。
這里SeetaFace
的特征都是float
數組,特征對比方式是向量內積。
3.1 人臉特征提取
首先可以構造人臉識別器以備用:
#include <seeta/FaceRecognizer.h>
seeta::FaceRecognizer *new_fr() {
seeta::ModelSetting setting;
setting.append("face_recognizer.csta");
return new seeta::FaceRecognizer(setting);
}
特征提取過程可以分為兩個步驟:1. 根據人臉5個關鍵點裁剪出人臉區域
;2. 將人臉區域
輸入特征提取網絡提取特征。
這兩個步驟可以分開調用,也可以獨立調用。
兩個步驟分別對應seeta::FaceRecognizer
的CropFaceV2
和ExtractCroppedFace
。也可以用Extract
方法一次完成兩個步驟的工作。
這里列舉使用Extract
進行特征提取的函數:
#include <seeta/FaceRecognizer.h>
#include <memory>
std::shared_ptr<float> extract(
seeta::FaceRecognizer *fr,
const SeetaImageData &image,
const std::vector<SeetaPointF> &points) {
std::shared_ptr<float> features(
new float[fr->GetExtractFeatureSize()],
std::default_delete<float[]>());
fr->Extract(image, points.data(), features.get());
return features;
}
同樣可以給出相似度計算的函數:
#include <seeta/FaceRecognizer.h>
#include <memory>
float compare(seeta::FaceRecognizer *fr,
const std::shared_ptr<float> &feat1,
const std::shared_ptr<float> &feat2) {
return fr->CalculateSimilarity(feat1.get(), feat2.get());
}
注意:這里
points
的關鍵點個數必須是SeetaFace
提取的5點關鍵點。
特征長度是不同模型可能不同的,要使用GetExtractFeatureSize
方法獲取當前使用模型提取的特征長度。
相似度的范圍是[0, 1]
,但是需要注意的是,如果是直接用內積計算的話,因為特征中存在復數,所以計算出的相似度可能為負數。識別器內部會將負數映射到0。
在一些特殊的情況下,需要將特征提取分開兩步進行,比如前端裁剪處理圖片,服務器進行特征提取和對比。下面給出分步驟的特征提取方式:
#include <seeta/FaceRecognizer.h>
#include <memory>
std::shared_ptr<float> extract_v2(
seeta::FaceRecognizer *fr,
const SeetaImageData &image,
const std::vector<SeetaPointF> &points) {
std::shared_ptr<float> features(
new float[fr->GetExtractFeatureSize()],
std::default_delete<float[]>());
seeta::ImageData face = fr->CropFaceV2(image, points.data());
fr->ExtractCroppedFace(face, features.get());
return features;
}
函數中間臨時申請的face
和features
的對象大小,在識別器加載后就已經固定了,所以這部分的內存對象是可以復用的。
特別指出,如果只是對一個圖像中最大人臉做特征提取的函數可以實現為:
std::shared_ptr<float> extract(
seeta::FaceDetector *fd,
seeta::FaceLandmarker *fl,
seeta::FaceRecognizer *fr,
const SeetaImageData &image) {
auto faces = fd->detect_v2(image);
if (faces.empty()) return nullptr;
std::partial_sort(faces.begin(), faces.begin() + 1, faces.end(),
[](SeetaFaceInfo a, SeetaFaceInfo b) {
return a.pos.width > b.pos.width;
});
auto points = fl->mark(image, faces[0].pos);
return extract(fr, image, points);
}
3.2 人臉特征對比
在上一節我們已經介紹了特征提取和對比的方法,這里我們直接展開講述特征相似度計算的方法。
下面我們直接給出特征對比的方法,C語言版本的實現:
float compare(const float *lhs, const float *rhs, int size) {
float sum = 0;
for (int i = 0; i < size; ++i) {
sum += *lhs * *rhs;
++lhs;
++rhs;
}
return sum;
}
基於內積的特征對比方法,就可以采用gemm
或者gemv
的通用數學方法優化特征的對比的性能了。
3.3 各種識別模型特點
SeetaFace6
第一步共開放了三個識別模型,其對比說明如下:
文件名 | 特征長度 | 一般閾值 | 說明 |
---|---|---|---|
face_recognizer.csta | 1024 | 0.62 | 通用場景高精度人臉識別 |
face_recognizer_mask.csta | 512 | 0.48 | 帶口罩人臉識別模型 |
face_recognizer_light.csta | 512 | 0.55 | 輕量級人臉是被模型 |
這里的一般閾值是一般場景使用的推薦閾值。一般來說1比1的場景下,該閾值會對應偏低,1比N場景會對應偏高。
需要注意的是,不同模型提取的特征是不具備可比較性的,哪怕特征一樣。如果在正在運行的系統替換了識別模型的話,所有底庫照片都不需要重新提取特征再進行比較才行。
3.4 關於相似度和閾值
在3.3
節我們給出了不同模型的一般閾值
。閾值起到的作用就是給出識別結果是否是同一個人的評判標准。如果兩個特征的相似度超過閾值,則認為兩個特征所代表的人臉就是同一個人。
因此該閾值和對應判斷的相似度,是算法統計是否一個人的評判標准,並不等同於自然語義下的人臉的相似度。這樣表述可能比較抽象,說個具體的例子就是,相似度0.5,在閾值是0.49的時候,就表示識別結果就是一個人,這個相似度也不表示兩個人臉有一半長的一樣。同理相似度100%,也不表示兩個人臉完全一樣,連歲月的痕跡也沒有留下。
但是0.5
就表示是同一個人,從通常認知習慣中往往80%
相似才是一個人。這個時候我們一般的做法是做相似度映射,將只要通過系統識別是同一個人的時候,就將其相似度映射到0.8以上。
綜上所述,識別算法直接給出來的相似度如果脫離閾值就沒有意義。識別算法的性能好不好,主要看其給出的不同樣本之間的相似度有沒有區分性,能夠用閾值將正例和負例樣本區分開來。
而比較兩個識別算法精度的時候,一般通常的算法就是畫出ROC
,也就是得出不同閾值下的性能做統一比較。這種情況下,及時使用了相似度變換手段,只要是正相關的映射,那么得到的ROC
曲線也會完全一致。
這里對一種錯誤的測試方式給出說明。經常有人提出問題,A算法比B算法效果差,原因是拿兩張照片,是同一個人,A算法給出的相似度比B算法給出的低。誠然,“效果”涉及到的因素很多,比如識別為同一個人就應給給出極高的相似度。但是經過上述討論,希望讀者能夠自然的明白這種精度測試方式的片面性。
3.5 1比1和1比N
一般的人臉識別應用,我們都可以這樣去區分,1比1和1比N。當然也有說法是1比1就是當N=1時的1比N。這里不詳細展開說明這兩者的區別。實際應用還是要因地制宜,並沒有統一的模板套用。這里我們給出一般說法的兩種應用。
一般的1比1識別,狹義上講就是人證對比,使用度讀卡器從身份證,或者其他介質上讀取到一張照片,然后和現場抓拍達到照片做對比。這種一般是做認證的場景,用來判別證件、或者其他憑證方式是否是本人在進行操作。因此廣義上來講,員工刷工卡,然后刷臉認證;個人賬戶進行刷臉代替密碼;這種知道待識別人員身份,然后進行現場認證的方式,都可以屬於1比1識別的范疇。
1比N識別與1比1區別在於,對於現場待識別的人臉,不知道其身份,需要在一個底庫中去查詢,如果在底庫中給出對應識別結果,如果不在底庫中,報告未識別。如果業務場景中,確定待識別的人臉一定在底庫中,那么就是一個閉集測試,也可以稱之為人臉檢索。反之,就是開集測試。
由描述可以看出,開集場景要比閉集場景更困難,因為要着重考慮誤識別率。
1比N識別從接口調用上來說,首先需要將底庫中的人臉全部提取特征,然后,對現場抓拍到的待識別人臉提取特征,最終使用特征與底庫中的特征進行比較選出相似度最高的人臉,這時相似度若超過閾值,則認為識別成功,反之待識別人員不在底庫。
而常見的1比N識別就是攝像頭下的動態人臉識別。這個時候就必要提出我們后面要講解的模塊人臉跟蹤
和質量評估
。
這兩者在動態人臉識別中用來解決運行效率和精度的問題。首先動態人臉識別系統中,往往待識別人員會再攝像頭下運動,並保持一段時間。這個時候需要跟蹤從一開始確定,攝像頭下現在出現的是一個人。在這個基礎上,同一個人只需要識別一次即可。而這個用來識別的代表圖片,就是要利用質量評估模塊,選出對識別效果最好的一張用來識別。
人臉跟蹤
和質量評估
兩個方法的配合使用,即避免了每一幀都識別對系統的壓力,也通過從多幀圖像中選擇質量過關且最好的圖片來識別,提高了識別精度。
3.6 人臉識別優化
在3.5節中文檔介紹了1比N的基本運行方式和手段,
由3.4節關於相似度和閾值的討論,可以得出基本推論:如果閾值設置越高,那么誤識別率越低,相對識別率也會變低;閾值設置越低,識別率會變高,同時誤識別率也會變高。
這種特性也會導致我們在使用的過程中,會陷入如何調整閾值都得到好的效果的尷尬境地。這個時候我們就不止說要調節表面的閾值,其實這個時候就必要引入人臉識別應用的時候重要的部分:底庫。
底庫人臉圖片的質量往往決定了系統整體的識別性能,一般對於底庫要求有兩個維度:1. 要求的底庫(人臉)圖片質量越高越好,從內容上基本都是正面、自然光照、自然表情、無遮擋等等,從圖像上人臉部分 128x128 像素一樣,無噪聲,無失真等等;2. 底庫圖片的成像越接近部署環境越好。而這兩個維度並往往可以統一考慮。
底庫圖片要求的兩個維度,一般翻譯到應用上:前者就是需要盡可能的人員近期照片,且圖像質量好,圖片傳輸要盡可能保真,這個一般做應用都會注意到;后者一般的操作我們稱之為現場注冊,使用相同的攝像頭,在待識別的場景下進行注冊。而后者,往往會包含對前者的要求,所以實際應用的效果會更好。
另外,綜合來說還應當避免:1. 對人臉圖片做失真編輯,如PS、美圖等。2. 濃妝。從成像的基本原理來說,識別算法必然會受到化妝的影響。基於之前底庫人臉照片的兩個原則,不完全強調使用素顏照片,但是要底庫照片和現場照片要保證最基本的可辨識程度。
人都做不到的事情,就不要難為計算機了。
上述對底庫的要求,同樣適用於對現場照片的要求。因為從特征對比的角度,兩個特征的來源是對稱的。
我們在后面的章節中會提到質量評估模塊,提供了一般性通用的質量評估方式。通過該模塊,就可以對不符合識別要求的圖片進行過濾,提高整體的識別性能。
備注:
SeetaFace6
會給出較為完整的動態人臉識別的開發實例,參見開放版的發布平台。
備注:人臉識別還有一個抽象程度更高的接口seeta::FaceDatabase
。具體使用參照SeetaFace2
開源版本。
參考:seeta/FaceRecognizer.h
、seeta/FaceDatabase.h
4. 活體檢測
在介紹人臉識別很關鍵的人臉跟蹤和質量評估之前,先看一個對應用很重要的活體檢測
模塊。
關於活體檢測模塊的重要程度和原因,這里不做過多贅述,這里直接給出SeetaFace
的活體檢測方案。
活體檢測結合了兩個方法,全局活體檢測
和局部活體檢測
。
全局活體檢測
就是對圖片整體做檢測,主要是判斷是否出現了活體檢測潛在的攻擊介質,如手機、平板、照片等等。
局部活體檢測
是對具體人臉的成像細節通過算法分析,區別是一次成像和二次成像,如果是二次成像則認為是出現了攻擊。
4.1 基本使用
區別於之前的模塊,活體檢測識別器可以加載一個局部檢測
模型或者局部檢測
模型+全局檢測
模型。
這里只加載一個局部檢測
模型:
#include <seeta/FaceAntiSpoofing.h>
seeta::FaceAntiSpoofing *new_fas() {
seeta::ModelSetting setting;
setting.append("fas_first.csta");
return new seeta::FaceAntiSpoofing(setting);
}
或者局部檢測
模型+全局檢測
模型,啟用全局檢測能力:
#include <seeta/FaceAntiSpoofing.h>
seeta::FaceAntiSpoofing *new_fas_v2() {
seeta::ModelSetting setting;
setting.append("fas_first.csta");
setting.append("fas_second.csta");
return new seeta::FaceAntiSpoofing(setting);
}
調用有兩種模式,一個是單幀識別,另外就是視頻識別。
其接口聲明分別為:
seeta::FaceAntiSpoofing::Status seeta::FaceAntiSpoofing::Predict( const SeetaImageData &image, const SeetaRect &face, const SeetaPointF *points ) const;
seeta::FaceAntiSpoofing::Status seeta::FaceAntiSpoofing::PredictVideo( const SeetaImageData &image, const SeetaRect &face, const SeetaPointF *points ) const;
從接口上兩者的入參和出參的形式是一樣的。出參這里列一下它的聲明:
class FaceAntiSpoofing {
public:
/*
* 活體識別狀態
*/
enum Status
{
REAL = 0, ///< 真實人臉
SPOOF = 1, ///< 攻擊人臉(假人臉)
FUZZY = 2, ///< 無法判斷(人臉成像質量不好)
DETECTING = 3, ///< 正在檢測
};
}
單幀識別
返回值會是REAL
、SPOOF
或FUZZY
。 視頻識別
返回值會是REAL
、SPOOF
、FUZZY
或DETECTING
。
兩種工作模式的區別在於前者屬於一幀就是可以返回識別結果,而后者要輸入多個視頻幀然后返回識別結果。在視頻識別
輸入幀數不滿足需求的時候,返回狀態就是DETECTING
。
這里給出單幀識別調用的示例:
void predict(seeta::FaceAntiSpoofing *fas, const SeetaImageData &image, const SeetaRect &face, const SeetaPointF *points) {
auto status = fas->Predict(image, face, points);
switch(status) {
case seeta::FaceAntiSpoofing::REAL:
std::cout << "真實人臉" << std::endl; break;
case seeta::FaceAntiSpoofing::SPOOF:
std::cout << "攻擊人臉" << std::endl; break;
case seeta::FaceAntiSpoofing::FUZZY:
std::cout << "無法判斷" << std::endl; break;
case seeta::FaceAntiSpoofing::DETECTING:
std::cout << "正在檢測" << std::endl; break;
}
}
這里需要注意face
和points
必須對應,也就是points
必須是face
表示的人臉進行關鍵點定位的結果。points
是5個關鍵點。當然image
也是需要識別的原圖。
如果是視頻識別
模式的話,只需要將predict
中的fas->Predict(image, face, points)
修改為fas->PredictVideo(image, face, points)
。
在視頻識別
模式中,如果該識別結果已經完成,需要開始新的視頻的話,需要調用ResetVideo
重置識別狀態,然后重新輸入視頻:
void reset_video(seeta::FaceAntiSpoofing *fas) {
fas->ResetVideo();
}
當了解基本調用接口之后,就可以直接看出來,識別接口直接輸入的就是單個人臉位置和關鍵點。因此,當視頻或者圖片中存在多張人臉的時候,需要業務決定具體識別哪一個人臉。一般有這幾種選擇,1. 只做單人識別,當出現兩個人的時候識別中止。2. 識別最大的人臉。3. 識別在指定區域中出現的人臉。這幾種選擇對精度本身影響不大,主要是業務選型和使用體驗的區別。
4.2 參數設置
設置視頻幀數:
void SetVideoFrameCount( int32_t number );
默認為10,當在PredictVideo
模式下,輸出幀數超過這個number
之后,就可以輸出識別結果。這個數量相當於多幀識別結果融合的融合的幀數。當輸入的幀數超過設定幀數的時候,會采用滑動窗口的方式,返回融合的最近輸入的幀融合的識別結果。一般來說,在10以內,幀數越多,結果越穩定,相對性能越好,但是得到結果的延時越高。
設置識別閾值:
void SetThreshold( float clarity, float reality );
默認為(0.3, 0.8)
。活體識別時,如果清晰度(clarity)低的話,就會直接返回FUZZY
。清晰度滿足閾值,則判斷真實度(reality
),超過閾值則認為是真人,低於閾值是攻擊。在視頻識別模式下,會計算視頻幀數內的平均值再跟幀數比較。兩個閾值都符合,越高的話,越是嚴格。
設置全局檢測閾值:
void SetBoxThresh(float box_thresh);
默認為0.8,這個是攻擊介質存在的分數閾值,該閾值越高,表示對攻擊介質的要求越嚴格,一般的疑似就不會認為是攻擊介質。這個一般不進行調整。
以上參數設置都存在對應的Getter
方法,將方法名稱中的Set
改為Get
就可以訪問對應的參數獲取了。
4.3 參數調試
在應用過程中往往不可避免對閾值產生疑問,如果要調試對應的識別的閾值,這里我們給出了每一幀分數的獲取函數。
下面給出識別之后獲取識別具體分數的方法:
void predict_log(seeta::FaceAntiSpoofing *fas, const SeetaImageData &image, const SeetaRect &face, const SeetaPointF *points) {
auto status = fas->Predict(image, face, points);
float clarity, reality;
fas->GetPreFrameScore(&clarity, &reality);
std::cout << "clarity = " << clarity << ", reality = " << reality << std::endl;
}
在Predict
或者PredictVideo
之后,調用GetPreFrameScore
方法可以獲取剛剛輸入幀的識別分數。
參考:
seeta/FaceAntiSpoofing.h
4.3 其他建議
活體識別內容增加了對圖像清晰度的判斷,但是還是有對其他方面的質量要求,比如人臉分辨率128x128以上,光照均勻,自然表情等。其中主要影響識別精度的為光照環境,這些都可以通過后面的質量評估
模塊做到。
另外局部活體檢測利用了人臉周圍的上下文信息,因此要求人臉不能夠靠近邊緣,處於圖像邊緣會導致上下文信息缺失影響精度。這種場景下一般會在屏幕交互時畫出ROI
區域,在ROI
區域內在合適的距離和人臉分辨率下進行識別。
5. 人臉跟蹤
人臉跟蹤也是基於人臉的基本模塊,其要解決的問題是在進行識別之前就利用視頻特性,首先就確認在視頻序列中出現的那些人是同一人。
這里先給出人臉跟蹤結果的結構體:
struct SeetaTrackingFaceInfo
{
SeetaRect pos;
float score;
int frame_no;
int PID;
int step;
};
struct SeetaTrackingFaceInfoArray
{
struct SeetaTrackingFaceInfo *data;
int size;
};
對比與SetaFaceInfo
增加了PID
字段。frame_no
和step
為內部調試保留字段,一般不使用。pos
字段是SeetaRect
類型,可以替代直接人臉檢測器的檢測結果的pos
使用。
PID
就是人員編號,對於視頻中出現的人臉,如果跟蹤分配了同一個PID
,那么就可以認為相同PID
的人臉屬於同一個人。
同樣,適用之前先要構造人臉跟蹤器:
#include <seeta/FaceTracker.h>
seeta::FaceTracker *new_ft() {
seeta::ModelSetting setting;
setting.append("face_detector.csta");
return new seeta::FaceTracker(setting, 1920, 1080);
}
這里代碼構造了用於跟蹤1920x1080
分辨率視頻的人臉跟蹤器,這里人臉跟蹤器要傳入的模型就是人臉檢測的模型。
下面就是打印出跟蹤到的人臉的PID以及坐標位置的函數:
#include <seeta/FaceTracker.h>
void track(seeta::FaceTracker *ft, const SeetaImageData &image) {
SeetaTrackingFaceInfoArray cfaces = ft->Track(image);
std::vector<SeetaTrackingFaceInfo> faces(cfaces.data, cfaces.data + cfaces.size);
for (auto &face : faces) {
SeetaRect rect = face.pos;
std::cout << "[" << rect.x << ", " << rect.y << ", "
<< rect.width << ", " << rect.height << "]: "
<< face.score << ", PID=" << face.PID << std::endl;
}
}
當檢測邏輯斷開,或者切換視頻的時候,就需要排除之前跟蹤的邏輯,這個時候調用Reset
方式清楚之前所有跟蹤的結果,重新PID
計數:
void reset(seeta::FaceTracker *ft) {
ft->Reset();
}
人臉跟蹤器和人臉檢測器一樣,可以設置檢測器的基本參數:
ft->SetMinFaceSize(80); // 設置最小人臉80
ft->SetThreshold(0.9f); // 設置檢測器的分數閾值
當然還有人臉跟蹤器專有的參數設置。
設置視頻穩定性:
void seeta::FaceTracker::SetVideoStable(bool stable = true);
當這個參數設為真的話,就會進行檢測結果的幀間平滑,使得檢測結果從視覺上更好一些。
設置檢測間隔:
void seeta::FaceTracker::SetInterval(int interval);
間隔默認值為10。這里跟蹤間隔是為了發現新增PID
的間隔。檢測器會通過整張圖像檢測人臉去發現是否有新增的PID
,所以這個值太小會導致跟蹤速度變慢(不斷做全局檢測);這個值太大會導致畫面中新增加的人臉不會立馬被跟蹤到。
SetInterval
的默認值是根據FPS為25設定的。在應用中往往會產生跳幀,如果跳幀為1的話,相當於FPS變成了12.5。這個時候就可以將跟蹤間隔設置為5。
人臉跟蹤不等同與人群計數算法。從目標上來說是為了保證同一個PID為同一個人為第一要務,這樣可以減小識別分析的壓力。另外,人臉跟蹤是基於出現的人臉,如果沒有漏出人臉也做計數的話,結果差距會很大。當然,統計PID個數來統計攝像頭前出現人臉的人次
還是可以的。
參考:
seeta/FaceTracker.h
6. 質量評估
SeetaFace6
開放的質量評估模塊包含了多個子模塊,包括亮度評估
、清晰度評估
、完整度評估
、清晰度評估(深度)
、姿態評估
、姿態評估(深度)
、分辨率評估
。
首先說明各個評估模塊的共有接口。
namespace seeta {
enum QualityLevel {
LOW = 0,
MEDIUM = 1,
HIGH = 2,
};
class QualityResult {
public:
QualityLevel level = LOW; ///< quality level
float score = 0; ///< greater means better, no range limit
};
class QualityRule {
public:
using self = QualityRule;
virtual ~QualityRule() = default;
/**
*
* @param image original image
* @param face face location
* @param points landmark on face
* @param N how many landmark on face given, normally 5
* @return Quality result
*/
virtual QualityResult check(
const SeetaImageData &image,
const SeetaRect &face,
const SeetaPointF *points,
int32_t N) = 0;
};
}
每一個子模塊繼承了QualityRule
基類,提供抽象的評估結果。子類需要實現check
方法,其傳入原始圖像image
,人臉位置face
以及N
個關鍵點的數組points
。這里注意N
一般情況下都是5
。
質量評估模塊返回值為QualityResult
,其兩個成員level
和score
。level
是直接LOW
、MEDIUM
、HIGH
分別表示質量差、良、優。而對應score
與質量評價正相關,但是不保證取值范圍,越大質量越好。level
作為直接質量判斷依據,當需要細分區分兩個人臉質量時,則使用score
判斷。
所以,所有評估其調用全部使用check
函數進行,以下子模塊介紹的時候只重點說明評估器的構造特性。
由於涉及的模塊比較多,具體的接口定義參見開放版本下載地址中的詳細接口文檔。
6.1 亮度評估
亮度評估就是評估人臉區域內的亮度值是否均勻正常,存在部分或全部的過亮和過暗都會是評價為LOW
。
評估器聲明,見文件 seeta/QualityOfBrightness.h
class QualityOfBrightness : public QualityRule;
評估器構造
QualityOfBrightness();
QualityOfBrightness(float v0, float v1, float v2, float v3);
其中{v0, v1, v2, v3}
的默認值為{70, 100, 210, 230}
。評估器會將綜合的亮度從灰度值映射到level
,其映射關系為:
[0, v0), [v3, ~) => LOW
[v0, v1), [v2, v3) => MEDIUM
[v1, v2) => HIGH
6.2 清晰度評估
清晰度這里是傳統方式通過二次模糊后圖像信息損失程度統計的清晰度。
評估器聲明,見文件 seeta/QualityOfClarity.h
class QualityOfClarity : public QualityRule;
評估器構造
QualityOfClarity();
QualityOfClarity(float low, float height);
{low, high}
默認值為{0.1, 0.2}
,其映射關系為
[0, low) => LOW
[low, high) => MEDIUM
[high, ~) => HIGH
6.3 完整度評估
完整度評估是朴素的判斷人來是否因為未完全進入攝像頭而造成的不完整的情況。該方法不適用於判斷遮擋造成的不完整。
判斷方式為對人臉檢測框周圍做擴展,如果擴展超過了圖像邊緣,則認為該圖像是處於邊緣不完整的人臉。
評估器聲明,見文件 seeta/QualityOfIntegrity.h
class QualityOfIntegrity : public QualityRule;
評估器構造
QualityOfIntegrity();
QualityOfIntegrity(float low, float height);
{low, high}
默認值為{10, 1.5}
單位分別為像素和比例,其映射關系為:
- 人臉外擴high
倍數沒有超出圖像 => HIGH
- 人臉外擴low
像素沒有超出圖像 => MEDIUM
- 其他 => LOW
返回的score
在level
為MEDIUM
有意義,表示未超出圖像的比例。
6.4 姿態評估
這里的姿態評估器是傳統方式,通過人臉5點坐標值來判斷姿態是否為正面。
評估器聲明,見文件 seeta/QualityOfPose.h
class QualityOfPose : public QualityRule;
評估器構造
QualityOfPose();
這里的構造器並不需要任何參數。
6.5 姿態評估(深度)
這里的姿態評估器是深度學習方式,通過回歸人頭部在yaw
、pitch
、roll
三個方向的偏轉角度來評估人臉是否是正面。
評估器聲明,見文件 seeta/QualityOfPoseEx.h
class QualityOfPoseEx : public QualityRule;
評估器構造
QualityOfPoseEx(const SeetaModelSetting &setting);
這里構造QualityOfPoseEx
需要傳入模型pose_estimation.csta
。前面章節已經列舉大量如何傳入模型的示例,這里不再進行贅述。
參數設置
由於該模塊參數較多,構造函數並未進行參數設置,而通用參數設置方法為:
void set(PROPERTY property, float value);
可以設置的屬性包括:
YAW_LOW_THRESHOLD yaw 方向低分數閾值
YAW_HIGH_THRESHOLD yaw 方向高分數閾值
PITCH_LOW_THRESHOLD pitch 方向低分數閾值
PITCH_HIGH_THRESHOLD pitch 方向高分數閾值
ROLL_LOW_THRESHOLD roll 方向低分數閾值
ROLL_HIGH_THRESHOLD roll 方向高分數閾值
以下是構造評估器對象,並設置成默認閾值的操作:
auto qa = new seeta::QualityOfPoseEx(seeta::ModelSetting("pose_estimation.csta"));
qa->set(seeta::QualityOfPoseEx::YAW_LOW_THRESHOLD, 25);
qa->set(seeta::QualityOfPoseEx::YAW_HIGH_THRESHOLD, 10);
qa->set(seeta::QualityOfPoseEx::PITCH_LOW_THRESHOLD, 20);
qa->set(seeta::QualityOfPoseEx::PITCH_HIGH_THRESHOLD, 10);
qa->set(seeta::QualityOfPoseEx::ROLL_LOW_THRESHOLD, 33.33f);
qa->set(seeta::QualityOfPoseEx::ROLL_HIGH_THRESHOLD, 16.67f);
delete qa;
根據對應的閾值,當三個角度都滿足最高分數閾值時評估結果HIGH
,當有一個角度為最低時,評價為LOW
,其他情況會評價為MEDIUM
。
6.6 分辨率評估
這個是質量評估模塊里相對最簡單的部分了,就是判斷人臉部分的分辨率。
評估器聲明,見文件 seeta/QualityOfResolution.h
class QualityOfResolution : public QualityRule;
評估器構造
QualityOfResolution();
QualityOfResolution(float low, float height);
{low, high}
默認值為{80, 120}
,其映射關系為
[0, low) => LOW
[low, high) => MEDIUM
[high, ~) => HIGH
6.7 清晰度評估(深度)
質量評估(深度)模塊因為歷史原因,並未繼承QualityRule
,這里直接給出在使用時繼承QualityRule
的版本。
#include "seeta/QualityStructure.h"
#include "seeta/QualityOfLBN.h"
namespace seeta {
class QualityOfClarityEx : public QualityRule {
public:
QualityOfClarityEx() {
m_lbn = std::make_shared<QualityOfLBN>(ModelSetting("quality_lbn.csta"));
}
QualityOfClarityEx(float blur_thresh) {
m_lbn = std::make_shared<QualityOfLBN>(ModelSetting("quality_lbn.csta"));
m_lbn->set(QualityOfLBN::PROPERTY_BLUR_THRESH, blur_thresh);
}
QualityResult check(const SeetaImageData &image, const SeetaRect &face, const SeetaPointF *points, int32_t N) override {
assert(N == 5);
QualityResult result;
int light, blur, noise;
m_lbn->Detect(image, points, &light, &blur, &noise);
if (blur == QualityOfLBN::BLUR) {
return {QualityLevel::LOW, 0};
} else {
return {QualityLevel::HIGH, 1};
}
}
private:
std::shared_ptr<QualityOfLBN> m_lbn;
};
}
注意該代碼並非以開放代碼庫接口,使用時需要拷貝置項目使用。
這里的QualityOfLBN
開始使用了模型quality_lbn.csta
。其構造的時候設置blur_thresh
,默認為0.8
。其評估對應分值超過選項之后就認為是模糊圖片。
6.8 遮擋評估
這次要注意的是,遮擋判斷的算法也是策略上的使用。以下的代碼可以以QualityRule
的方式完成遮擋判斷。判斷的遮擋物為五個關鍵點,分別是左右眼中心、鼻尖和左右嘴角。
#include "seeta/QualityStructure.h"
#include "seeta/FaceLandmarker.h"
namespace seeta {
class QualityOfNoMask : public QualityRule {
public:
QualityOfNoMask() {
m_marker = std::make_shared<seeta::FaceLandmarker>(ModelSetting("face_landmarker_mask_pts5.csta"));
}
QualityResult check(const SeetaImageData &image, const SeetaRect &face, const SeetaPointF *points, int32_t N) override {
auto mask_points = m_marker->mark_v2(image, face);
int mask_count = 0;
for (auto point : mask_points) {
if (point.mask) mask_count++;
}
QualityResult result;
if (mask_count > 0) {
return {QualityLevel::LOW, 1 - float(mask_count) / mask_points.size()};
} else {
return {QualityLevel::HIGH, 1};
}
}
private:
std::shared_ptr<seeta::FaceLandmarker> m_marker;
};
}
這里雖然使用了seeta::FaceLandmarker
模塊,但是需要使用face_landmarker_mask_pts5.csta
模型。其提供了對每個檢測到的關鍵點的遮擋信息判斷。
6.9 評估器使用
到這里,我們就完全介紹了質量評估可以使用的模塊,由於每個模塊都按照QualityRule
的方式封裝,除了每個模塊構造和初始化的時候存在差異,對每一項的評估現在都可以以相同的方式操作了。
這里先給出如何使用QualityRule
進行質量評估並打印結果:
void plot_quality(seeta::QualityRule *qr,
const SeetaImageData &image,
const SeetaRect &face,
const std::vector<SeetaPointF> &points) {
const char *level_string[] = {"LOW", "MEDIUM", "HIGH"};
seeta::QualityResult result = qr->check(image, face, points.data(), int(points.size()));
std::cout << "Level=" << level_string[int(result.level)] << ", score=" << result.score << std::endl;
}
這里以QualityOfResolution
為例,在獲取到人臉和關鍵點后,評估分辨率的調用代碼如下:
seeta::QualityRule *qr = new seeta::QualityOfResolution();
plot_quality(qr, image, face, points);
delete qr;
如果需要評估其他的質量,只需要修改QualityOfResolution
部分即可。
當然這個代碼塊不代表應用的形式。因為qr
對象可以被持久化,不需要使用的時候臨時構造和釋放。
在應用中,往往需要建立多個QualityRule
,根據實際規則,往往需要多個QualityRule
全部返回HIGH
才認為圖像合格。
當然也可以根據業務需求,加入其它的質量評估模塊。
以上質量評估模塊的默認閾值,都是經過調整的,一般不需要調整即可使用。默認閾值已經能夠很好的配合我們的SeetaFace
其它模塊運行了。
注意:該模塊的所有子模塊都包含在
SeetaQualityAssessor300
庫中。參考:
seeta/QualityOfBrightness.h
,seeta/QualityOfIntegrity.h
,seeta/QualityOfPose.h
,seeta/QualityOfResolution.h
,seeta/QualityOfClarity.h
,seeta/QualityOfLBN.h
,seeta/QualityOfPoseEx.h
,
7. 人臉屬性檢測
SeetaFace
目前開放的屬性有年齡識別
和性別識別
。
7.1 年齡識別
同樣,先看一下構造識別器的函數:
#include <seeta/AgePredictor.h>
seeta::AgePredictor *new_ap() {
seeta::ModelSetting setting;
setting.append("age_predictor.csta");
return new seeta::AgePredictor(setting);
}
調用識別器進行識別並打印識別結果的函數如下:
#include <seeta/AgePredictor.h>
void plot_age(seeta::AgePredictor *ap,
const SeetaImageData &image,
const std::vector<SeetaPointF> &points) {
assert(points.size() == 5);
int age = 0;
ap->PredictAgeWithCrop(image, points.data(), age);
std::cout << "age=" << age << std::endl;
}
一般age
在不同的年齡段都會有不同的誤差,在使用中一般要講數字的年齡,映射到用於需要的年齡段,如青中老年。
在視頻中做逐幀識別的時候往往會觀測到每一幀的年齡都會有波動,因為深度學習算法特性,當輸入輕微擾動,可能對結果產生明顯波動。
如果單幀識別不會出現這種觀測現象。視頻分析中,一般前置人臉跟蹤,所以識別結果對於一個人只會給出一個。
當然朴素的對人臉識別的優化結論都對屬性識別也有用,主要是:1. 使用質量評估對低質量差的圖片進行過濾;2. 進行多次識別進行結果集成。
7.2 性別識別
同樣,先看一下構造識別器的函數:
#include <seeta/GenderPredictor.h>
seeta::GenderPredictor *new_gp() {
seeta::ModelSetting setting;
setting.append("gender_predictor.csta");
return new seeta::GenderPredictor(setting);
}
調用識別器進行識別並打印識別結果的函數如下:
#include <seeta/GenderPredictor.h>
void plot_gender(seeta::GenderPredictor *gp,
const SeetaImageData &image,
const std::vector<SeetaPointF> &points) {
assert(points.size() == 5);
seeta::GenderPredictor::GENDER gender = 0;
gp->PredictGenderWithCrop(image, points.data(), gender);
std::cout << "gender="
<< (gender == seeta::GenderPredictor::FEMALE ? "female" : "male")
<< std::endl;
}
當然可以推論,性別識別也是可以使用質量評估去保證識別圖像的質量,從而提高識別精度。
8. 戴口罩人臉識別
8.1 口罩檢測
同樣,先看一下構造識別器的函數:
#include <seeta/MaskDetector.h>
seeta::MaskDetector *new_md() {
seeta::ModelSetting setting;
setting.append("mask_detector.csta");
return new seeta::MaskDetector(setting);
}
調用識別器進行識別並打印識別結果的函數如下:
#include <seeta/MaskDetector.h>
void plot_mask(seeta::MaskDetector *md,
const SeetaImageData &image,
const SeetaRect &face) {
float score = 0;
bool mask = md->detect(image, face, &score);
std::cout << std::boolalpha << "mask=" << mask
<< ", score=" << score
<< std::endl;
}
一般性的,score
超過0.5,則認為是檢測帶上了口罩。
8.2 口罩人臉識別
口罩人臉識別,其底層還是調用口罩人臉識別模塊,需要替換的是口罩人臉識別模型,如下是構造用於口罩人臉識別的識別器:
#include <seeta/FaceRecognizer.h>
seeta::FaceRecognizer *new_mask_fr() {
seeta::ModelSetting setting;
setting.append("face_recognizer_mask.csta");
return new seeta::FaceRecognizer(setting);
}
需要注意,口罩人臉識別和普通人臉識別試用CropFaceV2
方法裁剪出來的人臉是不同的。
一般帶口罩人臉識別系統可以這樣工作:
可以看到帶口罩人臉識別其實就是利用了未遮擋部分的信息做識別的。
這樣就可以有基本的推論:
在通用未帶口罩場景下,戴口罩的人臉識別精度是比不上通用人臉識別精度的模型的。
在戴口罩場景下,戴口罩的人臉識別模型表現會更好。
當然系統要求不高的時候,不需要兩個模型同時使用,戴口罩的人臉識別模型在不帶口罩的人臉上也是可以利用不會被口罩遮擋的面部信息提取特征的。在系統中比較大量的是帶口罩的人臉的話,可以只使用戴口罩人臉識別模型。
這里要再次強調,不同識別模型提取的特征是不具備可比較性的,特征只有在相同模型提取的前提下才能進行比較。
9. 多指令集支持
到這里,主要模塊和功能使用全部講完了,本節我們討論一個部署相關的情況。
我們核心的計算庫為libtennis.so
(Windows下為tennis.dll
)。
其需要AVX
和FMA
指令集支持,為了在不支持指令集的CPU上運行。我們設計了動態運行庫的策略。
在打包中我們還包含了tennis_haswell
,tennis_sandy_bridge
、tennis_pentium
。
其分別對應了不同的架構,而這些架構支持指令集為:
庫名稱 | AVX | SSE | FMA |
---|---|---|---|
tennis_haswell | ON | ON | ON |
tennis_sandy_bridge | ON | ON | OFF |
tennis_pentium | OFF | ON | OFF |
在部署的時候,這些庫要全部放到tennis
庫所在的目錄。
當然,如果了解運行平台的指令集支持的話,可以只使用一個動態庫,例如已知支持AVX
和FMA
指令支持,就可以將tennis_haswell
重命名為tennis
,部署時只安裝這個庫即可。
10. 其他語言
SeetaFace6 只維護了核心的 C++ 的接口,開發者在需要其他語言支持的時候,需要通過語言內置的方式進行擴展。
這里給出不同語言的的擴展參考文件:
《Python 擴展C/C++》
《Java 之JNI基礎篇》
《Oracle Java Documentation》
《C# 使用C++進行.NET編程》
《C# 從托管代碼調用本機函數》
特別指出,Android
調用使用java
開發,在對應的SDK
打包中包含了JNI
封裝的代碼,可以直接使用。其他平台的JNI
封裝也可以參考其實現。
a. FAQ
-
版本號不統一?
關於版本號的額外說明,該開放版本立項的時候,就是作為社區版v3發布,而執行過程中調整至發布版本為商用版本v6。這個版本不統一是因為商用版迭代的版本管理和社區版不統一造成的。現在統一版本為v6。但是項目過程中還是存在SeetaFace3
的表述,大家不同擔心,v6和v3其實就是一個版本 -
這個版本什么時候開源?
之前開源放出是基於新的商業版本的更新,這次直接放出了商業版本,所以近期內不會進行開源。當商業版本的SeetaFace7
商用發布后,會考慮進行v6開源。 -
算法是否可以處理近灰度圖像?
對於灰度圖像,SeetaFace
的工具鏈,除了活體檢測模塊的其他模塊都可以對應運行。對應精度會受到影響變低。
需要注意將灰度圖像輸入每個模塊時,需要通過圖像庫轉成BGR
通道格式輸入算法。 -
算法是否可以處理近紅外圖片?
SeetaFace6
開放版都是基於可見光彩色圖像做處理的。不支持近紅外圖像。
這里需要說明的是近紅外圖像
不等價於灰度圖像
雖然前者往往只有黑白色。從成像來說,灰度圖像成像的波段還是可見光,近紅外圖像已經是不可見光成像了。這兩者有本質的不同。
灰度圖像可以做兼容處理。近紅外圖像不直接支持。 -
需要什么配置運行?
從運行加速上來說,雖然有了多指令集支持,我們還是建議使用擁有加速指令集的CPU
運行。X86
架構支持AVX
和FMA
指令集,支持OpenMP
。ARM
架構v8
以上,支持NEON
,OpenMP
。
有了以上的支持都會有基本的體驗,當然體驗和價格基本上是成正比的。
沒有單純硬件的最低配置,各種配置都會對應有其解決方案。 -
是否可以訓練?
訓練代碼會開放,但是要基於視拓的海龍框架。這個事情牽扯到現有的商業合作,還在籌划當中。 -
SeetaFace6開放版可以免費用於商用么?
可以。 -
可以在Windows XP系統下兼容運行么?
可以,只要將最終的目標文件設置成XP
即可。但是XP
系統不支持動態指令集。
b. 附錄
c. 代碼塊
seeta::ModelSetting
部分封裝:
class ModelSetting : public SeetaModelSetting {
public:
using self = ModelSetting;
using supper = SeetaModelSetting;
enum Device
{
AUTO,
CPU,
GPU
};
~ModelSetting() = default;
ModelSetting()
: supper( {
SEETA_DEVICE_AUTO, 0, nullptr
} ) {
this->update();
}
ModelSetting( const supper &other )
: supper( {
other.device, other.id, nullptr
} ) {
if( other.model ) {
int i = 0;
while( other.model[i] ) {
m_model_string.emplace_back( other.model[i] );
++i;
}
}
this->update();
}
ModelSetting( const self &other )
: supper( {
other.device, other.id, nullptr
} ) {
this->m_model_string = other.m_model_string;
this->update();
}
ModelSetting &operator=( const supper &other ) {
this->operator=( self( other ) );
return *this;
}
ModelSetting &operator=( const self &other ) {
this->device = other.device;
this->id = other.id;
this->m_model_string = other.m_model_string;
this->update();
return *this;
}
Device set_device( SeetaDevice device ) {
auto old = this->device;
this->device = device;
return Device( old );
}
int set_id( int id ) {
const auto old = this->id;
this->id = id;
return old;
}
void clear() {
this->m_model_string.clear();
this->update();
}
void append( const std::string &model ) {
this->m_model_string.push_back( model );
this->update();
}
size_t count() const {
return this->m_model_string.size();
}
private:
std::vector<const char *> m_model;
std::vector<std::string> m_model_string;
/**
* \brief build buffer::model
*/
void update() {
m_model.clear();
m_model.reserve( m_model_string.size() + 1 );
for( auto &model_string : m_model_string ) {
m_model.push_back( model_string.c_str() );
}
m_model.push_back( nullptr );
this->model = m_model.data();
}
};