【Python/C++】画像を2値化する方法【OpenCV】

2値化処理

画像処理では、画像を白と黒の2種類に分ける2値化処理がよく使用されます。
2値化は、

  • 物体と背景を分離する
  • 輪郭を検出する
  • 文字領域を抽出する
  • 面積や形状を計測する

といった処理の前段として使用される基本的な画像処理です。

OpenCVでは、固定した閾値を使用する方法だけでなく、画像から自動的に閾値を決定すす大津法や、領域ごとに異なる閾値を使用する適応的2値化も利用できます。

今回はOpenCVを使用して画像を2値化する方法を紹介します。

1. 2値化処理の理論

1.1. 2値化とは

2値化とは、画像の画素値をある条件によって2種類の値へ変換する処理です。
例えばグレースケール画像の場合、各画素は通常、

0 ~ 255

の範囲の値を持っています。
ここで、しきい値を TH として、

画素値 < TH  → 0
画素値 >= TH → 255

のように変換すると、画像は黒と白だけで構成された画像になります。
このような画像を2値画像と呼びます。

数式で表すと、入力画素を f(x, y)、出力画素を g(x, y) とした場合、

g(x,y)={0f(x,y)<TH255f(x,y)THg(x,y)= \begin{cases} 0 & f(x,y)<TH \\ 255 & f(x,y)\ge TH \end{cases}

となります。
2値化処理では、どの値を閾値として使用するかによって結果が大きく変化します。


1.2. 固定閾値による2値化

最も基本的な方法は、閾値をあらかじめ指定する方法です。
例えば、

TH = 128

とした場合、

0 ~ 127   → 黒
128 ~ 255 → 白

に変換します。

OpenCVではthreshold()を使用して2値化できます。

画像の明るさがほぼ一定で、対象物と背景の画素値が明確に分かれている場合には固定閾値でも十分に分離できます。

一方で、撮影環境によって画像の明るさが変化する場合には、適切な閾値も変化します。
そのため、固定値だけでは安定して2値化できない場合があります。


1.3. 大津法による2値化

大津法は、画像のヒストグラムから適切な閾値を自動的に決定する方法です。
画像のヒストグラムを2つのクラスに分けたとき、2つのクラスが最もよく分離される(クラス間分散が最大になる)値を閾値として選択します。

閾値THによって画素を2つのクラスC0、C1に分け、それぞれの画素の割合をω0、ω1、平均画素値をμ0、μ1とすると、クラス間分散は次の式で求められます。

σB2=ω0ω1(μ0μ1)2\sigma_B^2 = \omega_0 \omega_1 (\mu_0 – \mu_1)^2

大津の方法では閾値THを変化させながらクラス間分散を計算し、

THOtsu=arg maxTHσB2(TH)TH_{\mathrm{Otsu}} = \argmax_{TH} \sigma_B^2(TH)

となる閾値を採用します。

例えば、

閾値THTHクラス間分散σB2\sigma_B^2
50120
100350
130500
150420
200100

だった場合、σB2\sigma_B^2の最大値が500、その時の閾値THは130なので、

THOtsu=arg maxTHσB2(TH)=130TH_{\mathrm{Otsu}} = \argmax_{TH} \sigma_B^2(TH)=130

となります。

大津法はヒストグラムが2つの山に分かれているような画像で特に有効ですが、対象物と背景の明るさが似ている場合や、照明ムラが大きい場合、画像全体を1つの閾値で分離することが難しく、うまく2値化できないことがあります。


1.4. 適応的2値化

通常の2値化や大津法では画像全体に対して1つの閾値を使用します。
一方、適応的2値化では、各画素の周辺領域から閾値を計算し、場所ごとに異なる閾値を適応して2値化します。

OpenCVの適応的2値化では主に2種類の方法があります。

1.4.1. 平均値(ADAPTIVE_THRESH_MEAN_C)

周辺領域の画素値の平均から定数Cを引いて閾値を求めます。

TH(x,y)=1N(i,j)Ωf(i,j)CTH(x,y) = \frac{1}{N} \sum_{(i,j) \in \Omega} f(i,j) – C
  • x,yx,y:これから2値化する対象画素の座標
  • i,ji, j:対象画素の周辺にある各画素の座標
  • Ω\Omega:閾値計算に使う周辺領域
  • f(i,j)f(i,j):座標(i,j) の画素値
  • NN:周辺領域に含まれる画素数
  • CC:平均値から引く定数
対象画素(x,y)と周辺画素(i, j)のイメージ

1.4.2. ガウシアン重み付き平均(ADAPTIVE_THRESH_GAUSSIAN_C)

対象画素に近い画素ほど大きな重みを与えて平均を求めます。

TH(x,y)=(i,j)Ωw(i,j)f(i,j)(i,j)Ωw(i,j)CTH(x,y) = \frac{\sum_{(i,j) \in \Omega} w(i,j) f(i,j)}{\sum_{(i,j) \in \Omega} w(i,j)} – C
  • x,yx,y:これから2値化する対象画素の座標
  • i,ji, j:対象画素の周辺にある各画素の座標
  • Ω\Omega:閾値計算に使う周辺領域
  • f(i,j)f(i,j):座標(i,j) の画素値
  • w(i,j)w(i,j):座標(i,j)に対するガウシアンの重み
  • CC:加重平均から引く定数

例えば、3×3のガウシアンカーネルを考えてみます。

K=116[121242121]K = \frac{1}{16} \begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix}

