基於ArkUI框架開發——圖片模糊處理的實現

OpenHarmony開發者發表於2023-05-05

原文:https://mp.weixin.qq.com/s/vwXVj5vmAxDRG_jTk_8hPA,點選連結檢視更多技術內容。

現在市面上有很多APP,都或多或少對圖片有模糊上的設計,所以,圖片模糊效果到底怎麼實現的呢?

首先,我們來了解下模糊效果的對比

圖片

從視覺上,兩張圖片,有一張是模糊的,那麼,在實現圖片模糊效果之前,我們首先需要了解圖片模糊的本質是什麼?

在此介紹模糊本質之前,我們來了解下當前主流的兩個移動端平臺(Android與iOS)的實現。對Android開發者而言,比較熟悉且完善的圖片變換三方庫以glide-transformations(https://github.com/wasabeef/glide-transformations)為樣例,來看看它是基於什麼實現的。

Android中有兩種實現:
1、 FastBlur,根據stackBlur模糊演算法來操作圖片的畫素點實現效果,但效率低,已過時。
2、 RenderScript,這個是Google官方提供的,用來在Android上編寫一套高效能程式碼的語言,可以執行在CPU及其GPU上,效率較高。

而對iOS開發者而言,GPUImage(https://github.com/BradLarson/GPUImage/)比較主流。我們可以在其中看到高斯模糊過濾器(GPUImageGaussianBlurFilter),它裡面是根據OpenGL來實現,透過GLSL語言定義的著色器,操作GPU單元,達到模糊效果。

所以,我們可以看出,操作GPU來達到我們所需要的效果效率更高。因此我們在OpenHarmony上也能透過操作GPU,來實現我們想要的高效能模糊效果。

迴歸正題,先來了解下模糊的本質是什麼?

本質

模糊,可以理解為圖片中的每個畫素點都取其周邊畫素的平均值。

圖片

上圖M點的畫素點就是我們的焦點畫素。周圍ABCDEFGH都是M點(焦點)周圍的畫素點,那麼根據模糊的概念:M(rgb)  =(A+B+C+D+E+F+G+H)/ 8

我們根據畫素點的r、g、b值,得到M點的畫素點值,就這樣,一個一個畫素點的操作,中間點相當於失去視覺上的焦點,整個圖片就產生模糊的效果。但這樣一邊倒的方式,在模糊的效果上,達不到需求的,所以,我們就需要根據這個模糊的本質概念,去想想,加一些東西或者更改取平均值的規則,完成我們想要的效果。故,高斯模糊,一個家喻戶曉的名字,就出現在我們面前。

高斯模糊

高斯模糊,運用了正態分佈函式,進行各個加權平均,正態分佈函式如下:

圖片

其中引數:μ為期望值,σ為標準差,當μ=0,σ=0的時候,為標準的正態分佈,其形狀參考如下圖:

圖片

可以看出:

其一,離中心點越近,分配的權重就越高。這樣我們在計算圖片的焦點畫素值時,將該點當作中心點,當作1的權重,其他周圍的點,按照該正態分佈的位置,去分配它的權重,這樣我們就可以根據該正態分佈函式及其各個點的畫素ARGB值,算出經過正態分佈之後的畫素ARGB值。

其二,離中心點越近,若是設定的模糊半徑很小,代表其模糊的焦點周圍的畫素點離焦點的畫素相差就不大,這樣模糊的效果就清晰。而模糊半徑越大,其周圍分佈的畫素色差就很大,這樣的模糊效果就越模糊。

透過圖片的寬高拿到每個畫素點的資料,再根據這個正態分佈公式,得到我們想要的畫素點的ARGB值,之後將處理過的畫素點重新寫入到圖片中,就能實現我們想要的圖片模糊效果。

流程

根據上面的闡述,就可以梳理出在OpenHarmony中的具體的實現流程:
● 獲取整張圖片的畫素點資料
● 迴圈圖片的寬高,獲取每個畫素點的焦點
● 在上述迴圈裡,根據焦點按照正態分佈公式進行加權平均,算出各個焦點周圍新的畫素值
● 將各個畫素點寫入圖片

關鍵依賴OpenHarmony系統基礎能力如下:

第一、獲取圖片的畫素點,系統有提供一次性獲取整張圖片的畫素點資料,其介面如下。

readPixelsToBuffer(dst: ArrayBuffer): Promise<void>;
readPixelsToBuffer(dst: ArrayBuffer, callback: AsyncCallback<void>): void;

可以看出,系統將獲取到畫素點資料ARGB值,儲存到ArrayBuffer中去。

第二、迴圈獲取每個畫素點,將其x、y點的畫素點當作焦點。

for (y = 0; y < imageHeight; y++) {  for (x = 0; x < imageWidth; x++) {        //......   獲取當前的畫素焦點x、y  }}

第三、迴圈獲取焦點周圍的畫素點(以焦點為原點,以設定的模糊半徑為半徑)。

for ( let m = centPointY-radius; m < centPointY+radius; m++) {  for ( let n = centPointX-radius; n < centPointX+radius; n++) {     //......     this.calculatedByNormality(...); //正態分佈公式化處理畫素點     //......  }}

第四、將各個圖片的畫素資料寫入圖片中。系統有提供一次性寫入畫素點,其介面如下。

writeBufferToPixels(src: ArrayBuffer): Promise;writeBufferToPixels(src: ArrayBuffer, callback: AsyncCallback): void;

透過上面的流程,我們可以在OpenHarmony系統下,獲取到經過正態分佈公式處理的畫素點,至此圖片模糊效果已經實現。

但是,經過測試發現,這個方式實現模糊化的過程,很耗時,達不到我們的效能要求。若是一張很大的圖片,就單單寬高迴圈來看,比如1920*1080寬高的圖片就要迴圈2,073,600次,非常耗時且對裝置的CPU也有非常大的消耗,因此我們還需要對其進行效能最佳化。

模糊效能最佳化思路

如上面所訴,考慮到OpenHarmony的環境的特點及其系統提供的能力,可以考慮如下幾個方面進行最佳化:

第一、參照社群已有成熟的圖片模糊演算法處理,如(Android的FastBlur)。
第二、C層效能要比JS層更好,將畫素點的資料處理,透過NAPI機制,將其放入C層處理。如:將其迴圈獲取焦點及其透過正態分佈公式處理的都放到C層中處理。
第三、基於系統底層提供的OpenGL,操作頂點著色器及片元著色器操作GPU,得到我們要的模糊效果。

首先,我們來根據Android中的FastBlur模糊化處理,參照其實現原理進行在基於OpenHarmony系統下實現的程式碼如下:

let imageInfo = await bitmap.getImageInfo();
let size = {
  width: imageInfo.size.width,
  height: imageInfo.size.height
}

if (!size) {
  func(new Error("fastBlur The image size does not exist."), null)
  return;
}

let w = size.width;
let h = size.height;
var pixEntry: Array<PixelEntry> = new Array()
var pix: Array<number> = new Array()


let bufferData = new ArrayBuffer(bitmap.getPixelBytesNumber());
await bitmap.readPixelsToBuffer(bufferData);
let dataArray = new Uint8Array(bufferData);

for (let index = 0; index < dataArray.length; index+=4) {
  const r = dataArray[index];
  const g = dataArray[index+1];
  const b = dataArray[index+2];
  const f = dataArray[index+3];

  let entry = new PixelEntry();
  entry.a = 0;
  entry.b = b;
  entry.g = g;
  entry.r = r;
  entry.f = f;
  entry.pixel = ColorUtils.rgb(entry.r, entry.g, entry.b);
  pixEntry.push(entry);
  pix.push(ColorUtils.rgb(entry.r, entry.g, entry.b));
}

let wm = w - 1;
let hm = h - 1;
let wh = w * h;
let div = radius + radius + 1;

let r = CalculatePixelUtils.createIntArray(wh);
let g = CalculatePixelUtils.createIntArray(wh);
let b = CalculatePixelUtils.createIntArray(wh);

let rsum, gsum, bsum, x, y, i, p, yp, yi, yw: number;
let vmin = CalculatePixelUtils.createIntArray(Math.max(w, h));

let divsum = (div + 1) >> 1;
divsum *= divsum;
let dv = CalculatePixelUtils.createIntArray(256 * divsum);
for (i = 0; i < 256 * divsum; i++) {
  dv[i] = (i / divsum);
}
yw = yi = 0;
let stack = CalculatePixelUtils.createInt2DArray(div, 3);
let stackpointer, stackstart, rbs, routsum, goutsum, boutsum, rinsum, ginsum, binsum: number;
let sir: Array<number>;
let r1 = radius + 1;
for (y = 0; y < h; y++) {
  rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
  for (i = -radius; i <= radius; i++) {
    p = pix[yi + Math.min(wm, Math.max(i, 0))];
    sir = stack[i + radius];
    sir[0] = (p & 0xff0000) >> 16;
    sir[1] = (p & 0x00ff00) >> 8;
    sir[2] = (p & 0x0000ff);
    rbs = r1 - Math.abs(i);
    rsum += sir[0] * rbs;
    gsum += sir[1] * rbs;
    bsum += sir[2] * rbs;
    if (i > 0) {
      rinsum += sir[0];
      ginsum += sir[1];
      binsum += sir[2];
    } else {
      routsum += sir[0];
      goutsum += sir[1];
      boutsum += sir[2];
    }
  }
  stackpointer = radius;

  for (x = 0; x < w; x++) {

    r[yi] = dv[rsum];
    g[yi] = dv[gsum];
    b[yi] = dv[bsum];

    rsum -= routsum;
    gsum -= goutsum;
    bsum -= boutsum;

    stackstart = stackpointer - radius + div;
    sir = stack[stackstart % div];

    routsum -= sir[0];
    goutsum -= sir[1];
    boutsum -= sir[2];

    if (y == 0) {
      vmin[x] = Math.min(x + radius + 1, wm);
    }
    p = pix[yw + vmin[x]];

    sir[0] = (p & 0xff0000) >> 16;
    sir[1] = (p & 0x00ff00) >> 8;
    sir[2] = (p & 0x0000ff);

    rinsum += sir[0];
    ginsum += sir[1];
    binsum += sir[2];

    rsum += rinsum;
    gsum += ginsum;
    bsum += binsum;

    stackpointer = (stackpointer + 1) % div;
    sir = stack[(stackpointer) % div];

    routsum += sir[0];
    goutsum += sir[1];
    boutsum += sir[2];

    rinsum -= sir[0];
    ginsum -= sir[1];
    binsum -= sir[2];

    yi++;
  }
  yw += w;
}
for (x = 0; x < w; x++) {
  rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
  yp = -radius * w;
  for (i = -radius; i <= radius; i++) {
    yi = Math.max(0, yp) + x;

    sir = stack[i + radius];

    sir[0] = r[yi];
    sir[1] = g[yi];
    sir[2] = b[yi];

    rbs = r1 - Math.abs(i);

    rsum += r[yi] * rbs;
    gsum += g[yi] * rbs;
    bsum += b[yi] * rbs;

    if (i > 0) {
      rinsum += sir[0];
      ginsum += sir[1];
      binsum += sir[2];
    } else {
      routsum += sir[0];
      goutsum += sir[1];
      boutsum += sir[2];
    }

    if (i < hm) {
      yp += w;
    }
  }
  yi = x;
  stackpointer = radius;
  for (y = 0; y < h; y++) {
    // Preserve alpha channel: ( 0xff000000 & pix[yi] )
    pix[yi] = (0xff000000 & pix[Math.round(yi)]) | (dv[Math.round(rsum)] << 16) | (dv[
    Math.round(gsum)] << 8) | dv[Math.round(bsum)];

    rsum -= routsum;
    gsum -= goutsum;
    bsum -= boutsum;

    stackstart = stackpointer - radius + div;
    sir = stack[stackstart % div];

    routsum -= sir[0];
    goutsum -= sir[1];
    boutsum -= sir[2];

    if (x == 0) {
      vmin[y] = Math.min(y + r1, hm) * w;
    }
    p = x + vmin[y];

    sir[0] = r[p];
    sir[1] = g[p];
    sir[2] = b[p];

    rinsum += sir[0];
    ginsum += sir[1];
    binsum += sir[2];

    rsum += rinsum;
    gsum += ginsum;
    bsum += binsum;

    stackpointer = (stackpointer + 1) % div;
    sir = stack[stackpointer];

    routsum += sir[0];
    goutsum += sir[1];
    boutsum += sir[2];

    rinsum -= sir[0];
    ginsum -= sir[1];
    binsum -= sir[2];

    yi += w;
  }
}


let bufferNewData = new ArrayBuffer(bitmap.getPixelBytesNumber());
let dataNewArray = new Uint8Array(bufferNewData);
let index = 0;

for (let i = 0; i < dataNewArray.length; i += 4) {
  dataNewArray[i] = ColorUtils.red(pix[index]);
  dataNewArray[i+1] = ColorUtils.green(pix[index]);
  dataNewArray[i+2] = ColorUtils.blue(pix[index]);
  dataNewArray[i+3] = pixEntry[index].f;
  index++;
}
await bitmap.writeBufferToPixels(bufferNewData);
if (func) {
  func("success", bitmap);
}

從上面程式碼,可以看出,按照FastBlur的邏輯,還是逃不開上層去處理單個畫素點,逃不開圖片寬高的迴圈。經過測試也發現,在一張400*300的圖片上,完成圖片的模糊需要十幾秒,所以第一個最佳化方案,在js環境上是行不通的。

其次,將其畫素點處理,透過NAPI的機制,將畫素點資料ArrayBuffer傳入到C層,由於在C層也需要迴圈去處理每個畫素點,傳入大資料的ArrayBuffer時對系統的native的消耗嚴重。最後經過測試也發現,模糊的過程也很緩慢,達不到效能要求。

所以對比分析之後,最終的最佳化方案是採取系統底層提供的OpenGL,透過GPU去作業系統的圖形處理器,解放出CPU的能力。

基於OpenGL操作GPU來提升模糊效能

在進行基於OpenGL進行效能提升前,我們需要了解OpenGL中的頂點著色器(vertex shader)及其片元著色器(fragment shader)。著色器(shader)是執行在GPU上的最小單元,功能是將輸入轉換輸出且各個shader之間是不能通訊的,需要使用的開發語言GLSL。這裡就不介紹GLSL的語言規則了。

頂點著色器(vertex shader)
確定要畫圖片的各個頂點(如:三角形的角的頂點),注意:每個頂點執行一次。一旦最終位置已知,OpenGL將獲取可見的頂點集,並將它們組裝成點、線和三角形。且以逆時針繪製的。

片元著色器(fragment shader)
生成點、線或三角形的每個片元的最終顏色,並對每個fragment執行一次。fragment是單一顏色的小矩形區域,類似於計算機螢幕上的畫素,簡單的說,就是將頂點著色器形成的點、線或者三角形區域,新增顏色。

片元著色器的主要目的是告訴GPU每個片元的最終顏色應該是什麼。對於圖元(primitive)的每個fragment,片元著色器將被呼叫一次,因此如果一個三角形對映到10000個片元,那麼片元著色器將被呼叫10000次。

OpenGL簡單的繪製流程:
讀取頂點資訊 ----------> 執行頂點著色器 ----------> 圖元裝配----------> 執行片元著色器----------> 往幀緩衝區寫入----------> 螢幕上最終效果

簡單的說,就是根據頂點著色器形成的點、線、三角形形成的區域,由片元著色器對其著色,之後就將這些資料寫入幀緩衝區(Frame Buffer)的記憶體塊中,再由螢幕顯示這個緩衝區。

那模糊的效果怎麼來實現呢?

首先我們來定義我們的頂點著色器及其片元著色器。如下程式碼:頂點著色器:

const char vShaderStr[] =
      "#version 300 es                            \n"
      "layout(location = 0) in vec4 a_position;   \n"
      "layout(location = 1) in vec2 a_texCoord;   \n"
      "out vec2 v_texCoord;                       \n"
      "void main()                                \n"
      "{                                          \n"
      "   gl_Position = a_position;               \n"
      "   v_texCoord = a_texCoord;                \n"
      "}                                          \n";

片元著色器:

const char fShaderStr0[] =
    "#version 300 es                                    \n"
    "precision mediump float;                           \n"
    "in vec2 v_texCoord;                                \n"
    "layout(location = 0) out vec4 outColor;            \n"
    "uniform sampler2D s_TextureMap;                    \n"
    "void main()                                        \n"
    "{                                                  \n"
    "    outColor = texture(s_TextureMap, v_texCoord);  \n"
    "}";

其中version代表OpenGL的版本,layout在GLSL中是用於著色器的輸入或者輸出,uniform為一致變數。在著色器執行期間一致變數的值是不變的,只能在全域性範圍進行宣告,gl_Position是OpenGL內建的變數(輸出屬性-變換後的頂點的位置,用於後面的固定的裁剪等操作。所有的頂點著色器都必須寫這個值),texture函式是openGL採用2D紋理繪製。然後,我們還需要定義好初始的頂點座標資料等;

//頂點座標
const GLfloat vVertices[] = {
      -1.0f, -1.0f, 0.0f, // bottom left
      1.0f, -1.0f, 0.0f, // bottom right
      -1.0f,  1.0f, 0.0f, // top left
      1.0f,  1.0f, 0.0f, // top right
};

//正常紋理座標
const GLfloat vTexCoors[] = {
      0.0f, 1.0f, // bottom left
      1.0f, 1.0f, // bottom right
      0.0f, 0.0f, // top left
      1.0f, 0.0f, // top right
};

//fbo 紋理座標與正常紋理方向不同(上下映象)
const GLfloat vFboTexCoors[] = {
      0.0f, 0.0f,  // bottom left
      1.0f, 0.0f,  // bottom right
      0.0f, 1.0f,  // top left
      1.0f, 1.0f,  // top right
};

下面就進行OpenGL的初始化操作,獲取display,用來建立EGLSurface的

m_eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); 

初始化 EGL 方法

eglInitialize(m_eglDisplay, &eglMajVers, &eglMinVers)

獲取 EGLConfig 物件,確定渲染表面的配置資訊

eglChooseConfig(m_eglDisplay, confAttr, &m_eglConf, 1, &numConfigs)

建立渲染表面 EGLSurface,使用 eglCreatePbufferSurface 建立螢幕外渲染區域

m_eglSurface = eglCreatePbufferSurface(m_eglDisplay, m_eglConf, surfaceAttr)

建立渲染上下文 EGLContext

m_eglCtx = eglCreateContext(m_eglDisplay, m_eglConf, EGL_NO_CONTEXT, ctxAttr);

繫結上下文

eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglCtx)

透過預設的頂點著色器與片元著色器,載入到GPU中

GLuint GLUtils::LoadShader(GLenum shaderType, const char *pSource)
{
    GLuint shader = 0;
    shader = glCreateShader(shaderType);
    if(shader)
    {
         glShaderSource(shader, 1, &pSource, NULL);
         glCompileShader(shader);
         GLint compiled = 0;
         glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
         if (!compiled){
            GLint infoLen = 0;
            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
           if (infoLen)
           {
               char* buf = (char*) malloc((size_t)infoLen);
               if (buf)
               {
                   glGetShaderInfoLog(shader, infoLen, NULL, buf);
                    LOGI("gl--> GLUtils::LoadShader Could not link shader:%{public}s", buf);
                   free(buf);
               }
               glDeleteShader(shader);
               shader = 0;
           }
         }
    }
   return shader;
}

建立一個空的著色器程式物件
program = glCreateProgram();

將著色器物件附加到program物件

glAttachShader(program, vertexShaderHandle);glAttachShader(program, fragShaderHandle);

連線一個program物件
glLinkProgram(program);

建立並初始化緩衝區物件的資料儲存

glGenBuffers(3, m_VboIds);
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vVertices), vVertices, GL_STATIC_DRAW);
     
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vFboTexCoors), vTexCoors, GL_STATIC_DRAW);
     
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_VboIds[2]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
     
