学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为2832字,预计阅读7分钟
前言
接上一篇《Android CameraX NDK OpenCV(二)-- 实现Dnn人脸检测》,本篇我们直接在这个基础上做一个小玩意----人脸替换贴图,其实现在相机里很多都有这个功能了,这里就简单的实现一下。
实现效果
上面是Gif动画和视频的效果,代码还是和上面的一样,最后地址还是会放出来。
效果实现
微卡智享
01
加入的布局按钮
按钮在人脸检测的上传更新的Demo中就已经实现了,不过上篇文章没有说,这里简单的说一下,在activity_main.xml中加入了一个TextView和一个FloatingActionButton。
<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
图像分析处理
@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
换脸贴图
//人脸贴图
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中的函数用到。
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
完
扫描二维码
获取更多精彩
微卡智享
「 往期文章 」
使用.Net5尝鲜的一些小总结及Configuration.Json读取配置文件的使用