YUV⇒RGB変換(WebGL編)

Canvas WebGL Contextによる描画(YUVテクスチャ)

<canvas>要素に”WebGL Context”を使用してRGBA画像のテクスチャとして描画を行えるようになりましたので、次はY, U, Vのデータで作成されたテクスチャを使用して描画を行いたいと思います。

既にローカルに存在するYUVファイルからのデータの読み込みやWebGLで<canvas>上に四角を表示したり、そこにテクスチャとして画像を貼り付けるコードは出来ていますので、マルチテクスチャへの対応等とシェーダープログラムでYUV⇒RGB変換を行うようにして行きます。

index.htmlへの各要素の追加配置2

“WebGL Context”にY, U, Vのテクスチャを使用した描画用に以下の要素を追加しておきます。

要素 type id 概要
canvas canvasGLYUV 画像表示用 CIFサイズ
button button btnOpenFileGLYUV ファイルオープン
button button btnPlayGLYUV 再生開始
input text txtFileNameGLYUV ファイル名表示 readonly
input file fileOpenDialogGLYUV ファイル選択 hidden

下記のHTMLを<main>要素の子要素として追加します。

<h2>&ltcanvas&gtにWebGLでYUVを直接GPUにセットして画像を表示(YUV)</h2>
<div class="d-flex bd-highlight mb-3 mt-5 justify-content-center">
  <canvas id="canvasGLYUV" width="352" height="288">
                CIFサイズのYUV画像を表示します。
              </canvas>
</div>
<div class="d-flex bd-highlight mb-3 justify-content-center">
  <button type="button" class="btn btn-primary mx-2" disabled id="btnPlayGLYUV">Play</button>
  <button type="button" class="btn btn-primary mx-1" id="btnOpenFileGLYUV">File...</button>
  <input type="text" id="txtFileNameGLYUV" readonly>
</div>

<!-- ファイルオープンダイアログのためのボタン(見えない) -->
<input type="file" name="file-open" id="fileOpenDialogGLYUV" accept=".yuv" hidden>
    

テクスチャ作成 / セット

此処でYUVデータからテクスチャを作成し、セットするクラスYUVTextureを作成しておきます。

使用するYUVデータは、YUVFileオブジェクトで作成するプライベートフィールド”_y”, “_u”, “_v”を使用します。
なので、YUVTextureクラスはYUVFileクラスを継承します。

import { YUVFile } from "./yuvfile";

export type YUVColorSpace = 'y' | 'u' | 'v';

export class YUVTexture extends YUVFile {
    private textureY: WebGLTexture | null = null;
    private textureU: WebGLTexture | null = null;
    private textureV: WebGLTexture | null = null;

    constructor(file: File, gl: WebGL2RenderingContext) {
        super(file);
        this.textureY = this.createYUVTexture(gl);
        this.textureU = this.createYUVTexture(gl);
        this.textureV = this.createYUVTexture(gl);
    }

    public finalize(gl: WebGL2RenderingContext) {
        gl.deleteTexture(this.textureY);
        this.textureY = null;
        gl.deleteTexture(this.textureU);
        this.textureY = null;
        gl.deleteTexture(this.textureV);
        this.textureY = null;
    }

    protected createYUVTexture(gl: WebGL2RenderingContext): WebGLTexture | null {
        const texture: WebGLTexture | null = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        return texture;
    }

