実験: レガシーなImage ProcessingをElixirのパイプで書いてみる

#1.動機
Deep Learning以前のレガシーな Compute Vision(以下CVと略す)ってやつは、大胆に要約すれば

> 画像というマッシブなデータを、いくつもの篩に掛けて必要とする情報に絞り込む処理だ

カメラのFOV(Field of View)全域あるいは ROI(Region of Interest)を、輝度強調やら、ノイズ除去やら、二値化処理やら、投影処理やら、エッジ検出やら、トポロジーチェックやらの諸々の Image Processing(以下IPと略す)に通し、画像が持つ大部分の情報を篩い落として目的とする情報を抽出しているのだ。これをCVエンジニアと呼ばれる人々が手書きしていた。

ん?! 待てよ。このアーキテクチャ・パターンは、Elixirのパイプ演算子のパターンと同じではないか。てことは、IPをパイプで繋いで書くことができれば、もしかして楽しいかも…
よし、早速、実験してみよう:blush:

#2.軽量IPライブラリ CImgのNIFsモジュール
さて、エンジンとなる IPライブラリは何にしようかな。OpenCVをはじめとする重量級のIPライブラリは、ビルド環境を用意するのが面倒くさそうだ。ちょこっと実験したいだけなので、最近贔屓にしている CImgにするかぁ。CImgは、お世辞にもIP/CVの機能が充実しているとは言えないライブラリだが、C++のテンプレートで記述されているので、基本的にヘッダーファイル"CImg.h"をインクルードするだけで利用できる。そんな、お手軽なところが気に入っている:wink:

---
実験のつもりではあるが、アクションを明確にするために、ざっくりと設計しておこう。

● **要求仕様**
画像imageを加工するコードをこんな風に書きたい。

```elixir
image
|> resize(xsize,ysize)
|> mirror(:y)
|> convert2gray
|> draw_box(x0,y0,x1,y1,:RED)
|> save("GRAY_416x416.jpg")
```
● **概略設計**

 1. CImgライブラリの機能を呼び出す NIFsモジュールとして実装する
 2. 画像オブジェクトの生成・加工は全てNIFs側で行い、Elixir側は Nifsから渡された画像識別ハンドルの保持・管理を担う
 3. 画像オブジェクトの実体(ヒープ)は Erlangの Resourceオブジェクトにくるみ、そのライフサイクルを Elixir/Erlangに委ねる
 4. Elixirの Guard機構を利用できるように、裸の画像識別ハンドル(Resouceオブジェクト)を構造体でくるむ
 5. 画像の加工は、CImgのマナーに従い mutableに行うものとする

こんなところかな:sunglasses:

---

ここから、具体的な実装について見ていくが、はっきり言って姉妹記事(参考文献[4])の二番煎じだ:sweat_smile: 説明を要しそうなコード部分だけに触れ、サクッと流すことにする。悪しからず。

Elixir側のAPIモジュールCImgは下記の通り。
概略設計4で述べた通り、構造体 %CImg{}を defstructし、NIFsから受け取ったハンドル(Resource)をくるんでいる。CImgの各関数では、引数を %CImg{}とパターンマッチすることでチェックしている。

関数createと grayは新たな image objectを生成して返す。関数resize,mirror,draw_boxは引数のimage objectの画像に mutableな加工を加えるので、その旨を意識して使用すべし。

このコードでは、NIFsモジュールから exportされる関数を、サブモジュール CImg.NIFに押し込んで名前空間を分離している。正直に話すと、当初の設計では defdelegateで CImg API関数を NIFsにフォワードする予定だったが、Elixirの Gaurd機構によるパラメタチェックが使えなくなるので心変わりした。その残骸だ‥‥まぁこういう書き方も良いかなとは思う。

そうそう、パイプ演算子を利用したいので、見ての通り関数の第一引数は %CImg{}としている。このコードでは、NIFsの export関数も %CImg{}を受け取るようになっているが、CImg API関数が wrapperの役割をしているので、本当は %CImg.handleを渡せば十分だ。これまた先の defdelegateの名残だ💦

