大家好,我係蒼王。
以下是我這個系列的相關文章,有興趣可以參考一下,可以給個喜歡或者關注我的文章。
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);
}
複製程式碼
相容角度計算,這個是在shader載入的時候需要調整角度的
//獲取角度
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毫秒以上,那麼首次顯示到螢幕上會黑屏一下。
如果大家有優化的方法可以告訴我這邊,我也繼續試驗完善。
新建一個專欄群,希望有興趣的同學多多討論。