    public async setNextYUVTextures(gl: WebGL2RenderingContext, texlocation: Map<YUVColorSpace, WebGLUniformLocation>) {
        await this.readYUV();
        if (this.available) {
            const texlocationY: WebGLUniformLocation | undefined = texlocation.get('y');
            if ((texlocationY != undefined) && (this.y != undefined)) {
                gl.uniform1i(texlocationY, 0);
                gl.activeTexture(gl.TEXTURE0);
                gl.bindTexture(gl.TEXTURE_2D, this.textureY);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, YUVFile.CIF_WIDTH, YUVFile.CIF_HEIGHT, 0, gl.RED, gl.UNSIGNED_BYTE, this.y);
            }
            const texlocationU: WebGLUniformLocation | undefined = texlocation.get('u');
            if ((texlocationU != undefined) && (this.u != undefined)) {
                gl.uniform1i(texlocationU, 1);
                gl.activeTexture(gl.TEXTURE1);
                gl.bindTexture(gl.TEXTURE_2D, this.textureU);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, (YUVFile.CIF_WIDTH / 2), (YUVFile.CIF_HEIGHT / 2), 0, gl.RED, gl.UNSIGNED_BYTE, this.u);
            }
            const texlocationV: WebGLUniformLocation | undefined = texlocation.get('v');
            if ((texlocationV != undefined) && (this.v != undefined)) {
                gl.uniform1i(texlocationV, 2);
                gl.activeTexture(gl.TEXTURE2);
                gl.bindTexture(gl.TEXTURE_2D, this.textureV);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, (YUVFile.CIF_WIDTH / 2), (YUVFile.CIF_HEIGHT / 2), 0, gl.RED, gl.UNSIGNED_BYTE, this.v);
            }
        } else {
            throw new Error('Failed to set textures');
        }
    }
}
    

YUVTextureクラスのコンストラクタ

YUVTextureクラスのコンストラクタでは、引数にFileオブジェクトを取り、そのまま親クラスであるYUVFileのコンストラクタに渡します。

その後、引数で渡された”WebGL Content”を使用し、createYUVTextureメソッドでY, U, V其々のテクスチャオブジェクトを作成します。
なお、作成したテクスチャオブジェクトは他のメソッドで使用しますので、プライベートフィールドとして保持しておきます。

YUVTextureクラスのファイナライザ

YUVTextureクラスのファイナライザは、引数にWebGLコンテキストを取り、deleteTextureメソッドでテクスチャオブジェクトを破棄します。
なおdeleteTextureメソッドは引数にnullをセットする事もできますので、事前にnullかどうかのチェックは必要ありません。

createYUVTextureメソッド

引数で渡された”WebGL Content”を使用し、createTextureメソッドでテクスチャオブジェクトを作成します。
その後、bindTextureメソッドで作成したテクスチャオブジェクトをバインドした後、メモリアライメントと画像の拡縮時の補間方法の設定をしておきます。
因みにY, U, Vのデータは符号なしのバイトデータですので、メモリアライメントは”1バイト”となります。

setNextTextureメソッド

setNextTextureメソッドではテクスチャにY, U, Vデータを割り当ててセットします。

なお、テクスチャはシェーダープログラム中でユニフォーム変数として使用されますので、テクスチャに画像データを割り当てる場合には、ユニフォーム変数を表すIDが必要となります。

setNextTextureメソッドではWebGLコンテキストと共に引数としてユニフォーム変数を表すID”texlocation”をMapオブジェクトとして渡しています。
MapオブジェクトのキーはUnion型で定義した”YUVColorSpace”を使用します。

setNextTextureメソッドでは先ずreadYUVメソッドで次のフレームのYUVデータを取得します。
なお、readYUVメソッドは非同期関数ですので、awaitでYUVデータの取得が完了するまで待ちます。
更に、setNextTextureメソッドも非同期関数となりますので、async修飾子を付加しておきます。

YUVデータの取得に成功したら、Y, U, V其々のデータをテクスチャに割り当てます。
基本的なフローはRGBTextureクラスのsetNextTextureメソッドと同じです。
ただ、YUVTextureオブジェクトではテクスチャを3つ同時に使用しますので、少々工夫が必要となります。

