省型旧形国電の残影を求めて

戦前型旧形国電および鉄道と変褪色フィルム写真を中心とした写真補正編集の話題を扱います。他のサイトでは得られない、筆者独自開発の写真補正ツールや補正技法についても情報提供しています。写真補正技法への質問はコメント欄へどうぞ

GIMP / Python プログラミング Tips: drawable の最高・最低値をどう取得するか

 今、久しぶりに再度 GIMP 2.10.x 用プラグインを書いていますが、drawable の最高値、最低値を取得する GIMP API が見つかりません。gimp_drawable_histogram() という API は見つかりましたが、これで平均値、中央値、標準偏差などは取得できますが、非常に単純に思える最高・最低値は取得できません。

 いくら探しても見つからないので、とはいえ、こんな基本的な情報が API で得られないはずはないと思って、pixls.us のオンラインディスカッションで尋ねたところ、Ofnuts氏から、gimp_histogram() で取得できる値から、二分探索を使って最高、最低値を取得するコードを教えてもらいました。結局、drawable の最高値、最低値を取得する GIMP API はない、ということのようです。

 ただ、これで気付いたのですが、GIMP では、オブジェクトをいろいろ操作する API はそれなりに揃っていますが、オブジェクトから値を取得する API が非常に少ないです。これは非常に困ります。

 以下、Ofnuts 氏に教えて貰った gimp_histogram() 用のコードを gimp_drawable_histogram() 用にアレンジしたコードを示します。なお、gimp_histogram() と gimp_drawable_histogram() の違いですが、前者は 8 bit 用のコードで、値が 0 - 255 を前提とするのに対し、後者は、2.10 以降の bit 深度拡張に対応した API で、値が 0.0 - 1.0 を前提とします。

-------------------

def minmax(image,drawable):
    lo=0
    hi=1.0
    while hi - lo > 0.01:
        test = (lo + hi) / 2
        _, _, _, pixels, count, _ = \
            pdb.gimp_drawable_histogram(drawable,HISTOGRAM_VALUE, 0,test)
        if pixels != count:
            lo = test
        else:
            hi = test
        max = hi
    lo=0.0
    hi=1.0
    while hi - lo > 0.01:
        test=(lo + hi) / 2
        _, _, _, pixels, count, _ = \
            pdb.gimp_drawable_histogram(drawable,HISTOGRAM_VALUE, test,1.0)
        if pixels != count:
            hi = test
        else:
            lo = test
    min = lo
    gimp.message('input min and max value: '+ str(min) + " - " + str(max))
    return min, max

---------------

 上のコードで、 pixels という変数で取得しているのは、drawable の全体のピクセル数です。それに対し、count は gimp_drawable_histogram() の、3番目と4番目の引数で指定された範囲のピクセル数です。

 前半の while 文で、0 から (lo + hi) /2 (初期値は、 0.5) の間のピクセル数を使って、最高値の探索を続けますが、この間、全体のピクセル数と指定された範囲のピクセル数が一致しない限り、(lo + hi) /2  および lo の値がだんだん上がって hi と lo の差が、0.01になるまで探索を続けます。全体のピクセル数と指定された範囲のピクセル数が一致すると、そこで hi の値を (lo + hi) /2 で更新します。しかし、ピクセル数が一致したとしても、そこが最高値とは限りません。探索範囲が、より高い方向へフライングしている可能性があるからです。しかし、(lo + hi) /2 で更新するので、これ以上 lo の値を変えずに hi の値のみ下げることで探索範囲を下げて、lo と hi の差が所定の値(この場合は0.01)に達するまで探索を続けます。但し、再び今度は探索範囲が低すぎるほうにフライングする可能性があります。その場合は、再び lo が上方へ更新されますので再び探索範囲を上げていきます。

 つまり lo の値を 0 から上げつつ、行き過ぎがあると、今度は hi の値を下げて... と探索範囲の上げ下げを続けながらながら最高値を探索しています。この間 hi と lo の差が所定の差に至るまで一致点が見つからなければ、初期値の 1.0 が最高値となります。