```Elixir:cimg.ex
defmodule CImg do
  @moduledoc """
  CImg image processing extention.
  """
  alias __MODULE__
  
  # image object
  defstruct handle: nil

  defmodule NIF do
    @moduledoc """
    NIFs entries.
    """
    # loading NIF library
    @on_load :load_nif
    def load_nif do
      nif_file = Application.app_dir(:cimg, "priv/cimg_nif")
      :erlang.load_nif(nif_file, 0)
    end
  
    # stub implementations for NIFs (fallback)
    def cimg_create(_s),        do: raise "NIF cimg_create/1 not implemented"
    def cimg_save(_c,_s),       do: raise "NIF cimg_save/2 not implemented"
    def cimg_get_wh(_c),        do: raise "NIF cimg_get_wh/1 not implemented"
    def cimg_resize(_c,_x,_y),  do: raise "NIF cimg_resize/3 not implemented"
    def cimg_mirror(_c,_axis),  do: raise "NIF cimg_mirror/2 not implemented"
    def cimg_get_gray(_c,_pn),  do: raise "NIF cimg_get_gray/2 not implemented"
    def cimg_get_flatbin(_c),   do: raise "NIF cimg_get_flatbin/1 not implemented"
    def cimg_draw_box(_c,_x0,_y0,_x1,_y1,_rgb),
                                 do: raise "NIF cimg_draw_box/6 not implemented"
  end

  @doc """
  load the image file and create new image object.
  """
  def create(fname) do
    with {:ok, h} <- NIF.cimg_create(fname)
    do
      %CImg{
        handle: h
      }
    end
  end

  @doc "save image object to the file"
  def save(%CImg{}=cimg, fname),    do: NIF.cimg_save(cimg, fname)
  
  @doc "get width and height of the image object"
  def get_wh(%CImg{}=cimg),         do: NIF.cimg_get_wh(cimg)
  
  @doc "resize the image object"
  def resize(%CImg{}=cimg, [x, y]), do: NIF.cimg_resize(cimg, x, y)

  @doc "mirroring the image object on the axis"
  def mirror(%CImg{}=cimg, axis) when axis in [:x, :y] do
    NIF.cimg_mirror(cimg, axis)
  end

  @doc """
  create new gray image object from the image object
  """
  def get_gray(%CImg{}=cimg, opt_pn \\ 0) do
    with {:ok, gray} <- NIF.cimg_get_gray(cimg, opt_pn)
    do
      %CImg{
        handle: gray
      }
    end
  end
  
  @doc """
  get the flat binary from the image object
  """
  def to_flatbin(%CImg{}=cimg) do
    with \
      {:ok, bin} <- NIF.cimg_get_flatbin(cimg),
      shape      <- NIF.cimg_get_wh(cimg)
    do
      %{
        descr: "<u1",
        shape: shape,
        data:  bin
      }
    end
  end

  @doc """
  draw the colored box on the image object
  """
  def draw_box(%CImg{}=cimg, x0, y0, x1, y1, {_r, _g, _b}=rgb) do
    NIF.cimg_draw_box(cimg, x0, y0, x1, y1, rgb)
  end
end
```

NIFsモジュールはこうなる。
cimg_create関数の実装を見れば分かるが、画像の実体はヒープ上にとられた CImgU8インスタンスだ。そのポインタを Erlangの Resourceオブジェクトでくるみ、CImgU8インスタンスのライフサイクルをElixir/Erlangに委ねている。ライフサイクル関与する関数群は、コメント/***** ERL RESOURCE HANDLING *****/から始まるブロックに纏めてある。

毎度々々思うことだが、Resourceオブジェクトを利用すると、Resourceを剥がして操作実体(大概はヒープを指すポインタ)を取り出す作業が煩雑になる。ましてや今回は、そのResourceを %CImg{}でくるんだモノを受け取るようにしてしまった:sweat_smile: これはもう helper関数を用意するっきゃないでしょ。%CImg{}を分解して CImgU8ポインタを取り出す関数 enif_get_cimgu8()と、CImgU8ポインタをくるみ Resourceオブジェクトを生成する関数 enif_make_mycimage_resource()を用意した。

あとは説明不要かな。