先ず、uniform1iメソッドでテクスチャユニットとユニフォーム変数のIDを結びつけます。
その後、activeTextureでテクスチャユニットをアクティベートします。
なお、uniform1iメソッドで指定したテクスチャユニットの番号とactiveTextureで指定するテクスチャユニット”TEXTUREn”の”n”は合わせるようにして下さい。
因みにテクスチャユニットはOpenGL ES 2.0でも最低8つは使用できますので、今回のように3つであれば問題ないかと思います。
心配な場合にはgl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);でテクスチャユニットの数を取得できますので確認してください。

次にbintTextureメソッドでユニフォーム変数を2次元のテクスチャにバインドした後、texImage2Dメソッドで実際のデータをテクスチャにセットします。
RGBTextureクラスの時と異なり、YUVTextureクラスでは符号なし8ビット整数の配列Uint8Arrayオブジェクトである”y”, “u”, “v”データをそのままtexImage2Dメソッドにセットしますので、画像の幅と高さを指定する必要がある事に注意が必要です。
また、セットする”y”, “u”, “v”データは其々、符号なし8ビット整数の配列ですので、内部フォーマットと画像フォーマットは”R8”と”RED”を指定します。
更に更にタイプは”UNSIGNED_BYTE”を指定する必要があります。

因みにtexImage2Dメソッドの内部フォーマットと画像フォーマット、タイプについては、指定できる値の組み合わせが決まっています。
詳細はKhronosグループのWebGL 2.0 Specificationで確認して下さい。

因みにYUVデータのの取得を失敗した場合には、例外を投げるようにしています。

Canvas WebGL Contextの要素のイベント対応(YUVテクスチャ)

canvas要素にWebGLコンテキストを使用し、YUVデータをテクスチャとして描画する際のイベント対応をEvents4ElementsGLYUVクラスとして纏めます。
各イベントに対する動作のテンプレートは抽象クラスEvents4Elementsを継承したEvents4ElementsGLクラスに纏めていますので、Events4ElementsGLYUVクラスはEvents4ElementsGLクラスを継承するようにします。

import { Element } from "./events4elements";
import { Events4ElementsGL } from "./events4elementsgl";
import { YUVColorSpace, YUVTexture } from "./yuvtexture";

export class Events4ElementsGLYUV extends Events4ElementsGL {
        :
        :
}
    

Events4ElementsGLYUVクラスのシェーダープログラム

シェーダープログラムは文字列のクラス定数として定義します。

なお、Events4ElementsGLRGBクラスの時と異なり、シェーダープログラム内では#version 300 esディレクティブを記載しています。

Events4ElementsGLYUVクラスのバーテックスシェーダープログラム

Events4ElementsGLYUVクラスのバーテックスシェーダープログラムは以下の通りです。

static readonly VERTEX_SOURCE = `#version 300 es
    in vec3 position;
    in vec2 vertexUV;
    out vec2 uv;
    void main(void)
    {
        gl_Position.xyz = position;
        gl_Position.w = 1.0;
        uv = vertexUV;
    }`;
        

なお、バージョンディレクティブはstatic readonly VERTEX_SOURCE = `直後に記述します。

static readonly VERTEX_SOURCE = `
    #version 300 es
        :
        :
        

のようにバージョンディレクティブを次行に記述した場合にはコンパイル時にエラーとなりますので注意が必要です。

バーテックスシェーダープログラムは基本的にEvents4ElementsGLRGBクラスの時と概ね同じです。
図形の回転や移動、変形、更に視点の移動や視野角の指定等はマルっと・・・・無視していますので、3次元ベクタとして”position”にセットされた頂点座標データをそのまま座標データの組み込み変数”gl_Position”に出力しています。

2次元ベクタとして”vertexUV”にセットされたテクスチャ座標データは、そのまま”uv”としてフラグメントシェーダープログラムに渡しています。

因みにGLSL ES 3.00ではattributeinに、varyingoutを使用するように変わっています。
なお変数に対する精度は特に指定しない限りGLSL ES 3.00以上ではheighpとなっているようです。

Events4ElementsGLYUVクラスのフラグメントシェーダープログラム

