LoginSignup
30
30

More than 5 years have passed since last update.

canvasで輪郭を検出してアートっぽくする

Last updated at Posted at 2015-10-08

前回のcanvasでクロマキー合成っぽいことしてみるの派生。
映像の中から輪郭を検出して色を変えてみる。

完成物
demo

やること

  1. 映像を取り込む
  2. canvasに描画する
  3. 描画されたcontextを走査し、隣接する色の差によって色を塗り分ける
  4. 色の塗り方を変える

前回と同じ処理の説明は割愛

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>Outline</title>
</head>
<body>

    <div>
        <input id="color" type="color" value="#ff0000" />
        <input id="distance" type="number" value="10" />
    </div>

    <div>
        <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に処理を書いていく。

省略

描画されたcontextを走査し、隣接する色の差によって色を塗り分ける

ユークリッド距離を求める関数を使い、今回は隣接するピクセルの色が遠いかどうかを判定する。
隣り合う色のユークリッド距離の差が大きければ、映像上そこは境界であると認識できる。

左上から順に走査していき、垂直方向の境界線は右隣の色、水平方向の境界線は下の色と比較をして検出する。

アウトライン処理
var outlineColor = {r: 255, g: 0, b: 0},
    colorDistance = 10;

// アウトライン処理
var outline = 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) {

        // この条件の時、currentは右端の色、nextは1px下の段の左端の色になるので透明にしてスキップする
        if ((i / 4 + 1) % canvas.width === 0) {
            data[i + 3] = 0;
            continue;
        }

        var currentIndex = i,
            nextIndex = currentIndex + 4,
            underIndex = currentIndex + (canvas.width * 4),

            // チェックするピクセルの色
            current = {
                r: data[currentIndex],
                g: data[currentIndex + 1],
                b: data[currentIndex + 2]
            },
            // 右隣の色
            next = {
                r: data[nextIndex],
                g: data[nextIndex + 1],
                b: data[nextIndex + 2]
            },
            // 下の色
            under = {
                r: data[underIndex],
                g: data[underIndex + 1],
                b: data[underIndex + 2]
            };

        // 現在のピクセルと右隣、下の色の三次元空間上の距離を閾値と比較する
        // 閾値より大きい(色が遠い)場合、境界線とみなしそのピクセルをoutlineColorに変更
        // 閾値より小さい(色が近い)場合、そのピクセルを消す
        if (getColorDistance(current, next) > colorDistance || getColorDistance(current, under) > colorDistance) {
            data[i] = outlineColor.r;
            data[i + 1] = outlineColor.g;
            data[i + 2] = outlineColor.b;
        } else {
            // alpha値を0にすることで見えなくする
            data[i + 3] = 0;
        }
    }

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

ここまでで映像内の輪郭を検出して単色で塗り、その他の部分を透明にすることができる。

色の塗り方を変える

単色塗りだとおもしろくないので、指定された色からその色の補色にグラデーションをかけるようにする。

補色の求め方はAdobeぱいせんを参考にした。
参考:Adobe Illustrator/カラーの調整#カラーの反転色または補色への変更

補色を求める関数
// 補色の計算
var getComplementaryColor = function (rgb) {
    var max = Math.max(rgb.r, rgb.g, rgb.b),
        min = Math.min(rgb.r, rgb.g, rgb.b),
        sum = max + min;
    return {
        r: sum - rgb.r,
        g: sum - rgb.g,
        b: sum - rgb.b
    };
};

指定色とその補色をもとに、contextの上から一段ずつ色を変えるように計算する。

アウトライン処理部分を次のように修正。

グラデーションをで塗るアウトライン処理
var outlineColor = {r: 255, g: 0, b: 0},
    complementaryColor = {r: 0, g: 255, b: 255},
    colorDistance = 10;

