【Python・OpenCV】適応的閾値処理による二値化(cv2.adaptiveThreshold)

※当サイトではアフィリエイト広告を利用しています

Python プログラミング 画像処理

【Python・OpenCV】適応的閾値処理による二値化(cv2.adaptiveThreshold)

2023-12-21

はじめに

二値化の方法として、cv2.threshold関数を紹介しました。cv2.threshold関数では大津、トライアングルの2つのアルゴリズムで閾値を自動的に決定する事ができましたが、画像全体に対して一意の閾値を決定するものでした。適応的閾値処理の二値化処理では、局所的な領域ごとに異なる閾値を適用することで、照明条件が不均一な画像に対して優れた二値化結果を提供します。このアルゴリズムは、各ピクセルの閾値をその周囲の領域の平均値や重み付き平均値といった統計的手法を用いて動的に調整します。
特に、画像に明るさのムラがある場合に効果を発揮する二値化プロセスと言えます。

本記事ではOpenCVで提供される適応的閾値処理の二値化処理のcv2.adaptiveThreshold関数を紹介します。

適応的閾値処理

cv2.adaptiveThreshold関数のアルゴリズムをOpenCVのソースコードも参考にしながら確認しました。

アルゴリズム

cv2.adaptiveThreshold関数で採用されているアルゴリズムは、入力画像の各ピクセルに対して、その周囲の一定領域内のピクセルの値に基づいて閾値を算出するものです。閾値の算出方法は、以下の2種類から選択できます。

具体的なアルゴリズムは、以下のとおりです。

  1. 入力画像の各ピクセルについて、その周囲の一定領域内のピクセルの値を取得する。
  2. 閾値の算出方法に応じて、領域内のピクセル値の平均値または加重平均値を計算する。
  3. 計算した閾値を、入力画像の該当ピクセルに適用する。

確認コード

実際に前述のアルゴリズムに沿ってコードを書いて、cv2.adaptiveThreshold関数との比較をしました。
下のコードではadaptive_threshold関数で上記の1〜3の処理を行っています。パラメーターの条件を同様にして、OpenCVのcv2.adaptiveThreshold関数の結果と比較するサンプルコードとなっています。

閾値の算出方法は、周囲の領域内のピクセル値の平均値を閾値とするcv2.ADAPTIVE_THRESH_MEAN_C、二値化の方法として画素値が閾値より大きい場合"255"とするcv2.THRESH_BINARYを用います。

import cv2
import numpy as np
import matplotlib.pyplot as plt

def adaptive_threshold(src, blockSize, c):
    """
    適応的閾値処理アルゴリズムの確認関数
    (前提条件 : thresholdType=cv2.THRESH_BINARY, adaptiveMethod=cv2.ADAPTIVE_THRESH_MEAN_C)

    Args:
        src: 入力画像
        blockSize: 近傍領域のサイズ
        c: 閾値のオフセット
    """

    # 入力画像のサイズを取得する
    rows, cols = src.shape[:2]

    # 出力画像を準備
    dst = np.zeros((rows, cols), dtype=np.uint8)

    # boxFilter関数で局所的な領域の平均値を計算
    mean = cv2.boxFilter(src, -1, (blockSize, blockSize), borderType=cv2.BORDER_REPLICATE|cv2.BORDER_ISOLATED)

    # 閾値を算出する
    threshold = mean - c

    # 閾値を適用する
    for x in range(0, cols):
        for y in range(0, rows):
            if src[y, x] >= threshold[y, x]:
                dst[y, x] = 0
            else:
                dst[y, x] = 255
    # 白黒反転            
    dst = cv2.bitwise_not(dst)

    return dst


if __name__ == "__main__":
    # 入力画像を読み込む
    image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)

    # 適応的閾値処理アルゴリズムの確認関数
    dst = adaptive_threshold(image, 7, 20)

    # OpenCVのadaptiveThreshold関数
    dst_cv = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 7, 20)

    # 入力画像と二値化画像を表示
    plt.rcParams["figure.figsize"] = [12, 3.5]                          # 表示領域のアスペクト比を設定
    title = "cv2.adaptiveThreshold: codevace.com"
    plt.figure(title)                                                   # ウィンドウタイトルを設定
    plt.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98)   # 余白を設定
    plt.subplot(131)                                                    # 1行3列の1番目(左)の領域にプロットを設定
    plt.imshow(image, cmap='gray')                                      # 画像をRGBで表示
    plt.title('Source Image')                                           # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
    plt.subplot(132)                                                    # 1行3列の2番目(真ん中)の領域にプロットを設定
    plt.imshow(dst, cmap='gray')                                        # 画像をRGBで表示
    plt.title('adaptive_threshold')                                     # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
    plt.subplot(133)                                                    # 1行3列の3番目(右)の領域にプロットを設定
    plt.imshow(dst_cv, cmap='gray')                                     # 画像をRGBで表示
    plt.title('cv2.adaptiveThreshold')                                  # 画像タイトル設定
    plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
    plt.show()