glGenVertexArrays(1, m_VaoIds);
     
glBindVertexArray(m_VaoIds[0]);

到這,整個OpenGL的初始化操作,差不多完成了,接下來,我們就要去基於OpenGL去實現我們想要的模糊效果。

考慮到模糊的效果,那麼我們需要給開發者提供模糊半徑blurRadius、模糊偏移量blurOffset、模糊的權重sumWeight。所以我們需要在我們模糊的片元著色器上,定義開發者輸入,其模糊的片元著色器程式碼如下:

const char blurShaderStr[] =
                     "#version 300 es\n"
                     "precision highp float;\n"
                        "uniform lowp sampler2D s_TextureMap;\n" 
                     "in vec2 v_texCoord;\n"
                     "layout(location = 0) out vec4 outColor;\n"
                     
                        "uniform highp int blurRadius;\n" 
                        "uniform highp vec2 blurOffset;\n" 
                        "\n" 
                        "uniform highp float sumWeight;\n" 
                     "float PI = 3.1415926;\n"
                        "float getWeight(int i)\n"
                     "{\n" 
                        "float sigma = float(blurRadius) / 3.0;\n"    
                        "return (1.0 / sqrt(2.0 * PI * sigma * sigma)) * exp(-float(i * i) / (2.0 * sigma * sigma)) / sumWeight;\n"
                     "}\n" 
                      
                     "vec2 clampCoordinate(vec2 coordinate)\n" 
                     "{\n" 
                        " return vec2(clamp(coordinate.x, 0.0, 1.0), clamp(coordinate.y, 0.0, 1.0));\n" 
                     "}\n" 
                        "\n" 
                        "void main()\n" 
                        "{\n" 
                        "vec4 sourceColor = texture(s_TextureMap, v_texCoord);\n" 
                        "if (blurRadius <= 1)\n" 
                        "{\n" 
                           "outColor = sourceColor;\n" 
                           "return;\n" 
                        "}\n" 
                        "float weight = getWeight(0);\n" 
                        "vec3 finalColor = sourceColor.rgb * weight;\n" 
                        "for (int i = 1; i < blurRadius; i++)\n" 
                        "{\n" 
                           "weight = getWeight(i);\n" 
                           "finalColor += texture(s_TextureMap, clampCoordinate(v_texCoord - blurOffset * float(i))).rgb * weight;\n" 
                           "finalColor += texture(s_TextureMap, clampCoordinate(v_texCoord + blurOffset * float(i))).rgb * weight;\n" 
                        "}\n" 
                        "outColor = vec4(finalColor, sourceColor.a);\n" 
                        "}\n";

