canvasに表示されている映像（画像）の色の判別する

Webカメラの映像を解析(?)することがあったので、その工程をざっくりとまとめてみる。
今回は、前回[（Webカメラの映像をcanvasに表示させる）](https://qiita.com/chelcat3/items/02c77b55d080d770530a)の続き。

（説明する際、constで宣言したものを便宜上、変数といわせていただきます。）


## やること

- canvasからデータを取得する。
- データを判別し、加工する。


## canvasからデータを取得する

取得する方法は簡単。
描画済みのコンテキストさんからデータを「ちょうだい(´ω`)」すればよいのである。

```javascript:javascript
const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
const data      = imageData.data;
```

コンテキストさんの`getImageData()メソッド`で、指定した範囲のピクセルデータである`ImageDataオブジェクト`を取得している。
`変数data`に代入しているのは`ImageDataオブジェクト`内のRGBAのデータ配列。

実際にコンソールに出してみるとこんな感じ。
上が`変数imageData`に代入した`ImageDataオブジェクト`、下が`変数data`に代入した`ImageDataオブジェクト`内の`data`（Uint8ClampedArray）。

![img_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316493/445c3695-7dee-de09-104d-7c67a488f8d2.png)

`Uint8ClampedArray`ってなんぞ？ ………ぶっちゃけよくわからない。
1ビットを8つ並べた8ビットは1バイト… 2進数で8桁… 8ビットで表せるのは2の8乗で0〜255の256通り…
とりあえず、`0〜255の符号なし整数が入る配列`と認識している。
0〜255の範囲外の数値を指定した場合は、0もしくは255が設定されるらしい。
整数ではない値を指定すると、最も近い整数に設定されるらしい。

…で、これから使うのは`変数data`に代入した`ImageDataオブジェクト`内のRGBAのデータ配列。（Uint8ClampedArray）
配列内の情報はいたって単純。
1ピクセルのRGBAが順番に格納されているのである。（アルファ値も0〜255になる）

![img_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316493/fab5ba20-6a63-278c-45b7-96d2145da957.png)


1ピクセル目はrgba(77, 61, 66, 1)、2ピクセル目はrgba(73, 61, 63, 1)、3ピクセル目はrgba(96, 73, 52, 1)………といった具合である。

次はここで取得したデータを元に特定の色を変えていこうと思う。


## データを判別し、加工する。

canvasを1ピクセルずつ色を確認し、該当すれば色を変更する。

```javascript:javascript
// 変更したい色の範囲を決めておく
const minColor = { r: 108, g: 0, b: 0 };
const maxColor = { r: 255, g: 60, b: 60 };

// コンテキストからデータ取得
const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
const data      = imageData.data; // rgba、1バイト×4のデータ

// ここに現在のピクセル情報を入れていく
const currentColor = {};

// 1ピクセルずつ確認していく
for(let i = 0, len = data.length; i < len; i += 4) {
  currentColor.r = data[i];
  currentColor.g = data[i + 1];
  currentColor.b = data[i + 2];

  // 指定したrgb内であれば黄色に変換する
  if(_checkTargetColor(currentColor, minColor, maxColor)) {
    data[i]     = 255;
    data[i + 1] = 255;
    data[i + 2] = 0;
    // data[i + 3] = 0; => アルファ値なので、0にすれば透明になる
  }
}

// ImageDataオブジェクトに、変更済みのRGBAデータ（変数data）を代入する
imageData.data = data;

// canvasに変更済みのImageDataオブジェクトを描画する
canvasCtx.putImageData(imageData, 0, 0);


// 色の判定用の関数（引数：現在のピクセルのrgb、指定色の最小値、指定色の最大値）
// 指定したrgb内であれば true を返す
function _checkTargetColor(current, min, max) {
  if(min.r > current.r || current.r > max.r) return;
  if(min.g > current.g || current.g > max.g) return;
  if(min.b > current.b || current.b > max.b) return;
  return true;
};
```

説明するまでもないが、for文で使っている変数iが4ずつ増えていくのは、配列がRGBA4つのデータで1ピクセルのセットだからである。
data[0]〜data[3] => 1ピクセル目、data[4]〜data[7] => 2ピクセル目、data[8]〜data[11] => 3ピクセル目……

内容についてはソースのコメントにほぼ書いてしまったが、
for文で1ピクセルずつチェックしていき、該当する色であればRGBAの配列データをお好きなように変更する。
変更したデータをコンテキストさんの`ImageDataオブジェクト`に代入し、コンテキストさんに描画を「おねがい(´ω`)」すればよい。


## 全体のソース

ここまではあくまで説明しやすくしているため、このまま使ってもきちんと描画されないと思う。
なぜなら`requestAnimationFrame()メソッド`のループに組み込んでいないからである。
なので、ここで全体のソースを書いておく。

```html:html
<div id="videoPreview">
  <p>video preview</p>
</div>

<div id="canvasPreview">
  <p>canvas preview</p>
</div>
```

```javascript:javascript
const cameraSize = { w: 360, h: 240 };
const canvasSize = { w: 360, h: 240 };
const resolution = { w: 1080, h: 720 };
const minColor   = { r: 108, g: 0, b: 0 };
const maxColor   = { r: 255, g: 60, b: 60 };
let video;
let media;
let canvas;
let canvasCtx;

// video要素をつくる
video          = document.createElement('video');
video.id       = 'video';
video.width    = cameraSize.w;
video.height   = cameraSize.h;
video.autoplay = true;
document.getElementById('videoPreview').appendChild(video);

// video要素にWebカメラの映像を表示させる
media = navigator.mediaDevices.getUserMedia({
  audio: false,
  video: {
    width: { ideal: resolution.w },
    height: { ideal: resolution.h }
  }
}).then(function(stream) {
  video.srcObject = stream;
});

// canvas要素をつくる
canvas        = document.createElement('canvas');
canvas.id     = 'canvas';
canvas.width  = canvasSize.w;
canvas.height = canvasSize.h;
document.getElementById('canvasPreview').appendChild(canvas);

// コンテキストを取得する
canvasCtx = canvas.getContext('2d');

// video要素の映像をcanvasに描画する
_canvasUpdate();

function _canvasUpdate() {
  canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
  _changePixelColor(); // ループにこれが追加される
  requestAnimationFrame(_canvasUpdate);
};

// canvasを1ピクセルずつ色を確認し、該当すれば色を変更する
function _changePixelColor() {
  // コンテキストからデータ取得
  const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
  const data      = imageData.data; // rgba、1バイト×4のデータ

  // ここに現在のピクセル情報を入れていく
  const currentColor = {};

  // 1ピクセルずつ確認していく
  for(let i = 0, len = data.length; i < len; i += 4) {
    currentColor.r = data[i];
    currentColor.g = data[i + 1];
    currentColor.b = data[i + 2];

    // 指定したrgb内であれば黄色に変換する
    if(_checkTargetColor(currentColor, minColor, maxColor)) {
      data[i]     = 255;
      data[i + 1] = 255;
      data[i + 2] = 0;
      // data[i + 3] = 0; => アルファ値なので、0にすれば透明になる
    }
  }

  // ImageDataオブジェクトに、変更済みのRGBAデータ（変数data）を代入する
  imageData.data = data;

  // canvasに変更済みのImageDataオブジェクトを描画する
  canvasCtx.putImageData(imageData, 0, 0);
};

// 色の判定用の関数（引数：現在のピクセルのrgb、指定色の最小値、指定色の最大値）
// 指定したrgb内であれば true を返す
function _checkTargetColor(current, min, max) {
  if(min.r > current.r || current.r > max.r) return;
  if(min.g > current.g || current.g > max.g) return;
  if(min.b > current.b || current.b > max.b) return;
  return true;
};
```

![img_03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316493/51de025c-31c8-f491-619f-64764b796004.jpeg)



UIをつくって指定する色を変更できるようにするのがベスト。

あと、canvas要素でクリックした箇所1ピクセルの色データが出るようにしてあげるととても便利。

```javascript:javascript
let targetPixel;

function _getPointColor(e) {
  targetPixel = canvasCtx.getImageData(e.offsetX, e.offsetY, 1, 1);
  console.log(`rgba(${targetPixel.data[0]}, ${targetPixel.data[1]}, ${targetPixel.data[2]}, ${targetPixel.data[3]})`);
};

canvas.addEventListener('mousedown', _getPointColor);
```


今回は色の最小値〜最大値でチェックしていったが、色差を使ってチェックする方法もある。（ユークリッド距離）

```javascript:javascript
// 色の最小値〜最大値ではなく、色を一色決める
const targetColor = { r: 200, g: 0, b: 0 };

// 色の距離
let colorDistance = 100;


// 中略


// _changePixelColor()内の色判定が、「色の距離がcolorDistance以下であれば〜」に変わる
if(_getColorDistance(currentColor, targetColor) < colorDistance) {
  data[i]     = 255;
  data[i + 1] = 255;
  data[i + 2] = 0;
  // data[i + 3] = 0; => アルファ値なので、0にすれば透明になる
}


// 中略


// 色の判定で使っていた _checkTargetColor()の代わり
function _getColorDistance(rgb1, rgb2) {
  const result = Math.sqrt(
    Math.pow((rgb1.r - rgb2.r), 2) +
    Math.pow((rgb1.g - rgb2.g), 2) +
    Math.pow((rgb1.b - rgb2.b), 2)
  );
  return result;
};
```

おそらくこんな感じかと。
色の距離を大きくすればするほど許容する色の範囲が大きくなるので、黄色に変わる色が多くなる。


## まとめ

はやく語彙力のあるゴリゴリのエンジニアになりたい。（2回目）