俗话说:
不基于需求的敲代码都是耍流氓
一、人脸检测:
1.准备材料
首先需要准备人脸的训练数据,这个在官方的Github可以下载到,这里用:lbpcascade_frontalface.xml
然后有请世界上,最傻最天真,最美丽,最善良的Girl登场
:
2.Java/Kotlin层面
本想全用Kotlin写的,不过发现Kotlin竟然无法自动生成JNI函数...
但我又懒得找id,就混着用吧,使用TolyCV提供native方法。
---->[src/main/java/com/toly1994/toly_cv/TolyCV.java]---- public class TolyCV { public static native int faceDetector(Bitmap bitmap, Bitmap.Config argb8888, String path); }
在Kotlin的Activity中,点击图片时使用faceDetector,让C++对图片进行操作
由于人脸识别需要xml的模型文件,这里通过copyCascadeFile将文件考到包里
---->[src/main/java/com/toly1994/toly_cv/MainActivity.kt]---- class MainActivity : AppCompatActivity() { private lateinit var mCascadeFile: File private lateinit var mFaceBitmap: Bitmap override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) copyCascadeFile(R.raw.lbpcascade_frontalface,"lbpcascade_frontalface.xml") iv_photo.setOnClickListener { mFaceBitmap = BitmapFactory.decodeResource(resources, R.mipmap.kqq2) val count= TolyCV.faceDetector(mFaceBitmap,Bitmap.Config.ARGB_8888, mCascadeFile.absolutePath) title="检测到$count 个人脸" iv_photo.setImageBitmap(mFaceBitmap) } } companion object { init { System.loadLibrary("toly_cv") } } private fun copyCascadeFile( id:Int,name:String) { try { val inputStream = resources.openRawResource(id) val cascadeDir = getDir("cascade", Context.MODE_PRIVATE) mCascadeFile = File(cascadeDir, name) if (mCascadeFile.exists()) return val os = FileOutputStream(mCascadeFile) val buffer = ByteArray(4096) var bytesRead: Int = inputStream.read(buffer) while (bytesRead != -1) { os.write(buffer, 0, bytesRead) bytesRead = inputStream.read(buffer) } inputStream.close() os.close() } catch (e: IOException) { e.printStackTrace() } } }
3.C++层面使用OpenCV进行人脸识别
很多教程都把代码塞到JNI的cpp里,感觉看着太混乱,太难受了
根据单一职责原则,这里定义一个FaceDetector类
专门用于识别传入的图片数组
并通过detectorFace
方法进行识别后返回识别到的结果集,这样思路就清晰多了。
---->[src/main/cpp/FaceDetector.h]---- #include <android/bitmap.h> #include <opencv2/opencv.hpp> using namespace cv; #include <vector> using std::vector;//有分号 class FaceDetector{ public: //加载文件 static void loadCascade(const char *filename); //识别矩阵,返回脸的矩形列表 static vector<Rect> detectorFace(Mat &src); };
cpp文件进行方法的实现,核心是
CascadeClassifier#detectMultiScale
方法
---->[src/main/cpp/FaceDetector.cpp]---- #include "FaceDetector.h" CascadeClassifier cascadeClassifier; //人脸检测 vector<Rect> FaceDetector::detectorFace(Mat &src) { vector<Rect> faces;//脸的数组 Mat temp_mat;//用于存放识别到的图像临时矩阵 cvtColor(src, temp_mat, COLOR_BGRA2GRAY);//灰度图,加快解析速度 equalizeHist(temp_mat, temp_mat);//直方图均衡化 //多尺度人脸检测 cascadeClassifier.detectMultiScale(temp_mat, faces, 1.1,3,0, Size(300,300)); return faces; } void FaceDetector::loadCascade(const char *filename) { cascadeClassifier.load(filename); }
核心方法
detectMultiScale
介绍:
CV_WRAP void detectMultiScale( InputArray image, 图像 CV_OUT std::vector<Rect>& objects, 人脸目标矩形集 double scaleFactor = 1.1, 每次图像尺寸减小的比例 int minNeighbors = 3, 构成检测目标的相邻矩形的最小个数(默认为3个) int flags = 0, 标识 Size minSize = Size(), 目标的最小尺寸 Size maxSize = Size() ); 目标的最大尺寸
其实上面已经识别出人脸,并到存到一个vector中。现在把它在图像上画出来
#include "FaceDetector.h" extern "C" JNIEXPORT jint JNICALL Java_com_toly1994_toly_1cv_TolyCV_faceDetector(JNIEnv *env, jclass clazz, jobject bitmap, jobject argb8888, jstring path_) { const char *path = env->GetStringUTFChars(path_, 0);//文件路径 FaceDetector::loadCascade(path);//加载文件 Mat srcMat;//图片源矩阵 bitmap2Mat(env, bitmap, &srcMat);//图片源矩阵初始化 auto faces = FaceDetector::detectorFace(srcMat);//识别图片源矩阵,返回矩形集 for (Rect faceRect : faces) {// 在人脸部分画矩形 rectangle(srcMat, faceRect, Scalar(0, 253, 255), 5);//在srcMat上画矩形 mat2Bitmap(env, srcMat, bitmap);// 把mat放回bitmap中 } env->ReleaseStringUTFChars(path_, path);//释放指针 return faces.size();//返回尺寸 }
根据不同的模型数据,可以检测到不同的部位,比如眼睛:
haarcascade_eye.xml
检测也会出现误差,此时可以通过一些判断来筛选结果,比如先检测人脸,之外的部分可以过滤
或者根据两眼间距,计算出不可能的矩形,将其剔除,这也是图片识别比较好玩的地方
二、自动尺寸裁剪
现在需求是:
根据一张照片(尺寸任意),截取人脸及周围,并裁成规定的尺寸,如两寸:413*626
就像这样:
1.Java/Kotlin层
新定义一个native方法faceDetectorResize方法进行执行该功能,返回一个处理过的图片
---->[src/main/java/com/toly1994/toly_cv/TolyCV.java]---- public class TolyCV { public static native int faceDetector(Bitmap bitmap, Bitmap.Config argb8888, String path); public static native Bitmap faceDetectorResize(Bitmap bitmap, Bitmap.Config argb8888 , String path,int width,int height); } ---->[src/main/java/com/toly1994/toly_cv/MainActivity.kt]---- iv_photo.setOnClickListener { mFaceBitmap = BitmapFactory.decodeResource(resources, R.mipmap.kqq) val bitmap= TolyCV.faceDetectorResize(mFaceBitmap,Bitmap.Config.ARGB_8888, mCascadeFile.absolutePath,413,626) iv_photo.setImageBitmap(bitmap) }
2.C++层
这里只针对一个人脸,多个人脸可以采取问题分化的思想。
首先要解决的是区域的问题:这个Rect是何许人也?如果你对一个对象有疑惑,debug是不二人选
extern "C" JNIEXPORT jint JNICALL Java_com_toly1994_toly_1cv_TolyCV_faceDetectorResize( JNIEnv *env, jclass clazz, jobject bitmap, jobject argb8888, jstring path_, jint width, jint height) { const char *path = env->GetStringUTFChars(path_, 0);//文件路径 FaceDetector::loadCascade(path);//加载文件 Mat srcMat;//图片源矩阵 bitmap2Mat(env, bitmap, &srcMat);//图片源矩阵初始化 auto faces = FaceDetector::detectorFace(srcMat);//识别图片源矩阵,返回矩形集 Rect faceRect= faces[0]; rectangle(srcMat, faceRect, Scalar(0, 253, 255), 5);//在srcMat上画矩形 env->ReleaseStringUTFChars(path_, path);//释放指针 return createBitmap(env,srcMat,argb8888);//返回图片 }
知道这些信息,就很容易构建目标区域(红色区域),剩下的就是裁切红色区域了
extern "C" JNIEXPORT jint JNICALL Java_com_toly1994_toly_1cv_TolyCV_faceDetectorResize(JNIEnv *env, jclass clazz, jobject bitmap, jobject argb8888, jstring path_, jint width, jint height) { const char *path = env->GetStringUTFChars(path_, 0);//文件路径 FaceDetector::loadCascade(path);//加载文件 Mat srcMat;//图片源矩阵 bitmap2Mat(env, bitmap, &srcMat);//图片源矩阵初始化 auto faces = FaceDetector::detectorFace(srcMat);//识别图片源矩阵,返回矩形集 Rect faceRect= faces[0]; rectangle(srcMat, faceRect, Scalar(0, 253, 255), 5);//在srcMat上画矩形 //识别目标区域区域--------------------------- Rect zone; int a= faceRect.width;//宽 int b= faceRect.height;//高 int offSetLeft=a/4;//x偏移 int offSetTop=b*0.5; zone.x=faceRect.x-offSetLeft; zone.y=faceRect.y-offSetTop; zone.width= a/4 *2+a; zone.height=zone.width*(height*1.0/width); rectangle(srcMat, zone, Scalar(253, 95, 47), 5);//在srcMat上画矩形 env->ReleaseStringUTFChars(path_, path);//释放指针 return createBitmap(env,srcMat,argb8888);//返回图片 }
裁剪是非常简单的
createBitmap(env,srcMat(zone),argb8888);//返回图片 复制代码
Mat类重载
()运算符
可以传入一个矩形,实现是通过构造生成一个新Mat
这样就完成了既定比例的裁切,并保证人脸始终在中上部。
---->[mat.hpp#Mat::operator()]---- /** @overload @param roi Extracted submatrix specified as a rectangle. */ Mat operator()( const Rect& roi ) const; ---->[mat.inl.cpp#Mat::operator()]---- inline Mat Mat::operator()( const Rect& roi ) const { return Mat(*this, roi); }
另外有一点需要注意:
当矩形范围超出Mat,会报错
,应该可以通过添白来处理,Mark一下
最后只剩重设尺寸了,
注意把你画的矩形线给去掉,不然会输出到结果中
extern "C" JNIEXPORT jobject JNICALL Java_com_toly1994_toly_1cv_TolyCV_faceDetectorResize(JNIEnv *env, jclass clazz, jobject bitmap, jobject argb8888, jstring path_, jint width, jint height) { //英雄所见... env->ReleaseStringUTFChars(path_, path);//释放指针 resize(srcMat(zone),srcMat,Size(width,height));//<----重定义尺寸 return createBitmap(env,srcMat,argb8888);//返回图片 }
OK,打完收工,再也不怕妹子让我帮她设置图片尺寸了。
对于大批量,形形色色的人物照片,想要裁剪规整,一个for循环搞定,程序是绝佳劳动力。
这样你对OpenCV应该多了那么一丢丢感觉了吧,其实只是在调一调已有的方法