裡面的邏輯暫時就不介紹了,有興趣的朋友可以去研究研究。

透過上述的LoadShader函式將其片元著色器載入到GPU的執行單元中去。

m_ProgramObj = GLUtils::CreateProgram(vShaderStr, blurShaderStr, m_VertexShader,
                             m_FragmentShader);
if (!m_ProgramObj)
{
   GLUtils::CheckGLError("Create Program");
   LOGI("gl--> EGLRender::SetIntParams Could not create program.");
   return;
}

m_SamplerLoc = glGetUniformLocation(m_ProgramObj, "s_TextureMap");
m_TexSizeLoc = glGetUniformLocation(m_ProgramObj, "u_texSize");

然後我們就需要將圖片的整個畫素資料傳入;

定義好ts層的方法:

setImageData(buf: ArrayBuffer, width: number, height: number) {
    if (!buf) {
        throw new Error("this pixelMap data is empty");
    }
    if (width <= 0 || height <= 0) {
        throw new Error("this pixelMap of width and height is invalidation");
    }
    this.width = width;
    this.height = height;
    this.ifNeedInit();
    this.onReadySize();
    this.setSurfaceFilterType();
    this.render.native_EglRenderSetImageData(buf, width, height);
};

將ArrayBuffer資料傳入NAPI層。透過napi_get_arraybuffer_info NAPI獲取ArrayBuffer資料。

