Android OpenGL+Camera2渲染(3)

您所在的位置:网站首页 眼球贴纸名字 Android OpenGL+Camera2渲染(3)

Android OpenGL+Camera2渲染(3)

2024-01-11 09:43| 来源: 网络整理| 查看: 265

Android OpenGL+Camera2渲染(1) —— OpenGL简单介绍

Android OpenGL+Camera2渲染(2) —— OpenGL实现Camera2图像预览

Android OpenGL+Camera2渲染(3) —— 大眼,贴纸功能实现

Android OpenGL+Camera2渲染(4) —— 美颜功能实现

Android OpenGL+Camera2渲染(5) —— 录制视频,实现快录慢录

———————————————— 版权声明:本文为CSDN博主「行走的荷尔蒙CC」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/wangchao1412/article/details/103832867

在实现大眼,贴纸的功能前提,贴纸需要知道人脸的坐标点,大眼自然是需要找到眼睛的坐标点。

本篇使用OpenCV来定位人脸, SeetaFaceEngine中的Alignment模块来定位眼睛,使用的原因的opencv定位眼睛的模型不好用,测试seeta中的眼睛定位还是可以的,可以定位五个点,左眼、右眼、鼻子、左嘴巴、右嘴巴。

 

首先需要交叉编译opencv,拿到库文件和头文件,对于seeTaAlignment,把代码直接拿过来,参考它的makeList文件修改即可。

我这里是先把模型文件加载到了本地,opencv需要的 lbpcascade_frontalface_improved 和 seeta 定位大眼的seeta_fa_v1.1.bin。