Events4ElementsGLYUVクラスのフラグメントシェーダープログラムは以下の通りです。

static readonly FRAGMENT_SOURCE = `#version 300 es
    precision highp float;
    const mat4 TORGB = mat4(
        1.164f,  1.164f, 1.164f, 0.0f,
        0.0f,   -0.392f, 2.017f, 0.0f,
        1.596f, -0.813f, 0.0f,   0.0f,
        0.0f,    0.0f,   0.0f,   1.0f);
    const vec4 DIFF = vec4(16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f, 0.0f);
    in vec2 uv;
    out vec4 color;
    uniform sampler2D textureSamplerY;
    uniform sampler2D textureSamplerU;
    uniform sampler2D textureSamplerV;
    void main(void)
    {
        vec4 fy = texture(textureSamplerY, uv);
        vec4 fu = texture(textureSamplerU, uv);
        vec4 fv = texture(textureSamplerV, uv);
        vec4 yuv = vec4(fy.r, fu.r, fv.r, 1.0f);
        yuv -= DIFF;
        vec4 rgb = TORGB * yuv;
        color = clamp(rgb, 0.0f, 1.0f);
    }`;
        

なお、バーテックスシェーダープログラムと同様、バージョンディレクティブはstatic readonly FRAGMENT_SOURCE = `直後に記述します。

static readonly FRAGMENT_SOURCE = `
    #version 300 es
        :
        :
        

のようにバージョンディレクティブを次行に記述した場合にはコンパイル時にエラーとなりますので注意が必要です。

2行目ではfloatに対する精度指定をしています。
GLSL ES 3.0のフラグメントシェーダープログラムでは、精度heighp, mediumpもしくはlowpの指定は必須のようなので記述しておきます。

テクスチャから色データを取得するには、texture関数を使用します。
GLSL ES 1.00ではtexture2D関数でしたが、GLSL ES 3.00ではtexture関数に変わりました。
なおYUVTextureクラスでtexImage2Dメソッドで内部フォーマットを”R8”と指定していますので、“textureSamplerY”, “textureSamplerU”, “textureSamplerV”で指定されるテクスチャと”uv”で指定される座標から取得される色データは、R成分のみが有効です。
ですので、R成分のみを集めてYUVの色データを4次元ベクタとして作成します。

作成されたYUVの四次元ベクタとRGB変換用の4×4の行列“TORGB”を用いてRGBAデータを計算します。

最後に、計算されたRGBAデータを[0.0, 1.0]の範囲でクリッピングして、色データとして出力します。

なお、GLSL ES 1.0では色データを組み込み変数であるフラグメントカラー”gl_FragColor”に出力していましたが、GLSL ES 3.0では廃止・・されています。
代わりに4次元ベクタのout変数を作成し、その変数にセットする事で出力する事が出来ます。

Events4ElementsGLYUVクラスのコンストラクタ

Events4ElementsGLYUVクラスのコンストラクタは以下の通りです。

static readonly MAP_GLYUV = new Map<Element, string>([
    ['canvas', 'canvasGLYUV'],
    ['filebtn', 'btnOpenFileGLYUV'],
    ['playbtn', 'btnPlayGLYUV'],
    ['filetxt', 'txtFileNameGLYUV'],
    ['dialog', 'fileOpenDialogGLYUV']
]);
        :
        :
constructor() {
    super(Events4ElementsGLYUV.MAP_GLYUV);
}
        

Events4ElementsGLYUVクラスのコンストラクタでは、“index.htmlに配置した要素の”id”のMapオブジェクトを引数に、親クラスEvents4ElementsGLのコンストラクタをコールします。

Events4ElementsGLYUVクラスのファイナライザ

private texImage: YUVTexture | undefined;
        :
        :
public finalize() {
    super.finalize();
    const gl: WebGL2RenderingContext | undefined = this.getContextGL();
    if ((gl != undefined) && (this.texImage != undefined)) {
        this.texImage.finalize(gl);
        this.texImage = undefined;
    }
}
        

