1. 程式人生 > 實用技巧 >使用 WebGL 進行實時視訊影象處理

使用 WebGL 進行實時視訊影象處理

這是我最近在 CodePen 上製作的 WebGL 演示案例。它可以捕獲網路攝像頭的資料(或在無法訪問網路攝像頭時,從placekitten獲取備用影象),並將其實時轉換為 ASCII 影象藝術。

為了獲得更多的復古性,我使用了 90 年代 DOS PC 中常見的 8x8 畫素光柵字型(您可能會在某些 BIOS 中看到這種字型)。

要將影象內容對映到特定字元,我通過使用亮度圖選擇最佳匹配。我計算每個 4x4 正方形的畫素。在畫板內向下滾動以檢視亮度圖:

我還為這些字型建立了一個編輯器:https://terabaud.github.io/

辦公資源網址導航 https://www.wode007.com

熊貓辦公https://www.wode007.com/sites/73654.html

若干 WebGL 基礎知識

我將介紹 WebGL 的一些基礎知識,但這裡僅涉及部分問題。獲取有關詳細指導,建議您訪問https://webglfundamentals.org

對於 WebGL,一個常見誤解是把它當作瀏覽器中的 3D 引擎。儘管 WebGL技術能使我們在瀏覽器中提供 GPU 加速的 3D 內容,但 WebGL 本身不是 3D 引擎。在 WebGL 之上,有專門用於 GPU 加速的 2D 或 3D 內容的圖形庫(例如用於 2D 的 Pixi,用於 3D 的 Threejs)。

WebGL 本身是很基礎的繪圖標準庫,並且是一個以 GPU 加速的方式,將點、線和三角形繪製到

html<canvas> 元素上的庫。

可以通過 getContext(類似於 2D canvas API)檢索 WebGL 渲染上下文:

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

一個 WebGL 程式包含多個著色器元件,著色器是執行在 GPU 上的程式碼,它們不是用JavaScript編寫的,而是具有自己的語言,稱為 GLSL(GL 著色器語言)。

GLSL 快速概覽

  • 類似 C語言,著色器程式包含 void main()
  • 變數宣告也像在 C 語言中一樣
  • 原始資料型別:int, float, double
  • 向量:vec2, vec3, vec4, ...
  • 矩陣:mat2, mat3, mat4, ...
  • 訪問紋理資料的型別:sampler2D
  • 內建向量、矩陣運算
  • 大量內建功能, 例如,求取向量的長度(length(v))

著色器的型別

WebGL 程式中有兩種型別的著色器。

  • 頂點著色器計算位置。
  • 片段著色器處理柵格化。

如果您的 WebGL 程式想要在螢幕上繪製一個三角形,它會把三角形的 3 個座標傳遞給頂點著色器。然後,片段著色器的任務是用畫素填充該三角形,這種逐畫素處理過程非常快,因為它是針對 GPU 上的每個畫素並行執行處理的。

在我的演示案例中,我使用 4 個向量座標來覆蓋適合整個螢幕的矩形,所有工作都在片段著色器中完成。

頂點著色器

顧名思義,頂點著色器存在於頂點。它從JavaScript程式碼提供的緩衝區中獲取一堆資料,並根據這些資料計算在畫布中的相應位置。

以下程式碼段將資料從緩衝區拉入一個 attribute 變數,並將其傳遞給該 gl_Position 變數:

attribute vec3 position;

void main() {
  gl_Position = vec4(position, 1.0);
}

片段著色器

precision highp float;

void main() {
  vec2 p = gl_FragCoord.xy;
  gl_FragColor = vec4(
1.0, .5 + .5 * sin(p.y), .5 + .5 * sin(p.x), 1.0);
}

片段著色器針對每個片段(畫素)並行執行。在上面的示例中,片段著色器從 gl_FragCoord 變數讀取當前畫素座標,並通過 gl_FragColor 中的 sin() 計算執行並輸出顏色。

gl_FragColor 是一個 vec4 向量,其中包含(紅色,綠色,藍色,alpha),取值各為 0 .. 1。

