Keras.js によるランドマーク検出の Web アプリケーション化2

前回 はランドマーク検出対象の画像サイズを固定(256x256)しましたが、今回は任意の画像サイズに対応できるように改造してみます。

ソースは http://github.com/fits/try_samples/tree/master/blog/20190506/

可変サイズ対応

ドラッグアンドドロップした画像のサイズに合わせてランドマーク検出を実施するようにしてみます。(ファイル構成などは 前回 と同じ)

ただ、Keras.js を通常とは異なる使い方をするため、何らかの不都合が生じるかもしれませんし、別バージョンでは動作しないかもしれません。

(a) UI 処理(src/app.js)

canvas のサイズを画像サイズに合わせて変更し、ランドマーク検出処理へ画像サイズ(幅、高さ)の情報を渡すようにします。

src/app.js
・・・
const loadImage = url => new Promise(resolve => {
    const img = new Image()

    img.addEventListener('load', () => {
        // canvas のサイズを画像サイズに合わせて変更
        canvas.width = img.width
        canvas.height = img.height

        ctx.clearRect(0, 0, canvas.width, canvas.height)

        ctx.drawImage(img, 0, 0)

        const d = ctx.getImageData(0, 0, canvas.width, canvas.height)
        resolve({width: img.width, height: img.height, data: imgToArray(d)})
    })

    img.src = url
})

・・・

const ready = () => {
    ・・・

    canvas.addEventListener('drop', ev => {
        ev.preventDefault()
        canvas.classList.remove('dragging')

        const file = ev.dataTransfer.files[0]

        if (imageTypes.includes(file.type)) {
            clearLandmarksInfo()

            const reader = new FileReader()

            reader.onload = ev => {
                loadImage(reader.result)
                    .then(d => {
                        detectDialog.showModal()
                        // 画像のサイズ情報を追加
                        worker.postMessage({type: 'predict', input: d.data, width: d.width, height: d.height})
                    })
            }

            reader.readAsDataURL(file)
        }
    }, false)
}

・・・

(b) ランドマーク検出処理(src/worker.js)

通常は(Keras.js の)モデル内でレイヤー毎の入出力の形状が固定化されているので、このままでは任意の画像サイズには対応できません。

そこで、検出処理の度に入出力の形状を強制的にリセットする処理(以下)を加える事で可変サイズに対応します。

  • (1) 入力データの形状(画像サイズ)を変更
  • (2) 各レイヤーの出力形状をクリア
  • (3) inputTensorsMap のリセット
src/worker.js
・・・

onmessage = ev => {
    switch (ev.data.type) {
        ・・・
        case 'predict':
            const inputLayerName = model.inputLayerNames[0]
            const outputLayerName = model.outputLayerNames[0]

            const w = ev.data.width
            const h = ev.data.height

            // (1) 入力データの形状(画像サイズ)を変更
            const inputLayer = model.modelLayersMap.get(inputLayerName)
            inputLayer.shape[0] = h
            inputLayer.shape[1] = w

            // (2) 各レイヤーの出力形状をクリア
            model.modelLayersMap.forEach(n => {
                if (n.outputShape) {
                    n.outputShape = null
                    n.imColsMat = null
                }
            })

            // (3) inputTensorsMap のリセット
            model.resetInputTensors()

            const data = {}
            data[inputLayerName] = ev.data.input

            Promise.resolve(model.predict(data))
                .then(r => {
                    const shape = model.modelLayersMap.get(outputLayerName)
                                                .output.tensor.shape

                    return new KerasJS.Tensor(r[outputLayerName], shape)
                })
                .then(detectLandmarks)
                .then(r => 
                    postMessage({type: ev.data.type, output: r})
                )
                .catch(err => {
                    console.log(err)
                    postMessage({type: ev.data.type, error: err.message})
                })

            break
    }
}

動作確認

(1) 画像サイズ 128x128

f:id:fits:20190506114638p:plain

(2) 画像サイズ 307x307

f:id:fits:20190506114655p:plain

(3) 画像サイズ 100x128

f:id:fits:20190506114713p:plain

(4) 画像サイズ 200x256

f:id:fits:20190506114736p:plain