適応型閾値を用いて画像をいい感じに2値化する(Python, C++)

# はじめに
1つの画像中で輝度が大きく異なっている場合、普通に固定の閾値を用いて2値化すると画像の明るい部分がホワイトアウトしたり、逆に暗い部分がブラックアウトしたりする場合があります。それを防ぐためには、あるピクセルの周囲の輝度値に応じて閾値を決定する「適応型閾値」を利用します。

下の3枚の画像は左からオリジナルの画像、固定閾値での2値化画像、適応型閾値での2値化画像です。中央の画像は暗い右上の部分はブラックアウトし、明るい左下の部分はホワイトアウトしていますが、一番右の画像はカードのデザイン全体を読み取ることができます。

![thresh_gray.jpg](https://qiita-image-store.s3.amazonaws.com/0/176284/78b45946-aebd-738d-41e3-94010b8bd60d.jpeg)![thresh.jpg](https://qiita-image-store.s3.amazonaws.com/0/176284/2360ea7b-e8d7-7e18-2300-eed2553fe4b7.jpeg)![thresh_adap_gausse.jpg](https://qiita-image-store.s3.amazonaws.com/0/176284/32adc9d7-4ff6-3864-785d-9950b4518ccd.jpeg)


# 環境について
MacOS High Sierra
OpenCV3.4
Python3.7.1
C++17
を利用しています。

# OpenCVの適応型閾値
あるピクセルPの適応型閾値は、Pの周囲n*nの領域にあるピクセルに対して加重平均をとり、そこから定数を引くことで求められます。そのため画像内の全てのピクセルごとに固有の閾値が割り当てられて、その値により閾値処理が行われます。

OpenCVでは`adaptiveThreshold`という関数で実装されています。

- C++

```C++
void cv::adaptiveThreshold(
  // 入力画像
  cv::InputArray  src,
  // 出力画像
  cv::OutputArray dst,
  // 2値に閾値処理する際、閾値以上のピクセルに割り当てる値
  double          maxValue,
  // 加重平均の取り方
  int             adaptiveMethod,
  // 使用する閾値の種類
  int             thresholdType,
  // 「Pの周囲n*nの領域にあるピクセル」のnになる
  int             blockSize,
  // 引き算する定数
  double          C
)
```

- Python

パラメータの意味はC++の時と同じです。

```Python3
adaptiveThreshold(
  src,
  maxValue,
  adaptiveMethod,
  thresholdType,
  blockSize,
  C
) -> dst
```

`adaptiveMethod`はあるピクセル周りn*nの領域に対してどのように加重平均をとるかを指示します。取りうる値は2種類で、`CV_ADAPTIVE_THRESH_MEAN_C`を指定すると重みをつけない普通の算術平均、`CV_ADAPTIVE_THRESH_GAUSSIAN_C`を指定すると正規分布で重み付けされた加重平均をとります。

# サンプルコード
ブロックサイズと定数Cは画像の状態や解析したい状況によって様々なので外部からパラメータとして渡します。
記事冒頭の画像は（C++の場合は）`./thresholdtest 51 0`として起動しました。定数Cを大きくするとその分閾値が下がることになり、今回のような場合だと背景がホワイトアウトするので0としました。
## C++

```C++:thresholdtest.cpp
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
    if(argc != 3) {
        cout << "lack of arguments" << "\n";
        return -1;
    }
    cv::Mat src = cv::imread("../sample/threshold.jpg", CV_LOAD_IMAGE_GRAYSCALE);
    if(src.empty()) {
        cout << "cannot read image" << endl;
        return -1;
    }
    cv::Mat dst_normal, dst_adaptive;
    cv::threshold(src, dst_normal, 50, 255, CV_THRESH_BINARY);
    cv::adaptiveThreshold(src, dst_adaptive, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, atoi(argv[1]), atoi(argv[2]));
    cv::imshow("original", src);
    cv::imshow("normal_threshold", dst_normal);
    cv::imshow("adaptive_threshold", dst_adaptive);
    cv::waitKey(0);
    return 0;
}
```

## Python

```Python:thresholdtest.py
import cv2
import numpy as np
import sys
def main(blockSize, C):
    src = cv2.imread("sample/threshold.jpg", cv2.IMREAD_GRAYSCALE)
    _, dst_normal = cv2.threshold(src, 50, 255, cv2.THRESH_BINARY)
    dst_adaptive = cv2.adaptiveThreshold(src, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize, C)
    cv2.imshow('original', src)
    cv2.imshow('threshold_normal', dst_normal)
    cv2.imshow('threshold_adaptive', dst_adaptive)
    cv2.waitKey(0)

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("bad arguments")
        exit()
    main(int(sys.argv[1]), int(sys.argv[2]))
```

## adaptiveMethodをいじると
adaptiveMethodをCV_ADAPTIVE_THRESH_MEAN_Cにしてみると
左：CV_ADAPTIVE_THRESH_GAUSSIAN_C
右：CV_ADAPTIVE_THRESH_MEAN_C
![thresh_adap_gausse.jpg](https://qiita-image-store.s3.amazonaws.com/0/176284/41f49bd1-e121-9a2c-6843-7a1dcbd3deae.jpeg) ![adaptive_mean.jpg](https://qiita-image-store.s3.amazonaws.com/0/176284/45f9b2ad-79ab-4e42-bf13-902083630211.jpeg)

カード中央付近の文字がはっきりと表示されなくなりました。これは平均を取る範囲内のピクセルに対して均等な重みを割り当てているからです。背景の模様も細い線などは消えてしまっています。このように、解析目的に応じてadaptiveMethodも考える必要があります。