GLSL 變數的型別

  • attribute: 頂點著色器從緩衝區中提取一個值並將其儲存在屬性變數中。
  • uniform: 從js端設定統一變數。例如,您可以統一使用諸如當前滑鼠、輕敲位置之類的內容傳遞給著色器。您還可以使用統一變數來訪問從 JavaScript 上傳的紋理資料。
  • varying: 將值從頂點傳遞到片段著色器並進行插值。

上傳影象資料

您可以使用影象資料訪問到著色器中的 WebGLRenderingContext,並將其上傳到紋理中。(另請參見:WebGL 基礎知識:影象處理)

您可以使用 texImage2D內部方法 WebGLRenderingContext 將影象資料上傳到紋理中。

// gl is the WebGLRenderingContext 
const texture = gl.createTexture()
gl.activeTexture(gl.TEXTURE0 + textureIndex);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);

// more info about these parameters in the webglfundamentals
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

您傳遞給 texImage2D 的影象資料,可以是 img 元素、視訊元素、ImageData 等。

由於視訊的影象資料不斷變化,因此您必須在 requestAnimationFrame 動畫迴圈內更新紋理。以下是獲取完成的 texSubImage2D。

gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, video);

讀取紋理資料

在著色器程式碼中讀取紋理資料,您可以通過 texture 的 2Dglsl函式訪問紋理的畫素資料。

當紋理座標從(0,0)變為(1,1)時,影象會上下顛倒。同時,我正處於水平映象影象中(就像用相機自拍一樣)。

uniform sampler2D texture0;

void main() {
  vec2 coord = 1.0 - gl_FragCoord.xy / vec2(width, height);
  gl_FragColor = texture2D(texture1, coord);
}

訪問網路攝像頭

要從網路攝像頭獲取影象資料,我們可以使用 video 標籤,並使用 getUserMediaAPI:

function accessWebcam(video) {
  return new Promise((resolve, reject) => {
    const mediaConstraints = { audio: false, video: { 
        width: 1280, 
        height: 720,
        brightness: {ideal: 2} 
      }
    };

    navigator.mediaDevices.getUserMedia(
      mediaConstraints).then(mediaStream => {
        video.srcObject = mediaStream;
        video.setAttribute('playsinline', true);
        video.onloadedmetadata = (e) => {
          video.play();
          resolve(video);
        }
      }).catch(err => {
        reject(err);
      });
    }
  );
}

// 使用說明:
// const video = await accessWebcam(document.querySelector('video'));
// or via promises:
// accessWebcam(document.querySelector('video')).then(video => { ... });

要訪問網路攝像頭,您可以使用 getUserMedia API 來訪問網路攝像頭,如上所述。

提供後備影象

如果使用者阻止了對網路攝像頭的訪問,或者沒有可用的網路攝像頭,則可以提供一個備用影象供您使用。

我也將 new Image() 中的 onload 操作包裝成一個 promise。

function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = 'Anonymous';
    img.src = url;
    img.onload = () => {
      resolve(img);
    };
    img.onerror = () => {
      reject(img);
    };
  });
}

合併全部操作

為了使事情變得容易一些,我將常用的 WebGL函式放入了我建立的一個小助手庫GLea中。

它初始化 WebGL 應用上下文,編譯 WebGL 著色器程式碼,併為頂點著色器建立屬性和緩衝區:

預設情況下,position 為頂點著色器提供一個屬性,該屬性帶有一個緩衝區,該緩衝區包含 4 個 2D 座標,覆蓋整個螢幕上的 2 個三角形。

import GLea from 'glea.js';

const frag = ` ... `; // 片段著色器程式碼
const vert = ` ... `; // 頂點著色器程式碼
const glea = new GLea({
  shaders: [
    GLea.fragmentShader(frag),
    GLea.vertexShader(vert)
  ]
}).create();

function loop(time = 0) {
  const { gl, width, height } = glea;
  glea.clear();
  glea.uniV('resolution', [width, height]);
  glea.uni('time', time * 1e-3);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(loop);
}

window.addEventListener('resize', () => {
  glea.resize();
});
loop(0);

結論

基本上就是這樣。我希望您喜歡閱讀本文,並對自己探索 WebGL 感到好奇。我會在這裡放一些資源。

如果我還有沒介紹到的內容,請隨時發表補充評論 =)