「...サクッと画像加工するノリ」の猿まねをやってみた💦

## 1.はじめに

@piacerexさんのQiita記事【「JupyterNotebook + NumPyでサクッと画像加工するノリ」をElixirでやってみた】に触発されて、拙作の cimg_exでもやってみようと思い立った。要は猿まねである🐵

まず最初に、Livebookのインストールならびに起動etc.に関しては、@piacerexさんの[記事](https://qiita.com/piacerex/items/533d26c81bada4741422)を参照して頂きたい。そう、手抜きである。以下、Livebookが起動できて、無事ブラウザを接続出来たものとして話を進める。

本記事を通して必要となるモジュールは下記の4つである。
`Elixir cell`の左上の"Evaluate"ボタンを押すか、cell内でctrl+Enterをキーインしてインストールを実行する。

```elixir
Mix.install([
  {:cimg, "~> 0.1.6"},
  {:nx, "~> 0.1.0"},
  {:kino, "~> 0.3.1"},
  {:download, "~> 0.0.4"}
])
```

次に、@piacerexさんの記事に倣って、旧世代の画像処理屋にとっては超有名人のLennaさんの画像をダウンロードしておく。ここまでが前準備だ。

```elixir
File.rm("Lenna_%28test_image%29.png")

lenna =
  Download.from("https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png")
  |> elem(1)
```

## 2.CImgでサクッと猿まねする

猿まねなので、やってみる画像加工の課題は元記事と同じく次の３つだ。

1. Lennaさん画像をロードして、KinoでLivebookに表示
2. グレイ画像に変換して表示
3. 反転画像に変換して表示

課題１のコードは次の通り。CImg.load/1で画像を読み込み、その画像をjpegフォーマットに変換し Kino.Image.new/2に渡しておしまい。

```elixir
img = CImg.load(lenna)

img
|> CImg.to_jpeg() # CImg画像をjpegバイナリに変換する
|> Kino.Image.new(:jpeg)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/1299101c-733c-fdcf-c39a-7de1a5534071.png)

課題２のコードは次の通り。

```elixir
img
|> CImg.gray() # グレイ画像に変換する
|> CImg.to_jpeg()
|> Kino.Image.new(:jpeg)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/0e6431ba-c0ea-281c-01a1-0f83f790a285.png)

課題３のコードは次の通り。

```elixir
img
|> CImg.invert() # カラー反転画像に変換する
|> CImg.to_jpeg()
|> Kino.Image.new(:jpeg)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/45c67805-96e2-ee6a-f2df-00190e4ee3ba.png)

……記事になる内容が無かった orz

## ３.Nxを使ってごにょごにょやってみる

このままでは薄っぺらな記事にしかならないので、Nxを使って次の２つの課題をやってみる。要は記事の水増しである。

1. Lennaさんの画像をR/G/Bそれぞれのカラー・プレーンに分割して表示
2. Rプレーンを差し替えた画像を合成して表示

これらの課題は、cimg_exが目的とする用途ではたぶん発生しないので、機能として実装していない。ゆえに、Nxを使ってごにょごにょとやることになる。数行のコードで終わるなんてことはない…きっと

課題4に取り掛かろう。
まず CImg画像を Nx.tensorに変換する。CImg.to_flat/2を用いると CImg画像をバイナリにシリアライズすることができる(正しくはNpy形式)。そのバイナリを Nx.from_binary/3に食わして、Nx.reshape/3で整形し Nx.tensor `rgb`を得る。この時点では、tensorに格納されている画像データは所謂 (N)HWC形式になっている。

```elixir
rgb =
  CImg.to_flat(img, [{:dtype, "<u1"}]).data  # CImg画像をバイナリにシリアライズする(NHWCオーダー)
  |> Nx.from_binary({:u, 8})
  |> Nx.reshape({512, 512, :auto})
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/56d72b9e-1b69-e715-bead-757de0663671.png)