public GlRenderWrapper(GlRenderView glRenderView) { this.glRenderView = glRenderView; Context context = glRenderView.getContext(); //拷贝 模型 OpenGlUtils.copyAssets2SdCard(context, "lbpcascade_frontalface_improved.xml", "/sdcard/lbpcascade_frontalface.xml"); OpenGlUtils.copyAssets2SdCard(context, "seeta_fa_v1.1.bin", "/sdcard/seeta_fa_v1.1.bin"); } 在onSurfaceChanged中,创建FaceTracker @Override public void onSurfaceChanged(GL10 gl, int width, int height) { //1080 1899 .... .... / tracker = new FaceTracker("/sdcard/lbpcascade_frontalface.xml", "/sdcard/seeta_fa_v1.1.bin", camera2Helper); tracker.startTrack(); ... } FaceTracker jni类, public class FaceTracker { static { System.loadLibrary("native-lib"); } .... .... //传入模型文件, 创建人脸识别追踪器和人眼定位器 private native long native_create(String model, String seeta); //开始追踪 private native void native_start(long self); //停止追踪 private native void native_stop(long self); //检测人脸 private native Face native_detector(long self, byte[] data, int cameraId, int width, int height); .... .... } 这里我们只看一下native_detector的实现。 JNIEXPORT jobject JNICALL Java_com_example_cameraglrender_face_FaceTracker_native_1detector(JNIEnv *env, jobject thiz, jlong self, jbyteArray data_, jint camera_id, jint width, jint height) { if (self == 0) { return NULL; } jbyte *data = env->GetByteArrayElements(reinterpret_cast(data_), NULL); FaceTracker *faceTracker = reinterpret_cast(self); Mat src(height + height / 2, width, CV_8UC1, data); //颜色格式的转换 nv21->RGBA //将 nv21的yuv数据转成了rgba cvtColor(src, src, COLOR_YUV2RGBA_I420); // 正在写的过程 退出了,导致文件丢失数据 if (camera_id == 1) { //前置摄像头,需要逆时针旋转90度 rotate(src, src, ROTATE_90_COUNTERCLOCKWISE); //水平翻转 镜像 flip(src, src, 1); } else { //顺时针旋转90度 rotate(src, src, ROTATE_90_CLOCKWISE); } Mat gray; //灰色 cvtColor(src, gray, COLOR_RGBA2GRAY); //增强对比度 (直方图均衡) equalizeHist(gray, gray); vector rects; //送去定位 faceTracker->detector(gray, rects); env->ReleaseByteArrayElements(data_, data, 0); int w = src.cols; int h = src.rows; src.release(); int ret = rects.size(); LOGD(" ret :%d", ret); if (ret) { jclass clazz = env->FindClass("com/example/cameraglrender/face/Face"); jmethodID costruct = env->GetMethodID(clazz, "", "(IIII[F)V"); int size = ret * 2; //创建java 的float 数组 jfloatArray floatArray = env->NewFloatArray(size); for (int i = 0, j = 0; i < size; j++) { float f[2] = {rects[j].x, rects[j].y}; env->SetFloatArrayRegion(floatArray, i, 2, f); i += 2; } Rect2f faceRect = rects[0]; int width = faceRect.width; int height = faceRect.height; jobject face = env->NewObject(clazz, costruct, width, height, w, h, floatArray); return face; } return 0; }

 

faceTracker->detector(gray, rects);

 

void FaceTracker::detector(Mat src, vector &rects) { vector faces; //检测人脸 tracker->process(src); //拿到人脸坐标信息 tracker->getObjects(faces); if (faces.size()) { Rect face = faces[0]; rects.push_back(Rect2f(face.x, face.y, face.width, face.height)); //seeta 可以检测五个坐标点 seeta::FacialLandmark points[5]; seeta::ImageData imageData(src.cols, src.rows); imageData.data = src.data; seeta::FaceInfo faceInfo; seeta::Rect bbox; bbox.x = face.x; bbox.y = face.y; bbox.width = face.width; bbox.height = face.height; faceInfo.bbox = bbox; //检测 人眼 等五个点 faceAlignment->PointDetectLandmarks(imageData, faceInfo, points); for (int i = 0; i < 5; ++i) { rects.push_back(Rect2f(points[i].x, points[i].y, 0, 0)); } } }

这里首先将传入的图像,旋转摆正,保证图像是正的,进行灰度化,直方图均衡,因为彩色图像对于定位没有任何作用,只能增加程序执行负担,执行 faceTracker->detector定位,传入 vector rects;这里把opemcv定位到的人脸,和seeta定位的五个点全部放入vector集合中,然后判断rects的长度大小,不为0代表识别到了,通过 FindClass反射java中Face类,把坐标点赋值到Face类中,然后返回Face对象。

 

所以在我们执行native_detector就可以拿到人脸和人眼的坐标点操作了。

 

然后呢,定位是需要图像的,所以在Camera2中,使用ImageReader获取图像。

private void createCameraPreviewSession() { try { // This is the output Surface we need to start preview. mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface surface = new Surface(mSurfaceTexture); // We set up a CaptureRequest.Builder with the output Surface. mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.addTarget(surface); mPreviewRequestBuilder.addTarget(imageReader.getSurface()); // Here, we create a CameraCaptureSession for camera preview. mCameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { ..... ... .... }, null ); } catch (CameraAccessException e) { e.printStackTrace(); } } mPreviewRequestBuilder.addTarget(imageReader.getSurface()); 把ImageReader交给mPreviewRequestBuilder,有图像会自动回掉 ImageReader.OnImageAvailableListener中 onImageAvailable方法。这里需要从ImageReader 中读取YUV byte数组。

ImageReader 读取 YUV420数据。

private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireNextImage(); if (image == null) { return; } Image.Plane[] planes = image.getPlanes(); int width = image.getWidth(); int height = image.getHeight(); byte[] yBytes = new byte[width * height]; byte[] uBytes = new byte[width * height / 4]; byte[] vBytes = new byte[width * height / 4]; byte[] i420 = new byte[width * height * 3 / 2]; for (int i = 0; i < planes.length; i++) { int dstIndex = 0; int uIndex = 0; int vIndex = 0; int pixelStride = planes[i].getPixelStride(); int rowStride = planes[i].getRowStride(); ByteBuffer buffer = planes[i].getBuffer(); byte[] bytes = new byte[buffer.capacity()]; buffer.get(bytes); int srcIndex = 0; if (i == 0) { for (int j = 0; j < height; j++) { System.arraycopy(bytes, srcIndex, yBytes, dstIndex, width); srcIndex += rowStride; dstIndex += width; } } else if (i == 1) { for (int j = 0; j < height / 2; j++) { for (int k = 0; k < width / 2; k++) { uBytes[dstIndex++] = bytes[srcIndex]; srcIndex += pixelStride; } if (pixelStride == 2) { srcIndex += rowStride - width; } else if (pixelStride == 1) { srcIndex += rowStride - width / 2; } } } else if (i == 2) { for (int j = 0; j < height / 2; j++) { for (int k = 0; k < width / 2; k++) { vBytes[dstIndex++] = bytes[srcIndex]; srcIndex += pixelStride; } if (pixelStride == 2) { srcIndex += rowStride - width; } else if (pixelStride == 1) { srcIndex += rowStride - width / 2; } } } System.arraycopy(yBytes, 0, i420, 0, yBytes.length); System.arraycopy(uBytes, 0, i420, yBytes.length, uBytes.length); System.arraycopy(vBytes, 0, i420, yBytes.length + uBytes.length, vBytes.length); if (onPreviewListener != null) { onPreviewListener.onPreviewFrame(i420, i420.length); } } image.close(); } };

回掉到onPreviewFrame方法。

@Override public void onPreviewFrame(byte[] data, int len) { if (tracker != null && (stickEnable || bigEyeEnable)) tracker.detector(data); }

判断贴纸功能或则大眼功能是否开启,开启,就送去定位坐标。

detector 会到另外一个线程中去执行,是FaceTracker中的Handler子线程中,避免因为识别太久,阻塞预览界面卡顿。

mHandler = new Handler(mHandlerThread.getLooper()) { @Override public void handleMessage(Message msg) { //子线程 耗时再久 也不会对其他地方 (如:opengl绘制线程) 产生影响 synchronized (FaceTracker.this) { //定位 线程中检测 mFace = native_detector(self, (byte[]) msg.obj, cameraHelper.getCameraId(), cameraHelper.getSize().getWidth(),cameraHelper.getSize().getHeight()); } } };

现在我们需要人脸坐标,直接拿FaceTracker的mFace就可以了。

 

以上是定位人脸和眼睛坐标的实现,接下来就是实现大眼和贴纸的功能了。

大眼和贴纸的开关:

public void enableBigEye(final boolean isChecked) { queueEvent(new Runnable() { @Override public void run() { glRender.enableBigEye(isChecked); } }); } public void enableBeauty(final boolean isChecked) { queueEvent(new Runnable() { @Override public void run() { glRender.enableBeauty(isChecked); } }); }

所有OpenGL的渲染,都需要在GL线程中执行,使用 queueEvent 把执行抛给GL线程中。

glRender 也就是 GlRenderWrapper 中的实现 public void enableBigEye(boolean isChecked) { this.bigEyeEnable = isChecked; if (isChecked) { bigeyeFilter = new BigEyeFilter(glRenderView.getContext()); bigeyeFilter.prepare(screenSurfaceWid, screenSurfaceHeight, screenX, screenY); } else { bigeyeFilter.release(); bigeyeFilter = null; } } public void enableBeauty(boolean isChecked) { this.beautyEnable = isChecked; if (isChecked) { beaytyFilter = new BeautifyFilter(glRenderView.getContext()); beaytyFilter.prepare(screenSurfaceWid, screenSurfaceHeight, screenX, screenY); } else { beaytyFilter.release(); beaytyFilter = null; } }

根据开关,创建 BigEyeFilter和 BeautifyFilter 来执行,都是FBO的操作。BigEyeFilter和BeautifyFilter的创建和

https://blog.csdn.net/wangchao1412/article/details/103833620

一文中提到的差不多,区别就是大眼的片元着色器中,需要传入

uniform vec2 left_eye;//左眼 uniform vec2 right_eye;//右眼

两个坐标点。

看一下大眼的片元着色器。 precision mediump float; varying vec2 aCoord; uniform sampler2D vTexture; uniform vec2 left_eye;//左眼 uniform vec2 right_eye;//右眼 //实现 公式 : 得出需要采集的改变后的点距离眼睛中心点的位置 // r:原来的点距离眼睛中心点的位置 //rmax: 放大区域 float fs(float r, float rmax){ //放大系数 float a = 0.4; return (1.0 - pow((r/rmax -1.0), 2.0) *a); } //根据需要采集的点 aCoord 计算新的点(可能是需要改变为眼睛内部的点,完成放大的效果) vec2 newCoord(vec2 coord, vec2 eye, float rmax){ vec2 q = coord; //获得当前需要采集的点与眼睛的距离 float r = distance(coord, eye); //在范围内 才放大 if (r < rmax){ //想要方法需要采集的点 与 眼睛中心点的距离 float fsr = fs(r, rmax); // 新点-眼睛 / 老点-眼睛 = 新距离/老距离 //(newCoord - eye) / (coord-eye) = fsr/r; //(newCoord - eye) = fsr/r * (coord-eye) q = fsr * (coord - eye) +eye; } return q; } void main(){ //最大作用半径 rmax //计算两个点的距离 float rmax = distance(left_eye, right_eye)/2.0; // 如果属于 左眼 放大区域的点 得到的就是 左眼里面的某一个点(完成放大效果) // 如果属于 右眼放大区域的点 或者都不属于 ,那么 newCoord还是 aCoord vec2 q = newCoord(aCoord, left_eye, rmax); q = newCoord(q, right_eye, rmax); // 采集到 RGBA 值 gl_FragColor = texture2D(vTexture, q); }

就是根据左右眼的距离的一般认为是大眼有效作用的最大范围,使用固定的公式,算出大眼区域中,外围的像素转为眼睛内的像素操作,赋值给 gl_FragColor。

onDrawFrame中 @Override public void onDrawFrame(GL10 gl) { int textureId; // 配置屏幕 //清理屏幕 :告诉opengl 需要把屏幕清理成什么颜色 GLES20.glClearColor(0, 0, 0, 0); //执行上一个:glClearColor配置的屏幕颜色 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); //更新获取一张图 mSurfaceTexture.updateTexImage(); mSurfaceTexture.getTransformMatrix(mtx); //cameraFiler需要一个矩阵,是Surface和我们手机屏幕的一个坐标之间的关系 cameraFilter.setMatrix(mtx); textureId = cameraFilter.onDrawFrame(mTextures[0]); if (bigEyeEnable) { bigeyeFilter.setFace(tracker.mFace); textureId = bigeyeFilter.onDrawFrame(textureId); } if (stickEnable) { stickerFilter.setFace(tracker.mFace); textureId = stickerFilter.onDrawFrame(textureId); } int id = screenFilter.onDrawFrame(textureId); }

首先把获取到的人脸信息传入bigeyeFilter和stickerFilter中,执行onDrawFrame进行FBO绘制。

bigeyeFilter.onDrawFrame @Override public int onDrawFrame(int textureId) { if (mFace == null) return textureId; GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]); GLES20.glUseProgram(mProgramId); mGlVertexBuffer.position(0); GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGlVertexBuffer); GLES20.glEnableVertexAttribArray(vPosition); mGlTextureBuffer.position(0); GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGlTextureBuffer); GLES20.glEnableVertexAttribArray(vCoord); /** * 传递眼睛的坐标 给GLSL */ float[] landmarks = mFace.landmarks; //左眼的x 、y opengl : 0-1 float x = landmarks[2] / mFace.imgWidth; float y = landmarks[3] / mFace.imgHeight; left.clear(); left.put(x); left.put(y); left.position(0); GLES20.glUniform2fv(left_eye, 1, left); //右眼的x、y x = landmarks[4] / mFace.imgWidth; y = landmarks[5] / mFace.imgHeight; right.clear(); right.put(x); right.put(y); right.position(0); GLES20.glUniform2fv(right_eye, 1, right); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); GLES20.glUniform1i(vTexture, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); return mFBOTextures[0]; }

把左眼和右眼的坐标传如片元着色器,片元着色器中已经写好了转换代码,然后glDrawArrays就可以了。

stickerFilter.onDrawFrame @Override public int onDrawFrame(int textureId) { if (null == mFace) return textureId; GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]); GLES20.glUseProgram(mProgramId); mGlVertexBuffer.position(0); GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGlVertexBuffer); GLES20.glEnableVertexAttribArray(vPosition); mGlTextureBuffer.position(0); GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGlTextureBuffer); GLES20.glEnableVertexAttribArray(vCoord); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); GLES20.glUniform1i(vTexture, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); onDrawStick(); return mFBOTextures[0]; }

