Python で画像フィルタパラメータを blackbox 最適化で探索してみるメモ

## 背景

対象の関数(問題)がブラックボックス的なものですと, そのパラメータを求めるのは

blackbox 最適化や, 機械学習の分野では hyperparameter 最適化(探索)と呼ばれているようですね.

画像処理でも blackbox 最適化をしたい要求がよくあります.

たとえば, ターゲットとなるオシャンティな画像(インスタ映え画像)に見た目を合わせて, 自分のとったちょっとイマイチな写真でもオシャンティな画像にしたいとか. この場合, 明るさとか, セピア調/フィルム調フィルタとかのフィルタパラメータを探索します.
(手動で見つけるだと無限に時間が溶けてしまいつらい)

  
今回はもう少し問題を単純化して, ImageMagick でブラーをかけた画像で, そのブラーのパラメータを推定してみます.

画像の誤差にはとりあえず RMSE を使ってみます
(一致するほど値が低くなる)

Python で blackbox 関数の最適化をするメモ
https://qiita.com/syoyo/items/6c33acb0fd475e651f2f

Python で外部プログラムを実行して結果の行をパースするメモ
https://qiita.com/syoyo/items/d13af423604192cee41c

を参考にして, 外部コマンドで ImageMagick を動かし, benderopt で最適化してみます.

## データとコード

ぼかし前

![shimokita.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36275/ba9ca4e8-c47a-40dc-8ef9-bd80c1a65695.jpeg)

ぼかし後
(`-blur 27x20`)

![shimokita-blur.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36275/60fdcced-cccd-51bc-d949-06b2fdd17865.jpeg)


```py
from benderopt import minimize
import numpy as np
import logging
import subprocess
import parse

logging.basicConfig(level=logging.DEBUG) # logging.INFO will print less information

blurred_filename = "shimokita-blur.jpg" # convert -blur 27x20
ref_filename = "shimokita.jpeg"

k_num_eval = 10000

def extract_result(lines):
    for line in lines:
        print(line.decode("utf-8"))
        ret = parse.parse("{:g} ({:g})", line.decode("utf-8"))
        if ret:
            return ret[0]

    raise RuntimeError("Failed to extract value from result.")

count = 0

def f(radius, sigma):
    global count

    count += 1

    print("run {} of {} ...".format(count, k_num_eval))
    tmp_filename = "shimokita-tmp.jpg"
    cmd = "convert {} -blur {}x{} {}".format(ref_filename, radius, sigma, tmp_filename)
    ret = subprocess.run(cmd, shell=True)


    # Compare two images using RMSE
    cmp_cmd = "compare -metric rmse {} {} null:".format(tmp_filename, blurred_filename)
    ret = subprocess.run(cmp_cmd, shell=True, capture_output=True)
    # `compare`(ImageMagick) outputs result into stderr, not stdout
    lines = ret.stderr.splitlines()

    val = extract_result(lines)

    return val


# We define the parameters we want to optimize:
optimization_problem_parameters = [
    {
        "name": "radius",
        "category": "uniform",
        "search_space": {
            "low": 0,
            "high": 100,
        }
    },
    {
        "name": "sigma",
        "category": "uniform",
        "search_space": {
            "low": 0,
            "high": 100,
        }
    }
]

# We launch the optimization
best_sample = minimize(f, optimization_problem_parameters, number_of_evaluation=k_num_eval)

print("radius", best_sample["radius"])
print("sigma", best_sample["sigma"])
print("err = ", f(best_sample["radius"], best_sample["sigma"]))

```

## 結果

10,000 回まわしました.

```
radius 27.993241302558445
sigma 19.957452459359814

err =  49.5512

```

Voila!

radius は 27(真値), 28(推定値)と 1 違いますが, それなりに真値に近しい結果が得られました.


![shimokita-blur-estimated.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36275/f2690e99-9a7b-d2d0-de19-5f23fe62f135.jpeg)



idiff で差分を取ってみます.

https://openimageio.readthedocs.io/en/release-2.2.8.0/idiff.html

![diff.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36275/db5b8db0-ce58-65eb-9f9d-85e2d157b03b.jpeg)

(差分を 20 倍. 差分等倍だと視覚的にはほぼ真っ黒(= 一致))
  
jpg 圧縮の影響が大きそうですね...

ちなみに 100 回では全然だめで, 1000 回でまあまあそこそこ近い, という結果でした.

## TODO

* [ ] Bayesian Optimization など試したい.
* [ ] 画像の場合は非圧縮形式(PNG, BMP, TIFF)でやりましょう.
* [ ] ファイルに結果を書くタイプだと, ディスク I/O 消費が気になる(SSD だと寿命が縮まりそう?)ので memdisk を使えるか考えてみる
* [ ] benderopt は並列処理に対応していないので, 非マルチスレッドな外部プログラムを実行だと処理時間がかかってしまうので並列化できるライブラリの利用を検討します.
* [ ] 同様のやりかたで Photoshop あたりを操作してインスタ映え画像の生成を極める(コマンドライン制御できたような)