前言
接上一篇《Android CameraX NDK OpenCV(二)-- 实现Dnn人脸检测》,本篇我们直接在这个基础上做一个小玩意----人脸替换贴图,其实现在相机里很多都有这个功能了,这里就简单的实现一下。
实现效果
上面是Gif动画和视频的效果,代码还是和上面的一样,最后地址还是会放出来。
效果实现
微卡智享
01
加入的布局按钮
按钮在人脸检测的上传更新的Demo中就已经实现了,不过上篇文章没有说,这里简单的说一下,在activity_main.xml中加入了一个TextView和一个FloatingActionButton。
代码语言:javascript复制 <TextView android:id="@ id/tvStatus" android:layout_width="wrap_content" android:layout_height="40dp" android:layout_marginBottom="24dp" android:textSize="13pt" android:gravity="center" android:text="TextView" android:textColor="@color/design_default_color_secondary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toRightOf="@id/btnchange" />
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@ id/btnchange" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="32dp" android:layout_marginBottom="24dp" android:scaleType="fitCenter" android:src="@drawable/convert" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" />
然后是MainActivity中加入这两个组件
定义了一个显示类型,默认0为灰度图,然后定义了一个MutableListof的动态列表,后面再加功能的话,直接在这里修改就可以。
02
点击按钮切换
按钮的事件中写实现方式,上面定义的itype类型,每点击一次就 1然后再除List的集合数取余,这样就实现了点击循环的方式,showtvStatus就是让文本显示出当前的状态。
调用的setTypeId的方式里面,重点说一下,这里用的是mView.postDelayed的方式,后面有50毫秒的延时,主要是如果直接用post在测试的过程中点击有的时候并没有切换,而且存入到的缓存中,当下一次触发post的时候两次都执行,改成postDelayed的方式后解决这个问题。
03
图像分析处理
代码语言:javascript复制 @SuppressLint("UnsafeExperimentalUsageError") override fun analyze(imgProxy: ImageProxy) { val image = imgProxy.image if (image == null) { imgProxy.close() return }
try { //将ImageProxy图像转为ByteArray val buffer = ImageUtils.imageProxyToByteArray(imgProxy) var bytes: ByteArray? = buffer var w = image.width var h = image.height
//判断如果是竖屏,图像旋转90度 if (mView.width < mView.height) { //根据宽度和高度将图像旋转90度 bytes = ImageUtils.rotateYUVDegree90(buffer, w, h) //设置变量当宽和高修改过来 w = image.height h = image.width } else { //用的横屏PAD测试后,发布横屏的要将图像旋转180度 //正常的横屏应该不用处理这个,如果遇到不对,可以屏蔽这一句 bytes = ImageUtils.rotateYUVDegree180(buffer, w, h) }
when (mTypeId) { //0-灰度图 0 -> { //调用Jni实现灰度图并返回图像的Pixels val grayPixels = jni.grayShow(bytes!!, w, h) //将Pixels转换为Bitmap然后画图 grayPixels?.let { val bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) bmp.setPixels(it, 0, w, 0, 0, w, h) val str = "width:${w}" " height:${h}"
mView.post { mView.drawBitmap(bmp) mView.drawText(str) } } } //1-人脸检测 1 -> { //调用人脸检测返回矩形 val detectorRects = jni.facedetector(bytes!!, w, h) //判断如果检测到 detectorRects?.let { mView.post { mView.drawRect(it, w, h) } } } //2-贴图换脸 2 -> { //调用人脸检测返回矩形 val faceRects = jni.facedetector(bytes!!, w, h) //判断如果检测到 faceRects?.let { mView.post { mView.drawfaceBitmap(it, w, h) } } } } } catch (e: Exception) { Log.e("except", e.message.toString()) Snackbar.make(mView, e.message.toString(), Snackbar.LENGTH_SHORT).show() } finally { imgProxy.close() } }
上面的分析处理中,把原来的if else改为了when的写法,处理的流程比较简单,还是用的人脸检测,返回的矩形,只不过在画矩形时不能再调用原来人脸检测的那个红框了,需要改为指定位置画图片的方式。
04
换脸贴图
代码语言:javascript复制 //人脸贴图 private var mFaceBitmap = BitmapFactory.decodeResource(resources, R.drawable.vaccae) private var mFaceRect = Rect(0, 0, mFaceBitmap.width, mFaceBitmap.height) private var mFaceRects: List<Rect>? = null
在ViewOverlay中加入了专门为人脸贴图定义的几个变量,mFaceBitmap直接加载的资源里面的png图片,mFaceRect的矩形也是直接获取加载后的mFaceBitmap的矩形大小,定义的这两个主要为了drawBitmap中的函数用到。
代码语言:javascript复制 fun drawfaceBitmap(rect: List<Rect>?, w: Int = width, h: Int = height) { rect?.let { mFaceRects = rect mScaleWidth = w.toFloat() / width mScaleHeight = h.toFloat() / height } invalidate() } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas)
try { mBmp?.let { canvas?.drawBitmap(it, x, y, Paint()) } mRects?.let { it.forEach { p -> p.left = (p.left / mScaleWidth).toInt() p.top = (p.top / mScaleHeight).toInt() p.right = (p.right / mScaleWidth).toInt() p.bottom = (p.bottom / mScaleHeight).toInt() canvas?.drawRect(p, paint); } } mText?.let { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { val builder = StaticLayout.Builder.obtain(it, 0, it.length, textpaint, width) val myStaticLayout = builder.build() canvas?.let { t -> t.translate(x, y) myStaticLayout.draw(t) } } else { canvas?.drawText(it, x, y, textpaint) } }
mFaceRects?.let { it.forEach { p -> p.left = (p.left / mScaleWidth).toInt() - 10 p.top = (p.top / mScaleHeight).toInt() - 10 p.right = (p.right / mScaleWidth).toInt() 10 p.bottom = (p.bottom / mScaleHeight).toInt() 10
canvas?.drawBitmap( mFaceBitmap, mFaceRect, p, Paint() ) } }
} catch (e: Exception) { e.message?.let { Snackbar.make(this, it, Snackbar.LENGTH_SHORT).show() } } }
onDraw事件里针对每个一Rect矩形,我们都在原矩形的基础上再扩大10,所以除了位置偏移后再对每个点做了一个10的固定偏移,最后用drawBitmap画出图像就实现了贴图的效果。
Demo地址
https://github.com/Vaccae/AndroidCameraXNDKOpenCV.git
完