fetchでcanvasの画像をファイルとしてアップロード

Javascript

canvasの画像をfetchでAPIにPOSTしたいのですがどうやったらいいでしょうか?

シップ

canvasのtoBlob()メソッドを使ってBlobに変換してからFormDataに追加するといいですよ

今回の内容
  • 2つの画像ファイルを2つCanvasに描画
  • それぞれのCanvasをBlobに変換
  • 各Blobをfetchで送信
  • 受信した画像データをimgで表示

使用するAPIは、オリジナル画像と、消したい部分をマスクした画像をアップロードすると、画像からマスクした部分が自然な形で消えるClipDropClianup APIです。

画像をCanvasに描写

オリジナル:<canvas id="cnvs_original" width="1590" height="1061"></canvas>
マスク:<canvas id="cnvs_mask" width="1590" height="1061"></canvas>

まず、HTML側でcanvasを二つ用意します。このcanvasに画像ファイルの内容を描画します。

var cnvs_original = document.getElementById('cnvs_original').getContext('2d');
var img = new Image();
img.addEventListener('load', function () {
    cnvs_original.drawImage(img, 0, 0);
}, false);
img.src = 'img/original.jpg';

var cnvs_mask = document.getElementById('cnvs_mask').getContext('2d');
var img2 = new Image();
img2.addEventListener('load', function () {
    cnvs_mask.drawImage(img2, 0, 0);
}, false);
img2.src = 'img/mask.png';

original.jpgと、mask.pngimg/ディレクトリに保存しておいてください。

画像を読み込んだら、drawImage()メソッドで、画像をcanvasに描画しています。

CanvasをBlobに変換

Blobとはファイルのデータに、ファイルタイプなどの情報が付与されたバイナリデータです。CanvasをBlobに変換するにはtoBlob()メソッドを使います。

void canvas.toBlob(callback, mimeType, qualityArgument);
var blob_original;  //Blob
document.getElementById('cnvs_original').toBlob(function (blob) {
    blob_original = blob;  //変換されたBlobが渡されるので、blob_originalへセット
});

Blobへの変換は非同期で行われるので、コールバックでBlob変換されたデータを取得できます。今回2つのCanvasを変換してから次のAPI呼び出しフェーズへ移行したいのでPromiseを使用します。

<button id="cleanup-btn" type="button">除去する</button>
var cleanup_btn = document.getElementById('cleanup-btn');
cleanup_btn.addEventListener('click', function () {
    var blob_original, blob_mask;

    const promise1 = new Promise((resolve) => {
        document.getElementById('cnvs_original').toBlob(function (blob) {
            blob_original = blob;
            resolve();
        });
    }).then(() => {
        console.log("オリジナルキャンバスからBlobへの変換完了");
    });

    const promise2 = new Promise((resolve) => {
        document.getElementById('cnvs_mask').toBlob(function (blob) {
            blob_mask = blob;
            resolve();
        });
    }).then(() => {
        console.log("マスクキャンバスからBlobへの変換完了");
    });

    Promise.all([promise1, promise2]).then(() => {
        console.log("全てのキャンバスのBLOBへの変換完了");
    });
}, false);

promise1とpromise2で各CanvasからBlobへの変換を行うPromiseを定義しておきます。

Promise.all()にその2つのPromiseを配列で渡すことで、すべてのPromiseが実行された後に行う処理を記述できます。

各Blobをfetchで送信

Blogに変換できたらFormDataに追加して、fetchで、POST送信を行います。

var formData = new FormData();
formData.append('image_file', blob_original);
formData.append('mask_file', blob_mask);

fetch('https://apis.clipdrop.co/cleanup/v1', {
    method: 'POST',
    headers: {
         'X-API-Key': 'YOUR API KEY',
    },
    body: formData,
})
    .then(response => response.arrayBuffer())
    .then(buffer => {
        //APIから帰ってきた画像データを処理
        //....
    });

FormDataを作成し、BlobをAPIで指定されたフィールド名image_filemask_fileというそれぞれのフォーム項目にセットします。

fetchリクエストを作成します。bodyに、Blobをセット済みのFormDataを指定して送信されるようにします。