次の入力画像を使用してサンプルコードを動作させます。

入力画像の影を適応的閾値処理の二値化で取り除きます。
下の画像はサンプルコードの出力結果です。
左が入力画像、中央がadaptive_threshold関数の出力結果、右はOpenCVのcv2.adaptiveThreshold関数の出力結果です。中央と右の画像を比べると細部で違いはありますが、とてもよく似たものとなりました。

また、入力画像の影もほぼ取り除くことができました。

cv2.adaptiveThreshold関数の内部プロセスについて、理解ができたのではないでしょうか。

cv2.adaptiveThreshold関数

OpenCVのcv2.adaptiveThreshold関数の使い方について説明します。

cv2.adaptiveThreshold(入力画像, maxValue, adaptiveMethod, thresholdType, blockSize, C)

引数

名称説明
入力画像(必須)処理対象の画像(8bitのグレースケール画像)。カラー画像を処理する場合は、事前にグレースケール変換が必要です。
maxValue(必須)二値化後に与える最大の値
adaptiveMethod(必須)閾値の計算方法
thresholdType(必須)二値化の手法
blockSize(必須)局所的な閾値を計算するための近傍領域のサイズ。奇数である必要があります。
C(必須)計算された閾値から引かれる定数。値が大きいほど、全体的な閾値が低くなります。

adaptiveMethod(閾値の計算方法)は以下の値が指定できます。AdaptiveThresholdTypesを参照。

type説明
cv2.ADAPTIVE_THRESH_MEAN_C各領域の平均値を計算して閾値を決定します。周囲の画素値の平均が閾値になります。この手法は比較的シンプルで高速です。
cv2.ADAPTIVE_THRESH_GAUSSIAN_C各領域の画素値にガウス関数で重み付けし、重み付けされた平均値を閾値とします。ガウス関数により、周囲の画素に対して中心の画素への影響がより遠くまで及びます。これにより、より滑らかな結果が得られることがあります。
adaptiveMethod(閾値の計算方法)

thresholdType(二値化の手法)は以下の値が指定できます。ThresholdTypesを参照。
cv2.threshold関数と異なり、下記の2つのみが指定可能です。

type説明
cv2.THRESH_BINARY画素の輝度値が閾値を超える場合は maxval、そうでない場合は 0 に設定。
cv2.THRESH_BINARY_INV画素の輝度値が閾値を超える場合は 0、そうでない場合は maxval に設定。
thresholdType(二値化の手法)

引数選定のポイント

cv2.adaptiveThreshold関数を期待通りに動作させるためには、いくつかのポイントがあります。

  1. 適切なadaptiveMethodの選択:
    • 画像の特性に合ったadaptiveMethodを選ぶことが重要です。照明条件が均一であればcv2.ADAPTIVE_THRESH_MEAN_C、不均一であればcv2.ADAPTIVE_THRESH_GAUSSIAN_Cが有効です。
  2. 適切なblockSizeの選択:
    • 必ず奇数を選択します。
    • blockSizeは局所的な閾値を計算するための近傍領域のサイズであり、画像の特性によって適切な値を選びます。細かい特徴を保持したい場合は小さな値、平滑化が必要な場合は大きな値が適しています。
    • 画像が細かな特徴が多い場合、小さなblockSizeが有効です。
    • 画像が平滑で滑らかな領域が広がっている場合、大きなblockSizeを選ぶことが適しています。
  3. 適切なCの調整:
    • Cは計算された閾値から引かれる定数であり、照明条件の影響を調整します。特に不均一な照明がある場合Cの調整が効果的です。
    • 画像が均一な照明条件を持つ場合、通常はCを0に近づけます。
    • 明るい領域ではCを大きくし、暗い領域では小さくします。
  4. 画像の前処理:
    • 画像の前処理として、必要に応じてノイズの除去や平滑化を行うと結果が向上します。cv2.medianBlurcv2.GaussianBlurなどを検討してください。ノイズ除去に関するこちらの記事を参考にしてみて下さい。
  5. 適切なmaxValueの選択:
    • 二値化後の画素値の最大値maxValueを画像の特性に合わせて選びます。通常は白(255)を選びますが、具体的な目的によって変えることがあります。

これらのポイントを考慮してパラメータを調整すると、cv2.adaptiveThreshold関数を効果的に利用できます。適切なパラメータ調整により、不均一な照明条件や異なる領域の特性にも適応できるようになります。

戻り値

cv2.adaptiveThreshold関数の戻り値は閾値処理が適用された結果の画像です。