次に、対象画素(x,y)(x, y)を中心とした3×3の画素値を、

F=[f(x1,y1)f(x,y1)f(x+1,y1)f(x1,y)f(x,y)f(x+1,y)f(x1,y+1)f(x,y+1)f(x+1,y+1)]F = \begin{bmatrix} f(x-1,y-1) & f(x,y-1) & f(x+1,y-1) \\ f(x-1,y) & f(x,y) & f(x+1,y) \\ f(x-1,y+1) & f(x,y+1) & f(x+1,y+1) \end{bmatrix}

とします。
それぞれの画素値にガウシアンの重みをかけるので、

TH(x,y)=116[f(x1,y1)+2f(x,y1)+f(x+1,y1)+2f(x1,y)+4f(x,y)+2f(x+1,y)+f(x1,y+1)+2f(x,y+1)+f(x+1,y+1)]CTH(x,y) = \frac{1}{16} \left[ f(x-1,y-1) + 2f(x,y-1) + f(x+1,y-1) + 2f(x-1,y) + 4f(x,y) + 2f(x+1,y) + f(x-1,y+1) + 2f(x,y+1) + f(x+1,y+1) \right] – C

となります。
例えば画素値が

F=[100110120110150130120130140]F = \begin{bmatrix} 100 & 110 & 120 \\ 110 & 150 & 130 \\ 120 & 130 & 140 \end{bmatrix}

だった場合、

TH(x,y)=1×100+2×110+1×120+2×110+4×150+2×130+1×120+2×130+1×14016CTH(x,y) = \frac{ 1 \times 100 + 2 \times 110 + 1 \times 120 + 2 \times 110 + 4 \times 150 + 2 \times 130 + 1 \times 120 + 2 \times 130 + 1 \times 140 }{16} – C

計算すると、

TH(x,y)=204016CTH(x,y) = \frac{2040}{16} – C
TH(x,y)=127.5CTH(x,y) = 127.5 – C

例えばC=2なら

TH(x,y)=125.5TH(x,y) = 125.5

となり、対象画素f(x,y)=150f(x, y)=150と閾値125.5を比較して2値化するという流れになります。

ここではガウシアンによる重みづけの考え方を分かりやすくするため、3×3のガウシアンカーネルを例として計算しています。
実際には後述するadaptiveThresholdの引数blockSizeで指定したblockSize×blockSizeの周辺画素に対してガウシアンによる重みづけを行います。

2. Pythonによる2値化処理

まず、OpenCVをインストールしていない場合は次のコマンドでインストールします。

pip install opencv-python

2.1. 固定閾値

最も基本的な2値化処理です。

import cv2

# グレースケールで画像を読み込む
image = cv2.imread("shimons_labo.png", cv2.IMREAD_GRAYSCALE)

# 2値化
threshold_value, binary = cv2.threshold(image, 128, 255, cv2.THRESH_BINARY)

# 保存
cv2.imwrite("shimons_labo_binary.png", binary)

2値化処理は、

cv2.threshold(image, 128, 255, cv2.THRESH_BINARY)

で行います。
引数は以下の通りです。

引数意味
image入力画像
1282値化閾値
255閾値を超えた場合に設定する値
cv2.THRESH_BINARYモード指定

cv2.THRESH_BINARYの場合、各画素は次のように変換されます。

