アルゴリズムから作る画像認識入門

こんにちは！フミです。
はじめに、この記事は[あくあたん工房のアドベントカレンダー](https://adventar.org/calendars/3951)17日目のものです。
他の方の記事もぜひ読んでみてください。

さて、今回は、機械学習無しで作ってみる画像認識システムについて軽く作り方を紹介したいと思います。
画像認識初心者が自分の体験をまとめているだけなので、最適化されたシステムになっていないのでご注意ください。

やっていることは課題に特化した処理ですが、画像認識で基礎となる技術をまあまあ使っているので、読んでみても損はないと思います（ホンマか）
## やりたいこと
今回は画像の中からサッカーボールを検出してみたいと思います。
背景は芝生、もしくは白と黒を頻繁に使用しない任意の背景とします。

![Screenshot from 2019-12-17 13-36-34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172855/be0c24f1-656d-56be-59e6-bcdddc2fc1f5.png)

今回は開発の効率を考えてJavaScriptで組んでいきます。
## 作戦
さて、まずは今回の作戦です。

- 画像をHSV形式に変換
- 画像から白色の部分と黒色の部分を検出
- ボール以外の白い部分や黒い部分を除去
- ボールの中の色飛び等を補完
- ボールをオブジェクトとして認識

## 2値化
まずは画像データの中から検出に使う要素を抜き出さなくてはなりません。
カメラからの入力データは[r1,g1,b1,a1,r2,g2,b,a2,r3,b3,.....]という配列の形式で提供されます。

さて、ここで質問です。
白黒の物体を検出するのに、RGBのデータは有用ですか？

答えはNO！

ということでRGB形式のデータをHSV形式に変換します。
HSVって何？となる方もいるはずなので、軽く説明すると、色情報をH(色相環的に色ごとに割り当てられた番号)、S(明度、城に近づくに連れて大きく、黒に近づくに連れて小さくなる)、V(彩度)の算要素で表す方法です。

変換方法は説明するのが面倒なのでコードを貼ります

``` js:main.js
//画像データはrgbaの順に1ピクセルづつ情報を並べた1次元配列pixelとしてhsv()に与えられる
function hsv(){
    for(i=0;i<pixel.length;i+=4){
            if(i%4==0){           
                if(pixel[i]>pixel[i+1]&&pixel[i]>pixel[i+2]){
                    //r
                    pixel_h[i/4]=60*((pixel[i+1]-pixel[i+2])/(MAX(pixel[i],pixel[i+1],pixel[i+2])-MIN(pixel[i],pixel[i+1],pixel[i+2])));
                }else if(pixel[i+1]>pixel[i]&&pixel[i+1]>pixel[i+2]){
                    //g
                    pixel_h[i/4]=60*((pixel[i+2]-pixel[i])/(MAX(pixel[i],pixel[i+1],pixel[i+2])-MIN(pixel[i],pixel[i+1],pixel[i+2])))+120;
                }else{
                    //b
                    pixel_h[i/4]=60*((pixel[i]-pixel[i+1])/(MAX(pixel[i],pixel[i+1],pixel[i+2])-MIN(pixel[i],pixel[i+1],pixel[i+2])))+240;
                }
                pixel_s[i/4]=(MAX(pixel[i],pixel[i+1],pixel[i+2])-MIN(pixel[i],pixel[i+1],pixel[i+2]))/MAX(pixel[i],pixel[i+1],pixel[i+2]);
                pixel_v[i/4]=MAX(pixel[i],pixel[i+1],pixel[i+2]);
            }
    };
    console.log("ok");
}


function MAX(a,b,c){
    if(a>b&&a>c){
        return a;
    }else if(b>a&&b>c){
        return b;
    }else{
        return c;
    }
}
function MIN(a,b,c){
    if(a<b&&a<c){
        return a;
    }else if(b<a&&b<c){
        return b;
    }else{
        return c;
    }
}

```

そろそろ疲れてきましたか？
ここまでまではただの前処理ですよ。
さて、それではいよいよお待ちかねの画像処理をしていきましょう！

まずは2値化。つまり画像のすべてのピクセルを、特定の条件を満たしていれば黒、満たしていなければ白とする手法です。
今回は、サッカーボールの黒い部分と白い部分を別々のデータとして検出しようと思います。

まずは黒い部分から。
この場合、本当に真っ黒な色（#000）だけを検出しても意味がありません。
写真に写ったサッカーボールの場合、大抵は光の関係や様々な要因で本当の黒になることは無いためです。周りの色が写り込んでいる場合もあります。
そこで、今回はHSVのSとVつまり明度と彩度のみで条件を設定することにします。

```
（明度が低い＝色が暗い、彩度が低い＝色味が少ない）≒黒色
```

ソースコードは以下の通り

```js:main.js
//画像のS,Vはそれぞれ1次元配列としてnitika()に渡される
function nitika(s,v)){
    let res=[];
    for(i=0;i<s.length;i++){
        if(s<50&&v<50){
            res[i]=255;
        }else{
            res[i]=0;
        }
    }
    return res
}
```
白についても同じく

```js:main.js
//画像のS,Vはそれぞれ1次元配列としてnitika()に渡される
function nitika(s,v)){
    let res=[];
    for(i=0;i<s.length;i++){
        if(s>220&&v<50){
            res[i]=255;
        }else{
            res[i]=0;
        }
    }
    return res
}
```
これで、画像の白い部分だけを抽出したデータと黒い部分だけを抽出したデータが完成しました。
## ノイズ除去1
2値化した値にはノイズが入っています。
ノイズとは処理に必要のないデータのことです。
この場合だと、背景の白飛びの部分や背景上の白や黒のゴミなどがノイズとして考えられます。
これらをなるべく除去するために画像中のサッカーボールの白黒部分が持っていて、ノイズが持っていなさそう（持っていないものが多そう）な特徴は何かを考えます。

今回の例では、ボールの白色の部分は近くに黒色の部分が、黒色の部分は近くに白色の部分が存在しています。
そこで、2値化したデータの白色の部分を膨張させ、膨張させた白色のデータとかぶっている黒色のデータのみを残します。

同じことを白のデータに関しても行います。

さて、ここで出てきた膨張という処理について軽く説明しましょう。
膨張とは2値化したデータの白い部分を膨らませてあげることです。

アルゴリズム的な話をすると、任意のピクセルを見たときに、そのピクセルもしくは隣接するいずれかのピクセルが白であればそのピクセルの色を白にします。この処理をすべてのピクセルに対してn回繰り返すことにより、データの白い部分を任意の幅だけ膨張させることができます。

サンプルコードは諸事情により公開できません。


## ノイズ除去2
さて、殆どのノイズが消えたことで、今や画像にはサッカーボールの黒い部分と白い部分のデータしかありません。個々までくれば白い部分と黒い部分のデータを別々に保持している必要もないので、1つの配列にまとめてしまいます。

しかし、ここまでの処理で、画像から不要な部分を厳し目の判定で取り除いてきたために、ボールのテカリやゴミのついている部分も消えてしまっています。

このボール内のノイズを除去するために、1度画像を膨張させてその後収縮させるという処理を行います。

収縮とは、先程解説した膨張が白い部分のデータの面積を増やしていたのに対して、黒い部分の面積を増やす処理のことです。膨張の処理のアルゴリズムの説明の「白」を「黒」、「黒」を「白」と読み替えてください。

まずはボール内のノイズが潰れるまで画像を膨張処理します
。
その後、同じ回数だけ収縮処理を行います。
すると、ボール内のノイズを取り除くことに成功しました。

ボールの形が変わっている気がしますが、気のせいです。（膨張のアルゴリズムの組み方が悪かったです......）

## 物体検出
さて、今や画像にはボールしかのこっていません。

わたしたちの目にはボールだけがくっきりと残っています。
しかしコンピューターからすると、ただの01配列に過ぎません。
コンピュータには各ピクセルしか見えておらず、そのピクセルの集合である「塊」を見つけられてはいません。

また、画像内にもしかすると2つ以上のボールがあるかもしれません。
この辺の処理は様々な方法がありますので、「ラベリング」や「blob処理」などで検索してみてください。


## 最後に

画像認識の基本処理やテクニックがなんとなく理解できたでしょうか？
今度は機械学習を用いた画像認識に挑戦してみたいと思っていますが、実用面で必要に駆られないとモチベがわかないので記事を書くかは未定です。

それでは楽しいクリスマスを！