Events4ElementsGLYUVクラスのファイナライザfinalizeメソッドでは、親クラスEvents4ElementsGLのファイナライザをコール後に、プライベートフィールド”texImage”に保持しているYUVTextureオブジェクトをfinalizeメソッドにより破棄します。

Events4ElementsGLYUVクラスのsetFileNameメソッド

Events4Elementsクラス内で抽象メソッドとして宣言されていたsetFileNameを実装します。

    :
    :
private vertexPosition: number = 0;
private vertexUV: number = 0;
private textureMap = new Map<YUVColorSpace, WebGLUniformLocation>;
        :
        :
protected setFileName(files: FileList | null | undefined) {
    if (this.texImage != undefined) {
        this.finalize();
    }
    const file: File | null = this.extractFileFromFileList(files);
    if (file != null) {
        const gl: WebGL2RenderingContext | undefined = this.getContextGL();
        if (gl != undefined) {
            this.initShaderProgram(gl, Events4ElementsGLYUV.VERTEX_SOURCE, Events4ElementsGLYUV.FRAGMENT_SOURCE);
            if (this.shaderProgram != null) {
                this.vertexPosition = gl.getAttribLocation(this.shaderProgram, 'position');
                this.vertexUV = gl.getAttribLocation(this.shaderProgram, 'vertexUV');
                const textureYLocation: WebGLUniformLocation | null = gl.getUniformLocation(this.shaderProgram, 'textureSamplerY');
                if (textureYLocation != null) {
                    this.textureMap.set(`y`, textureYLocation);
                }
                const textureULocation: WebGLUniformLocation | null = gl.getUniformLocation(this.shaderProgram, 'textureSamplerU');
                if (textureULocation != null) {
                    this.textureMap.set(`u`, textureULocation);
                }
                const textureVLocation: WebGLUniformLocation | null = gl.getUniformLocation(this.shaderProgram, 'textureSamplerV');
                if (textureVLocation != null) {
                    this.textureMap.set(`v`, textureVLocation);
                }
            };
            this.createBuffers(gl);
            this.texImage = new YUVTexture(file, gl);
        }
    }
}
        

setFileNameメソッド内では、先ず、既に割り当てられているYUVTextureオブジェクトが存在する場合、破棄しておきます。

次に、引数として渡されたファイルリストからextractFileFromFileListメソッドでFileオブジェクトを取得します。
取得したFileオブジェクトを使用し、新たにYUVTextureオブジェクトを作成し、プライベートフィールド”texImage”にセットします。

同時にバーテックスシェーダープログラム”VERTEX_SOURCE”とフラグメントシェーダープログラム”FRAGMENT_SOURCE”を引数にしてinitShaderProgramメソッドをコールする事でシェーダープログラムのコンパイル、リンク等の準備を行います。

シェーダープログラムの作成に成功したら、シェーダープログラム中のアトリビュート変数”position”と”vertexUV”のIDをgetAttribLocationメソッドで、ユニフォーム変数”textureSamplerY”, “textureSamplerU”, “textureSamplerV”のIDをgetUniformLocationメソッドで取得し、其々プライベートフィールドおよび<Map>オブジェクトに保存しておきます。

最後にcreateBuffersメソッドで頂点座標データとテクスチャ座標データの頂点バッファを作成しておきます。

Events4ElementsGLYUVクラスのshowNextImageメソッド

Events4Elementsクラス内で抽象メソッドとして宣言されていたshowNextImageを実装します。

