首先我们需要去官网下载一份OpenCV的SDK,点击打开官网下载,截止到本文发布,最新版本为V3.2,那我们就以此版本为例。
一、在Android Studio中导入OpenCV
1.新建一个安卓工程。
2.点击File->New->Import Module,选择到刚才下载并解压过的OpenCV SDK的java目录,Module Name自己起一个见面知意的就行了,然后一路Next,最后Finish。此处也可以将java目录里的所有内容复制到自己的项目里,不过会让项目看起来很臃肿,所以建议使用Module的方式引入。
3.此时不出意外会出现一些gradle的错误,不要急着下载缺少的文件,直接根据自己项目的gradle文件来修改这个Module的gradle就行,然后重新Sync即可。
4.打开Project Structure,选中左侧的app,点击加号,选择第三个Module Dependency,然后选择我们刚才添加的OpenCV的Module即可完成依赖。
5.然后在自己工程的里创建jniLibs文件夹,注意不是Module的目录中,然后将\OpenCV-android-sdk\sdk\native\libs下的文件夹复制进去,这里我就只复制armeabi和armeabi-v7a,大家可以根据需求自己挑选。
6.在res下创建raw文件夹,将\OpenCV-android-sdk\sdk\etc\lbpcascades下的lbpcascade_frontalface.xml复制进去,这个是OpenCV的人脸模型文件,以后需要用到。
7.在清单文件中添加如下权限:
8.最终项目是这个样子就添加成功了。
二、人脸跟踪的调用
直接将如下代码添加到项目Activity中,这里面初始化了OpenCV和人脸模型文件,通过SDK中的JavaCameraView调用相机,具体源码有兴趣可以自己去看,等会我们也会进行一些探究。
public class OpenCvCameraActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener {
JavaCameraView openCvCameraView;
private CascadeClassifier cascadeClassifier;
private Mat grayscaleImage;
private int absoluteFaceSize;
private void initializeOpenCVDependencies() {
try {
// Copy the resource into a temp file so OpenCV can load it
InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);
File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
File mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");
FileOutputStream os = new FileOutputStream(mCascadeFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
is.close();
os.close();
// Load the cascade classifier
cascadeClassifier = new CascadeClassifier(mCascadeFile.getAbsolutePath());
} catch (Exception e) {
Log.e("OpenCVActivity", "Error loading cascade", e);
}
// And we are ready to go
openCvCameraView.enableView();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_open_cv_camera);
openCvCameraView = (JavaCameraView) findViewById(R.id.jcv);
openCvCameraView.setCameraIndex(-1);
openCvCameraView.setCvCameraViewListener(this);
}
@Override
public void onResume() {
super.onResume();
if (!OpenCVLoader.initDebug()) {
Log.e("log_wons", "OpenCV init error");
}
initializeOpenCVDependencies();
}
@Override
public void onCameraViewStarted(int width, int height) {
grayscaleImage = new Mat(height, width, CvType.CV_8UC4);
// The faces will be a 20% of the height of the screen
absoluteFaceSize = (int) (height * 0.2);
}
@Override
public void onCameraViewStopped() {
}
@Override
public Mat onCameraFrame(Mat aInputFrame) {
// Create a grayscale image
Imgproc.cvtColor(aInputFrame, grayscaleImage, Imgproc.COLOR_RGBA2RGB);
MatOfRect faces = new MatOfRect();
// Use the classifier to detect faces
if (cascadeClassifier != null) {
cascadeClassifier.detectMultiScale(grayscaleImage, faces, 1.1, 2, 2,
new Size(absoluteFaceSize, absoluteFaceSize), new Size());
}
// If there are any faces found, draw a rectangle around it
Rect[] facesArray = faces.toArray();
int faceCount = facesArray.length;
for (int i = 0; i < facesArray.length; i++) {
Imgproc.rectangle(aInputFrame, facesArray[i].tl(), facesArray[i].br(), new Scalar(0, 255, 0, 255), 3);
}
return aInputFrame;
}
}
布局文件:
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
android:id="@+id/jcv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:paddingStart="0dp"
app:paddingEnd="0dp"
/>
然后运行程序,就可以看到效果了。
三、拍照界面的调整
这样虽然已经可以识别人脸,但是左右两侧会留下黑色的边框,在一些机器上只需要去掉上方的标题栏即可实现全屏,但在某些机器上这样还是会留下黑框,这个
问题在国外的论坛里也是被问的很多的,但一直没有一个很明确的答复,这里我就抛砖引玉提出一种解决方法。首先在OpenCV的包里找到CameraBridgeViewBase,
找到416行附近,做如下修改:
原始代码:
if (mScale != 0) {
canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),
(int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),
(int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),
(int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);
} else {
canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,
(canvas.getHeight() - mCacheBitmap.getHeight()) / 2,
(canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(),
(canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null);
}
修改为:
if (mScale != 0) {
canvas.drawBitmap(mCacheBitmap, new Rect(0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
new Rect(0, 0, canvas.getWidth(), canvas.getHeight()),null);
} else {
canvas.drawBitmap(mCacheBitmap, new Rect(0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
new Rect(0, 0, canvas.getWidth(), canvas.getHeight()),null);
}
这样即可实现全屏,原理是直接对整个Canvas进行绘制,强行拉伸了画面,会使比例有一些不对,如果谁有更好的办法欢迎发出来。
四、捕获人脸后自动拍照
捕获人脸后自动拍照,这个需求可能是最最常见的了,那在OpenCV里要如何实现呢?首先我们来观察一下JavaCameraView这个类,它继承自CameraBridgeViewBase
这个类,再往下翻会发现一个非常熟悉的Camera对象,没错这个类里其实是使用了Android原生的API构造了一个相机对象(还好是原生的,至今还没忘却被JNI相机
支配的恐惧...),然后这个类实现了PreviewCallback接口,经常做相机开发的同学一点不陌生,那么我们就从这里入手吧。
一旦实现了PreviewCallback接口,肯定会有onPreviewFrame(byte[] frame,Camera camera)这个回调函数,这里面的字节数组frame对象,就是当前的视频帧,注意这里是视频
帧,是YUV编码的,并不能直接转换为Bitmap。这个回调函数在预览过程中会一直被调用,那么只要确定了哪一帧有人脸,只需要在这里获取就行,代码如下。
private boolean takePhotoFlag = false;
@Override
public void onPreviewFrame(byte[] frame, Camera arg1) {
if (takePhotoFlag){
Camera.Size previewSize = mCamera.getParameters().getPreviewSize();
BitmapFactory.Options newOpts = new BitmapFactory.Options();
newOpts.inJustDecodeBounds = true;
YuvImage yuvimage = new YuvImage(
frame,
ImageFormat.NV21,
previewSize.width,
previewSize.height,
null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
yuvimage.compressToJpeg(new Rect(0, 0, previewSize.width, previewSize.height), 100, baos);
byte[] rawImage = baos.toByteArray();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bmp = BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length, options);
try {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.flush();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
bmp.recycle();
takePhotoFlag = false;
}
synchronized (this) {
mFrameChain[mChainIdx].put(0, 0, frame);
mCameraFrameReady = true;
this.notify();
}
if (mCamera != null)
mCamera.addCallbackBuffer(mBuffer);
}
这里我们先在外层声明一个布尔类型的变量,在无限的回调过程中,一旦次布尔值为真,就对该视频帧保存为文件,上面的代码就将YUV视频帧转换为Bitmap对象的方法,然后
将Bitmap存成文件,当然这也的结构不太合理,我只是为了展示方便这样书写。
现在已经可以抓取照片了,那么如何才能判断是不是有人脸来修改这个布尔值呢,我们再定义这样一个方法:
public void takePhoto(String name){
fileName = name;
takePhotoFlag = true;
}
一旦调用了takePhoto这个方法,传入一个保存路径,就能修改此布尔值,完成拍照,我们离完成越来越接近了。那么回到我们一开始的Activity,这里面包含刚刚修改的
JavaCameraView对象,可以对他进行操作。然后找到Activity的onCameraFrame回调函数,修改为:
int faceSerialCount = 0;
@Override
public Mat onCameraFrame(Mat aInputFrame) {
Imgproc.cvtColor(aInputFrame, grayscaleImage, Imgproc.COLOR_RGBA2RGB);
MatOfRect faces = new MatOfRect();
if (cascadeClassifier != null) {
cascadeClassifier.detectMultiScale(grayscaleImage, faces, 1.1, 2, 2,
new Size(absoluteFaceSize, absoluteFaceSize), new Size());
}
Rect[] facesArray = faces.toArray();
int faceCount = facesArray.length;
if (faceCount > 0) {
faceSerialCount++;
} else {
faceSerialCount = 0;
}
if (faceSerialCount > 6) {
openCvCameraView.takePhoto("sdcard/aaa.jpg");
faceSerialCount = -5000;
}
for (int i = 0; i < facesArray.length; i++) {
Imgproc.rectangle(aInputFrame, facesArray[i].tl(), facesArray[i].br(), new Scalar(0, 255, 0, 255), 3);
}
return aInputFrame;
}
首先在外层定义一个faceSerialCount的整数,代表人脸连续出现的次数。当使用OpenCV的CascadeClassifier后,可以回去当前人脸的个数,然后我们用faceCount来记录下来,
一旦该变量大于0,就让faceSerialCount自增,else的话就清零,如果faceSerialCount>6就调用刚才我们定义的takePhoto方法进行拍照,这样一切就大功告成了。这里再解释
下为何让连续出现的次数大于6是再拍照,因为有可能只出现一次时拍照会有很模糊的情况,或者识别到了一个非人脸的东西,这属于误差,所以当6帧都有人脸时,基本可以判
断当前可以拍照,具体这个阈值大家可再自己探索。
现在一切都完成了,但这样还是让项目加入了好多so文件和Module,那么有没有办法更加简洁呢,甚至一个文件就搞定?下次我将分享AAR组件开发的相关经验,谢谢大家的
捧场,如有哪里不对,欢迎指正。