dst(x,y)={255src(x,y)>1280src(x,y)128dst(x,y) = \begin{cases} 255 & src(x,y) > 128 \\ 0 & src(x,y) \leq 128 \end{cases}

つまり、画素値が128より大きければ255(白)、128以下なら0(黒)になります。
また、cv2.THRESH_BINARY_INVを指定すると白と黒が逆になります。

cv2.threshold()は2つの引数を返すので、通常は次のように記述します。

threshold_value, binary = cv2.threshold(image, 128, 255, cv2.THRESH_BINARY)

thresholdは使用された閾値、binaryには2値化後の画像が格納されます。
閾値を受け取らない場合は

_, binary = cv2.threshold(image, 128, 255, cv2.THRESH_BINARY)

と書くこともできます。


2.2. 大津法

次に、大津法を使用して閾値を自動的に決定します。

import cv2

image = cv2.imread("shimons_labo.png", cv2.IMREAD_GRAYSCALE)

threshold_value, binary = cv2.threshold(image, 0, 255, cv2.THRESH_OTSU)
threshold_value_inv, binary_inv = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)

print("Threshold:", threshold_value)
print("Threshold INV:", threshold_value_inv)

cv2.imwrite("shimons_labo_binary_otsu.png", binary)
cv2.imwrite("shimons_labo_binary_inv_otsu.png", binary_inv)

大津法を使用する場合は

cv2.THRESH_OTSU

を追加します。
また、

cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU

とすることで、白と黒反転したパターンの2値化も可能です。

引数の閾値は使用しないので今回は0を指定しています。


2.3. 適応的2値化

照明ムラなどがある場合には、適応的2値化を使用できます。

import cv2

image = cv2.imread("shimons_labo.png", cv2.IMREAD_GRAYSCALE)

binary_mean = cv2.adaptiveThreshold(
    image,
    255,
    cv2.ADAPTIVE_THRESH_MEAN_C,
    cv2.THRESH_BINARY,
    11,
    2
)

binary_gaussian = cv2.adaptiveThreshold(
    image,
    255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    11,
    2
)

cv2.imwrite("binary_adaptive_mean.png", binary_mean)
cv2.imwrite("binary_adaptive_gaussian.png", binary_gaussian)
引数意味
src入力画像。
8bitのグレースケール画像を指定する
maxValue条件を満たした画素に設定する最大値。
2値画像では通常255
adaptiveMethod適応的しきい値を求める方法。
ADAPTIVE_THRESH_MEAN_C
・ADAPTIVE_THRESH_GAUSSIAN_C
thresholdType2値化方法。
THRESH_BINARY
THRESH_BINARY_INV
blockSize閾値計算に使用する近傍領域のサイズ。
3以上の奇数を指定する
C計算した局所的なしきい値から減算する定数

3. C++による2値化処理

C++でもOpenCVのcv::threshold()やcv::adaptiveThreshold()を使用して画像を2値化できます。

3.1. 固定閾値

最も基本的な2値化処理です。

#include <opencv2/opencv.hpp>

int main(void){
    // グレースケールで画像を読み込む
    cv::Mat mat_img = cv::imread("shimons_labo.png", cv::IMREAD_GRAYSCALE);

    // 2値化
    cv::Mat mat_bin;
    double threshold_value = cv::threshold(mat_img, mat_bin, 128, 255, cv::THRESH_BINARY);

    // 保存
    cv::imwrite("shimons_labo_binary.png", mat_bin);

    return 0;
}

C++では

cv::threshold(mat_img, mat_bin, 128, 255, cv::THRESH_BINARY);

で2値化を行います。
Python版では2値化後の画像が戻り値として返されますが、C++版では第2引数に出力先のcv::Matを指定します。

また、cv::threshold()の戻り値は実際に使用した閾値です。
固定閾値の場合、threshold_valueには指定した128が格納されます。

3.2. 大津法

大津法を使用して閾値を自動的に決定します。

#include <opencv2/opencv.hpp>
#include <iostream>

int main(void){
    cv::Mat mat_img = cv::imread("shimons_labo.png", cv::IMREAD_GRAYSCALE);

    cv::Mat mat_bin;
    cv::Mat mat_bin_inv;

    double threshold_value = cv::threshold(mat_img, mat_bin, 0, 255, cv::THRESH_OTSU);
    double threshold_value_inv = cv::threshold(mat_img, mat_bin_inv, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);

    std::cout << "Threshold: " << threshold_value << std::endl;
    std::cout << "Threshold INV: " << threshold_value_inv << std::endl;

    cv::imwrite("shimons_labo_binary_otsu.png", mat_bin);
    cv::imwrite("shimons_labo_binary_inv_otsu.png", mat_bin_inv);

    return 0;
}