```C++:cimg_nif.cc
#include <string>
#include <erl_nif.h>

#include "myCImg.h"
typedef CImg<unsigned char> CImgU8;

/***** MACRO *****/
#define enifOk(env)     enif_make_atom(env, "ok")
#define enifError(env)  enif_make_atom(env, "error")


/***** ERL RESOURCE HANDLING *****/
static ErlNifResourceType* _ResType_MyCImg = NULL;

struct MyCImg {
    //std::mutex   m_mutex;
    CImgU8*      m_img;
};

void cimg_destroy(ErlNifEnv* env, void* ptr)
{
    MyCImg* mycimg_ptr = reinterpret_cast<MyCImg*>(ptr);

    if (!mycimg_ptr->m_img) {
        delete mycimg_ptr->m_img;
    }
}

int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info)
{
    _ResType_MyCImg = enif_open_resource_type(env, NULL, "mycimg", cimg_destroy, ERL_NIF_RT_CREATE, NULL);
    return 0;
}


/***** ERL TERM CONVERTER *****/
int enif_get_cimgu8(ErlNifEnv* env, ERL_NIF_TERM term, CImgU8** cimgu8)
{
    ERL_NIF_TERM key;
    ERL_NIF_TERM handle;
    MyCImg* mycimg_ptr;

    if (enif_make_existing_atom(env, "handle", &key, ERL_NIF_LATIN1)
    &&  enif_get_map_value(env, term, key, &handle)
    &&  enif_get_resource(env, handle, _ResType_MyCImg, (void**)&mycimg_ptr)) {
        *cimgu8 = mycimg_ptr->m_img;
        return true;
    }
    else {
        return false;
    }
}

ERL_NIF_TERM enif_make_mycimg_resource(ErlNifEnv* env, CImgU8* cimgu8)
{
    MyCImg* mycimg_ptr = new(enif_alloc_resource(_ResType_MyCImg, sizeof(MyCImg))) MyCImg;
    if (!mycimg_ptr) {
        return enif_make_tuple2(env, enifError(env), enif_make_string(env, "Faild to allocate resource", ERL_NIF_LATIN1));
    }
    mycimg_ptr->m_img = cimgu8;

    ERL_NIF_TERM term = enif_make_resource(env, mycimg_ptr);
    enif_release_resource(mycimg_ptr);
    
    return enif_make_tuple2(env, enifOk(env), term);
}


/***** Elixir.CImg.functions *****/
ERL_NIF_TERM cimg_create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    ErlNifBinary bin;

    if (argc != 1
    ||  !enif_inspect_binary(env, argv[0], &bin)) {
        return enif_make_badarg(env);
    }
    std::string fname((const char*)bin.data, bin.size);

    CImgU8* img;
    try {
        img = new CImgU8(fname.c_str());
    }
    catch (CImgException& e) {
        return enif_make_tuple2(env, enifError(env), enif_make_string(env, e.what(), ERL_NIF_LATIN1));
    }

    return enif_make_mycimg_resource(env, img);
}

ERL_NIF_TERM cimg_save(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    CImgU8* img;
    ErlNifBinary bin;

    if (argc != 2
    ||  !enif_get_cimgu8(env, argv[0], &img)
    ||  !enif_inspect_binary(env, argv[1], &bin)) {
        return enif_make_badarg(env);
    }

    std::string fname((const char*)bin.data, bin.size);

    img->save(fname.c_str());

    return enifOk(env);
}

ERL_NIF_TERM cimg_get_wh(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    CImgU8* img;

    if (argc != 1
    ||  !enif_get_cimgu8(env, argv[0], &img)) {
        return enif_make_badarg(env);
    }

    int width  = img->width();
    int height = img->height();

    return enif_make_list2(env, enif_make_int(env, width), enif_make_int(env, height));
}

ERL_NIF_TERM cimg_resize(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    CImgU8* img;
    int width, height;

    if (argc != 3
    ||  !enif_get_cimgu8(env, argv[0], &img)
    ||  !enif_get_int(env, argv[1], &width)
    ||  !enif_get_int(env, argv[2], &height)) {
        return enif_make_badarg(env);
    }

    img->resize(width, height);

    return argv[0];
}

ERL_NIF_TERM cimg_mirror(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    CImgU8* img;
    char axis[2];

    if (argc != 2
    ||  !enif_get_cimgu8(env, argv[0], &img)
    ||  !enif_get_atom(env, argv[1], axis, 2, ERL_NIF_LATIN1)
    ||  (axis[0] != 'x' && axis[0] != 'y')) {
        return enif_make_badarg(env);
    }

    img->mirror(axis[0]);

    return argv[0];
}

ERL_NIF_TERM cimg_get_gray(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    CImgU8* img;
    int opt_pn;

    if (argc != 2
    ||  !enif_get_cimgu8(env, argv[0], &img)
    ||  !enif_get_int(env, argv[1], &opt_pn)) {
        return enif_make_badarg(env);
    }

    CImgU8* gray;
    try {
        gray = new CImgU8(img->getRGBtoGRAY(opt_pn));
    }
    catch (CImgException& e) {
        return enif_make_tuple2(env, enifError(env), enif_make_string(env, e.what(), ERL_NIF_LATIN1));
    }

    return enif_make_mycimg_resource(env, gray);
}


ERL_NIF_TERM cimg_draw_box(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    CImgU8* img;
    double x0, y0, x1, y1;
    char color[3];
    
    int arity;
    const ERL_NIF_TERM* terms;

    if (argc != 6
    ||  !enif_get_cimgu8(env, argv[0], &img)
    ||  !enif_get_double(env, argv[1], &x0)
    ||  !enif_get_double(env, argv[2], &y0)
    ||  !enif_get_double(env, argv[3], &x1)
    ||  !enif_get_double(env, argv[4], &y1)
    ||  !enif_get_tuple(env, argv[5], &arity, &terms)
    ||  arity != 3) {
        return enif_make_badarg(env);
    }
    
    for (int i = 0; i < arity; i++) {
        int tmp;
        enif_get_int(env, terms[i], &tmp);
        color[i] = tmp;
    }
    
    int width  = img->width();
    int height = img->height();

    int ix0 = x0*width;
    int iy0 = y0*height;
    int ix1 = x1*width;
    int iy1 = y1*height;
    
    img->draw_rectangle(ix0, iy0, ix1, iy1, color, 1, ~0U);

    return argv[0];
}

// Let's define the array of ErlNifFunc beforehand:
static ErlNifFunc nif_funcs[] = {
    // {erl_function_name, erl_function_arity, c_function, dirty_flags}
    {"cimg_create",   1, cimg_create,   0},
    {"cimg_save",     2, cimg_save,     0},
    {"cimg_get_wh",   1, cimg_get_wh,   0},
    {"cimg_resize",   3, cimg_resize,   0},
    {"cimg_mirror",   2, cimg_mirror,   0},
    {"cimg_get_gray", 2, cimg_get_gray, 0},
    {"cimg_draw_box", 6, cimg_draw_box, 0},
};

ERL_NIF_INIT(Elixir.CImg.NIF, nif_funcs, load, NULL, NULL, NULL)
```

