1.はじめに
@piacerexさんのQiita記事【「JupyterNotebook + NumPyでサクッと画像加工するノリ」をElixirでやってみた】に触発されて、拙作の cimg_exでもやってみようと思い立った。要は猿まねである🐵
まず最初に、Livebookのインストールならびに起動etc.に関しては、@piacerexさんの記事を参照して頂きたい。そう、手抜きである。以下、Livebookが起動できて、無事ブラウザを接続出来たものとして話を進める。
本記事を通して必要となるモジュールは下記の4つである。
Elixir cell
の左上の"Evaluate"ボタンを押すか、cell内でctrl+Enterをキーインしてインストールを実行する。
Mix.install([
{:cimg, "~> 0.1.6"},
{:nx, "~> 0.1.0"},
{:kino, "~> 0.3.1"},
{:download, "~> 0.0.4"}
])
次に、@piacerexさんの記事に倣って、旧世代の画像処理屋にとっては超有名人のLennaさんの画像をダウンロードしておく。ここまでが前準備だ。
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でサクッと猿まねする
猿まねなので、やってみる画像加工の課題は元記事と同じく次の3つだ。
- Lennaさん画像をロードして、KinoでLivebookに表示
- グレイ画像に変換して表示
- 反転画像に変換して表示
課題1のコードは次の通り。CImg.load/1で画像を読み込み、その画像をjpegフォーマットに変換し Kino.Image.new/2に渡しておしまい。
img = CImg.load(lenna)
img
|> CImg.to_jpeg() # CImg画像をjpegバイナリに変換する
|> Kino.Image.new(:jpeg)
課題2のコードは次の通り。
img
|> CImg.gray() # グレイ画像に変換する
|> CImg.to_jpeg()
|> Kino.Image.new(:jpeg)
課題3のコードは次の通り。
img
|> CImg.invert() # カラー反転画像に変換する
|> CImg.to_jpeg()
|> Kino.Image.new(:jpeg)
……記事になる内容が無かった orz
3.Nxを使ってごにょごにょやってみる
このままでは薄っぺらな記事にしかならないので、Nxを使って次の2つの課題をやってみる。要は記事の水増しである。
- Lennaさんの画像をR/G/Bそれぞれのカラー・プレーンに分割して表示
- 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形式になっている。
rgb =
CImg.to_flat(img, [{:dtype, "<u1"}]).data # CImg画像をバイナリにシリアライズする(NHWCオーダー)
|> Nx.from_binary({:u, 8})
|> Nx.reshape({512, 512, :auto})
画像データをR/G/Bの各カラー・プレーンに分割するには、(N)HWC形式よりも(N)CHW形式の画像データの方が都合が良いので、Nx.transpose/2でrgb
の軸を入れ替えて(N)CHW形式に変換する。これで tensorの一軸目のインデックスが色を表すことになったので、あとは red=tensor[0], green=tensor[1], blue=tensor[2]の様に分割すればよい。
[red, green, blue] =
Nx.transpose(rgb, axes: [2, 0, 1]) # NHWCからNCHWに変換する
|> (&[&1[0], &1[1], &1[2]]).() # R/G/Bに分割する
さて、無事に画像データをR/G/Bの tensor - red, green, blue - に分割出来たわけだが、今やそれらのtensorは 24bitカラー情報を持っておらず、カラー・プレーン毎の 8bitの輝度情報しか持っていない。つまり、red, green, blueをそのままCImg画像に戻すと、赤,緑,青の画像にはならないのだ。それぞれで足らない色情報をゼロで補い、24bitカラー情報に加工する必要がある。
加工で必要となる zero tensorを用意しよう。
zero = Nx.broadcast(0, {512, 512}) |> Nx.as_type({:u, 8})
課題4の総仕上げだ。
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画像に戻す。あとは例の如く。(完)
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)
課題5は、課題4の応用だ。
Rプレーンのデータを Gプレーンで置き換えてみた。ブルーが映えるモノクロームな画像になった。予想外だ。コードの説明は省略する。
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)
4.まとめ
以上、猿まねとNxでごにょごにょをやってみた。結局中身が無い記事になったよーな…
[注釈]
拙作の cimg_exは、組み込みDeep Learningエンジンと組み合すことを狙いとした、ライト・ウエイトな画像処理モジュールを目指している。そのため、SHIFT局所特徴量検出やCannyエッジ検出、はたまた表示機能やカメラ制御など、現場ではほぼ不要と思われる機能は搭載していない。そう、実はこの記事の様な汎用的な用途には向かないのだ。パッと見には機能が少なく貧相に見えるが、同じく拙作のTensorflow lite拡張 TflInterpと組み合わせていくつか Deep Learningのデモを書いてみたところ、案外これで十分だったりするのだが…