yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

Go + Webassemblyでアップロードされた画像をセピア調に変換して表示してみる

Webassemblyを試してみたく、Goでアップロードされた画像を白黒、セピア調に変換するプログラムを書いてみました。

前回の記事

https://yuki-ikegaya.net/2023/08/20/docker%e3%81%a8gin%e3%81%a7go%ef%bc%8bwebassembly%e3%83%97%e3%83%ad%e3%82%b8%e3%82%a7%e3%82%af%e3%83%88%e3%81%ae%e9%9b%9b%e5%bd%a2%e3%82%92%e4%bd%9c%e3%81%a3%e3%81%9f/ Github https://github.com/ikeyu0806/webassembly-image-app

このうち、セピア調に変換するプログラムについて実装を振り返ってみます。白黒に変換するプログラムも大体内容は同じです。

書いたコード(Goの画像処理部分)

package convert_image

import (
    "bytes"
    "encoding/base64"
    "image"
    "image/color"
    "image/png"
    "syscall/js"
)

func ConvertToSepia(this js.Value, args []js.Value) interface{} {
    imageData := args[0]
    uint8Array := js.Global().Get("Uint8Array").New(imageData)
    imageBytes := make([]byte, uint8Array.Length())
    js.CopyBytesToGo(imageBytes, uint8Array)

    img, _, err := image.Decode(bytes.NewReader(imageBytes))
    if err != nil {
        return err.Error()
    }

    sepiaImg := convertToSepiaImage(img)

    var encodedImage bytes.Buffer
    err = png.Encode(&encodedImage, sepiaImg)
    if err != nil {
        return err.Error()
    }

    dataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(encodedImage.Bytes())

    imgElement := js.Global().Get("document").Call("createElement", "img")
    imgElement.Set("src", dataURI)

    var body = js.Global().Get("document").Get("body")
    body.Call("appendChild", imgElement)

    return nil

func convertToSepiaImage(src image.Image) image.Image {
    bounds := src.Bounds()
    sepiaImg := image.NewRGBA(bounds)

    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            originalColor := src.At(x, y)
            originalRGBA := color.RGBAModel.Convert(originalColor).(color.RGBA)

            // 赤、緑、青の合計を3で割ることでグレースケールの輝度値を取得
            gray := (uint32(originalRGBA.R) + uint32(originalRGBA.G) + uint32(originalRGBA.B)) / 3
            // グレースケールの輝度値にセピア調のフィルター適用
            sepiaR := gray + 112
            sepiaG := gray + 66
            sepiaB := gray + 20

            if sepiaR > 255 {
                sepiaR = 255
            }
            if sepiaG > 255 {
                sepiaG = 255
            }
            if sepiaB > 255 {
                sepiaB = 255
            }

            sepiaImg.Set(x, y, color.RGBA{R: uint8(sepiaR), G: uint8(sepiaG), B: uint8(sepiaB), A: originalRGBA.A})
        }
    }

    return sepiaImg
}

コードの解説

ConvertToSepia関数

  • imageData := args[0]でjs.Value型でブラウザからinputタグで取得したファイルを取得できます
  • js.Global().Get("Uint8Array").New(imageData) でJavaScriptのUint8Arrayオブジェクトにデータを変換します。これにより、データがJavaScriptのバイト配列として利用できるようになります。
  • uint8Array.Length()でUint8Arrayの長さを取得し、それを使用してGoのバイトスライス imageBytes を作成します。次に、- js.CopyBytesToGo(imageBytes, uint8Array)を使用してJavaScriptからGoのバイトスライスへデータをコピーします。
  • image.Decode(bytes.NewReader(imageBytes)) でimageパッケージで扱えるよう画像データをデコードします。デコードに失敗した場合、エラーが返されます。
  • convertToSepiaImage関数を使用して、デコードされた画像をセピア調に変換します。
  • png.Encode(&encodedImage, sepiaImg)を使用して、セピア調に変換された画像をPNG形式でエンコードします。
  • エンコードされた画像をData URI形式に変換し、dataURI 変数に格納します。Data URIは、画像データをBase64エンコードして、HTML内で直接表示できる形式です。
  • js.Global().Get("document").Call("createElement", "img")を使用して、HTMLのimg要素を作成し、そのsrc属性に先ほど生成したData URIを設定します。最後に、body.Call("appendChild", imgElement)を使用して、img要素をHTML文書に追加し、画像を表示します。

convertToSepiaImage関数

  • bounds := src.Bounds()
    • 画像の境界情報 (幅と高さ) を取得します。これにより、後続のループで画像のピクセルにアクセスできます。
  • sepiaImg := image.NewRGBA(bounds)
    • 画像をRGBA型で取得してカラーコードを扱えるようにします。
  • gray := (uint32(originalRGBA.R) + uint32(originalRGBA.G) + uint32(originalRGBA.B)) / 3
    • 元の色情報から赤、緑、青の平均値を取得してグレースケールの輝度値を計算しています。

上記関数の呼び出し

main.go

package main

import (
    "syscall/js"

    "go-webassembly/webassembly/analyze_image"
    "go-webassembly/webassembly/convert_image"
)

func main() {
    c := make(chan struct{}, 0)

    js.Global().Set("getFileSize", js.FuncOf(analyze_image.GetFileSize))
    js.Global().Set("showUploadedImage", js.FuncOf(analyze_image.ShowUploadedImage))
    js.Global().Set("convertToBlackAndWhite", js.FuncOf(convert_image.ConvertToBlackAndWhite))
    js.Global().Set("convertToSepia", js.FuncOf(convert_image.ConvertToSepia))

    <-c
}

js.Global().SetでGoの関数をJSの関数として登録できます。

HTML実装

<!DOCTYPE html>
<html>
<head>
    <title>Go WebAssembly</title>
</head>
<body>
    <h1>Go WebAssembly</h1>
    <input type="file" id="fileInput" accept="image/*">
    <button id="convertToSepiaButton">選択画像をセピア調に変換</button>
    <script src="/js/wasm_exec.js"></script>
    <script>
        const go = new Go();

        WebAssembly.instantiateStreaming(fetch("/wasm/main.wasm"), go.importObject)
            .then(result => {
                go.run(result.instance);

                const fileInput = document.getElementById('fileInput');

                const convertToSepiaButton = document.getElementById('convertToSepiaButton');

                convertToSepiaButton.addEventListener('click', function() {
                    const file = fileInput.files[0];
                    if (file) {
                        const reader = new FileReader();
                        reader.onload = function() {
                            const arrayBuffer = this.result;
                            convertToSepia(arrayBuffer);
                        };
                        reader.readAsArrayBuffer(file);
                    }
                });
            })
            .catch(err => {
                console.error(err);
            });
    </script>
</body>
</html>

これでファイルをアップロードしたのちに「選択画像をセピア調に変換」ボタンを押すとセピア調の画像が画面に表示されます。