把大眼的FBO纹理ID,写入到当前的FBO纹理ID中,然后执行onDrawStick 进行合成贴纸

首先合成贴纸,需要有一个专门存储贴纸的纹理ID,是在prepare方法中实现的 @Override public void prepare(int width, int height, int x, int y) { super.prepare(width, height,x,y); mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.erduo_000); mTextureId = new int[1]; OpenGlUtils.glGenTextures(mTextureId); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId[0]); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); }

把图片转成Bitmap,把bitmap放入纹理所绑定的2D图像中。

onDrawStick() 讲贴纸进行 private void onDrawStick() { //开启混合模式 GLES20.glEnable(GLES20.GL_BLEND); //设置贴图模式 // 1:src 源图因子 : 要画的是源 (耳朵) // 2: dst : 已经画好的是目标 (从其他filter来的图像) //画耳朵的时候 GL_ONE:就直接使用耳朵的所有像素 原本是什么样子 我就画什么样子 // 表示用1.0减去源颜色的alpha值来作为因子 // 耳朵不透明 (0,0 (全透明)- 1.0(不透明)) 目标图对应位置的像素就被融合掉了 不见了 GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); float x = mFace.landmarks[0]; float y = mFace.landmarks[1]; //这里的坐标是相对于 传入opencv识别的图像的像素,需要转换为在屏幕的位置 x = x / mFace.imgWidth * mOutputWidth; y = y / mFace.imgHeight * mOutputHeight; //要绘制的位置和大小,贴纸是画在耳朵上的,直接锁定人脸坐标就可以 GLES20.glViewport((int) x, (int) y - mBitmap.getHeight(), (int) ((float) mFace.width / mFace.imgWidth * mOutputWidth), mBitmap.getHeight()); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]); GLES20.glUseProgram(mProgramId); mGlVertexBuffer.position(0); GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGlVertexBuffer); GLES20.glEnableVertexAttribArray(vPosition); mGlTextureBuffer.position(0); GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGlTextureBuffer); GLES20.glEnableVertexAttribArray(vCoord); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId[0]); GLES20.glUniform1i(vTexture, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); GLES20.glDisable(GLES20.GL_BLEND); }

GLES20.glEnable 开启混合模式,就会把贴纸叠加在现有的图像上,而不是覆盖,然后根据人脸的坐标,锁定要绘制的区域,贴纸就画在锁定的区域上进行混合。

 

github项目地址:https://github.com/wangchao0837/OpenGlCameraRender

 

 

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3