Colab上でお絵描きしてマスク画像をつくるツール

# Inpainting用のマスクをすぐ作れる
![Feb-12-2024 20-38-20.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/66de3348-8664-b463-e86d-1c01042076ce.gif)
![myPhoto_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/3c257fcc-cf11-ff07-8e18-eb09a402abfe.png)

Inpainting（AI消しゴム）には画像と、マスク画像が必要です。
マスク画像とは、オリジナル画像と同じサイズで、消す部分が白、背景が黒、のグレイスケール画像です。
これをつくるツールってあんまりないので、Colabですぐお絵描きしてマスク画像が作れるツールが以下です。
描画を白黒マスクで保存でき、消しゴム機能もあります。

[**Colabリンク(セルを実行するとツールが出ます)**](https://colab.research.google.com/drive/18F6ixLEN1Hr5dIMt6knGbZNFzrdpkzbD?usp=sharing)

IPythonでHTMLを表示して、javascriptからPython関数を呼んでいます。

```python
#@title Run and Paint then click Finish

import base64, os
from IPython.display import HTML, Image, display_png
from base64 import b64decode
import matplotlib.pyplot as plt
import numpy as np
from shutil import copyfile
import shutil
from google.colab import files

# ファイルをアップロードする
uploaded_files = files.upload()
fname = list(uploaded_files.keys())[0]  # アップロードされた最初のファイル名
dir_name = "mask_data"

if not os.path.exists(dir_name):
    os.mkdir(dir_name)

original_path = os.path.join(dir_name, fname)
copyfile(fname, original_path)
os.remove(fname)

# 画像を読み込んでbase64エンコーディングする
image64 = base64.b64encode(open(original_path, 'rb').read()).decode('utf-8')

# オリジナル画像のサイズを取得
original_img = plt.imread(original_path)
original_height, original_width = original_img.shape[:2]

# 表示用のリサイズ係数を計算 (ここでは幅を600pxに合わせる例)
resize_factor = 400 / original_width
display_width = int(original_width * resize_factor)
display_height = int(original_height * resize_factor)

# JavaScriptで使用するリサイズ係数、オリジナルサイズ、線の色と太さを渡す
canvas_html = f"""
<div style="position: relative; width: {display_width}px; height: {display_height}px;">
    <canvas id="backgroundCanvas" width="{display_width}" height="{display_height}" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
    <canvas id="drawingCanvas" width="{display_width}" height="{display_height}" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
    <canvas id="saveCanvas" width="{display_width}" height="{display_height}" style="display: none;"></canvas>
</div>
<button id="drawBtn">Draw</button>
<button id="eraseBtn">Erase</button>
<button id="finishBtn">Finish</button>
<label for="lineWidth">Line Width:</label>
<input type="range" id="lineWidth" min="1" max="50" value="5">
<script>
    var bgCanvas = document.getElementById('backgroundCanvas');
    var drawCanvas = document.getElementById('drawingCanvas');
    var saveCanvas = document.getElementById('saveCanvas');
    var bgCtx = bgCanvas.getContext('2d');
    var drawCtx = drawCanvas.getContext('2d');
    var saveCtx = saveCanvas.getContext('2d');
    var lineWidthRange = document.getElementById('lineWidth');
    var drawBtn = document.getElementById('drawBtn');
    var eraseBtn = document.getElementById('eraseBtn');
    var finishBtn = document.getElementById('finishBtn');
    var drawing = true;

    var img = new Image();
    img.onload = function() {{
        bgCtx.drawImage(img, 0, 0, bgCanvas.width, bgCanvas.height);
    }};
    img.src = "data:image/png;base64,{image64}";

    function setDrawMode() {{
        drawing = true;
        drawCtx.globalCompositeOperation = 'source-over';
        drawCtx.strokeStyle = 'red'; // 表示用の描画色
        saveCtx.strokeStyle = 'white'; // 保存用の描画色
        updateButtonStyles();
    }}

    function setEraseMode() {{
        drawing = false;
        drawCtx.globalCompositeOperation = 'destination-out';
        saveCtx.globalCompositeOperation = 'destination-out'; // 保存用キャンバスでも消去
        updateButtonStyles();
    }}

    function updateButtonStyles() {{
        drawBtn.style.backgroundColor = drawing ? '#add8e6' : '';
        eraseBtn.style.backgroundColor = drawing ? '' : '#add8e6';
    }}

    saveCanvas.width = {original_width};
    saveCanvas.height = {original_height};

    // 描画イベントの座標をオリジナルのサイズに合わせてスケーリング
    drawCanvas.onmousedown = function(e) {{
        var rect = drawCanvas.getBoundingClientRect();
        var scaleX = saveCanvas.width / rect.width; // X座標のスケーリング係数
        var scaleY = saveCanvas.height / rect.height; // Y座標のスケーリング係数
        var x = (e.clientX - rect.left) * scaleX;
        var y = (e.clientY - rect.top) * scaleY;
        drawCtx.lineWidth = lineWidthRange.value;
        saveCtx.lineWidth = lineWidthRange.value * scaleX;

        drawCtx.beginPath();
        saveCtx.beginPath();
        drawCtx.moveTo(x / scaleX, y / scaleY); // 表示用キャンバスに適用（スケーリングを戻す）
        saveCtx.moveTo(x, y); // 保存用キャンバスに適用
        drawCanvas.onmousemove = function(e) {{
            var x = (e.clientX - rect.left) * scaleX;
            var y = (e.clientY - rect.top) * scaleY;
            drawCtx.lineTo(x / scaleX, y / scaleY); // 表示用キャンバスに適用（スケーリングを戻す）
            drawCtx.stroke();
            saveCtx.lineTo(x, y); // 保存用キャンバスに適用
            saveCtx.stroke();
    }};
        drawCanvas.onmouseup = function() {{
            drawCanvas.onmousemove = null;
        }};
    }};

    finishBtn.onclick = function() {{
        // 一時キャンバスを作成
        var tempCanvas = document.createElement('canvas');
        var tempCtx = tempCanvas.getContext('2d');
        tempCanvas.width = saveCanvas.width;
        tempCanvas.height = saveCanvas.height;

        // 背景を黒で塗りつぶし
        tempCtx.fillStyle = 'black';
        tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);

        // 描画内容（白の描画）を一時キャンバスに追加
        // 注: 描画モードが白であることを確認してください
        tempCtx.globalCompositeOperation = 'source-over'; // 念のため、描画モードをデフォルトに設定
        tempCtx.drawImage(saveCanvas, 0, 0);

        // 一時キャンバスの内容をエンコードしてPython関数に渡す
        var dataURL = tempCanvas.toDataURL('image/png');
        google.colab.kernel.invokeFunction('notebook.SaveImage', [dataURL], {{}});
    }};


    setDrawMode();
    drawBtn.onclick = setDrawMode;
    eraseBtn.onclick = setEraseMode;
</script>
"""

# キャンバスを表示
display(HTML(canvas_html))

# JavaScriptからPython関数を呼び出すための準備
def SaveImage(image_data_str):
    image_data = b64decode(image_data_str.split(',')[1])
    mask_path = os.path.join(dir_name, f"{fname.split('.')[0]}_mask.png")
    with open(mask_path, 'wb') as f:
        f.write(image_data)
    print(f'Mask image saved in {mask_path}')
    display(Image(mask_path, width=display_width, height=display_height))
# IPythonとの連携を設定
output.register_callback('notebook.SaveImage', SaveImage)
```

下記はツールで作ったマスク画像で HD Painter を使ってマスク部分をスーツにしてみた画像です。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/cb5dd31f-e2ca-38bf-e31f-ddd2c85ec7ed.jpeg width=400>
![myPhoto_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/d74350f2-34ec-41b5-6a06-1f245a8f0878.png)
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/6c43775f-8294-8098-d8a5-766210c4bb1f.jpeg width=400>

HD Painter については以下。

https://qiita.com/john-rocky/items/efbad38a7f62db9a495a

🐣
****
フリーランスエンジニアです。
AIについて色々記事を書いていますのでよかったらプロフィールを見てみてください。

もし以下のようなご要望をお持ちでしたらお気軽にご相談ください。
AIサービスを開発したい、ビジネスにAIを組み込んで効率化したい、AIを使ったスマホアプリを開発したい、
ARを使ったアプリケーションを作りたい、スマホアプリを作りたいけどどこに相談したらいいかわからない…

いずれも中間コストを省いたリーズナブルな価格でお請けできます。

お仕事のご相談はこちらまで
rockyshikoku@gmail.com

機械学習やAR技術を使ったアプリケーションを作っています。
機械学習／AR関連の情報を発信しています。

[X](https://twitter.com/JackdeS11)
[Medium](https://medium.com/@rockyshikoku)
[GitHub](https://github.com/john-rocky)