検索の初期段階

 このアルゴリズムのロジックを追っていくのは、プログラムの素朴そうな外観とは裏腹に意外に面倒なので、今の説明を分かりやすいように図にしてみました。上の図の、上の部分は探索の最初の段階です。今赤い部分にピクセルが分布しているとします。探索初期段階では、(Lo + Hi /2) は、0.5 ですので、0 ~ 0.5の範囲を探索します。赤い範囲のピクセル数が Pixels に入りますが、Count には、青い部分のピクセル数しか取得されず Pixcels > Count になります。このため Lo の値を(Lo + Hi /2) で更新して上昇させ、検索範囲をより右に拡大していきます。

Pixels = Count が一致した段階
(但し、多くの場合探索範囲を上げすぎ状態)

 この範囲が、赤い部分の上限以上に達すると Pixels と Count のピクセル数が一致しますので、これ以上 lo の値の更新 (上昇) を止めるとともに、Hi の値に (Lo + Hi) /2 を代入することで、Hi と Lo の差が 所定の値 (この場合は、0.01) になるまで、逆に探索範囲を下げ続け、Pixcels が存在する上限と、探索の上限の一致点(つまり、Pixcel の最高値地点)を探っていきます。

 しかし、再び探索範囲を下げ過ぎたらどうなるでしょうか? それは下の図になります。

検索範囲を下げ過ぎた場合
→再び検索範囲を上昇させる

 再び Lo が更新され (上昇し)、探索範囲を上昇させます。こうして探索範囲の上昇、下降を繰り返し、Lo と Hi の差が 0.01 になるまで収束点を探って、最高値を探索します。

均衡点 (最高値) が探せた場合

 なお、探索の範囲が 0 ~ 0.5 の範囲から始めますが、もし、最高値がそれより下でも大丈夫なのでしょうか? 結論から言うと大丈夫です。つまり探索範囲は上昇一方ではなく、行き過ぎれば、下降にも転じるからです。探索範囲の初期値よりも、最高値が低ければ、Pixels = Count になりますので、hi の値を(hi + lo) / 2 で入れ替えて切り下げ、探索範囲をもっと下げていきます。こうして同様に探索範囲が上がったり、下がったりを繰り返して、収束点に至るか Hi と Lo の差が0.01になるまで、ルーティンを繰り返します。

最高値が 0.5 より低い場合の最高値探索初期段階

 後半の while 文は、(lo + hi) /2 (初期値は、 0.5) から 1.0 の間のピクセル数を使って探索を続けますが、この間、全体のピクセル数と指定された範囲のピクセル数が一致しない限り、(lo + hi) /2 および hi の値がだんだん下がって hi と lo の差が、0.01 になるまで最低点の探索を続けます。全体のピクセル数と指定された範囲のピクセル数が一致すると、そこで hi の値の下降更新が止まり、 lo の値が上昇更新され、探索範囲が上昇に転じます。こうして探索範囲の上がり下がりを繰り返して収束に至ったとき、その時の lo 値が最低値として確定し、探索も終了します。

 探索範囲が (hi + lo) /2 ~ 1.0 に変わり、探るのは最高値ではなく最低値になりますが、基本的な理屈は前半と同じですので、図は省略します。

 なお、pixels は原理的に count よりも少なくなることはありませんので、

    if pixels != count:

 は必ずしも if pixels > count: にしなくても構いません(しても構いませんが)。

 また、この探索の原理上、基本的には近似値を取得することになります。ですが (もちろんたまたま一致する場合もある)、実用上まずまず使えると思います。気になるようでしたら、max = hi を、max = hi -0.01、 min = lo を min = lo + 0.01 などと狭めに調整すると良いと思います。また、ルーティンを停止する限界値である Hi - Lo = 0.01 の値をもっと小さくすれば精度は上がりますが、時間はより掛かります。

 また、一般的な画像で使う場合、多くは特異値ピクセルを除外した方が良いので、例えば、全体ピクセルの 98 % と一致していれば OK にしたいならば、

        if pixels != count:

        if pixels * 0.98 > count:

などと変えると良いかと思います。

 なお、このアルゴリズム、収束に至るまで時間がかかるように思われそうですが、結構高速です。Ofnuts氏も見かけよりも結構いいです、と言っていました。