最近在使用百度離線人臉識別做開發,官方的Demo裏面的功能演示都是帶視頻預覽的,但是我們的需求是希望做成服務的形式,不需要視頻預覽;但是,我們都知道這種是屬於對用戶來說很危險的操作——在不預覽的情況下獲取攝像頭數據。這一部分我們在後面慢慢介紹,本篇主要介紹在假設實現上述功能的情況下,如何利用百度Demo對SDK封裝接口爲我們所服務。本來是想自己單獨再做一下數據的封裝然後調用SDK接口來實現功能,最後想想時間週期以及效率,爲什麼不用百度大神們寫好的代碼了,好了,下面就來分析一下百度離線人臉識別Demo對SDK的封裝邏輯;對人臉識別感興趣的也可以看一下我之前寫的關於人臉識別的介紹
1,提出問題
下面結合實際的場景應用比較多的視頻流實時檢測人臉並與數據庫數據比對功能演示RgbVideoIdentityActivity.java入口開始介紹,我們打開該活動的時候就可以通過
private void addListener() {
// 設置回調,回調人臉檢測結果。
faceDetectManager.setOnFaceDetectListener(new FaceDetectManager.OnFaceDetectListener() {
@Override
public void onDetectFace(int retCode, FaceInfo[] infos, ImageFrame frame) {
// TODO 顯示檢測的圖片。用於調試,如果人臉sdk檢測的人臉需要朝上,可以通過該圖片判斷
Log.e(TAG, "onDetectFace: " );
final Bitmap bitmap =
Bitmap.createBitmap(frame.getArgb(), frame.getWidth(), frame.getHeight(), Bitmap.Config
.ARGB_8888);
handler.post(new Runnable() {
@Override
public void run() {
testView.setImageBitmap(bitmap);
}
});
if ( retCode == FaceTracker.ErrCode.OK.ordinal() && infos != null) {
asyncIdentity(frame, infos);
}
else {
Log.e(TAG, "onDetectFace: 無人臉" );
}
//showFrame(frame, infos);
}
});
}
添加OnFacedetectListener回調監聽並獲取人臉檢測數據,究竟內部是怎樣的一個邏輯了,我們今天就來一探究竟。
2,切入點
這裏我們先是通過在onCreate()方法中調用init()方法初始化一些數據:
private void init() {
Intent intent = getIntent();
if (intent != null) {
groupId = intent.getStringExtra("group_id");
}
faceDetectManager = new FaceDetectManager(getApplicationContext());
// 從系統相機獲取圖片幀。
final CameraImageSource cameraImageSource = new CameraImageSource(this);
// 圖片越小檢測速度越快,閘機場景640 * 480 可以滿足需求。實際預覽值可能和該值不同。和相機所支持的預覽尺寸有關。
// 可以通過 camera.getParameters().getSupportedPreviewSizes()查看支持列表。
// cameraImageSource.getCameraControl().setPreferredPreviewSize(1280, 720);
cameraImageSource.getCameraControl().setPreferredPreviewSize(640, 480);
// 設置最小人臉,該值越小,檢測距離越遠,該值越大,檢測性能越好。範圍爲80-200
FaceSDKManager.getInstance().getFaceDetector().setMinFaceSize(100);
// FaceSDKManager.getInstance().getFaceDetector().setNumberOfThreads(4);
// 設置預覽
// cameraImageSource.setPreviewView(previewView);
// 設置圖片源
faceDetectManager.setImageSource(cameraImageSource);
// 設置人臉過濾角度,角度越小,人臉越正,比對時分數越高
faceDetectManager.getFaceFilter().setAngle(20);
textureView.setOpaque(false);
// 不需要屏幕自動變黑。
textureView.setKeepScreenOn(true);
boolean isPortrait = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
if (isPortrait) {
previewView.setScaleType(PreviewView.ScaleType.FIT_WIDTH);
// 相機堅屏模式
cameraImageSource.getCameraControl().setDisplayOrientation(CameraView.ORIENTATION_PORTRAIT);
} else {
previewView.setScaleType(PreviewView.ScaleType.FIT_HEIGHT);
// 相機橫屏模式
cameraImageSource.getCameraControl().setDisplayOrientation(CameraView.ORIENTATION_HORIZONTAL);
}
setCameraType(cameraImageSource);
}
主要實現的功能就是:
- 初始化FaceDetectmanager
- 爲其添加CameraImageSource
然後在onStart()方法中調用如下方法開啓檢測
faceDetectManager.start();
3,FaceDetectManager的引入
這個類一看就是作爲人臉檢測的管理接口,下面,我們就一步一步跟進去看看start()方法中有什麼:
public void start() {
LogUtil.init();
this.imageSource.addOnFrameAvailableListener(onFrameAvailableListener);
processThread = new HandlerThread("process");
processThread.setPriority(9);
processThread.start();
processHandler = new Handler(processThread.getLooper());
uiHandler = new Handler();
this.imageSource.start();
}
其中有兩行特別重要,就是第一行和最後一行,第一行我們先不說,先看最後一行,到這裏start()究竟怎麼開啓檢測的還是沒有蹤影,它只是調用了FaceDetectManager中的成員變量imageSource中的start()方法。那我們知道,在RgbVideoIdentityActivity的init()方法中我們通過如下兩句初始化了這個成員變量:
final CameraImageSource cameraImageSource = new CameraImageSource(this);
faceDetectManager.setImageSource(cameraImageSource);
可見,這是一個CameraImageSource實例
4,CameraImageResource的引入
這個類是封裝了相機圖片資源的類,我們知道圖片資源可能來自
- 普通相機
- 來自文件
- 網絡相機;
對應到百度Demo封裝的接口就是:
- CameraImageSource
- FileImageSource
- RtspImageSource
它們的抽象是ImageSource。
那我們再跟進到CameraImageSorce中的start()方法看一看:
@Override
public void start() {
super.start();
cameraControl.start();
}
看到這裏我們又懵逼了,它也沒有具體實現,只是調用了一個接口的ICameraControl接口的start()方法,沒有關係,這個cameraControl也是CameraImageResource中的一個成員變量,我們看看它是在哪裏初始化的就可以找到它的具體實現子類了,這樣我們就可以查看到start()最終實現邏輯了;下面是CameraImageResource的構造方法代碼:
public CameraImageSource(Context context) {
this.context = context;
//原始Demo
cameraControl = new Camera1Control(getContext());
cameraControl.setCameraFacing(cameraFaceType);
cameraControl.setOnFrameListener(new ICameraControl.OnFrameListener<byte[]>() {
@Override
public void onPreviewFrame(byte[] data, int rotation, int width, int height) {
// Log.e(TAG, "onPreviewFrame: " );
int[] argb = argbPool.acquire(width, height);
if (argb == null || argb.length != width * height) {
argb = new int[width * height];
}
rotation = rotation < 0 ? 360 + rotation : rotation;
long startTime = System.currentTimeMillis();
FaceDetector.yuvToARGB(data, width, height, argb, rotation, 0);
// 旋轉了90或270度。高寬需要替換
if (rotation % 180 == 90) {
int temp = width;
width = height;
height = temp;
}
ImageFrame frame = new ImageFrame();
frame.setArgb(argb);
frame.setWidth(width);
frame.setHeight(height);
frame.setPool(argbPool);
ArrayList<OnFrameAvailableListener> listeners = getListeners();
for (OnFrameAvailableListener listener : listeners) {
listener.onFrameAvailable(frame);
}
argbPool.release(argb);
}
});
}
我們目前只看其中一句:
cameraControl = new Camera1Control(getContext());
嗯,我們終於找到了,就是這個Camera1Control。
5,相機封裝的結構與內部邏輯
上面提到了ICameraControl接口和Camera1Control實現類,其實這裏百度Demo對攝像頭的調用做了分類封裝的,主要是針對Camera和Camera2調用方式區別做的不同的封裝,對應到具體類就是:
- Camera1Control
- Camera2Control
這裏我們後續介紹無預覽獲取攝像頭數據的時候會自己封裝一個攝像頭Control出來,好了,回到主題上,那我們就進去看看它的start()方法究竟是怎麼實現的:
public void start() {
postStartCamera();
}
其中postStartCamera()具體邏輯如下:
private void postStartCamera() {
if (cameraHandlerThread == null || !cameraHandlerThread.isAlive()) {
cameraHandlerThread = new HandlerThread("camera");
cameraHandlerThread.start();
cameraHandler = new Handler(cameraHandlerThread.getLooper());
uiHandler = new Handler(Looper.getMainLooper());
}
if (cameraHandler == null) {
return;
}
cameraHandler.post(new Runnable() {
@Override
public void run() {
try {
startCamera();
} catch (RuntimeException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
這裏的各種線程管理我們不看,只看一句,那就是打開攝像頭:
startCamera();
其邏輯如下:
private void startCamera() {
Log.e(TAG, "startCamera1: " );
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
if (permissionCallback != null) {
permissionCallback.onRequestPermission();
}
return;
}
if (camera == null) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0; i < Camera.getNumberOfCameras(); i++) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == cameraFacing) {
cameraId = i;
}
}
camera = Camera.open(cameraId);
}
int detectRotation = 0;
if (cameraFacing == ICameraControl.CAMERA_FACING_FRONT) {
int rotation = ORIENTATIONS.get(displayOrientation);
rotation = getCameraDisplayOrientation(rotation, cameraId, camera);
camera.setDisplayOrientation(rotation);
detectRotation = rotation;
if (displayOrientation == CameraView.ORIENTATION_PORTRAIT) {
if (detectRotation == 90 || detectRotation == 270) {
detectRotation = (detectRotation + 180) % 360;
}
}
} else if (cameraFacing == ICameraControl.CAMERA_FACING_BACK){
int rotation = ORIENTATIONS.get(displayOrientation);
rotation = getCameraDisplayOrientation(rotation, cameraId, camera);
camera.setDisplayOrientation(rotation);
detectRotation = rotation;
} else if (cameraFacing == ICameraControl.CAMERA_USB){
camera.setDisplayOrientation(0);
detectRotation = 0;
}
opPreviewSize(preferredWidth, preferredHeight);
final Camera.Size size = camera.getParameters().getPreviewSize();
if (detectRotation % 180 == 90) {
previewView.setPreviewSize(size.height, size.width);
} else {
previewView.setPreviewSize(size.width, size.height);
}
final int temp = detectRotation;
try {
if (cameraFacing == ICameraControl.CAMERA_USB) {
camera.setPreviewTexture(textureView.getSurfaceTexture());
} else {
surfaceTexture = new SurfaceTexture(11);
camera.setPreviewTexture(surfaceTexture);
uiHandler.post(new Runnable() {
@Override
public void run() {
if (textureView != null) {
surfaceTexture.detachFromGLContext();
textureView.setSurfaceTexture(surfaceTexture);
}
}
});
}
camera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
LogUtil.i("wtf", "onPreviewFrame-->");
Log.e(TAG, "onPreviewFrame: 第一次拿到原始數據" );
onFrameListener.onPreviewFrame(data, temp, size.width, size.height);
}
});
} catch (IOException e) {
e.printStackTrace();
LogUtil.i("wtf", e.toString());
} catch (RuntimeException e) {
e.printStackTrace();
LogUtil.i("wtf", e.toString());
}
}
這裏,原來走到最後就是開啓攝像頭的邏輯,在這裏,我們也說拿到攝像頭最原始數據的地方,並且我們還調用了
onFrameListener.onPreviewFrame(data, temp, size.width, size.height);
6,數據回傳
上面我們拿到了攝像頭原始數據並調用了onFrameListener的onPreViewFrame()方法回調數據,這裏的onFrameListener是ICameraControl內部接口OnFrameListener的實例,那我們知道,我們是在CameraImageSource的構造方法中獲取的Camera1Control也就是ICameraControl的實例,所以,其回調的實現必然也在這裏,上面的代碼中其實已經給出來了:
cameraControl.setOnFrameListener(new ICameraControl.OnFrameListener<byte[]>() {
@Override
public void onPreviewFrame(byte[] data, int rotation, int width, int height) {
// Log.e(TAG, "onPreviewFrame: " );
int[] argb = argbPool.acquire(width, height);
if (argb == null || argb.length != width * height) {
argb = new int[width * height];
}
rotation = rotation < 0 ? 360 + rotation : rotation;
long startTime = System.currentTimeMillis();
FaceDetector.yuvToARGB(data, width, height, argb, rotation, 0);
// 旋轉了90或270度。高寬需要替換
if (rotation % 180 == 90) {
int temp = width;
width = height;
height = temp;
}
ImageFrame frame = new ImageFrame();
frame.setArgb(argb);
frame.setWidth(width);
frame.setHeight(height);
frame.setPool(argbPool);
ArrayList<OnFrameAvailableListener> listeners = getListeners();
for (OnFrameAvailableListener listener : listeners) {
listener.onFrameAvailable(frame);
}
argbPool.release(argb);
}
});
我們最終目標是要搞明白攝像頭原始數據是怎麼通過Demo封裝的接口轉化爲我們所需要的人臉數據的,所以到這裏還不夠,我們需要繼續跟進,從上面代碼我們可知,在OnFrameListener的onPreviewFrame()中只是做了一個數據轉換而已:
- YUV轉爲ARGB
- 封裝ImageFrame數據
待數據轉換完成之後,又走了一個OnFrameAvailableListener的回調,哎,沒有關係,我們接着跟進,這裏的回調接口來自於
ArrayList<OnFrameAvailableListener> listeners = getListeners();
其中,getListener是其父類ImageSource的一個方法:
/** 獲取監聽器列表 */
protected ArrayList<OnFrameAvailableListener> getListeners() {
return listeners;
}
這裏,我們必須要找到它的具體實現,不然我們就沒有辦法走下去了,但是這個OnFrameAvailableListener就是什麼時候傳進來的,如果沒有記錯的話,我在上面FaceDetectmanager的start()方法中有說過第一行我們下面介紹:
this.imageSource.addOnFrameAvailableListener(onFrameAvailableListener);
這裏的onFrameAvailableListener獲取代碼如下:
private OnFrameAvailableListener onFrameAvailableListener = new OnFrameAvailableListener() {
@Override
public void onFrameAvailable(ImageFrame imageFrame) {
lastFrame = imageFrame;
processRunnable.run();
}
};
這裏只是做了賦值和開啓一個線程任務操作,我們看看這個線程任務究竟做了哪些工作:
private Runnable processRunnable = new Runnable() {
@Override
public void run() {
if (lastFrame == null) {
return;
}
int[] argb;
int width;
int height;
ArgbPool pool;
synchronized (lastFrame) {
argb = lastFrame.getArgb();
width = lastFrame.getWidth();
height = lastFrame.getHeight();
pool = lastFrame.getPool();
lastFrame = null;
}
process(argb, width, height, pool);
}
};
在這裏,我們取出之前在onPreviewFrame()中封裝的攝像頭輸出的元素數據,並調用process()方法進行處理,快了快了,馬上整改調用流程就浮出水面了,其中process代碼如下:
private void process(int[] argb, int width, int height, ArgbPool pool) {
int value;
ImageFrame frame = imageSource.borrowImageFrame();
frame.setArgb(argb);
frame.setWidth(width);
frame.setHeight(height);
frame.setPool(pool);
// frame.retain();
for (FaceProcessor processor : preProcessors) {
if (processor.process(this, frame)) {
break;
}
}
if (useDetect) {
long startTime = System.currentTimeMillis();
value = FaceSDKManager.getInstance().getFaceDetector().detect(frame);
FaceInfo[] faces = FaceSDKManager.getInstance().getFaceDetector().getTrackedFaces();
Log.e("wtf", value + " process->" + (System.currentTimeMillis() - startTime));
if (value == 0) {
faceFilter.filter(faces, frame);
}
if (listener != null) {
listener.onDetectFace(value, faces, frame);
}
}
frame.release();
}
其中:
if (useDetect) {
long startTime = System.currentTimeMillis();
value = FaceSDKManager.getInstance().getFaceDetector().detect(frame);
FaceInfo[] faces = FaceSDKManager.getInstance().getFaceDetector().getTrackedFaces();
Log.e("wtf", value + " process->" + (System.currentTimeMillis() - startTime));
if (value == 0) {
faceFilter.filter(faces, frame);
}
if (listener != null) {
listener.onDetectFace(value, faces, frame);
}
}
完成了圖像數據處理並通過OnFaceDetectListener的onDetectFace()回調我們這裏的RgbVideoIdentityActivity。
總結:整個流程,我們可以整理如下
- (1)RgbVideoIdentityActivity活動添加FaceDetectManager.OnFaceDetectListener數據回調監聽實時獲取人臉檢測數據
- (2)獲取FaceDetectManager實例,開啓檢測,並添加OnFrameAvailableListener數據回調監聽
- (3)獲取CameraImageResource實現,在構造方法中添加ICameraControl.OnFrameListener數據回調監聽
- (4)CameraImageResource調用start(),內部調用Camera1Control start()方法,並最終開啓攝像頭獲取原始攝像頭數據
- (5)在獲取攝像頭原始數據的回調Camera.PreviewCallback中回調ICameraControl.OnFrameListener的onPreviewFrame()方法
- (6)在onPreviewFrame()的具體實現中,實現數據轉換,並回調OnFrameAvailableListener的onFrameAvailable()接口
- (7)在onFrameAvailable()的具體實現中,我們又調用FaceDetectManager.OnFaceDetectListener的onDetectFace(),這樣數據就成功的傳到了我們的RgbVideoIdentityActivity中
說複雜也複雜,說簡單也沒有錯,就是從外向內一步一步添加回調監聽,從內向外一步一步調用回調簡單接口傳遞數據。但是不得不說,這裏封裝的接口接口確實很清晰,很容易理解。有很多值得學習的地方。
以上所有代碼均來自百度離線人臉識別Demo,本人未參與編寫。後面,我會介紹結合自己的需求以及百度Demo現有的封裝實現無預覽人臉檢測和識別。
注:歡迎掃碼關注