応答はAPIによって、画像から余分な部分が取り除かれた後の画像がバイナリで返ってくるので、バイナリ列をarrayBufferの形で読み取ります。

返ってきた画像を表示

実行結果:<img id="img_result" width="1590" height="1061"></img>
var bytes = new Uint8Array(buffer);
var binaryData = "";
for (var i = 0, len = bytes.byteLength; i < len; i++) {
    binaryData += String.fromCharCode(bytes[i]);
}
var img = document.getElementById("img_result");
img.src = "data:image/png;base64," + window.btoa(binaryData);

バイナリ画像をBase64のData URI形式に変換し、画像のsrcにセットすることで、画像を表示しています。

ソースコード全体

<!DOCTYPE html>
<html>
<head>
    <script src="clip.js"></script>
</head>
<body>
  オリジナル:<canvas id="cnvs_original" width="1590" height="1061"></canvas>
  マスク:<canvas id="cnvs_mask" width="1590" height="1061"></canvas>
  実行結果:<img id="img_result" width="1590" height="1061"></img>
  <button id="cleanup-btn" type="button">除去する</button>
</body>
</html>
var cnvs_original = document.getElementById('cnvs_original').getContext('2d');
var img = new Image();
img.addEventListener('load', function () {
    cnvs_original.drawImage(img, 0, 0);
}, false);
img.src = 'img/original.jpg';

var cnvs_mask = document.getElementById('cnvs_mask').getContext('2d');
var img2 = new Image();
img2.addEventListener('load', function () {
    cnvs_mask.drawImage(img2, 0, 0);
}, false);
img2.src = 'img/mask.png';

var cleanup_btn = document.getElementById('cleanup-btn');
cleanup_btn.addEventListener('click', function () {
    var blob_original, blob_mask;

    const promise1 = new Promise((resolve) => {
        document.getElementById('cnvs_original').toBlob(function (blob) {
            blob_original = blob;
            resolve();
        });
    }).then(() => {
        console.log("オリジナルキャンバスからBlobへの変換完了");
    });

    const promise2 = new Promise((resolve) => {
        document.getElementById('cnvs_mask').toBlob(function (blob) {
            blob_mask = blob;
            resolve();
        });
    }).then(() => {
        console.log("マスクキャンバスからBlobへの変換完了");
    });

    Promise.all([promise1, promise2]).then(() => {
        console.log("全てのキャンバスのBLOBへの変換完了");

        var formData = new FormData();
        formData.append('image_file', blob_original);
        formData.append('mask_file', blob_mask);

        fetch('https://apis.clipdrop.co/cleanup/v1', {
            method: 'POST',
            headers: {
                'X-API-Key': 'YOUR API KEY',
            },
            body: formData,
        })
            .then(response => response.arrayBuffer())
            .then(buffer => {
                //APIから帰ってきた画像データを画像にして表示
                var bytes = new Uint8Array(buffer);
                var binaryData = "";
                for (var i = 0, len = bytes.byteLength; i < len; i++) {
                    binaryData += String.fromCharCode(bytes[i]);
                }
                var img = document.getElementById("img_result");
                img.src = "data:image/png;base64," + window.btoa(binaryData);
            })
            .then(result => {
                console.log('API呼び出し成功', result);
            })
            .catch(error => {
                console.error('API呼び出し失敗', error);
            });

    });
}, false);

参考サイト

今回サンプルで使用したClipDropClianup APIですが、マスク画像を作る手間は必要ですが、背景が複雑な写真からでも結構自然に対象物を消してくれたので驚きでした。APIの呼び出しが初回100回までは無料で使えて、その後は1回あたり最低$0.1かかるので安いとは言えないですが、最近のAIはすごいなと技術の進歩を感じました。

この記事を書いた人

PHPが好物な個人開発プログラマ。フリーランスエンジニアとしてWebサービス作ったりしてます。15年の経験を生かしてMENTAでメンターもやってます。WordPressやPHPでお困りのことがあればご相談に乗りますのでDMください。

Follow on SNS
Javascript
SOHO MIND

Comments