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");
}
@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();
...
}
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);
....
....
}
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;
}
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
所以在我们执行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线程中。
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。
@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绘制。
@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就可以了。
@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 进行合成贴纸
@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图像中。
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