Android CameraX NDK OpenCV(三)-- 人脸贴图替换

2021-01-06 12:04:47 浏览数 (1)

前言

接上一篇《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

0 人点赞