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

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<jbyteArray>(data_), NULL);

    FaceTracker *faceTracker = reinterpret_cast<FaceTracker *>(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<Rect2f> 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, "<init>", "(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<Rect2f> &rects) {

    vector<Rect> 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<Rect2f> 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

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值