使い方

サンプルコードを紹介します。
入力画像はカラー画像ではなくグレースケール画像を対象としています。

blockSizeとCの値を変えて結果の比較をしました。また、同じ画像でcv2.threshold関数でどの様な結果になるかも比較しています。

import cv2
import matplotlib.pyplot as plt

# 入力画像を読み込む
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)

# OpenCVのadaptiveThreshold関数
maxValue = 255
C = 20
# blockSizeを可変
blockSize_3 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 3, C)
blockSize_7 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 7, C)
blockSize_11 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 15, C)

blockSize = 7
# Cを可変
C_10 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize, 1)
C_20 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize, 20)
C_30 = cv2.adaptiveThreshold(image, maxValue, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize, 40)

# cv2.threshold関数との比較
# Type: cv2.THRESH_BINARY, 閾値の指定: 127
retval, thresh_binary = cv2.threshold(image, 54, 255, cv2.THRESH_BINARY)
# Type: cv2.THRESH_TRIANGLE, 閾値の指定は不要ですが任意の値として0を設定
retval, thresh_triangle = cv2.threshold(image, 0, 255, cv2.THRESH_TRIANGLE)


# 入力画像と二値化画像を表示
plt.rcParams["figure.figsize"] = [12, 9]                            # 表示領域のアスペクト比を設定
title = "cv2.adaptiveThreshold: codevace.com"
plt.figure(title)                                                   # ウィンドウタイトルを設定
plt.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98)   # 余白を設定
plt.subplot(331)                                                    # 3行3列の1番目(上段左)の領域にプロットを設定
plt.imshow(image, cmap='gray')                                      # 画像をグレースケールで表示
plt.title('Source Image')                                           # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.subplot(332)                                                    # 3行3列の2番目(上段中央)の領域にプロットを設定
plt.imshow(thresh_binary, cmap='gray')                              # 画像をグレースケールで表示
plt.title('cv2.THRESH_BINARY, thresh=54')                           # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.subplot(333)                                                    # 3行3列の3番目(上段右)の領域にプロットを設定
plt.imshow(thresh_triangle, cmap='gray')                            # 画像をグレースケールで表示
plt.title('cv2.THRESH_TRIANGLE')                                    # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.subplot(334)                                                    # 3行3列の4番目(中段左)の領域にプロットを設定
plt.imshow(blockSize_3, cmap='gray')                                # 画像をグレースケールで表示
plt.title('blockSize=3, C=20')                                      # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.subplot(335)                                                    # 3行3列の5番目(中段中央)の領域にプロットを設定
plt.imshow(blockSize_7, cmap='gray')                                # 画像をグレースケールで表示
plt.title('blockSize=7, C=20')                                      # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.subplot(336)                                                    # 3行3列の6番目(中段右)の領域にプロットを設定
plt.imshow(blockSize_11, cmap='gray')                               # 画像をグレースケールで表示
plt.title('blockSize=11, C=20')                                     # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.subplot(337)                                                    # 3行3列の7番目(下段左)の領域にプロットを設定
plt.imshow(C_10, cmap='gray')                                       # 画像をグレースケールで表示
plt.title('blockSize=7, C=10')                                      # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.subplot(338)                                                    # 3行3列の8番目(下段中央)の領域にプロットを設定
plt.imshow(C_20, cmap='gray')                                       # 画像をグレースケールで表示
plt.title('blockSize=7, C=20')                                      # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.subplot(339)                                                    # 3行3列の9番目(下段右)の領域にプロットを設定
plt.imshow(C_30, cmap='gray')                                       # 画像をグレースケールで表示
plt.title('blockSize=7, C=30')                                      # 画像タイトル設定
plt.axis("off")                                                     # 軸目盛、軸ラベルを消す
plt.show()

blockSizeが小さいと線の輪郭が抽出され、大きくなるにつれて線全体が二値化されています。また、Cは大きくなるにつれてノイズが除去され、明るなる事がわかります。
cv2.threshold関数では影の部分が残ってしまったり、音符の一部が消えてしまい影だけを除去するのは難しいですね。
局所的に閾値を変えることで、影を除去することが可能である事が確認できました。

おわりに

cv2.adaptiveThreshold関数は画像ごとに異なるblockSizeとCの値の選定が難しいので、場合によっては使いにくいことがあります。
都度、調整しながら使う場合では、容易に目的が達成できそうです。
blockSizeとCの値の選定方法として、グリッドサーチなどが使えるかもしれませんが、評価関数をよく考える必要がありそうです。

ご質問や取り上げて欲しい内容などがありましたら、コメントをお願いします。
最後までご覧いただきありがとうございました。

参考リンク

■(広告)OpenCVの参考書としてどうぞ!■

-Python, プログラミング, 画像処理
-