画像データをR/G/Bの各カラー・プレーンに分割するには、(N)HWC形式よりも(N)CHW形式の画像データの方が都合が良いので、Nx.transpose/2で`rgb`の軸を入れ替えて(N)CHW形式に変換する。これで tensorの一軸目のインデックスが色を表すことになったので、あとは red=tensor[0], green=tensor[1], blue=tensor[2]の様に分割すればよい。

```elixir
[red, green, blue] =
  Nx.transpose(rgb, axes: [2, 0, 1]) # NHWCからNCHWに変換する
  |> (&[&1[0], &1[1], &1[2]]).()  # R/G/Bに分割する
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/9baf61e7-977a-5ede-3b7c-232fad6c46da.png)

さて、無事に画像データをR/G/Bの tensor - red, green, blue - に分割出来たわけだが、今やそれらのtensorは 24bitカラー情報を持っておらず、カラー・プレーン毎の 8bitの輝度情報しか持っていない。つまり、red, green, blueをそのままCImg画像に戻すと、赤,緑,青の画像にはならないのだ。それぞれで足らない色情報をゼロで補い、24bitカラー情報に加工する必要がある。

加工で必要となる zero tensorを用意しよう。

```elixir
zero = Nx.broadcast(0, {512, 512}) |> Nx.as_type({:u, 8})
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/8071d45e-a877-8585-bd97-b19c68c3a51d.png)

課題４の総仕上げだ。
tensorの組、赤{red,zero,zero}, 緑{zero,green,zero}, 青{zero,zero,blue}を、それぞれ Nx.concatenate/2(4行目)で合成し、それら3つの合成 tensorを縦に繋げて一つにする(6行目)。これを Nx.to_binary/2でシリアライズし、CImg.create_from_bin/6で CImg画像に戻す。あとは例の如く。(完)

```elixir
res =
  [red, zero, zero, zero, green, zero, zero, zero, blue]
  |> Enum.map(&Nx.reshape(&1, {512, 512, 1}))  # 4行目のconcatenate/2の為に軸を増やす
  |> Enum.chunk_every(3)
  |> Enum.map(&Nx.concatenate(&1, axis: 2))  # R/G/B [512][512][3] のそれぞれの画像データを合成する
  |> IO.inspect()
  |> Nx.concatenate()  # R/G/BをH方向に繋げて一つの画像データにする
  |> Nx.to_binary()
  |> CImg.create_from_bin(512, 512 * 3, 1, 3, "<u1")  # CImg画像に変換する
  |> CImg.to_jpeg()
  |> Kino.Image.new(:jpeg)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/53af938a-f724-035b-4cbf-c3ab5238214f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/1aacd3a5-cd43-1b3d-e87c-c0c5ef48218c.png)

課題５は、課題４の応用だ。
Rプレーンのデータを Gプレーンで置き換えてみた。ブルーが映えるモノクロームな画像になった。予想外だ。コードの説明は省略する。

```elixir
res =
  [green, green, blue]  # RのデータをGのデータで置き換える
  |> Enum.map(&Nx.reshape(&1, {512, 512, 1}))
  |> Nx.concatenate(axis: 2)
  |> IO.inspect()
  |> Nx.to_binary()
  |> CImg.create_from_bin(512, 512, 1, 3, "<u1")
  |> CImg.to_jpeg()
  |> Kino.Image.new(:jpeg)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/e713be97-d69d-3d2e-aa1d-650e2293ddc4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/4c51e552-75ea-7117-3d46-ed16173c517e.png)

## 4.まとめ
以上、猿まねとNxでごにょごにょをやってみた。結局中身が無い記事になったよーな…

[注釈]
拙作の cimg_exは、組み込みDeep Learningエンジンと組み合すことを狙いとした、ライト・ウエイトな画像処理モジュールを目指している。そのため、SHIFT局所特徴量検出やCannyエッジ検出、はたまた表示機能やカメラ制御など、現場ではほぼ不要と思われる機能は搭載していない。そう、実はこの記事の様な汎用的な用途には向かないのだ。パッと見には機能が少なく貧相に見えるが、同じく拙作のTensorflow lite拡張 TflInterpと組み合わせていくつか Deep Learningのデモを書いてみたところ、案外これで十分だったりするのだが…