古くて新しい2値化画像処理を動かしてみる(ECCV 2020論文)

R&D チームの徳田(@dakuton)です。 今年の8月に開催されたECCV 2020のacceptされた論文のなかに、使い古された2値化画像処理をもとにした論文(+実装)があり気になったので、今回はそちらを少し調べて動かしてみた結果をご紹介します。

対象論文

A Generalization of Otsu’s Method and Minimum Error Thresholding

要点

簡単にいうと「既存手法をまとめて、ちょっとだけ融通の効く2値化画像処理にしたもの」の提案です。

  • ヒストグラムベースでの2値化しきい値を自動決定する手法
  • 下記の既存手法を特殊ケースとして内包できるよう、一般化した式にまとめた
    1. Kittlerの2値化(MET: Minimum Error Thresholding): 2クラスの正規分布が存在すると仮定し、分布の重なる範囲にて別クラスに該当する画素が最小化されるようなしきい値を採用する
    2. 大津の2値化: しきい値を境界に2クラスとしたとき、分離度が良い(クラス内分散は小さく、クラス間分散は大きく)しきい値を採用する(=1次元のフィッシャー判別分析)
    3. 加重パーセンタイル: 輝度などのソート順で上位n%の要素が持つ値をしきい値として採用する(50%だと中央値)
  • 特殊ケースはハイパーパラメータの一部を極大値または極小値にすることで選択可能
  • 既存手法よりも優れているケースについては論文の図を参照

下記のような状況で、古典的な2値化処理をわざわざ再定義するのか?疑問にもつかもしれません。

  • 2値化自体機械学習的アプローチで代替可能になっている
  • モデル前処理として、入力前に恣意的な2値化を加えると、有効な特徴が消滅する可能性

しかし、Introductionで触れられているとおり、簡単に訓練データが入手できない場合(医用画像など)もあるため、局所解になるとしても少ないデータ数で程度の自動識別が可能な手段で分析が始められることには一定の価値があると考えられているようです。

動かしてみる

内包する手法のうち、大津の2値化(OTSUモード)について動かしてみました。
OTSUモード(ν→∞,κ→0)で動作させる場合、極限指定しないハイパーパラメータω,τのうち、計算によりωは消去できますが、τは計算式に残存するため何らかの値を指定する必要があります。
こちらは、別途用意されているequivalentコードでの計算結果と一致するようにτ=1として確認しました。

コード

jonbarron/hist_threshから一部追加

import cv2
import numpy as np


csum = lambda z: np.cumsum(z)[:-1]
dsum = lambda z: np.cumsum(z[::-1])[-2::-1]
argmax = lambda x, f: np.mean(x[:-1][f == np.max(f)])  # Use the mean for ties.
clip = lambda z: np.maximum(1e-30, z)


def preliminaries(n, x):
    """Some math that is shared across multiple algorithms."""
    assert np.all(n >= 0)
    x = np.arange(len(n), dtype=n.dtype) if x is None else x
    assert np.all(x[1:] >= x[:-1])
    w0 = clip(csum(n))
    w1 = clip(dsum(n))
    p0 = w0 / (w0 + w1)
    p1 = w1 / (w0 + w1)
    mu0 = csum(n * x) / w0
    mu1 = dsum(n * x) / w1
    d0 = csum(n * x**2) - w0 * mu0**2
    d1 = dsum(n * x**2) - w1 * mu1**2
    return x, w0, w1, p0, p1, mu0, mu1, d0, d1


def GHT(n, x=None, nu=0, tau=0, kappa=0, omega=0.5, prelim=None):
    assert nu >= 0
    assert tau >= 0
    assert kappa >= 0
    assert omega >= 0 and omega <= 1
    x, w0, w1, p0, p1, _, _, d0, d1 = prelim or preliminaries(n, x)
    v0 = clip((p0 * nu * tau**2 + d0) / (p0 * nu + w0))
    v1 = clip((p1 * nu * tau**2 + d1) / (p1 * nu + w1))
    f0 = -d0 / v0 - w0 * np.log(v0) + 2 * (w0 + kappa *      omega)  * np.log(w0)
    f1 = -d1 / v1 - w1 * np.log(v1) + 2 * (w1 + kappa * (1 - omega)) * np.log(w1)
    return argmax(x, f0 + f1), f0 + f1


def im2hist(im, zero_extents=False):
    # Convert an image to grayscale, bin it, and optionally zero out the first and last bins.
    max_val = np.iinfo(im.dtype).max
    x = np.arange(max_val+1)
    e = np.arange(-0.5, max_val+1.5)
    assert len(im.shape) in [2, 3]
    im_bw = np.amax(im[...,:3], -1) if len(im.shape) == 3 else im
    n = np.histogram(im_bw, e)[0]
    if zero_extents:
        n[0] = 0
        n[-1] = 0
    return n, x, im_bw


def ght_thresh(im, nu=None, tau=None, kappa=None, omega=0.5):
    n, x, im_bw = im2hist(im)
    prelim = preliminaries(n, x)

    nu = np.sum(n) if nu is None else nu
    tau = np.sqrt(1/12) if tau is None else tau
    kappa = np.sum(n) if kappa is None else kappa

    t, score = GHT(n, x, nu, tau, kappa, omega, prelim)
    return t, score


def example():
    im = cv2.imread("input.png")
    gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)

    t, _ = ght_thresh(gray, nu=1e30, tau=1.0, kappa=1e-30)
    _, bw = cv2.threshold(gray, t, 255, cv2.THRESH_BINARY)
    cv2.imwrite("result_otsu_ght.png", bw)

    _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU)
    cv2.imwrite("result_otsu_opencv.png", bw)

if __name__ == '__main__':
    example()

サンプル

サンプルとして、社内で利用しているスライドフォーマットにて、テキストと背景が分離できるか?(テーブルであれば縞々が背景として除去できるか?)試すことにします。

既存の実装方法を内包できているか?確認のため、グレースケール変換した画像を入力として、下記1,2で選択されるしきい値を比較したところ、一致することが確認できました。

  1. 今回の論文実装(OTSUモード、τ=1)
  2. OpenCVで利用可能な大津の2値化cv2.threshold(im, 0, 255, cv2.THRESH_OTSU)

GHT実装はカラーのまま計算も可能ですが、この場合は一致しなくなります。
また、2(OpenCV)では1種類固定ですが、1(今回論文)はτを変更することで結果を微調整可能です。

スクリーンショット

実行結果も一応載せます(OpenCVでの大津の2値化と同じになるように動かした結果です)
使ったスライドはこちら(Rustの全マクロ種別が分かったつもりになれる話!)です。

処理前 https://cdn-ak.f.st-hatena.com/images/fotolife/o/optim-tech/20201009/20201009115813.png

処理後 https://cdn-ak.f.st-hatena.com/images/fotolife/o/optim-tech/20201009/20201009085924.png

実際使えそうか?

簡易的な2値化画像処理であたりをつけている場合に微調整しやすくなると考えると、利用シーンは出てくるかもしれません。
なお、繰り返しになりますが、前処理として2値化を加えると精度改善につながるとは限りません。2値化の利用目的が論文のようなテキスト認識の場合、背景ノイズの少ない画像に利用すると逆効果につながる場合があります。(今回のサンプルも2値化によりフッターのコピーライト表記が消えてしまっています)
論文内サンプルのようにしわなどノイズが多い状態の画像がターゲットになるでしょう。

オプティムでは手法が古いか新しいかにとらわれず、使える手法を選択できるエンジニアを募集しています。 www.optim.co.jp