scikit-imageのfiltersで二値化

はじめに

Pythonの画像処理ライブラリの一つscikit-imageはOpenCVやpillowにはない機能が多くあります。 その一つは二値化のためのthresholdを導出する機能で多くの関数が存在します。今回はそれらの関数を紹介します。 scikit-imageのドキュメントは必要最低限の記載しかなかったので、 実際にライブラリのコードでどんな処理を行っているかを確認しながらthresholding関数の比較を行いました。

thresholdを導出する関数はskimage.filtersに存在します。

前処理

前処理としてカラー画像をファイルから読み込んだ後、グレースケールに変換しておきます。

    from skimage import io
from skimage.color import rgb2gray
from skimage import filters

file_path = "../src_data/bird.jpg"

img = io.imread(file_path)
img = rgb2gray(img)
    

以下で紹介するもののなかでパラメータの設定が必要な関数は、 ある程度うまくいくパラメータを探して実行しています。

大津の二値化

これはOpenCVにもある関数です。大津の二値化については以前Rustで実装したときに説明をしました。 簡単に言うとクラス間の輝度分散を最大する分割輝度値を閾値にします。

    otu_thresh = filters.threshold_otsu(img)
io.imshow(img > otu_thresh)
    

Li thresholding

Li thresholdingはクロスエントロピー関数を極小にする閾値を最急降下法で導出する手法です。 最急降下法であることから最小ではなく語句賞を導出するので、initial_guess引数で初期値を考慮する必要があります。 クロスエントロピー関数では設定した閾値で分割した領域のヒストグラム(度数)と輝度平均値のクロスエントロピーを算出しています。 (https://github.com/scikit-image/scikit-image/blob/v0.19.2/skimage/filters/thresholding.py#L617-L763の_cross_entropyで確認)

    li_thresh = filters.threshold_li(img, initial_guess=filters.threshold_otsu)
io.imshow(img > li_thresh)
    

Local thresholding

Local thresholdingはフィルターで算出した値に基づいて閾値が局所的に決定されます。よって出力される閾値は2dベクトルになります。 フィルターの性質からエッジ部分が抽出されやすい傾向にあり、閾値のoffsetなどパラメータに敏感なため、パラメータの決定は慎重に行った方がよいと思います。 フィルターはデフォルトではgaussianですが、自作のフィルタも設定可能です。

    local_thresh = filters.threshold_local(img, block_size=31, method="gaussian", param=None)
print(local_thresh.shape)
io.imshow(img > local_thresh)
    

Mean thresholding

これは単純に輝度の平均を閾値として設定したものになります。

    mean_thresh = filters.threshold_mean(img)
io.imshow(img > mean_thresh)
    

Minimum thresholding

ヒストグラムを引数に取り、極大値が二つになるまで1dフィルターでヒストグラムが平滑化された後、 極大値間にある最小値を閾値と設定します。 ヒストグラムがNoneの場合は画像のヒストグラムを使用します。 ですので、これは画像から自作のヒストグラムを作成して二値化したい場合に便利な関数かと思います。

    min_thresh = filters.threshold_minimum(image=img, nbins=256, hist=None)
io.imshow(img > min_thresh)
    

Niblack thresholding

Niblack thresholdingはウィンドウサイズで設定した注目画素と近傍画素との平均と標準偏差から算出した値を その注目画素の閾値として設定します。画素ごとに閾値は計算されるので出力は元画像と同じ大きさの2dとなります。 閾値の計算式は以下となります。

$(注目画素の閾値) = (平均) - (定数) * (標準偏差)$

上記式から、エッジ部分に敏感に反応しやすいことがわかります。 $(定数)$でエッジに対する応答度合いを変えることができ、$(定数)$部分を大きくするとエッジやノイズが大きい部分は白色になりやすく鈍感となります。

    ni_thresh = filters.threshold_niblack(img, window_size=31, k=0.3)
io.imshow(img > ni_thresh)
    

Sauvola thresholding

Sauvola thresholdingもNiblackと同じくウィンドウサイズで設定した注目画素と近傍画素との平均と標準偏差から算出した値を閾値として設定します。 ただし計算式が変更されており、以下になります。

$(注目画素の閾値) = (平均) * (1 + (定数) * ((標準偏差) / (r) - 1))$

$r$は標準偏差の最大値を採用されますが、引数をNoneにする場合はグレースケールの最大輝度と最小輝度の差の半分の値が設定されます。 式からウィンドウ枠中の標準偏差が小さければほとんど白色になってしまうので、勾配の大きいエッジ部分が黒色になりやすいことがわかります。 niblackよりさらに適応的なthresholdingといえます。

    sauv_thresh = filters.threshold_sauvola(img, window_size=7, k=0.1)
io.imshow(img > sauv_thresh)
    

Triangle thresholding

Triangle thresholdingはヒストグラムで最大となる輝度値のピーク点と、 画像全体の(グレースケール)輝度の最小値または最大値のどちらか一方との差が大きい方の点を結んだ直線と ヒストグラムとの距離が最大となる輝度値を閾値として設定します。これはヒストグラムのピークがどちらか端っこにある場合に適用しやすいです。 以下のURLにimageJ実装の概念図があり分かりやすいかと思います。

https://forum.image.sc/t/understanding-imagej-implementation-of-the-triangle-algorithm-for-threshold/752

    tri_thresh = filters.threshold_triangle(img)
io.imshow(img > tri_thresh)
    

Yen's thresholding

Yen's thresholdingはヒストグラムの分布(PMF)において各輝度値で二つに分割し、 それぞれの領域の2乗累積和の積が最大となる輝度値を閾値として設定します。

    yen_thresh = filters.threshold_yen(image=img, nbins=256, hist=None)
io.imshow(img > yen_thresh)
    

まとめ

それぞれ算出した閾値から二値化処理を行った結果が以下になります。 鳥画像を使用して二値化を行いましたが、前景と背景の分割という点ではLocal thresholdingとSauvola thresholdingがうまくいったようです。

Reference

  1. https://scikit-image.org/docs/stable/api/skimage.filters.html?highlight=filters
Next Post Previous Post
No Comment
Add Comment
comment url