napi_value EGLRender::RenderSetData(napi_env env, napi_callback_info info) {
      ....
      
      void* buffer;   
      size_t bufferLength;
      napi_status buffStatus=   napi_get_arraybuffer_info(env,args[0],&buffer,&bufferLength);
      if (buffStatus != napi_ok) {
          return nullptr;
      }
      
      ....
      
     EGLRender::GetInstance()->SetImageData(uint8_buf, width, height);
     return nullptr;
 }

將其資料繫結到OpenGL中的紋理中去

void EGLRender::SetImageData(uint8_t *pData, int width, int height){
    if (pData && m_IsGLContextReady)
    {
        ...
        
        m_RenderImage.width = width;
        m_RenderImage.height = height;
        m_RenderImage.format = IMAGE_FORMAT_RGBA;
        NativeImageUtil::AllocNativeImage(&m_RenderImage);
        memcpy(m_RenderImage.ppPlane[0], pData, width*height*4);
         
        glBindTexture(GL_TEXTURE_2D, m_ImageTextureId);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]);
        glBindTexture(GL_TEXTURE_2D, GL_NONE);
        
        ....
    }
}

然後就是讓開發者自己定義模糊半徑及其模糊偏移量,透過OpenGL提供的

glUniform1i(location,(int)value);   設定int 片元著色器blurRadius變數
glUniform2f(location,value[0],value[1]);  設定float陣列  片元著色器blurOffset變數