protected showNextImage() {
    const gl: WebGL2RenderingContext | undefined = this.getContextGL();
    if ((gl != undefined) && (this.texImage != undefined)) {
        gl.useProgram(this.shaderProgram);
        this.texImage.setNextYUVTextures(gl, this.textureMap).then(() => {
            gl.clear(gl.COLOR_BUFFER_BIT);

            gl.enableVertexAttribArray(this.vertexPosition);
            gl.enableVertexAttribArray(this.vertexUV);

            gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
            gl.vertexAttribPointer(
                this.vertexPosition,
                3, // サイズ
                gl.FLOAT, // タイプ
                false, // 正規化?
                0, // ストライド
                0 //vertexBuffer // 配列バッファ
            );

            gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer);
            gl.vertexAttribPointer(
                this.vertexUV,
                2, // サイズ
                gl.FLOAT, // タイプ
                false, // 正規化?
                0, // ストライド
                0 //textureBuffer // 配列バッファ
            );

            gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

            gl.disableVertexAttribArray(this.vertexUV);
            gl.disableVertexAttribArray(this.vertexPosition);
        }).catch(() => {
            this.stopPlay();
        });
    }
}
        

OpenGLで図形を描画する典型的なコードです。
基本的にはEvents4ElementsGLRGBクラスのshowNextImageメソッドと同じです。

先ず、useProgramメソッドで使用するシェーダープログラムを選択します。

次に、プライベートフィールド”texImage”を使用し、setNextTextureメソッドで次のフレームの画像をテクスチャにセットします。
なお、setNextTextureメソッドは非同期関数ですので、thenにより画像がテクスチャにセットされるのを待ってから描画を継続します。

描画については、enableVertexAttribArray, bindBuffer, vertexAttribPointerメソッドで頂点座標データとテクスチャ座標データをセットした後、drawArraysで3角形を2枚描画する事で四角の描画を行います。

もし、次のフレームの画像が無い等、例外が投げられた場合には、catch内でstopPlayをコールし、動画再生の停止処理等を行います。

Events4ElementsGLYUVクラスのrewindメソッド

Events4Elementsクラス内で抽象メソッドとして宣言されていたrewindを実装します。

RGBTextureオブジェクトのrewindメソッドをコールします。

protected rewind() {
    if (this.texImage != undefined) {
        this.texImage.rewind();
    }
}
        

index.tsのWebGLコンテキスト(YUV)対応

“index.ts”にEvents4ElementsGLYUVクラスを追加します。

import { Events4Elements2D } from "./events4elements2d";
import { Events4ElementsGLRGB } from "./events4elementsglrgb";
import { Events4ElementsGLYUV } from "./events4elementsglyuv";

var canvas2D: Events4Elements2D | undefined;
var canvasGLRGB: Events4ElementsGLRGB | undefined;
var canvasGLYUV: Events4ElementsGLYUV | undefined;

addEventListener('DOMContentLoaded', init);

function init() {
    window.addEventListener('close', finalize);

    canvas2D = new Events4Elements2D();
    canvasGLRGB = new Events4ElementsGLRGB();
    canvasGLYUV = new Events4ElementsGLYUV();
}

function finalize() {
    if (canvas2D != undefined) {
        canvas2D.finalize();
        canvas2D = undefined;
    }
    if (canvasGLRGB != undefined) {
        canvasGLRGB.finalize();
        canvasGLRGB = undefined;
    }
    if (canvasGLYUV != undefined) {
        canvasGLYUV.finalize();
        canvasGLYUV = undefined;
    }
}
        

init関数

“index.html”内のキャンバス要素への描画やボタンクリックイベントへの対応を記述したクラスEvents4ElementsWebGLYUVのオブジェクトの作成を追加します。

finalize関数

finalize関数内では、Events4ElementsGLYUV.finalizeメソッドで、作成したクラスオブジェクト内の不要なオブジェクトを明示的に削除する処理を追加しています。

Canvas WebGL Contextによる描画(YUVテクスチャ)の実行

この時点でコンパイルと実行を行い、動作を確認する事ができるはずです。

ここまでのコードはこちらからダウンロードできます。

なお、実際の動作はこちらで確認できます。

YUV⇒RGB変換