今回は、CImgライブラリにも少し拡張を加えた。なぜって？ どういう訳か、RGB画像をGRAY画像に直接変換する機能が無かったのだ。まあ確かに YUVフォーマットを経由すれば出来はするのだが‥‥

という訳で、下の様に RGB画像からGRAY画像を新たに生成するメンバ関数getRGBtoGRAY()を CImg<T>クラスに付け加えた。そう、CImgライブラリには、ユーザー定義のメンバ関数を付け加える仕組みが用意されているのだ。その仕組みは、"cimg_plugin"シンボルで Cプリプロセッサのインクルード機能を悪用💦して‥‥小生のような変人に受ける仕組みである。

その他、#defineでシンボルを定義することで、いろいろと機能をコンフィグすることが出来る様だ。下のコードでは、

- #define cimg_display 0 - 表示機能をdisable
- #define cimg_use_jpeg - jpegファイルの読み書きは libjpeg.aを使用

としている。libjpeg.aの使用は、何かと脆弱性の話題に事欠かない imagemagickを避けてのことなのだが‥‥ Nervesではいろいろと物議を醸すことになる。以前の記事[5]で話した通り。

```c++:myCImg.h
#ifndef cimg_plugin

#define cimg_plugin     "myCImg.h"
#define cimg_display    0
#define cimg_use_jpeg
#include "CImg.h"
using namespace cimg_library;

#else
/**************************************************************************}}}*/
/*** CImg Plugins:                                                          ***/
/**************************************************************************{{{*/
// option: image convert POSI/NEGA 
enum {
    cPOSI = 0,
    cNEGA
};

// get a GRAY converted image
CImg<T> getRGBtoGRAY(int optPN=cPOSI)
{
    if (_spectrum != 3) {
        throw CImgInstanceException(_cimg_instance
                                    "getRGBtoGRAY(): Instance is not a RGB image.",
                                    cimg_instance);
    }
    CImg<T> res(width(), height(), depth(), 1);
    T *R = data(0,0,0,0), *G = data(0,0,0,1), *B = data(0,0,0,2), *Y = res.data(0,0,0,0);
    const longT whd = (longT)width()*height()*depth();
    cimg_pragma_openmp(parallel for cimg_openmp_if_size(whd,256))
    for (longT i = 0; i < whd; i++) {
        Y[i] = (T)(0.299f*R[i] + 0.587f*G[i] + 0.114f*B[i]);

        if (optPN == cNEGA) {
            Y[i] = cimg::type<T>::max() - Y[i];
        }
    }
    return res;
}
#endif
```