將半徑及其偏移量設定到模糊的片元著色器上。之後,透過GPU將其渲染

napi_value EGLRender::Rendering(napi_env env, napi_callback_info info){
      // 渲染
      glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
      glBindVertexArray(GL_NONE);  
      glBindTexture(GL_TEXTURE_2D, GL_NONE);
      return nullptr;
}

最後,就剩下獲取圖片畫素的ArrayBuffer資料了,透過glReadPixels讀取到指定區域內的畫素點了

glReadPixels(x,y,surfaceWidth,surfaceHeight,GL_RGBA,GL_UNSIGNED_BYTE,pixels);

但是,在這裡,因為OpenGL裡面的座標系,在2D的思維空間上,與我們通常認知的是倒立的,所以需要對畫素點進行處理,得到我們想要的畫素點集

 int totalLength= width * height * 4;
 int oneLineLength = width * 4;
 uint8_t* tmp = (uint8_t*)malloc(totalLength);
 memcpy(tmp, *buf, totalLength);
 memset(*buf,0,sizeof(uint8_t)*totalLength);
 for(int i = 0 ; i< height;i ++){
       memcpy(*buf+oneLineLength*i, tmp+totalLength-oneLineLength*(i+1), oneLineLength);
 }
 free(tmp);

最後在上層,透過系統提供的createPixelMap得到我們想要的圖片,也就是模糊的圖片。

getPixelMap(x: number, y: number, width: number, height: number): Promise<image.PixelMap>{
    .....
    
    let that = this;
    return new Promise((resolve, rejects) => {
        that.onDraw();
        let buf = this.render.native_EglBitmapFromGLSurface(x, y, width, height);
        if (!buf) {
            rejects(new Error("get pixelMap fail"))
        } else {
            let initOptions = {
                size: {
                    width: width,
                    height: height
                },
                editable: true,
            }
            image.createPixelMap(buf, initOptions).then(p => {
                resolve(p);
            }).catch((e) => {
                rejects(e)
            })
        }
    })
}

綜上,本篇文章介紹了由單純的在JS中用正態分佈公式操作畫素點實現模糊效果,引出效能問題,最後到基於OpenGL實現模糊效果的最佳化,最後效能上也從模糊一張大圖片要十幾秒提升到100ms內,文章就介紹到這了,歡迎有興趣的朋友,可以參考學習下,下面提供具體的專案原始碼地址。

專案地址:https://gitee.com/openharmony-tpc/ImageKnife/tree/master/gpu_...

圖片

相關文章