canvasでクロマキー合成っぽいことしてみる


CMや映画でよく使われるクロマキー合成をcanvasで再現してみようと思った。
特殊な設備がなくてもいいように消す色を指定できるといいかもね。

![クロマキー合成のイメージ](https://qiita-image-store.s3.amazonaws.com/0/53664/b9bf0d6b-d31c-7552-90c1-f76569860eca.jpeg "movie_room.jpg")


## やること
1. 映像を取り込む
2. canvasに描画する
3. 描画されたcontextの指定された色の部分を透過にする
4. 背景に画像を入れる

ざっくりこんな感じ。
ファイルが別れると載せにくいのでstyleの設定はjsでやる。

## とりあえずhtmlファイルに要素を置いておく
必要なのはvideoとcanvas。
あとはどの色を透過にするか指定できるようにinputも置いた。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Language" content="ja">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0,minimal-ui">
<title>Chroma key</title>
</head>
<body>

    <div>
        <input id="color" type="color" value="#00ff00" />
        <input id="distance" type="number" value="30" />
    </div>

    <div id="bg">
        <video id="video" autoplay></video>
        <canvas id="canvas" width="480" height="360"></canvas>
    </div>

    <script type="text/javascript" src="./script.js"></script>

</body>
</html>
```

`script.js`に処理を書いていく。

## 映像を取り込む
`navigator.getUserMedia`でカメラの映像をvideoに取り込む。

```Javascript:映像を取り込む
    var video = document.getElementById('video');

    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
    navigator.getUserMedia({video: true, audio: false}, function (stream) {
        video.src = URL.createObjectURL(stream);
    }, function () {});
```

おｋ。


## canvasに描画する
videoに取り込んだだけだと映像に加工ができない。
なので取り込んだ映像をcanvasに描画し、あとで処理できるようにする。

```Javascript:canvasに描画する
    var video = document.getElementById('video');
    // videoは非表示にしておく
    video.style.display = 'none';

    var canvas = document.getElementById('canvas');
    // そのまま表示すると鏡像にならないので反転させておく
    canvas.style.transform = 'rotateY(180deg)';

    var context = canvas.getContext('2d');
    
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
    navigator.getUserMedia({video: true, audio: false}, function (stream) {
        video.src = URL.createObjectURL(stream);
        draw();
    }, function () {});
    
    // videoの映像をcanvasに描画する
    var draw = function () {
        context.drawImage(video, 0, 0, canvas.width, canvas.height);
        requestAnimationFrame(draw);
    };
```

おｋ。

## 描画されたcontextの指定された色の部分を透過にする
これがちょっと重め。
`getImageData`で色の情報を取得したあとに中身を書き換え、`putImageData `で再描画することでピクセル操作ができる。
これを利用し、指定された色に近い部分だけを透過にする。

色1と色2が近いかどうかは、正しい方法かどうかしらないけど[ユークリッド距離](https://ja.wikipedia.org/wiki/%E3%83%A6%E3%83%BC%E3%82%AF%E3%83%AA%E3%83%83%E3%83%89%E8%B7%9D%E9%9B%A2)で判定する。

```Javascript:ユークリッド距離を求める関数
	// r,g,bというkeyを持ったobjectが第一引数と第二引数に渡される想定
    var getColorDistance = function (rgb1, rgb2) {
    	// 三次元空間の距離が返る
        return Math.sqrt(
            Math.pow((rgb1.r - rgb2.r), 2) +
            Math.pow((rgb1.g - rgb2.g), 2) +
            Math.pow((rgb1.b - rgb2.b), 2)
        );
    };
```

videoの映像をcanvasに描画するときに定義した`draw`関数の中でクロマキー処理を行う。

```Javascript:クロマキー処理
    // videoの映像をcanvasに描画する
    var draw = function () {
        context.drawImage(video, 0, 0, canvas.width, canvas.height);
        // ここでクロマキー処理をする
        chromaKey();
        requestAnimationFrame(draw);
    };

	// 消す色と閾値
    var chromaKeyColor = {r: 0, g: 255, b: 0},
        colorDistance = 30;
    
    // クロマキー処理
    var chromaKey = function () {
        var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
            data = imageData.data;

        // dataはUint8ClampedArray
        // 長さはcanvasの width * height * 4(r,g,b,a)
        // 先頭から、一番左上のピクセルのr,g,b,aの値が順に入っており、
        // 右隣のピクセルのr,g,b,aの値が続く
        // n から n+4 までが1つのピクセルの情報となる

        for (var i = 0, l = data.length; i < l; i += 4) {
            var target = {
                    r: data[i],
                    g: data[i + 1],
                    b: data[i + 2]
                };

            // chromaKeyColorと現在のピクセルの三次元空間上の距離を閾値と比較する
            // 閾値より小さい（色が近い）場合、そのピクセルを消す
            if (getColorDistance(chromaKeyColor, target) < colorDistance) {
                // alpha値を0にすることで見えなくする
                data[i + 3] = 0;
            }
        }

        // 書き換えたdataをimageDataにもどし、描画する
        imageData.data = data;
        context.putImageData(imageData, 0, 0);
    };
```

おｋ。

## 色と閾値を変更できるようにする
試す場所によって消したい色が違うと思うので調整できるようにする。


```Javascript:色と閾値を変更できるようにする
    var color = document.getElementById('color');
    color.addEventListener('change', function () {
        // フォームの値は16進カラーコードなのでrgb値に変換する
        chromaKeyColor = color2rgb(this.value);
    });

    var color2rgb = function (color) {
        color = color.replace(/^#/, '');
        return {
            r: parseInt(color.substr(0, 2), 16),
            g: parseInt(color.substr(2, 2), 16),
            b: parseInt(color.substr(4, 2), 16)
        };
    };

    var distance = document.getElementById('distance');
    distance.style.textAlign = 'right';
    distance.addEventListener('change', function () {
        colorDistance = this.value;
    });
```


## 背景に画像を入れる
てきとうにbackgroundに設定すればおｋ。

```Javascript:背景に画像を入れる
    var bg = document.getElementById('bg');
    bg.style.width = canvas.width + 'px';
    bg.style.height = canvas.height + 'px';
    bg.style.background = 'url(./bg.jpg) 50% 50% no-repeat';
    bg.style.backgroundSize = 'cover';
```

## コード全体像

```Javascript:script.js
    var video = document.getElementById('video');
    // videoは非表示にしておく
    video.style.display = 'none';

    var canvas = document.getElementById('canvas');
    // そのまま表示すると鏡像にならないので反転させておく
    canvas.style.transform = 'rotateY(180deg)';

    var context = canvas.getContext('2d');

    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
    navigator.getUserMedia({video: true, audio: false}, function (stream) {
        video.src = URL.createObjectURL(stream);
        draw();
    }, function () {});

    // videoの映像をcanvasに描画する
    var draw = function () {
        context.drawImage(video, 0, 0, canvas.width, canvas.height);
        // ここでクロマキー処理をする
        chromaKey();
        requestAnimationFrame(draw);
    };

    // 消す色と閾値
    var chromaKeyColor = {r: 0, g: 255, b: 0},
        colorDistance = 30;

    // クロマキー処理
    var chromaKey = function () {
        var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
            data = imageData.data; //参照渡し

        // dataはUint8ClampedArray
        // 長さはcanvasの width * height * 4(r,g,b,a)
        // 先頭から、一番左上のピクセルのr,g,b,aの値が順に入っており、
        // 右隣のピクセルのr,g,b,aの値が続く
        // n から n+4 までが1つのピクセルの情報となる

        for (var i = 0, l = data.length; i < l; i += 4) {
            var target = {
                    r: data[i],
                    g: data[i + 1],
                    b: data[i + 2]
                };

            // chromaKeyColorと現在のピクセルの三次元空間上の距離を閾値と比較する
            // 閾値より小さい（色が近い）場合、そのピクセルを消す
            if (getColorDistance(chromaKeyColor, target) < colorDistance) {
                // alpha値を0にすることで見えなくする
                data[i + 3] = 0;
            }
        }

        context.putImageData(imageData, 0, 0);
    };

    // r,g,bというkeyを持ったobjectが第一引数と第二引数に渡される想定
    var getColorDistance = function (rgb1, rgb2) {
        // 三次元空間の距離が返る
        return Math.sqrt(
            Math.pow((rgb1.r - rgb2.r), 2) +
            Math.pow((rgb1.g - rgb2.g), 2) +
            Math.pow((rgb1.b - rgb2.b), 2)
        );
    };

    var color = document.getElementById('color');
    color.addEventListener('change', function () {
        // フォームの値は16進カラーコードなのでrgb値に変換する
        chromaKeyColor = color2rgb(this.value);
    });

    var color2rgb = function (color) {
        color = color.replace(/^#/, '');
        return {
            r: parseInt(color.substr(0, 2), 16),
            g: parseInt(color.substr(2, 2), 16),
            b: parseInt(color.substr(4, 2), 16)
        };
    };

    var distance = document.getElementById('distance');
    distance.style.textAlign = 'right';
    distance.addEventListener('change', function () {
        colorDistance = this.value;
    });

    var bg = document.getElementById('bg');
    bg.style.width = canvas.width + 'px';
    bg.style.height = canvas.height + 'px';
    bg.style.background = 'url(./bg.jpg) 50% 50% no-repeat';
    bg.style.backgroundSize = 'cover';
```


## demo

https://secure3521.sakura.ne.jp/keiskimu.com/Qiita/chroma-key/
※ chrome推奨

↓こんな感じ
![demo](https://qiita-image-store.s3.amazonaws.com/0/53664/895697de-dd81-a28c-217f-0602bd6c6fc6.png "Kobito.vvVuBd.png")


引きこもりでもハワイに来た気分になれるね！やったね！

## 追記
- 派生で[canvasで輪郭を検出してアートっぽくする](http://qiita.com/keiskimu/items/24456deeb6ef7665a6c1)を投稿