Home About Contact
Semantic Segmentation , PIL , Jimp , JavaScript

Semantic Segmentation の結果確認画像の合成 / 該当ピクセルを重ねて表示

自分で用意したパンの写真 120枚 を使って、Keras の Deeplab V3 plus を使ってファインチューニングしたモデルをつくった。 そのモデルを使って新たに撮った写真でパンの写っているピクセルを推測した結果。

semantic segmentation my-bread-120

※赤いピクセルで塗った部分がパンとして推測されたピクセル。

今回は、この元画像と結果を合成する方法について書く。

何をどうしたいか

以下のように元画像(src.jpg)と推測されたピクセル画像(mask.png) がある。 これを合成したい。

two image composition

mask.png 画像のパンとして推測されたピクセルは RGBA = (128, 0, 0, 150) で塗ってある。
src.jpg, mask.png, result.png

Python による合成

Python のPIL で実装する場合は以下の通り。

from PIL import Image

img_src  = Image.open('src.jpg').convert('RGBA')
img_mask = Image.open('mask.png')
Image.alpha_composite(img_src, img_mask).save('result.png')

簡単でした。

JavaScript による合成

$ npm init -y
$ npm install jimp
$ touch index.js

Jimp のバージョンは 0.22.10 です。

以下の画像ファイルをカレントディレクトリに用意。

そして合成のためのコード index.js:

const Jimp = require('jimp')

// 色を合成する.
const toCompositeColor = (srcR, srcG, srcB, maskR, maskG, maskB, maskA)=> {
    const alpha = (maskA / 255.0)
    const newR = srcR * (1.0 - alpha) + maskR * alpha
    const newG = srcG * (1.0 - alpha) + maskG * alpha
    const newB = srcB * (1.0 - alpha) + maskB * alpha

    return {
        r: newR,
        g: newG,
        b: newB,
        a: 255}
}

const srcImageFile  = 'src.jpg'
const maskImageFile = 'mask.png'
const resultImageFile = 'result.png'

const srcImagePromise  = Jimp.read(srcImageFile)
const maskImagePromise = Jimp.read(maskImageFile)

Promise.all([srcImagePromise, maskImagePromise]).then((images) => {
    const srcImage  = images[0]
    const maskImage = images[1]

    const imageW = srcImage.bitmap.width
    const imageH = srcImage.bitmap.height

    const newImage  = new Jimp(imageW, imageH, 0x00000000, (err, image) => {})

    newImage.scan(0, 0, imageW, imageH, (x, y, idx)=> {
        if( maskImage.bitmap.data[idx + 3] == 0 ){
            // 透明のピクセルならば:
            // → srcImage のピクセルをそのままこのピクセルに適用.
            newImage.bitmap.data[idx + 0] = srcImage.bitmap.data[idx + 0]
            newImage.bitmap.data[idx + 1] = srcImage.bitmap.data[idx + 1]
            newImage.bitmap.data[idx + 2] = srcImage.bitmap.data[idx + 2]
            newImage.bitmap.data[idx + 3] = 255
        }
        else if( maskImage.bitmap.data[idx + 3] > 0 ){
            // パンとして推測されたピクセルならば:
            // → srcImage と maskImage の色を合成.
            const newColor = toCompositeColor(
                srcImage.bitmap.data[idx + 0], // R
                srcImage.bitmap.data[idx + 1], // G
                srcImage.bitmap.data[idx + 2], // B
                maskImage.bitmap.data[idx + 0], // R
                maskImage.bitmap.data[idx + 1], // G
                maskImage.bitmap.data[idx + 2], // B
                maskImage.bitmap.data[idx + 3]) // A

            newImage.bitmap.data[idx + 0] = newColor.r
            newImage.bitmap.data[idx + 1] = newColor.g
            newImage.bitmap.data[idx + 2] = newColor.b
            newImage.bitmap.data[idx + 3] = newColor.a
        }
    })

    newImage.write(resultImageFile)
})

色の合成を行っている toCompositeColor 関数がポイントです。 これを使って パンと推測されたピクセルにおける期待する色を計算してそれを適用しています。 Python と比べるとコードは長いですが、処理内容は単純です。

出来上がった画像はこれです。

result

補足 toCompositeColor 関数

どうやって色を混ぜ合わせるかの計算問題。

// 色を合成する.
const toCompositeColor = (srcR, srcG, srcB, maskR, maskG, maskB, maskA)=> {
    const alpha = (maskA / 255.0)
    const newR = srcR * (1.0 - alpha) + maskR * alpha
    const newG = srcG * (1.0 - alpha) + maskG * alpha
    const newB = srcB * (1.0 - alpha) + maskB * alpha

    return {
        r: newR,
        g: newG,
        b: newB,
        a: 255}
}

下に置く色(srcR, srcG, srcB) と上に置く色(maskR, maskG, maskB, maskA) を合成するのだが、下と上の色をそれぞれどのくらい活かすかを alpha 値で調整する。 alpha 値は maskA を 255.0 で割っているので、 0.0..1.0 の範囲の値になっている。

maskA は上に置く色の Alpha 値なので、たとえば、 255 だった場合(alpha の値は 1.0 になる)は、下の色を全く透過させないで上に置く色を 100% 活かしたいわけだから...

const newR = srcR * (1.0 - 1.0) + maskR * 1.0
const newG = srcG * (1.0 - 1.0) + maskG * 1.0
const newB = srcB * (1.0 - 1.0) + maskB * 1.0

このような計算になり、srcR, srcG, srcB は 0.0 をかけることになり、全く反映されない。 一方で、maskR, maskG, maskB は 1.0 をかけることになり、全て反映される。

逆に、もし maskA の値が 0 の場合、これは 100% 下の色を透過させることになるので...

const newR = srcR * (1.0 - 0.0) + maskR * 0.0
const newG = srcG * (1.0 - 0.0) + maskG * 0.0
const newB = srcB * (1.0 - 0.0) + maskB * 0.0

ということになり、srcR, srcG, srcB は 1.0 をかけることでそのままの値が維持され、一方で、maskR, maskG, maskB は 0.0 をかけることで、全く反映されない。

これで重ね合わせた色として違和感のない色になる。

以上です。