からあげ博士の日常と研究と

博士課程を満期退学した人が好きなことを好きなままに書くところ。

pythonとOpenCVを使って画像を二値化 ――画像から100円玉を切り抜け!

 こんにちは。からあげ博士(@phd_karaage)です。みなさんは100円玉をご存じでしょうか?もちろんご存じですね。では画像から100円玉を見つけ出すことはできるでしょうか?

 なめるなよ。というお話ですね。これくらいどの位置にあるかなんて見れば分かります。ではコンピュータがこの100円玉を見つけるにはどうすればよいでしょうか?あるいは解析するときに邪魔な影をどのように取り除けばよいでしょうか?

 こうなるとなかなかアイディアが思い浮かばないかもしれませんし、この記事を検索で見つけた人からすれば、さっさと方法を教えろと思っていることでしょう。これを実行するアイディアの1つに画像の二値化があります。「画像の二値化」と言われると何をやっているんだとイメージが湧きにくいかもしれませんが、文字通り、画像を2つの値に分類します。

 そもそも画像、特にカラー画像は3次元配列の行列で表され、実体の中身としてはこのような数字の集合です。0~255でR,G,Bの強度を表しているといえばいいでしょうか。それが画像のサイズの行列として保存されています。

 それを2つに分類してあげれば、100円玉の位置を見つけられるんじゃないか、そういうアイディアな訳です。それではさっそく始めていきましょうか。

コード全体を見てみよう

 さっさと結果が得たい人はfile_pathcv2.imwrite()の引数を変えて実行してみてくださいね。

import cv2
import matplotlib.pyplot as plt

file_path = "E://DSC_1387.JPG"
img = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

val, th = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)

plt.imshow(th)

cv2.imwrite("E://DSC_1387_th.JPG", th)

 全体の流れとしては、画像の読み込み、グレースケール化、二値化、視覚化、保存という流れになっています。画像の読み込みについては大した記事ではないですが、過去に記事にしていたりしますね。

www.phd-karaage.com

 今回のコードもさほど長いコードではありませんので、それぞれ解説していきましょうか。

ライブラリの読み込み

import cv2
import matplotlib.pyplot as plt

 なんとなく可視化にはmatplotlibを使っていますが、cv2.imshow()でもいいと思います。個人的には使いにくいなあと思ってしまうので、matplotlibのほうを使う訳ですが。

画像の読み込み

file_path = "E://DSC_1387.JPG"
img = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)

 ここでは単純に画像の読み込みを行っています。ファイルパスを指定してあげて、どのような形式で読み込むかということを指定してあげている訳ですね。この辺りは前回紹介の記事でも詳しく書いた気がします。

グレースケール化

img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

 二値化を行うにあたって、画像をグレースケールに変換してあげる必要があります。正確には、2次元配列にしてあげる必要があります。そういう意味ではグレースケールに限る必要はありません。カラー画像は3次元配列によって表されると書きましたが、それはすなわち「横の画素数 × 縦の画素数 × 色数」の行列によって表されている訳です。その行列を2次元に変換して二値化を行っていきます。その変換で手っ取り早いのがグレースケール化という訳です。グレースケールに変換することで、白黒の濃淡に画像が変換されるので、2次元配列が得られるという仕組みになります。

 ちなみにこのグレースケール化ですが、画像の読み込み時点でできたりします。

img_gray_direct = cv2.imread(file_path, cv2.IMREAD_GRAYSCALE)

 そういう意味では、グレースケールを使って画像を二値化したいときは最初からグレースケールで読み込んであげるほうが手っ取り早いとも言えます。

二値化

 こうして得られたグレースケール画像を使って二値化を行っていきましょう。二値化とは2つの値に分類してあげることでしたね。この工程にはcv2.thresholdという関数を使ってあげます。その前にnumpyをインポートしてこの行列の中身について見てみましょう。

import numpy as np
print(np.unique(img_gray))

 この100円玉の画像の場合、このような値が返ってきました。

 これはどういうことでしょうか? np.unique()という関数はその行列の中にどのような値が含まれているかを返してくれます。この結果からわかることは、この画像が白黒の濃淡について80から255の値で表されているということが分かります。この行列を使って二値化を行ってみる訳です。