// アウトライン処理
var outline = 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つのピクセルの情報となる

    var currentOutlineColor = outlineColor;

    for (var i = 0, l = data.length; i < l; i += 4) {

        // この条件の時、currentは右端の色、nextは1px下の段の左端の色になるので透明にしてスキップする
        if ((i / 4 + 1) % canvas.width === 0) {
            data[i + 3] = 0;
            continue;
        }

        // 段が変わったら色を変える
        // 一段ずつoutlineColorからcomplementaryColorにグラデーションにする
        if ((i / 4) % canvas.width === 0) {
            var row = (i / 4) / canvas.width,
                r = (outlineColor.r - complementaryColor.r) / canvas.height,
                g = (outlineColor.g - complementaryColor.g) / canvas.height,
                b = (outlineColor.b - complementaryColor.b) / canvas.height;

            currentOutlineColor = {
                r: outlineColor.r - (r * row),
                g: outlineColor.g - (g * row),
                b: outlineColor.b - (b * row)
            };
        }

        var currentIndex = i,
            nextIndex = currentIndex + 4,
            underIndex = currentIndex + (canvas.width * 4),

            // チェックするピクセルの色
            current = {
                r: data[currentIndex],
                g: data[currentIndex + 1],
                b: data[currentIndex + 2]
            },
            // 右隣の色
            next = {
                r: data[nextIndex],
                g: data[nextIndex + 1],
                b: data[nextIndex + 2]
            },
            // 下の色
            under = {
                r: data[underIndex],
                g: data[underIndex + 1],
                b: data[underIndex + 2]
            };

        // 現在のピクセルと右隣、下の色の三次元空間上の距離を閾値と比較する
        // 閾値より大きい(色が遠い)場合、境界線とみなしそのピクセルをcurrentOutlineColorに変更
        // 閾値より小さい(色が近い)場合、そのピクセルを消す
        if (getColorDistance(current, next) > colorDistance || getColorDistance(current, under) > colorDistance) {
            data[i] = currentOutlineColor.r;
            data[i + 1] = currentOutlineColor.g;
            data[i + 2] = currentOutlineColor.b;
        } else {
            // alpha値を0にすることで見えなくする
            data[i + 3] = 0;
        }
    }

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

ここまでのコードをまとめると↓のようになる。

コード全体像

script.js
(function () {

    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);
        // ここでアウトライン処理をする
        outline();
        requestAnimationFrame(draw);
    };

    // 境界線とする閾値
    var outlineColor = {r: 255, g: 0, b: 0},
        complementaryColor = {r: 0, g: 255, b: 255},
        colorDistance = 10;

    // アウトライン処理
    var outline = 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つのピクセルの情報となる

        var currentOutlineColor = outlineColor;

        for (var i = 0, l = data.length; i < l; i += 4) {

            // この条件の時、currentは右端の色、nextは1px下の段の左端の色になるので透明にしてスキップする
            if ((i / 4 + 1) % canvas.width === 0) {
                data[i + 3] = 0;
                continue;
            }

            // 段が変わったら色を変える
            // 一段ずつoutlineColorからcomplementaryColorにグラデーションにする
            if ((i / 4) % canvas.width === 0) {
                var row = (i / 4) / canvas.width,
                    r = (outlineColor.r - complementaryColor.r) / canvas.height,
                    g = (outlineColor.g - complementaryColor.g) / canvas.height,
                    b = (outlineColor.b - complementaryColor.b) / canvas.height;

                currentOutlineColor = {
                    r: outlineColor.r - (r * row),
                    g: outlineColor.g - (g * row),
                    b: outlineColor.b - (b * row)
                };
            }

            var currentIndex = i,
                nextIndex = currentIndex + 4,
                underIndex = currentIndex + (canvas.width * 4),
                // チェックするピクセルの色
                current = {
                    r: data[currentIndex],
                    g: data[currentIndex + 1],
                    b: data[currentIndex + 2]
                },
                // 右隣の色
                next = {
                    r: data[nextIndex],
                    g: data[nextIndex + 1],
                    b: data[nextIndex + 2]
                },
                // 下の色
                under = {
                    r: data[underIndex],
                    g: data[underIndex + 1],
                    b: data[underIndex + 2]
                };

            // 現在のピクセルと右隣、下の色の三次元空間上の距離を閾値と比較する
            // 閾値より大きい(色が遠い)場合、境界線とみなしそのピクセルをcurrentOutlineColorに変更
            // 閾値より小さい(色が近い)場合、そのピクセルを消す
            if (getColorDistance(current, next) > colorDistance || getColorDistance(current, under) > colorDistance) {
                data[i] = currentOutlineColor.r;
                data[i + 1] = currentOutlineColor.g;
                data[i + 2] = currentOutlineColor.b;
            } else {
                // alpha値を0にすることで見えなくする
                data[i + 3] = 0;
            }
        }

        // 書き換えたdataをimageDataにもどし、描画する
        imageData.data = data;
        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値に変換する
        outlineColor = color2rgb(this.value);
        complementaryColor = getComplementaryColor(outlineColor);
    });

    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 getComplementaryColor = function (rgb) {
        var max = Math.max(rgb.r, rgb.g, rgb.b),
            min = Math.min(rgb.r, rgb.g, rgb.b),
            sum = max + min;
        return {
            r: sum - rgb.r,
            g: sum - rgb.g,
            b: sum - rgb.b
        };
    };

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

})();

ええでええで。

demo

※chrome推奨

デザイナーからみたらアートでもなんでもないんだろうけどな!!!!!

30
30
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
30