JuliaImagesの備忘録その４

# Factored kernelsを使った簡単な実験

前回の内容を踏まえて、Factored kernelsのご利益を検証してみました。

ドキュメントでFactored kernelsの例としてGaussian kernelsが引き合いに出されていましたが、`Kernel`モジュールは2次元配列をそのまま返してしまいますので、`KernelFactors`モジュールを使って比較します。なお、どちらも`ImageFiltering`パッケージに含まれています。

実験に使ったスクリプトは[こちら](https://gist.github.com/Dsuke-K/ef668674370ef8192e1d996c173ab421)


## 前準備

テスト画像とベンチマーク計測用の関数を用意しておきます。関数`filtering`と`filtering!`はそれぞれ`imfilter`と`imfilter!`用です。

```Julia
using Images, TestImages, ImageFiltering
using BenchmarkTools

println("nthreads = $(Threads.nthreads())")
img = testimage("")

function filtering(img, kernel)
    imfilter(img, kernel)  # precompile
    bm =  @benchmark imfilter(img, kernel)
    display("text/plain", bm)
    println("")
end

function filtering!(out, img, kernel)
    imfilter!(out, img, kernel)  # precompile
    bm =  @benchmark imfilter!(out, img, kernel)
    display("text/plain", bm)
    println("")
end
```

## パフォーマンスの比較

Gaussian blurを題材に、`Kernel`, `KernelFactors`と`Python+OpenCV`を比べてみます。

### Kernelモジュールの場合

まず、2次元のGaussianカーネルを準備します。カーネルサイズは13x13です。

```Julia
# Kernel module ver.
kernel = Kernel.gaussian((3, 3))
```

これを使って`imfilter`と`imfilter!`のベンチマークを取ってみます。なお、下記の結果は`export JULIA_NUM_THREADS=1`とした状態です。

```Julia
julia> filtering(img, kernel)
BenchmarkTools.Trial:
  memory estimate:  4.14 MiB
  allocs estimate:  309
  --------------
  minimum time:     12.670 ms (0.00% GC)
  median time:      13.316 ms (0.00% GC)
  mean time:        13.907 ms (4.15% GC)
  maximum time:     72.896 ms (78.81% GC)
  --------------
  samples:          359
  evals/sample:     1

julia> out = similar(img)
julia> filtering!(out, img, kernel)
BenchmarkTools.Trial:
  memory estimate:  413.09 KiB
  allocs estimate:  119
  --------------
  minimum time:     13.554 ms (0.00% GC)
  median time:      14.015 ms (0.00% GC)
  mean time:        14.413 ms (1.58% GC)
  maximum time:     73.900 ms (79.41% GC)
  --------------
  samples:          347
  evals/sample:     1
```

どちらも約14ms程度でした。これは遅い。

## KernelFactorsモジュールの場合

[前回の記事](https://qiita.com/Dsuke-K/items/8fc8cccb4bf0b675637d#factored-kernels)で紹介したFactored kernelの特性を利用した方法で計算させてみます。`Kernel`モジュールを`KernelFactors`に切り替えるだけで、あとは先程と同じです。

```Julia
## KernelFactors module ver.
kernel = KernelFactors.gaussian((3, 3))
```

先ほどと同じようにベンチマークを取ってみます。なお、下記の結果も`export JULIA_NUM_THREADS=1`とした状態です。

```Julia
julia> filtering(img, kernel)
BenchmarkTools.Trial:
  memory estimate:  4.13 MiB
  allocs estimate:  292
  --------------
  minimum time:     9.564 ms (0.00% GC)
  median time:      10.171 ms (0.00% GC)
  mean time:        10.691 ms (5.07% GC)
  maximum time:     73.211 ms (83.50% GC)
  --------------
  samples:          467
  evals/sample:     1

julia> out = similar(img)
julia> filtering!(out, img, kernel)
BenchmarkTools.Trial:
  memory estimate:  398.23 KiB
  allocs estimate:  102
  --------------
  minimum time:     10.450 ms (0.00% GC)
  median time:      10.801 ms (0.00% GC)
  mean time:        11.042 ms (1.67% GC)
  maximum time:     71.028 ms (82.64% GC)
  --------------
  samples:          453
  evals/sample:     1
```

4ms程度速くなりましたが、まだまだ遅く感じます。

## Python + OpenCVの場合

ここで、比較対象としてOpenCVがどの程度か確認してみます。

```Python
In [1]: import cv2
In [2]: img = cv2.imread("cameraman.png")
In [6]: %%timeit
   ...: cv2.GaussianBlur(img, (13, 13), 3)
   ...:
1.53 ms ± 215 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```

* Python 3.7.0
* opencv-python v3.4.2

## ちょっとしたチューニング

上記の結果は少し期待はずれだったのですが、上記の実験ではどうもCPUを使い切れていないようでした。そこで、Juliaが使えるスレッド数を増やしてパフォーマンスの変換を観察してみます。

やり方は、

* 今までの実験を一つのスクリプトとして保存しておきます
* `export JULIA_NUM_THREADS=n`(n=1~4)とする
* 先程のスクリプトを実行

この結果のサマリは次のようになりました。なお、`imfilter`と`imfilter!`の速度はほぼ同じだったので、以降では`imfilter!`の結果のみを示します。また、各スレッド数のベンチマークは`@benchmark`のmean timeのみを示します。

### Kernelモジュールとimfilter!

スレッドを増やすとパフォーマンスが落ちてしまいました。

| nthreads |    Benchmark|
|:---------|------------:|
| 1        |      14.413 | 
| 2        |      27.138 | 
| 3        |      27.249 | 
| 4        |      24.990 | 

### KernelFactorsモジュールとimfilter!

スレッド数2で頭打ちでした。

| nthreads |    Benchmark|
|:---------|------------:|
| 1        |      11.042 | 
| 2        |       5.211 | 
| 3        |       5.812 | 
| 4        |       5.394 | 

以上をまとめたのが次のグラフです。スレッド数が1か2以上でパフォーマンスが大きく変わってしまうみたいですね。

![imfilter!.png](https://qiita-image-store.s3.amazonaws.com/0/171142/e098b4bb-468b-b229-e945-523ca04fc5ba.png)

## あとがき

* Kernelのバージョンでスレッド数を増やすと、なんでこんなにパフォーマンス落ちたんでしょう。
* アルゴリズムの改良は大事ですね。
* OpenCV速いですね。

実行環境

```Julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.0.0 (2018-08-08)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> versioninfo()
Julia Version 1.0.0
Commit 5d4eaca0c9 (2018-08-08 20:58 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin14.5.0)
  CPU: Intel(R) Core(TM) i5-6360U CPU @ 2.00GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, skylake)
```