大家好,我系苍王。
以下是我这个系列的相关文章,有兴趣可以参考一下,可以给个喜欢或者关注我的文章。
OpenGL和音视频相关的文章,将会在
[OpenGL]未来视觉-MagicCamera3实用开源库 当中给大家呈现
里面会记录我编写这个库的一些经历和经验。
这一章是写图片加载。
你用Android上开发,如果想直接加载一个图片会怎么做?
1.直接使用一个ImageView加载图片?
2.使用一个view的onDraw来绘制一个Bitmap图片数据?
这里ImageView其实在源码里都是通过onDraw方法使用canvas画图片(Drawable对象,并不是bitmap)
而Canvas对象实现在底层中使用了skcanvas库,用于将图片转换到底层硬件可以显示的数据。
下面一个简单的ImageView源码分析ImageView核心源码分析
你需要知道几点
1.ImageView的canvas是通过绘制Drawable对象绘制,并不是bitmap
2.scaleType等转换,是通过java内置Matrix矩阵函数做转换的,最终通过canvas设置matrix矩阵
为什么你的canvas那么慢?浅析Android的canvas性能
这是canvas绘制的原理的一些分析
1.canvas是实用skia库来绘制的,实用cpu计算绘制
2.Android普通的view都是继承于GLES20RecordingCanvas,这个类绘制图都带有硬件加速
3.SurfaceView TextureView里面的Canvas并不是 GLES20RecordingCanvas,所以要特别注意,但是其实用纹理渲染是实用GPU的。
4.从Android绘制效率上说,硬件加速绘制>opengl绘制>canvas绘制
自定义view笔记-之关于硬件加速
这篇是关于硬件加速的一些浅析
1.view无法强制开启硬件加速,只能强制关闭。
2.并不是view的绘制操作都支持硬件加速
那还有其他方式绘制图片吗?如果我要在图片中加入一些滤镜效果应该怎么做?
我们可以使用SurfaceView、TextureView或者GLSurfaceView来绘制图片,他们都会持有canvas对象,而且这些对象都是非硬件加速的。同时他们都可以自定义使用opengles来绘制。
我们上面说过普通的view的canvas都是使用GLES20RecordingCanvas来绘制,从名字上来看就知道是用opengles2.0版本来做硬件加速的转换编写了。
那么我们也是可以通过这些view来自定义加载Opengles的。之前已经介绍过MagicCamera3中相机是使用SurfaceView+Opengles的纹理加载方式来编写的。
GLSurfaceView其本身就自带了GLThread并初始化了EGL环境,系统默认mode==RENDERMODE_CONTINUOUSLY,这样系统会自动重绘;mode==RENDERMODE_WHEN_DIRTY时,只有surfaceCreate的时候会绘制一次,然后就需要通过requestRender()方法主动请求重绘。同时也提到,如果你的界面不需要频繁的刷新最好是设置成RENDERMODE_WHEN_DIRTY,这样可以降低CPU和GPU的活动,可以省电。
而SurfaceView你只会在触发的时候绘制一次,没有模式可以切换。GLSurfaceView是继承于SurfaceView。
下面就说一下怎么使用SurfaceView来绘制一个纹理图片。
首先是要初始化Opengles,和之前介绍的摄像头的opengl的初始化类似,但是要传入surface对象,assets对象,图片的地址,以及图片的角度。
private fun initOpenGL(surface: Surface){
mExecutor.execute {
//传入surface对象,assets对象,图片地址和图片角度
val textureId = OpenGLJniLib.magicImageFilterCreate(surface,BaseApplication.context.assets,imagePath,ExifUtil.getExifOrientation(imagePath))
if (textureId < 0){
Log.e(TAG, "surfaceCreated init OpenGL ES failed!")
return@execute
}
mSurfaceTexture = SurfaceTexture(textureId)
//如果帧图有改变就画图
mSurfaceTexture?.setOnFrameAvailableListener {
//画图
drawOpenGL()
}
}
}
这里如果你有办法使用C++读取到图片数据头Exif数据,最好还是使用C++来做,这边因为网上找了很久都没有能简单使用C++读取exif数据的方法,故在讨巧的使用java层解析读取好,然后再传入native,至于角度有什么作用,就看后面的解析吧。
//图片滤镜surfaceView初始化的时候创建
JNIEXPORT jint JNICALL
Java_com_cangwang_magic_util_OpenGLJniLib_magicImageFilterCreate(JNIEnv *env, jobject obj,
jobject surface,jobject assetManager,jstring imgPath,jint degree) {
std::unique_lock<std::mutex> lock(gMutex);
if(glImageFilter){ //停止摄像头采集并销毁
glImageFilter->stop();
delete glImageFilter;
glImageFilter = nullptr;
}
//初始化native window
ANativeWindow *window = ANativeWindow_fromSurface(env,surface);
//初始化app内获取数据管理
aAssetManager= AAssetManager_fromJava(env,assetManager);
//初始化图片 jstring转为std::string
const char* addressStr = env->GetStringUTFChars(imgPath,0);
std::string nativeAddress = addressStr;
glImageFilter = new ImageFilter(window,aAssetManager,nativeAddress,degree);
env->ReleaseStringUTFChars(imgPath, addressStr);
//创建
return glImageFilter->create();
}
初始化时还需要设置图片角度
void ImageFilter::setFilter(AAssetManager* assetManager) {
if(filter != nullptr){
filter->destroy();
}
filter = new MagicNoneFilter(assetManager);
filter->setPool(pool);
//调整滤镜中的图片的方向问题
filter->setOrientation(degree);
ALOGD("set filter success");
}
void GPUImageFilter::setOrientation(int degree) {
this->degree = degree;
//获取绘制时需要的角度变换,这里只是兼容图片0,90,180,270度
mGLTextureBuffer = getRotation(degree, false, false);
}
兼容角度计算
//获取角度
float* getRotation(int degree, const bool flipHorizontal, const bool flipVertical){
const float* rotateTex;
//调整角度
switch (degree){
case 90:
rotateTex = TEXTURE_ROTATED_90;
break;
case 180:
rotateTex = TEXTURE_ROTATED_180;
break;
case 270:
rotateTex = TEXTURE_ROTATED_270;
break;
case 0:
default:
rotateTex = TEXTURE_NO_ROTATION;
break;
}
//垂直翻转
if (flipHorizontal){
const static float flipTran[]={
flip(rotateTex[0]),rotateTex[1],
flip(rotateTex[2]),rotateTex[3],
flip(rotateTex[4]),rotateTex[5],
flip(rotateTex[6]),rotateTex[7]
};
return const_cast<float *>(flipTran);
}
//水平翻转
if (flipVertical){
const static float flipTran[]={
rotateTex[0],flip(rotateTex[1]),
rotateTex[2],flip(rotateTex[3]),
rotateTex[4],flip(rotateTex[5]),
rotateTex[6],flip(rotateTex[7])
};
return const_cast<float *>(flipTran);
}
return const_cast<float *>(rotateTex);
}
创建纹理
int ImageFilter::create() {
//初始化,清空视口颜色
glDisable(GL_DITHER);
glClearColor(0,0,0,0);
glEnable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST);
//创建EGL环境
if (!mEGLCore->buildContext(mWindow,EGL_NO_CONTEXT)){
return -1;
}
//图片初始化
if (imageInput!= nullptr){
imageInput->init();
}
//滤镜初始化
if (filter!= nullptr)
filter->init();
//获取纹理id
mTextureId = get2DTextureID();
ALOGD("get textureId success");
return mTextureId;
}
输入视口大小,这里需要设置显示图片显示尺寸,以及屏幕尺寸
void ImageFilter::change(int width, int height) {
//设置视口
glViewport(0,0,width,height);
this->mScreenWidth = width;
this->mScreenHeight = height;
if (imageInput!= nullptr){
//触发输入大小更新
imageInput->onInputSizeChanged(width, height);
//初始化图片帧缓冲
imageInput->initFrameBuffer(imageInput->mImageWidth,imageInput->mImageHeight);
if (filter != nullptr){
//设置滤镜宽高
filter->onInputSizeChanged(width,height);
//设置图片的宽高
filter->onInputDisplaySizeChanged(imageInput->mImageWidth,imageInput->mImageHeight);
//设置矩阵
setMatrix(width,height);
//初始化滤镜帧缓冲
filter->initFrameBuffer(imageInput->mImageWidth,imageInput->mImageHeight);
} else{
//销毁图片帧缓冲
imageInput->destroyFrameBuffers();
}
}
}
这个网上找的时候,网上图片是以0度为标准,可以用以下代码来显示。通过正交投影很简单就能完成。
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
// Set the OpenGL viewport to fill the entire surface.
glViewport(0, 0, width, height);
final float aspectRatio = width > height ?
(float) width / (float) height :
(float) height / (float) width;
if (width > height) {
// Landscape
orthoM(projectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f);
} else {
// Portrait or square
orthoM(projectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f);
}
}
但是图片的角度会对图片大小显示比例会有影响,如果调整不正确,显示会问题非常凸显,这里就只区分90和270,还得通过屏幕尺寸、图片角度、图片尺寸来计算出正交矩阵,有些相机拍照后保存的图片是偏移这两种角度的。这里屏幕一直是竖屏方向,还没测试过横屏。
void ImageFilter::setMatrix(int width,int height){
memcpy(mvpMatrix,NONE_MATRIX,16);
if (degree == 90 || degree == 270){ //先判断角度
float x;
if(imageInput->mImageHeight>imageInput->mImageWidth){ //图片宽比高要大 ,屏幕宽/屏幕高 * 屏幕高/屏幕宽
x = width / (float) height *
(float) imageInput->mImageHeight / imageInput->mImageWidth;
} else{ //图片宽比高要大 ,屏幕高/屏幕宽 * 屏幕高/屏幕宽
x = height / (float) width
* (float) imageInput->mImageHeight / imageInput->mImageWidth;
}
ALOGD("x=%f",x);
orthoM(mvpMatrix, 0, -1, 1, -x, x, -1, 1);
} else{ //图片高比宽要大 ,屏幕宽/屏幕高 * 屏幕高/屏幕宽
float y;
if(imageInput->mImageHeight>imageInput->mImageWidth){
y = width / (float) height *
(float) imageInput->mImageHeight / imageInput->mImageWidth;
} else{ //图片高比宽要大 ,屏幕高/屏幕宽 * 屏幕宽/屏幕高
y = height / (float) width
* ((float) imageInput->mImageWidth / imageInput->mImageHeight);
}
ALOGD("y=%f",y);
orthoM(mvpMatrix, 0, -1, 1, -y, y, -1, 1);
}
filter->setMvpMatrix(mvpMatrix);
}
这里计算后显示到屏幕的尺寸是正常的。通过正交矩阵来做缩放比例,视口还是屏幕尺寸。
这种加载比屏幕大很多的图片的时候,会需要一定的延迟,因为解析成纹理也是需要时间的。
经过计算使用stb_image来加载3840*2160的图片,小米6上耗时700毫秒以上,那么首次显示到屏幕上会黑屏一下。
如果大家有优化的方法可以告诉我这边,我也继续试验完善。
新建一个专栏群,希望有兴趣的同学多多讨论。
客户端音视频Opengles群
网友评论