画像処理100本ノックにJavaScriptで挑戦してみた 【画像処理100本ノックJS】

![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/8c282e87-e882-1c82-5c55-ba0814b0dea3.gif)



# 概要

画像処理100本ノックをJavaScriptで挑戦してみました。
「ブラウザ上で完結させたい」 & 「デモを共有できたら面白い」という動機ではじめました。
まだ100問完了していませんが、ここまで解いてみた所感を書きます。

とりあえず、、、「100問は辛いです。」

# 画像処理100本ノックについて

<blockquote>
画像処理が初めての人のための問題集をつくったりました。（完成！！）

研究室の後輩用に作ったものです。

自然言語処理100本ノックがあるのに、画像処理のがなかったので作ってみました。

画像処理の基本のアルゴリズム理解につながると思います。

https://qiita.com/yoyoyo_/items/2ef53f47f87dcf5d1e14
</blockquote>

[GitHub](https://github.com/yoyoyo-yo) [Qiita](https://qiita.com/yoyoyo_/items/2ef53f47f87dcf5d1e14)

この「画像処理100本ノック」にはPythonとC++のコードが解答例として用意されています。


# デモの例 [※ → デモ はこちらから(Gasyori100KnockJS)](https://s-yoshiki.github.io/Gasyori100knockJS/#/) 


いくつかのデモの例を紹介します。



[
![201904071958_njvf00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/a529b9d8-6c70-8641-2ac9-dd7f788edebc.png)
**大津の2値化**
](https://s-yoshiki.github.io/Gasyori100knockJS/#/questions/ans4/iframe)

[
![20190518004109.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/2b3e4a1f-ecec-0fcc-e7e0-065d01f77447.png)
**プーリング (MAX値)**
](https://s-yoshiki.github.io/Gasyori100knockJS/#/questions/ans8/iframe)

[
![ 2019-05-26 19.56.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/8ec866ef-3727-a77d-5b2b-ed666763d786.png)
**メディアンフィルタ**
](https://s-yoshiki.github.io/Gasyori100knockJS/#/questions/ans10/iframe)

[
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/57a350e4-1bb8-6f95-8534-d36ca72de6d7.png)
**アフィン変換 (スキュー)**
](https://s-yoshiki.github.io/Gasyori100knockJS/#/questions/ans31/iframe)

[
![ 2019-05-26 19.51.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/e4a30df1-8475-fbf8-10f0-909276226126.png)
**テンプレートマッチング**
](https://s-yoshiki.github.io/Gasyori100knockJS/#/questions/ans57/iframe)

[
![ 2019-05-26 19.57.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/019ee13f-482a-427d-3d39-2c14e08261db.png)
**バイリニア補間**
](https://s-yoshiki.github.io/Gasyori100knockJS/#/questions/ans26/iframe)


[
![ 2019-05-26 20.03.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/d852202c-bc3d-102b-72f5-11d7bba01824.png)
**JPEG圧縮 (Step.2)DCT+量子化**
](https://s-yoshiki.github.io/Gasyori100knockJS/#/questions/ans38/iframe)

[
![ 2019-05-26 19.53.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/0846d974-d08f-cd3c-ee2a-260d1cdcec25.png)
**Harrisのコーナー検出**
](https://s-yoshiki.github.io/Gasyori100knockJS/#/questions/ans83/iframe)


# 実装の紹介

※ [GitHub](https://github.com/s-yoshiki/Gasyori100knockJS) にソースを置いてます。


## canvasを使った画像の表示と操作

### 画像の読み込み

画像の表示ピクセル値の操作にはcanvasのAPIを利用しています。

任意の画像を読み込む際には次のように実装しています。



```html
<canvas id="canvas"></canvas>
```


```js
// canvas関連のオブジェクト
const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")

// 任意の画像読み込み
let image = new Image()
image.src = "path/to/image.png"

// 読み込み完了時のイベント
image.onload = () => {
    canvas.width = image.width
    canvas.height = image.height
    ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
    // canvas描画後、画像の処理を実行
}
```



### ピクセル操作

![html5-canvas-imageData-example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/38622e13-3068-164c-5d7e-1327c76db99d.png)

[CREATE A PAINT BUCKET TOOL IN HTML5 AND JAVASCRIPT 
http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/](http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/) 
より

getImageDataメソッドでImageDataオブジェクトを取得しています。
このオブジェクトにはr, g, b, a の順に画像情報が格納されています。
putImageDataを使って編集したImageDataオブジェクトをcanvasに描画しています。

これを用いることにより大概の画像の処理を行うことができます。

(canvasを用いず、Imageオブジェクトの画像情報に対して直接参照する方法があればいいんですけど... )

```js

let src = ctx.getImageData(0, 0, canvas.width, canvas.height)
let dst = ctx.createImageData(canvas.width, canvas.height)
for (let i = 0; i < src.data.length; i += 4) {
    dst.data[i] = src.data[i]          // r
    dst.data[i + 1] = src.data[i + 1]  // g
    dst.data[i + 2] = src.data[i + 2]  // b
    dst.data[i + 3] = src.data[i + 3]  // a (透過度)
}
```

例えば、グレースケール画像であれば次のような処理になります。

```js

const grayscale = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b
// 略
for (let i = 0; i < src.data.length; i += 4) {
    let gray = grayscale(src.data[i], src.data[i + 1], src.data[i + 2])
    dst.data[i] = gray[0]
    dst.data[i + 1] = gray[1]
    dst.data[i + 2] = gray[2]
    dst.data[i + 3] = src.data[i + 3]
}
ctx.putImageData(dst, 0, 0)
```

こんな風に表示されます。

![201904071946_398cds.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/fa6d312f-9317-a586-6283-d1ff3bcbafe3.png)

[参考 : 画像をグレースケールに変換する JavaScript + canvas 【画像処理】](https://tech-blog.s-yoshiki.com/2019/04/1111/)



### ヒストグラムの表示



![ 2019-05-26 1.46.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82419/ca0e63c3-1ae5-2c05-759a-a2f893a45631.png)

このデモでは、ヒストグラムの表示にChart.jsを使っています。

実装については次のように行なっています。


**ヒストグラム描画**

```js

import Chart from "chart.js"

export default class Histogram {
  /**
   * ヒストグラムを描画する
   * @param {Object} canvas 
   * @param {Object} data 
   */
  static renderHistogram(canvas, data) {
    let labels = new Array(data.length).fill('')
    new Chart(canvas, {
      type: 'bar',
      data:{
        labels,
        datasets: [
          {
            label: '画素値',
            data,
            backgroundColor: "rgba(80,80,80,0.5)"
          }
        ],
      },
      options: {
        title: {
          display: true,
          text: 'Histogram'
        },
        scales: {
          yAxes: [{
            ticks: {
              suggestedMin: 0,
            }
          }]
        },
        animation: {
          duration: 0
        }
      }
    })
  }
}
```

```js
import Histogram from 'path/to/Histogram'

const grayscale = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b
// 略
let pixelValues = new Array(255).fill(0)
for (let i = 0; i < src.data.length; i += 4) {
    let gray = grayscale(src.data[i], src.data[i + 1], src.data[i + 2])
    gary = Math.floor(gray)
    pixelValues[gray]++
}
Histogram.renderHistogram(canvas, pixelValues)
```

[参考 : 画像のヒストグラムを表示する Char.js JavaScript canvas](https://tech-blog.s-yoshiki.com/2019/05/1245/)

## 他

フレームワークにVueを使っています。
またSPAにも挑戦しました。

コンポーネントの制御が難しく、処理がバグっている箇所があると思います()


# まとめ : JSで挑戦するメリット・デメリット

ブラウザ上で動かせるのがJSを使う最大のメリットだと思います。
加えて、チャート系のライブラリが豊富なので、matplotlibに比べ、グラフィカルな表現がしやすいのも良い点だと感じました。

一方で、行列演算に関してはJSではnumjsやmath.jsといったものはありますが、
Numpyほど簡潔に行列の処理を書くことはできません。

(※今回のデモではアフィン変換などの行列演算を多用する箇所で math.js を使いました。)

また、フーリエ変換のデモでは、実装に複素数を利用しますが、
Pythonは「j」が利用できるのに対し、JSの場合は実部と虚部に分けるような処理に実装する必要がありました。

改めてPython、Numpyの偉大さには感謝したいと思います。


# 他

[
**画像処理100本ノックJS**
https://s-yoshiki.github.io/Gasyori100knockJS/#/
](https://s-yoshiki.github.io/Gasyori100knockJS/#/)

[
**画像処理100本ノックJS - GitHub**
https://github.com/s-yoshiki/Gasyori100knockJS
](https://github.com/s-yoshiki/Gasyori100knockJS)

[
**JavaScriptで画像処理100本ノックに挑戦してみた**
https://tech-blog.s-yoshiki.com/entry/110
](https://tech-blog.s-yoshiki.com/entry/110)