https://github.com/shoz-f/cimg_ex

#3.実演
It's showtime!
早速、パイプで IPをずらずらと繋げて画像を加工してみよう。
(注:CImg.get_gray()の所で対象画像が切り替わっている)

```Elixir
C:\home\shozo\Elixir\cimg>iex -S mix
make: Nothing to be done for 'all'.
Interactive Elixir (1.10.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> CImg.create("test/IMG_9458.jpg") \
...(1)> |> CImg.resize([416,416]) \
...(1)> |> CImg.mirror(:y) \
...(1)> |> CImg.get_gray() \
...(1)> |> CImg.draw_box(0.2, 0.3, 0.4, 0.6, {255, 0, 0}) \
...(1)> |> CImg.save("test/GRAY_416x416.jpg")


(おまけ)
iex(2)> img = CImg.create("test/IMG_9458.jpg")
%CImg{handle: #Reference<0.3304398633.1283850242.178113>}
iex(3)> npy = struct(%Npy{}, CImg.to_flatbin(img))
%Npy{
  data: <<229, 200, 160, 229, 200, 160, 229, 200, 160, 229, 200, 160, 229, 200,
    160, 229, 200, 160, 229, 200, 160, 229, 200, 160, 229, 201, 161, 228, 200,
    160, 228, 200, 160, 228, 200, 160, 228, 200, 160, 228, 200, 160, 228, 200,
    160, 229, 201, 161, 227, ...>>,
  descr: "<i1",
  fortran_order: false,
  shape: [2448, 3264]
}
iex(4)> Npy.save("test/IMG_9458.npy", npy)
:ok
iex(5)>
```
![PIPE_CIMG.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/a7c69b44-28fc-a779-dcda-7f9c7f171222.jpeg)
GOOD! かな:blush:

#あとがき
レガシーなImage Processingをパイプで繋いで書けるようにしてみた。1 by 1で画像に加工を加えていくIP処理であれば、期待していた通りの見た目になったかな。(CImg.の接頭語が鬱陶しいが --;)

尤も、本格的なCV/IPでは、処理フローの途中で画像内に点在する特徴点にフォーカスした複数の処理に分かれ、そしてそれらの結果を統合し再び一本の処理に戻るといった流れが繰り返される。CImgモジュールを実用レベルに引き上げるには、そのような分岐-合流フローを心地よく表現できる仕組みが必要だろうな。

何はともあれ実験終了。

PS.予定では1時間弱で実装&テスト出来る筈だったが、久しぶりのNIFs故に、フック関数をdefpで定義すると言う間抜けな間違いになかなか気づかず半日を溶かしてしまった:sob:

#参考文献
[1] [CVonline: Visual Processing Software, Models & Environments](http://homepages.inf.ed.ac.uk/rbf/CVonline/SWEnvironments.htm)
[2] [The CImg Library - C++ Template Image Processing Toolkit](https://cimg.eu/)
[3] [Erlang Run-Time System Application (ERTS) Reference Manual: erl_nif](https://erlang.org/doc/man/erl_nif.html)
[4] 拙著/ [実況: mutableなストレージのNIFsを実装してみる](https://qiita.com/ShozF/items/79fd4a631374f3569a51)
[5] 拙著/ [Nerves/rpiで TensorFlow liteを使ってみる](https://qiita.com/ShozF/items/61bc3b632ed2539a456e)