【Python】画像のノイズを減らして二値化し、輪郭を抽出する

【Python】画像のノイズを減らして二値化し、輪郭を抽出する

画像解析には色々ありますが、その中の一つに「輪郭を抽出する」があります。輪郭を抽出することで、画像に存在する物体を検出することが出来ます。輪郭の抽出は、PythonのライブラリであるOpenCVを使うことで輪郭を抽出することが簡単に出来ます。今回はPythonで画像に存在する物質の輪郭を抽出する実装方法を紹介します。

前回、【Python】画像を平均化しノイズを除去してから二値化するで画像の平均化をしてノイズを減らし、二値化する方法について解説しました。今回は画像の平均化と二値化の処理を行ってから、輪郭抽出に入っていきます。
*本記事は、前回の記事を読んでないという方でも理解できるように書かれています。

動作検証済み環境
macOS Catalina(10.15.7), python3.7.10, Jupyter Notebook, OpenCV 3.4.2

*本記事では、Pythonを実行する環境としてJupyter Notebookを用いています。Jupyter Notebookのインストールがお済み出ない方は、こちらの記事を参考にインストールをお願いします。インストールは5分程度で完了します。

物体の輪郭を抽出するとは

下記の画像を御覧ください。画像の中には丸や四角や三角といった様々な図形があります。これらの図形の輪郭を緑色の線で抽出した画像が右の画像です。このように輪郭抽出とは、画像の中に存在する物体を見つけて、それらの輪郭を検出することを指します。

輪郭抽出前
輪郭抽出後(輪郭:緑色の線)

画像を平均化と二値化してから輪郭を抽出する方法(サンプルコードあり)

画像を平均化と二値化してから輪郭を抽出するのに必要なコード
画像を平均化と二値化してから輪郭を抽出をするためには、pythonのライブラリのOpenCVを使うと簡単に実現します。以下のようなコードを書きます。

*今回の記事はOpenCV3バージョンを用いています。4バージョンを使っている方は下記のコードではエラーが出ますので、11行目のimage, contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)imageを削除してから実行してください。

import cv2  #OpenCVのインポート

img = "stone.jpeg" #輪郭を抽出したい画像ファイル名
threshold = 100 #二値化に用いる閾値

img_color = cv2.imread(img)#画像を読み出しオブジェクトimg_colorに代入
img_gray = cv2.imread(img,cv2.IMREAD_GRAYSCALE) #画像をグレースケールで読み出しオブジェクトimg_grayに代入
img_blur = cv2.blur(img_gray,(9,9)) #第一引数で指定したオブジェクトgrayscale_imgを輝度で平均化処理する。第二引数は平均化するピクセル数で、今回の場合は9,9は9x9ピクセルの計81ピクセル。

ret, img_binary = cv2.threshold(img_blur, threshold, 255, cv2.THRESH_BINARY) #オブジェクトimg_blurを閾値threshold = 100で二値化しimg_binaryに代入
image, contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #二値化した画像オブジェクトimg_binaryに存在する輪郭を抽出
img_color_with_contours = cv2.drawContours(img_color, contours, -1, (0,255,0), 2) #抽出した輪郭の情報を用いて、オブジェクトimg_colorに書き出す

cv2.imwrite("stone_with_contours.jpeg", img_color_with_contours) #stone_with_contours.jpegとして輪郭抽出された画像を保存

実際に平均化と二値化した画像から輪郭を抽出するコードを実行してみよう

コードを記述したpythonファイルの保存場所
ex) 上で記述したコードはdrawContours_image.ipynbという名前のjupyter notebookファイルとして、Desktop/python/LabCode/drawContours_imageに保存します。

*コードを実行する前に
コードを実行する前に、お使いのJupyter notebook環境にOpenCVのインストールを行う必要があります。OpenCVのインストール方法については、こちらの記事に写真付きで詳しく解説してありますので、御覧ください。2分程度でOpenCVのインストールができると思います。

実行結果

こちらの石の画像を平均化と二値化してから輪郭を抽出していこうと思います。

stone.jpeg

stone.jpegdrawContours_image.ipynbと同じ階層(以下の画像だと、drawContours_imageフォルダの内)に保存しておきます。

上記のコードを実行すると、Desktop/LabCode/Pythonのフォルダ内にstone_with_contours.jpegが出来ていると思います。

実行結果

stone_with_contours.jpeg

画像の中にある石の輪郭が抽出されていますね!

抽出された輪郭の個数を数えてみる(サンプルコードあり)

ちなみに先程のコードの13行目にprint(len(contours))を一行加えると輪郭の個数もカウントしてくれるようになります。
*今回の記事はOpenCV3バージョンを用いています。4バージョンを使っている方は下記のコードではエラーが出ますので、11行目のimage, contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)imageを削除してから実行してください。

import cv2

img = "stone.jpeg"
threshold = 100

img_color = cv2.imread(img)
img_gray = cv2.imread(img,cv2.IMREAD_GRAYSCALE)
img_blur = cv2.blur(img_gray,(9,9))
ret, img_binary = cv2.threshold(img_blur, threshold, 255, cv2.THRESH_BINARY)
image, contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
img_color_with_contours = cv2.drawContours(img_color, contours, -1, (0,255,0), 2)