大津法を使用するときは

cv::THRESH_OTSU

を指定します。
また、

cv::THRESH_BINARY_INV | cv::THRESH_OTSU

とすることで、大津法で閾値を求めたうえで白と黒を反転して2値化できます。
大津法では第3引数は使用しないのでここでは0を指定しています。
cv::threshold()の戻り値には大津法で求められた閾値が格納されます。

3.3. 適応的2値化

照明ムラなどがある場合、C++でもcv::adaptiveThreshold()が使用できます。

#include <opencv2/opencv.hpp>

int main(void){
    cv::Mat mat_img = cv::imread("shimons_labo.png", cv::IMREAD_GRAYSCALE);

    cv::Mat mat_bin_mean;
    cv::Mat mat_bin_gaussian;

    // 周辺画素の平均値から閾値を求める
    cv::adaptiveThreshold(mat_img, mat_bin_mean, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 11, 2);

    // ガウシアン重み付き平均から閾値を求める
    cv::adaptiveThreshold(mat_img, mat_bin_gaussian, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 11, 2);

    cv::imwrite("shimons_labo_binary_adaptive_mean.png", mat_bin_mean);
    cv::imwrite("shimons_labo_binary_adaptive_gaussian.png", mat_bin_gaussian);

    return 0;
}

平均値を使用する場合は

cv::ADAPTIVE_THRESH_MEAN_C

ガウシアン重み付きを使用する場合は

cv::ADAPTIVE_THRESH_GAUSSIAN_C

を指定します。
今回のコードではblockSizeを11に指定しているため、対象画素を11×11の周辺領域から計算します。
また、最後の引数は2を指定しているため、計算した平均値またはガウシアン重み付き平均から2を引いた値が各画素の閾値として使用されます。

4. 実行結果

今回はこちらの画像に対して、

  • 固定閾値
  • 大津法
  • 適応的2値化

を試して結果を確認します。

4.1. 固定閾値

・閾値:70

THRESH_BINARY
THRESH_BINARY_INV

・閾値:128

THRESH_BINARY
THRESH_BINARY_INV

・閾値:200

THRESH_BINARY
THRESH_BINARY_INV

4.2. 大津法

・閾値:123(自動算出)

THRESH_BINARY
THRESH_BINARY_INV

4.3. 適応的2値化

・平均値

THRESH_BINARY
THRESH_BINARY_INV

・ガウシアン重み付き平均

THRESH_BINARY
THRESH_BINARY_INV

画像内に明るさのムラが存在する場合には、画像全体に同じしきい値を適用する方法よりも適応的2値化のほうがうまく分離できる場合があります。

5. 2値化の使い分け

2値化には複数の方法がありますが、画像の特徴によって使い分けます。
大津法や適応的2値化は便利ですが、外観検査など再現性が重要な場合はあまり適していないことがあります。
このような状況では閾値を自動計算するより撮影条件を合わせて閾値を固定する方が安定します。

一方、カメラのライブビューや動画など、リアルタイムで被写体や明るさが変わる場面では大津法のほうが有効な場面が多いです。

また、撮影条件が悪く輝度にムラがある場合は適応的2値化のほうが有効な場合があります。

ただし、どの方法でも画像にノイズが多い場合は不安定になるので、通常は2値化前にガウシアンフィルタやメディアンフィルタなどノイズ除去処理を行うと結果が改善される場合があります。

6. まとめ

今回はOpenCVを使用した2値化処理について紹介しました。

2値化処理は対象物と背景の分離や輪郭抽出、文字領域の抽出など、さまざまな処理の前処理として利用されます。

本記事では3種類取り上げましたが、この手法が最強というものはありません。
実際にはいくつか手法を試しながら結果を比較し、パラメータを調整していく必要があります。

それぞれの手法の特徴を理解したうえで、自分が解決したいタスクに合わせて最適な手法を選択していきましょう。

↓画像処理リファレンストップ

7. 参考サイト

基本的なしきい値処理操作
https://docs.opencv.org/5.0/tutorials/imgproc/threshold/threshold.html#cv::threshold