val, th = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
plt.imshow(th)

 実際の二値化はcv2.threshold()で行うことができます。この関数の引数はcv2.thresh(二値化したい行列, 閾値, 最大値, 方式)によって表されます。ここでは、img_grayを入力行列に、閾値、つまり二値化したい画像の2値の基準を0~255の半分である127に指定しています。最大値は255を指定していますが、返ってくる行列の最大値を指定するため、必ずしも255である必要はありません。この先の工程に応じて変更してかまいません。方式には様々ありますが、ここでは2つ紹介しましょう。cv2.THRESH_BINARYcv2.THRESH_BINARY_INVの2つです。この場合、この方式は2つとも0か255の値に分類することには変わりありません。違いは閾値を超えたら0にするのか、255にするのか、という点です。

 返り値は、val, thの2つになります。valには閾値が、thには実際に二値化された行列が返ってきます。この実行結果、画像がどうなったのかをplt.imshow(th)で確認をしている訳です。

 それではこのコインの画像について結果を確認してみましょう。

 は?

 コインがあったであろう場所は分かります。ですがコインの形状のようなものはなんとなくしか分かりません。鉄くずかもしれません。ちなみにこの図では黄色が255、紫色が0を示しています。

 一応、np.unique()で本当に二値に分類されているか確認してみましょう。

print(np.unique(th)

 img_grayと比べると一目瞭然、thには0か255のどちらかしか含まれていません。ではなぜコインを抽出することに失敗したのでしょうか?その答えは簡単です。閾値の値が適切ではないからです。それでは80と255の半分である、167で試してみましょう。

val_2, th_2 = cv2.threshold(img_gray, 167, 255, cv2.THRESH_BINARY)
plt.imshow(th_2)

 今度は先ほどよりはそれらしい形で100円玉が見えてきました。しかしまだ完全体ではありません。我々はこの閾値を決定するのにパラメータ職人にならなければならないのでしょうか?

 幸いにしてそんなことはありません。大津の二値化が解決してくれます。

大津の二値化

 その名前の通り、大津博士によって開発された手法で、ヒストグラムを2分割するときどこに閾値を設定すればよいかというアルゴリズムがあります。*1何をやっているかはヒストグラムの分散から分離度を求め……という内容ですが、詳細な内容は他サイトにお任せしましょう。

 この方法はOpenCVにも実装されていて、簡単に取り扱うことができます。

val_3, th_3 = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)
plt.imshow(th_3)

 この時引数の閾値を0に、方法の部分に、cv2.THRESH_OTSUを加えてあげることで実現します。この結果はどうなったでしょうか?

 ほぼ完ぺきではないでしょうか?もちろん切り抜けていない部分、コインの影を一部認識してしまっているようですが、それでもこれまでの結果から比べると遥かにマシです。ということで、実際の閾値はいくつに設定されたでしょうか?val_3の中身を見てみましょう。

print(val_3)

 203.0という値だったようです。運に任せて閾値を決めていたら1/255、パラメータ職人になっていたとしても、数十回の試行は必要だったでしょう。それがこの一瞬で求まるのです。感動ですね。

あとはこの画像を保存する

 二値化に成功したこのth_3という行列を画像形式で保存しておきましょう。保存はcv2.imwrite()で可能ですね。

cv2.imwrite("E://DSC_1387_th.JPG", th_3)

 cv2.imwrite(保存パス, 保存行列)という引数で保存されます。うまくいけばTrueとお返事があるはずです。

 かくして100円玉を二値化によって抽出することができました。よかったですね。ただ実際には円形を検出したり、面積を求めたり、他にもあらゆることをしたいはずです。その応用例は今後紹介していけたらなと思います。

二値化をするときの応用例

 二値化を行うときに、二次元配列の行列をcv2.thresholdにあげればよいと書きましたね。ということはグレースケールに限らず単色、つまりRGBのRの行列や、Lab色空間に変換して、そのうちのa(色相と彩度の一部)の行列を使うことも可能なはずです。試してみましょう。

img_R = img[:, :, 2] ##BGR配列で読み込まれている
val_4, th_4 = cv2.threshold(img_R, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)
plt.imshow(th_4)

 まずはRGBのRの行列を用いた場合。最初にimg_R = img[:, :, 2]でRの行列だけを抽出し2次元配列とします。表記はRGBですが、OpenCVではBGRの順に読み込まれるのでしたね。あとは同じく大津の二値化を行います。その結果がこちら。

 もしかして、グレースケールで実行するよりも良好な結果が得られている???そんな感じの結果ですね。

img_lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
img_a = img_lab[:, :, 1]
val_5, th_5 = cv2.threshold(img_a, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)
plt.imshow(th_5)

 まずはcv2.cvtColorによって色空間を変換します。そこから、a*の行列のみを抽出し、大津の二値化を実行するという同じことをしていますね。その結果はこちら。

 んー……これはうまくできているとは言い難いですね。

 という訳でいろいろな色空間の行列を使って試してみるのも面白そうですね。そんな感じでいろいろお試しくださいませ。

*1:Otsu, N. (1979). A threshold selection method from gray-level histograms. IEEE transactions on systems, man, and cybernetics, 9(1), 62-66.