print(len(contours)) # ← 抽出した輪郭の個数を表示するコードを挿入する

cv2.imwrite("stone_with_contours.jpeg", img_color_with_contours) #stone_with_contours.pngとして輪郭抽出された画像を保存

実行結果

今回抽出された石の輪郭の個数は「92」個のようです!

コードの解説

import cv2

OpenCVを呼び出しています。Pythonでは、ライブラリ名を最初に記述することで使用可能になります。

img = "stone.jpeg" 
threshold = 100 

輪郭抽出したい画像ファイル(stone.jpeg)をオブジェクトimgに代入しています。閾値として使いたい値(100)をオブジェクトthresholdに代入しています。

img_color = cv2.imread(img)
img_gray = cv2.imread(img,cv2.IMREAD_GRAYSCALE)

cv2.imreadは画像を読み出すメソッドです。

cv2.imread(img)で読みだした画像をオブジェクトimg_colorに代入しています。
cv2.imread(img,cv2.IMREAD_GRAYSCALE)では画像を読み出す際にカラー画像を白黒画像にしてからimg_grayに代入しています。

img_blur = cv2.blur(img_gray,(9,9))

cv2.blurが画像を平均化するためのメソッドとなります。第一引数で指定したオブジェクトimg_grayを、第二引数(9,9)で平均化します。平均化の仕組みについては、【Python】画像を平均化しノイズを除去してから二値化するで詳しく解説しておりますので、よろしければ合わせてご参照ください。

ret, img_binary = cv2.threshold(img_blur, threshold, 255, cv2.THRESH_BINARY)

cv2.thresholdによってOpenCVのthresholdメソッドを呼び出して二値化をしています。今回のthresholdメソッドの引数の解説は以下になります。

  • img_binary: 二値化したい画像
  • threshold : 設定したい二値化の閾値。今回で言うと100になっている。
  • 255: Maxのピクセル値
  • cv2.THRESH_BINARY: thresholdTypeで二値化の方法の1つ

ピクセルや、画像データに関する基礎知識は、【Python】画像データを数値データとして扱うには?にて解説しておりますので、こちらもぜひご参照ください。

二値化処理した画像を、オブジェクトimg_binaryに入れています。

retは説明すると難しいですが、簡単に言うとメソッドで作成した閾値の設定が入っていると思ってください。retが二値化の閾値の設定を受け取って、img_binaryで二値化した画像データを受け取っていると考えてもらって大丈夫です。

image, contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

cv2.findContoursメソッドによって、画像中の輪郭のデータを抽出します。今回のfindContoursメソッドの引数についての解説は以下になります。これらの引数は、輪郭抽出を実装する上だけならざっくりとした理解で大丈夫です。

  • img_binary : 輪郭を抽出したい画像
  • cv2.RETR_TREE : 輪郭を検出するモードの一つ。全輪郭を検出する設定
  • cv2.CHAIN_APPROX_SIMPLE : 輪郭の近似方法を指定するもの

cv2.findContoursメソッドの返り値の説明は以下になります。hierarchyは階層情報といって、輪郭の抽出に優先順位をつけるものです。今回の実装には関わってきませんが、この引数が無いと引数が足りずにエラーが出ますので加える必要があります。

  • image : 画像情報 (*OpenCV4を利用している場合は、この返り値は必要ありません。)
  • contours : 輪郭の情報を配列として持つ
  • hierarchy : 階層情報(輪郭の抽出に優先順位をつけるもの)
img_color_with_contours = cv2.drawContours(img_color, contours, -1, (0,255,0), 2)

cv2.drawContoursメソッドによって、画像中の輪郭のデータを抽出します。詳しい説明は割愛しますが、今回のfindContoursメソッドの引数の詳しい説明は割愛しますが、ざっくり解説をすると以下のようになります。輪郭を抽出したあとの情報をオブジェクトimg_color_with_contourに代入しています。

  • img_color : 実際に保存した輪郭の情報を反映したい画像オブジェクト
  • contours : 輪郭の情報を配列として持つ
  • -1 : 全輪郭を描画するための設定。ー1を指定します
  • (0,255,0), 2 : 輪郭を描画する色や線の太さといった情報。今回は、緑色の線を引く設定になっている
print(len(contours))

lenは配列の要素を数えるPythonの標準メソッドです。オブジェクトcontoursを出力してみると分かるのですが、輪郭情報が配列として代入されています(Fig.1)。そのため、len(contours) によって抽出された輪郭の個数を配列の要素として数えることができます。printはPythonで出力結果を得るときに使う関数です。len関数によって数えられた輪郭の数はprintによって出力されています。

Fig.1
cv2.imwrite("stone_with_contours.jpeg", img_color_with_contours)

cv2.imwriteはOpenCVの画像を書き出す機能となっております。第二引数で指定した画像を、第一引数の画像名で保存します。

まとめ

今回は、画像に存在する物体の輪郭をPythonのOpenCVを用いて実装する方法を紹介しました。しかし、実際のコード実行結果を見てみると、きちんと石の輪郭が抽出されていないところや、石ではない別の場所の輪郭も描かれていたりと正確で無いことがわかります。こちらの問題を解決するテクニックは、アドバンスな内容となってきますので、またの機会に紹介したいと思います。