源码:https://github.com/huangshuyuan/OpenCvDemo-Master/
一、导入程序
1. 确定SdkVersion
在导入程序之前,我们需要先确定待会的OpenCv工程中的一些和SdkVersion有关的配置,最好的办法就是先用AS建一个HelloWorld,也可以顺便熟悉一下Android Studio的开发流程。在工程中打开build.gradle,可以得到我们需要的信息:
这个随意,和开发的APP的兼容性有关`
compileSdkVersion 26
buildToolsVersion "26.0.2"
defaultConfig {
applicationId "org.opencv.samples.facedetect"
minSdkVersion 14
targetSdkVersion 26
ndk {
moduleName "detection_based_tracker"
abiFilters "armeabi-v7a"
}
}
2.导入samples\face-detection
第一步:
第二步:
找到opencv下载的demo
打开,直接下一步就行,我这边已经导入过了
此时,我们在AS左侧的Project窗口的打开如下的配置文件:
openCVSamplefacedetection->src->build.gradle
修改如下4个配置信息:
compileSdkVersion 26
buildToolsVersion 26.0.2
minSdkVersion 14
targetSdkVersion 26
在修改完成之后,需要重新进行”Gradle project sync”,点击Tools->Android->Sync Project with Gradle Files:
原因在于,工程openCVSamplefacedetection依赖于库工程openCVLibrary340,而库工程openCVLibrary340的build.gradle配置也需要修改。这里不再赘述,找到openCVLibrary340下的build.gradle进行修改即可。
修改完成后再次进行”Gradle project sync”,这一次”Gradle Sync”没有报错,随后AS会进行”Gradle build”,也顺利完成。
3. jni的配置
怎样解决上面出现的问题?
首先,我们需要先配置NDK的路径,点击File->Project Structure,在如下的界面上配置NDK的路径。
然后,在左侧的项目窗口中选中openCVSamplefacedetection,右键点击”Link C++ Project with Gradle”,在弹出的窗口中按照下图选择,点击”OK”。
ok,即可,会在gradle里面生成以下代码
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
接下来,在左右的项目窗口中的“External Build Files”下,选择Android.mk,修改OpenCV.mk的路径:
include ../../sdk/native/jni/OpenCV.mk
修改后的路径为,当然你需要根据自己电脑上的OpenCV4Android路径进行配置:
include D:\OpenCV-android-sdk\sdk\native\jni\OpenCV.mk
在第12行的ndk部分加入以下的声明:
abiFilters "armeabi-v7a"
最终得到:
ndk {
moduleName "detection_based_tracker"
abiFilters "armeabi-v7a"
}
至此,整个jni部分的配置就全部完成,这时再点击”构建“,可以发现已经可以生成成功。
二、剔除OpenCV Manager依赖
在上一小节中,我们已经可以成功地配置fd工程,并且编译生成都成功,此时我们可以将apk部署到手机上进行运行。但是等等,你还想被OpenCV Manager所困扰吗?说实话,每次我想要找到适合自己手机的OpenCV Manager,都要上网查一大堆资料,费时又费劲。
那么接下来我就基于face-detection工程,给大家分享一个去掉OpenCV Manager依赖的方法。
1、把OpenCV sdk for Android文件下F:\OpenCV-android-sdk\sdk\native下的libs文件夹拷贝到你的安卓项目下,即自己的项目\src\main下面,并且将libs改名为 jniLibs(需要)
2、将OpenCV-android-sdk\samples\image-manipulations\res\layout下的xml文件拷贝到自己的项目\src\main\res下面(不需要也行)
3、将OpenCV-android-sdk\samples\image-manipulations\src\org\opencv\samples\imagemanipulations下的java文件拷到自己的项目\src\main\java\你 ,MainActivity所在的包名,即和MainActivity同级目录(我改的是demo,没加)
4、在项目清单文件中为刚才导入的java文件进行配置,加上相应的权限(看demo)
gradle:
sourceSets.main {
jniLibs.srcDir 'src/main/libs' //set .so files directory to libs
jni.srcDirs = [] //disable automatic ndk-build call
}
三、常见问题
1、openCV默认是横屏,改成竖屏不全屏,竖屏无法识别人脸
2、openCV Demo 里面没有获取图片的地方
3、OpenCV Android 打开前置后置摄像头
1、摄像头竖屏全屏的设置
在CameraBridgeViewBase.java 文件修改 deliverAndDrawFrame()函数中,修改以下部分
if (canvas != null) {
canvas.rotate(90,0,0);
float scale = canvas.getWidth() / (float)mCacheBitmap.getHeight();
float scale2 = canvas.getHeight() / (float)mCacheBitmap.getWidth();
if(scale2 > scale){
scale = scale2;
}
if (scale != 0) {
canvas.scale(scale, scale,0,0);
}
canvas.drawBitmap(mCacheBitmap, 0, -mCacheBitmap.getHeight(), null);
// canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);
Log.d(TAG, "mStretch value: " + mScale);
/* 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);
}*/
因为opencv要在横屏时才能得到较好的结果,那么我可以先把竖屏时得到的图像顺时针旋转90度,这样就和横屏时一样了,然后我在把得到识别绿框的图像逆时针旋转90度,再输出这样就能做到竖屏时实现人脸检测了。
所以修改FaActivity中的onCameraViewStarted和onCameraFrame()函数修改如下
Mat Matlin, gMatlin;
int absoluteFaceSize;
public void onCameraViewStarted(int width, int height) {
// mGray = new Mat();
// mRgba = new Mat();
mRgba = new Mat(width, height, CvType.CV_8UC4);
mGray = new Mat(height, width, CvType.CV_8UC4);
Matlin = new Mat(width, height, CvType.CV_8UC4);
gMatlin = new Mat(width, height, CvType.CV_8UC4);
absoluteFaceSize = (int) (height * 0.2);
}
int faceSerialCount = 0;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame InputFrame) {
mGray = InputFrame.gray();
mRgba = InputFrame.rgba();
int rotation = mOpenCvCameraView.getDisplay().getRotation();
//使前置的图像也是正的
Core.flip(mRgba, mRgba, 1);
Core.flip(mGray, mGray, 1);
//MatOfRect faces = new MatOfRect();
if (rotation == Surface.ROTATION_0) {
MatOfRect faces = new MatOfRect();
Core.rotate(mGray, gMatlin, Core.ROTATE_90_CLOCKWISE);
Core.rotate(mRgba, Matlin, Core.ROTATE_90_CLOCKWISE);
if (mJavaDetector != null) {
mJavaDetector.detectMultiScale(gMatlin, faces, 1.1, 2, 2, new Size(absoluteFaceSize, absoluteFaceSize), new Size());
}
Rect[] faceArray = faces.toArray();
for (int i = 0; i < faceArray.length; i++)
Imgproc.rectangle(Matlin, faceArray[i].tl(), faceArray[i].br(), new Scalar(0, 255, 0, 255), 2);
Core.rotate(Matlin, mRgba, Core.ROTATE_90_COUNTERCLOCKWISE);
} else {
MatOfRect faces = new MatOfRect();
if (mJavaDetector != null)
mJavaDetector.detectMultiScale(mGray, faces, 1.1, 2, 2, // TODO: objdetect.CV_HAAR_SCALE_IMAGE
new Size(mAbsoluteFaceSize, mAbsoluteFaceSize), new Size());
Rect[] faceArray = faces.toArray();
for (int i = 0; i < faceArray.length; i++)
Imgproc.rectangle(mRgba, faceArray[i].tl(), faceArray[i].br(), new Scalar(0, 255, 0, 255), 2);
}
return mRgba;
}
2、捕获人脸后自动拍照
捕获人脸后自动拍照,这个需求可能是最最常见的了,那在OpenCV里要如何实现呢?首先我们来观察一下JavaCameraView这个类,它继承自CameraBridgeViewBase
这个类,再往下翻会发现一个非常熟悉的Camera对象,没错这个类里其实是使用了Android原生的API构造了一个相机对象(还好是原生的,至今还没忘却被JNI相机
一旦实现了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;
}
一旦该变量大于0,就让faceSerialCount自增,else的话就清零,如果faceSerialCount>6就调用刚才我们定义的takePhoto方法进
3、OpenCV Android 打开前置后置摄像头
mOpenCvCameraView.setCameraIndex(CameraBridgeViewBase.CAMERA_ID_FRONT);
//前置摄像头 CameraBridgeViewBase.CAMERA_ID_BACK为后置摄像头
兼容性适配
名词解析:
NDK:Native Development Kit
JNI:Java Native Interface
ABI: Application Binary Interface 应用二进制接口
1、Android Studio使用so库
1、使用和eclipse一样在libs目录下新建armeabi目录的方式
需要在build.gradle中添加指定jni库目录的语句
sourceSets {
main.jniLibs.srcDirs = ['libs'] //指定libs为jni的存放目录
}
2、使用AS默认的位置:src/main/jniLibs
直接在src/main/下新建jniLibs目录,将armeabi等目录放到该目录下即可
备注:AS可以直接右键新建同目录下的jniLibs目录,但该目录不是编译好的库文件目录,而是未编译的本地代码文件的目录(这里指的是与java同级的jni目录,放置cpp代码的)
android支持的cpu架构(目前是七种)
cpu | |
---|---|
armeabi | 第5代 ARM v5TE,使用软件浮点运算,兼容所有ARM设备,通用性强,速度慢 |
armeabi-v7a | 第7代 ARM v7,使用硬件浮点运算,具有高级扩展功能 |
arm64-v8a | 第8代,64位,包含AArch32、AArch64两个执行状态对应32、64bit |
x86 | intel 32位,一般用于平板 |
x86_64 | intel 64位,一般用于平板 |
mips | 少接触 |
mips64 | 少接触 |
安装时的兼容性检查:
安装到系统中后,so文件会被提取在:data/app/com.xxxxxxxx.app-x/lib/目录下(5.0版本)、/data/app-lib/目录下(4.2版本),其中armeabi和armeabi-v7a会生成arm目录,arm64-v8a会生成arm64目录。
安装app的时候,如果app使用了so文件,而不存在适合本机cpu架构的so文件,会报如下错误:
Installation failed with message INSTALL_FAILED_NO_MATCHING_ABIS.
例如:在x86模拟器上就必须有x86版本的so文件夹。不然无法安装成功。
运行时的兼容性检查:
1、检查目标目录下是否存在的so库文件
2、检查存在的so文件是否符合当前cpu架构。
对于情况一,一般规避的做法是:保证jnilibs目录下x86、x84_64、armeabi、armeabi-v7a、arm64-v8a等目录下的文件名称数量是一致的。
例如:项目中使用了A、B、C三个第三方库。其中A、B提供了armebi以及arm64-v8a版本的库文件,而C只提供了armebi、armebi-v7a版本的库文件。这时候只能够删除原有的arm64-v8a目录,保留armeabi目录,一般arm64的手机都能兼容使用armeabi版本的库。或者复制一份armeabi的so文件到缺少的目录中(推荐)。
生成so文件:
NDK交叉编译时选定APP_ABI := armeabi x86 ...可以生成支持相应芯片的so文件。APP_ABI := all生成支持所有芯片指令集(目前七种)so文件。
Android加载so文件规则:
当你只提供了armeabi目录时,armeabi-v7a、arm64-v8a架构的程序都会去armeabi里寻找,而当你同时也提供了armeabi-v7a、armeabi-v8a目录,而里面又不存在对应的so库时,系统就不会再去armeabi里面寻找了,直接找不到报错。其他平台也是如此。这里我踩了不少的坑,切记。
一般来说,一些比较有名的第三方库都会提供armeabi、armeabi-v7a、x86这三种类型的so文件,同时拥有这三种版本的app可以在所有机型上运行。另外,越来越多的SDK会同时提供arm64-v8a版本。只包含armeabi的项目也可以在所有设备上运行。
2、ABI
Application.mk 文件如下
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := armeabi-v7a #这句是设置生成的cpu指令类型,提示,目前绝大部分安卓手机支持armeabi,libs下太多类型,编译进去 apk 包会过大
APP_PLATFORM := android-8 #这句是设置最低安卓平台,可以不弄
3、关于abiFilters的使用
在app的gradle的defaultConfig里面加上这么一句
ndk {
abiFilters "armeabi-v7a" // 指定要ndk需要兼容的架构(这样其他依赖包里mips,x86,armeabi,arm-v8之类的so会被过滤掉)
}
这句话的意思就是指定ndk需要兼容的架构,把除了v7a以外的兼容包都过滤掉,只剩下一个v7a的文件夹。
以上是一种ABI的添加,多种如下
APP_ABI :=armeabi-v7a arm64-v8a armeabi mips mips64 x86 x86_64 //Application.mk中空格分开
gradle中逗号分开
ndk {
moduleName "detection_based_tracker"
abiFilters "armeabi-v7a","arm64-v8a","armeabi","mips","mips64","x86","x86_64"
}
官方说
参考文章
Android - OpenCV library
Android Camera动态人脸识别+人脸检测基于OpenCV(无需OpenCVManager) - CSDN博客
Android Studio-—使用OpenCV的配置方法和demo以及开发过程中遇到的问题解决 - 奋斗者—cyf - 博客园
关于opencv的人脸识别的Demo配置(android端不需要Manager) -
Android Studio配置并运行OpenCV4Android的face-detection - Android移动开发技术文章_手机开发 - 红黑联盟
Android NDK 入门与实践
Android.mk | Android Developers
使用AndroidStudio编译NDK的方法及错误解决方案 - 炉火纯青 - 博客园
NDK各版本下载 - CSDN博客
OpenCV Android 打开前置后置摄像头 - CSDN博客
OpenCV学习笔记(七)—— OpenCV for Android实时图像处理 - CSDN博客
【安卓随笔】使用OpenCV进行人脸跟踪和自动拍照 - CSDN博客
Android OpenCV 实例笔记3 -- 摄像头竖屏全屏的设置,更新完整代码 - CSDN博客
OpenCV on Android 开发 (4)竖屏预览图像问题解决方法-续 -