トップ画像
Python+OpenCVで物体の輪郭抽出と個数の推定をしよう

執筆者: ごま

最終更新: 2024/04/28

1. はじめに

お久しぶりです、ごまです。前回の記事更新からだいぶ期間が空いてしまいましたが、今回もPythonに関連する記事を書かせていただきました。

今回は、OpenCVを使って物体の輪郭抽出を行い、その個数を推定する、ということについて色々試してみたので、備忘録がてら記事にしてみました。 ちなみに、この「はじめに」を書いている段階ではプログラムは全く完成していません...。 今回、「物体の個数を推定する」という目的を達成するにあたって試した手法は次の通りです。

  1. 画像をグレースケール化し、二値化する
  2. フィルタを適応しノイズを除去する
  3. オープニング/クロージング処理を行い、物体の輪郭を強調する
  4. 輪郭を抽出し、物体の個数を推定する

他にも有効そうな手法はいくつかありそうですが、とりあえず今回はこれらの手法を試してみました。

2. 環境

今回使⽤した環境は次の通りです。

  • Python 3.9.7
  • opencv-python 4.9.0.80
  • numpy 1.22.4
  • matplotlib 3.6.2

それでは、実際の⼿順を⾒ていきましょう。

3. 作業手順

3.1. 画像をグレースケール化し、⼆値化する

3.1.1 通常の二値化

まずは、画像をグレースケール化し、⼆値化する⼿法を試してみます。 サンプル画像として、次の画像[1]を 使⽤します。

それでは、OpenCVを使って画像を読み込み、グレースケール化してみましょう。 画像の読み込みには cv2.imread()、グレースケール化にはcv2.cvtColor()を使⽤します。

import cv2

# 画像の読み込み
img = cv2.imread('image.jpg')

#グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

これをmatplotlibを使って表示してみると、次のようになります。

import matplotlib.pyplot as plt

plt.imshow(img_gray, cmap='gray')
plt.show()

では、画像を二値化してみます。二値化というのは、この場合は画像の明るさを基準にして、画像を白と黒の2値に変換する処理です。OpenCVではcv2.threshold()を使って、次のような形式で二値化を行います。

threshold = 127
ret,thresh1 = cv2.threshold(img,threshold,255,cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(img,threshold,255,cv2.THRESH_BINARY_INV)
ret,thresh3 = cv2.threshold(img,threshold,255,cv2.THRESH_TRUNC)
ret,thresh4 = cv2.threshold(img,threshold,255,cv2.THRESH_TOZERO)
ret,thresh5 = cv2.threshold(img,threshold,255,cv2.THRESH_TOZERO_INV)

返り値と引数は次の通りです。

返り値

返り値の名称

説明

retval

しきい値

dst

⼆値化後の画像

返り値としてのしきい値については後述します。二値化後の画像の形式はnumpy.ndarrayです。

引数

引数の名称

説明

src

⼊⼒画像

thresh

しきい値

maxval

しきい値を超えた場合に設定する値

type

⼆値化の種類

二値化の種類によりますが、基本的に第2引数で指定したしきい値を超えた場合に第3引数で指定した値に変換します。二値化の種類(type)は次の通りです。

  • cv2.THRESH_BINARY︓ しきい値を超えた場合はmaxvalの値に、それ以外は0に変換
  • cv2.THRESH_BINARY_INV︓ しきい値を超えた場合は0に、それ以外はmaxvalの値に変換
  • cv.THRESH_TRUNC︓ しきい値を超えた場合はしきい値に、それ以外はそのまま
  • cv.THRESH_TOZERO︓ しきい値を超えた場合はそのまま、それ以外は0に変換
  • cv.THRESH_TOZERO_INV︓ しきい値を超えた場合は0に、それ以外はそのまま

それでは、それぞれの二値化画像を表示してみましょう。

titles = ["GRAY", "BINARY", "BINARY_INV", "TRUNC", "TOZERO", "TOZERO_INV"]
images = [img_gray, thresh1, thresh2, thresh3, thresh4, thresh5]

for i in range(6):
    plt.subplot(2, 3, i + 1), plt.imshow(images[i], "gray")
    plt.title(titles[i])
plt.show()

どうでしょうか。正直あまり精度は良くないようです...。しきい値の問題かと思いましたが、数値を変更しても目立った改善は見られませんでした。

使用したコードの全文はこちら。

# binarize_normally.py
import cv2
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread("image.jpg")
print(img)
# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二値化のしきい値を設定
threshold = 128

ret, thresh1 = cv2.threshold(img_gray, threshold, 255, cv2.THRESH_BINARY)
ret, thresh2 = cv2.threshold(img_gray, threshold, 255, cv2.THRESH_BINARY_INV)
ret, thresh3 = cv2.threshold(img_gray, threshold, 255, cv2.THRESH_TRUNC)
ret, thresh4 = cv2.threshold(img_gray, threshold, 255, cv2.THRESH_TOZERO)
ret, thresh5 = cv2.threshold(img_gray, threshold, 255, cv2.THRESH_TOZERO_INV)

titles = ["GRAY", "BINARY", "BINARY_INV", "TRUNC", "TOZERO", "TOZERO_INV"]
images = [img_gray, thresh1, thresh2, thresh3, thresh4, thresh5]

for i in range(6):
    plt.subplot(2, 3, i + 1), plt.imshow(images[i], "gray")
    plt.title(titles[i])
plt.show()

3.1.2. 適応的しきい値処理

先ほどのコードでは、全体の⼆値化をする際にある⼀つの閾値を与えていました。しかし、画像全体に⼀つの閾値を与えるのは、画像全体の明るさが均⼀でない場合には適切ではありません。そこで、画像の⼀部分ごとに閾値を設定する「適応的しきい値処理」という⼿法があります。この⼿法は、cv2.adaptiveThreshold()を使って利⽤できます。

返り値は⼆値化画像のみです。

引数は次の通りです。

引数の名称

説明

src

⼊⼒画像

maxValue

しきい値を超えた場合に設定する値

adaptiveMethod

しきい値の計算⽅法

thresholdType

⼆値化の種類

blockSize

しきい値を計算する際のブロックサイズ

C

しきい値を決定する際に引かれる定数

adaptiveMethodには次の2つの⽅法があります。

cv2.ADAPTIVE_THRESH_MEAN_C︓近傍領域の平均値をしきい値とする

cv2.ADAPTIVE_THRESH_GAUSSIAN_C︓近傍領域のガウス分布による重み付け平均値をしきい値とする

thresholdTypeは先ほどの通常の⼆値化と同じです。cv2.THRESH_BINARYなどから⽬的にあったものを選択

します。

blockSizeはしきい値を計算する際の近傍領域のサイズを指定します。これは奇数である必要があります。

Cはしきい値を決定する際に引かれる定数です。実際の画像を⾒ながら適宜調整すると良いと思います。

それでは、適応的しきい値処理を⾏ってみましょう。

# binarize_adaptively.py
import cv2
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread("image.jpg")
# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二値化のしきい値を設定
threshold = 128

ret, thresh1 = cv2.threshold(img_gray, threshold, 255, cv2.THRESH_BINARY)
thresh2 = cv2.adaptiveThreshold(
    img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2
)
thresh3 = cv2.adaptiveThreshold(
    img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 5, 2
)

titles = ["GRAY", "BINARY", "MEAN_C", "GAUSSIAN_C"]
images = [img_gray, thresh1, thresh2, thresh3]

plt.figure(figsize=(10, 7))
for i in range(4):
    #キャンパスサイズを設定
    plt.subplot(2, 2, i + 1), plt.imshow(images[i], "gray")
    plt.title(titles[i])
plt.show()

BINARYは先ほどの通常の⼆値化処理です。MEAN_Cは近傍領域の平均値をしきい値として、GAUSSIAN_Cは近傍領域のガウス分布による重み付け平均値をしきい値としています。どうでしょうか︖先ほどと⽐べ、かなり精度がよく⾒えます。

3.1.3. ⼤津の⼆値化

ここまで紹介したアルゴリズムの他に、⼤津の⼆値化という⼿法があります。こちらも⾃動的にしきい値を 決定する⼿法なのですが、これはグレースケール画像のヒストグラムがが双峰性という性質(わかりやすく⾔うとヒストグラムが2つの⼭のようになっている状態)を持つ場合に有効です。ということでサンプル画像の ヒストグラムを確認してみたのですが...。

はい。 双峰性無い ですね。しかし、気は進みませんが一応試してみます。


# binarize_otsu.py
import cv2
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread("image.jpg")
# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

#普通の二値化
ret, thresh1 = cv2.threshold(img_gray, 128, 255, cv2.THRESH_BINARY)
#適応的しきい値処理
thresh2 = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2)
thresh3 = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 5, 2)
#大津の二値化
ret, thresh4 = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

titles = ["GRAY", "BINARY", "MEAN_C", "GAUSSIAN_C", "OTSU"]
images = [img_gray, thresh1, thresh2, thresh3, thresh4]

plt.figure(figsize=(10, 7))
for i in range(5):
    plt.subplot(2, 3, i + 1), plt.imshow(images[i], "gray")
    plt.title(titles[i])
plt.show()

うーん、普通の二値化よりはマシそうですが、やはり適応的しきい値処理の方が精度が高いようです。ということで、大津の二値化を使う場合は、双峰性のヒストグラムを持つ画像に対して使うのが良さそうです。

3.2. フィルタを適応しノイズを除去する

再び先程の手順で二値化した画像を見てみましょう。

こちらは先程局所的なしきい値処理を行った画像です。しかし、画像にはノイズが多く含まれていることがわかります。もともと影だった部分などですね。ここではこのノイズを除去することにより、輪郭抽出の精度を上げることを目指します。

フィルタによるノイズ除去にはいくつかの手法がありますが、ここでは次の手法を試してみます。

  • 平均化フィルタ
  • ガウシアンフィルタ
  • メディアンフィルタ

一つ一つ実際に使ってみます。


3.2.1. 平均化フィルタ

平均化フィルタは、画像の各画素に対して周囲の画素の平均値を計算し、その平均値を新しい画素値として設定するフィルタです。OpenCVではcv2.blur()を使って平均化フィルタを適応できます。

引数は次の通りです。

引数の名称

説明

src

⼊⼒画像

ksize

カーネルサイズ

anchor

カーネルの中⼼座標

borderType

境界処理の⽅法

ksizeはカーネルサイズを指定します。このサイズが⼤きいほど、広い範囲の画素を⽤いて平均化します。

これは奇数である必要があります。anchorはカーネルの中⼼座標を指定します。デフォルトは(-1, -1)です。borderTypeは境界処理の⽅法を指定します。

では実際に平均化フィルタを適応してみましょう。

# denoising_blur.py
import cv2
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread("image.jpg")

# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 局所的にしきい値を決定し二値化
img_binary = cv2.adaptiveThreshold(
    img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2
)

# カーネルサイズ
kernel = 3

# 平均化フィルタを適応
img_mean = cv2.blur(img_binary, (kernel, kernel))

# 二値化画像と平均化画像を表示
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.title("Binary")
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img_mean, cv2.COLOR_BGR2RGB))
plt.title(f"平均化フィルタ\nカーネルサイズ: {kernel}")
plt.show()

ノイズが除去されたというよりは、画像がぼやけてしまっているように⾒えますね。次は単なる平均化では なく、ガウシアンフィルタを試してみます。

3.2.2. ガウシアンフィルタ

ガウシアンフィルタは、平均化フィルタと同様に周囲の画素の値を⽤いて画素値を計算しますが、その際にガウス分布を⽤いて重み付けを⾏います。OpenCVではcv2.GaussianBlur()を使ってガウシアンフィルタを適応できます。

引数は次の通りです。

引数の名称

説明

src

⼊⼒画像

ksize

カーネルサイズ

sigmaX

X⽅向の標準偏差

sigmaY

Y⽅向の標準偏差

borderType

境界処理の⽅法

ksizeはカーネルサイズを指定します。このサイズが⼤きいほど、広い範囲の画素を⽤いて平均化します。

縦と横の値は異なっても問題ありませんが、奇数である必要があります。sigmaXsigmaYはそれぞれX⽅向

とY⽅向の標準偏差を指定します。sigmaYを指定しない場合はsigmaXと同じ値が使われ、sigmaXsigmaY

の両⽅が0の場合はカーネルサイズから⾃動的に計算されます。borderTypeは境界処理の⽅法を指定しま

す。

返り値はフィルタを適応した画像です。

それでは実際にガウシアンフィルタを適応してみましょう。

# denoising_blur.py
import cv2
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread("image.jpg")

# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 局所的にしきい値を決定し二値化
img_binary = cv2.adaptiveThreshold(
    img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2
)

# カーネルサイズ
kernel = 5

# ガウシアンフィルタを適応
img_gaussian = cv2.GaussianBlur(img_binary, (kernel, kernel), 0)

# 二値化画像と平均化画像を表示
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.title("Binary")
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img_gaussian, cv2.COLOR_BGR2RGB))
plt.title(f"ガウシアンフィルタ\nカーネルサイズ: {kernel}")
plt.show()

こちらもあまりノイズが除去されたという感じではありませんね...。

3.2.3. メディアンフィルタ

メディアンフィルタは、周囲の画素の中央値を計算し、その中央値を新しい画素値として設定するフィルタです。このフィルタは、周囲の画素の値が大きく異なる場合に有効です。OpenCVではcv2.medianBlur()を使ってメディアンフィルタを適応できます。

引数の名称

説明

src

⼊⼒画像

ksize

カーネルサイズ

ksizeはカーネルサイズを指定します。このサイズが⼤きいほど、広い範囲の画素を⽤いて平均化します。 これは奇数である必要があります。 返り値はフィルタを適応した画像です。 それでは実際にメディアンフィルタを適応してみましょう。

# denoising_median.py
import cv2
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread("image.jpg")

# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 局所的にしきい値を決定し二値化
img_binary = cv2.adaptiveThreshold(
    img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2
)

# カーネルサイズ
kernel = 3

# メディアンフィルタを適応
img_median = cv2.medianBlur(img_binary, kernel)

# 二値化画像と平均化画像を表示

plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.title("Binary")
plt.subplot(1, 2, 2)
plt.imshow(cv2.cvtColor(img_median, cv2.COLOR_BGR2RGB))
plt.title(f"メディアンフィルタ\nカーネルサイズ: {kernel}")
plt.show()

げぇっ...ノイズはかなり効果的に除去されていますが、肝⼼の輪郭も少し消えてしまっているようです。

3.2. オープニング/クロージング処理を⾏い、物体の輪郭を強調する

ここまでノイズ除去を試みましたが、あまりうまく⾏きませんでした...。そのため、⼀旦ノイズ除去を諦め、オープニング/クロージング処理を⾏い、物体の輪郭を強調することで検出の精度を上げることを⽬指します。

ここでモルフォロジー演算というものについて説明をしておきます。モルフォロジー演算とは、次のような膨張・収縮といった画像処理の基本的な演算です。

  • 膨張︓対象の画素の周辺に⽩い画素があれば、対象の画素を⽩くする
  • 収縮︓対象の画素の周辺に⿊い画素があれば、対象の画素を⿊くする

この操作を⾏うことで、例えば⿊い背景に⽩い物体がある画像の場合、膨張を⾏うことで物体が膨張し、収縮を⾏うことで物体が収縮します。この操作を繰り返す事により、ノイズ除去や物体中の⽳を埋めることができます。このような操作の繰り返しをオープニング処理、クロージング処理と呼びます。

  • オープニング処理︓収縮→膨張の順で処理を⾏う
  • クロージング処理︓膨張→収縮の順で処理を⾏う

それでは、まず膨張/収縮処理を⾏ってみましょう。

3.3.1. 膨張/収縮処理

膨張/収縮処理は、cv2.dilate()cv2.erode()を使って⾏います。 これらの関数を利⽤するコードを次に⽰します。

# dilate_erode.py
import cv2
import matplotlib.pyplot as plt
import numpy as np
# 画像の読み込み
img = cv2.imread("image.jpg")
# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 局所的にしきい値を決定し⼆値化
img_binary = cv2.adaptiveThreshold(
 img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2
)
# 膨張/収縮処理の回数を設定
iterations = 1
# カーネル(矩形)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# 膨張処理
img_dilate = cv2.dilate(img_binary, kernel, iterations=iterations)
# 収縮処理
img_erode = cv2.erode(img_binary, kernel, iterations=iterations)
# ⼆値化画像、膨張画像、収縮画像を表⽰
plt.figure(figsize=(18, 4))
plt.subplot(1, 3, 1)
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.title("Binary")
plt.subplot(1, 3, 2)
plt.imshow(cv2.cvtColor(img_dilate, cv2.COLOR_BGR2RGB))
plt.title(f"膨張処理\n回数: {iterations}")
plt.subplot(1, 3, 3)
plt.imshow(cv2.cvtColor(img_erode, cv2.COLOR_BGR2RGB))
plt.title(f"収縮処理\n回数: {iterations}")
plt.show()

どうでしょうか︖やや不格好ですが、処理⾃体は⾏われているようです。今回は背景が⽩、物体が⿊なので少し紛らわしいかもしれませんね。

では、次にオープニング処理とクロージング処理を⾏ってみましょう。

3.3.2. オープニング処理/クロージング処理

オープニング処理とクロージング処理は、cv2.morphologyEx()を使って⾏います。この関数は、モルフォロジー演算を⾏う関数です。 次のようなコードでオープニング処理とクロージング処理を⾏います。

# opening_closing.py

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

# 画像の読み込み
img = cv2.imread("image.jpg")

# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 局所的にしきい値を決定し二値化
img_binary = cv2.adaptiveThreshold(
    img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2
)

# オープニング/クロージング処理の回数を設定
iterations = 1

# カーネル(矩形)
kernel = np.array(
    [
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
    ],
    np.uint8,
)

# オープニング処理
img_opening = cv2.morphologyEx(
    img_binary, cv2.MORPH_OPEN, kernel, iterations=iterations
)

# クロージング処理
img_closing = cv2.morphologyEx(
    img_binary, cv2.MORPH_CLOSE, kernel, iterations=iterations
)

# 二値化画像、オープニング画像、クロージング画像を表示
plt.figure(figsize=(18, 4))
plt.subplot(1, 3, 1)
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.title("Binary")
plt.subplot(1, 3, 2)
plt.imshow(cv2.cvtColor(img_opening, cv2.COLOR_BGR2RGB))
plt.title(f"オープニング処理\n回数: {iterations}")
plt.subplot(1, 3, 3)
plt.imshow(cv2.cvtColor(img_closing, cv2.COLOR_BGR2RGB))
plt.title(f"クロージング処理\n回数: {iterations}")
plt.show()

物体中の⽳が埋まり、輪郭が強調されているように⾒えますね。これで輪郭抽出の精度が上がるかもしれません。ちなみにカーネルの形状があまり良くなかったので、⾃分で定義していますが、cv2.getStructuringElement()を使って簡単にカーネルを作成することもできます。

3.4. 輪郭抽出を⾏う

さて、ようやく輪郭抽出を⾏う段階になりましたが、ここまで処理を⾏ってもなお⼩さなノイズが画像内に存在します。したがって、このノイズを拾ってしまい輪郭として判断すると、正確な輪郭抽出ができません。そこで、抽出した輪郭の⾯積を計算し、⼀定の⾯積以下の輪郭は無視することで、物体の個数を推定しようと思います。

輪郭抽出にはcv2.findContours()を使います。この関数は、⼆値画像から輪郭を抽出します。返り値は輪郭のリストです。 とりあえず、何も設定せずに輪郭抽出を⾏ってみます。

# contour.py
import cv2
import matplotlib.pyplot as plt
import numpy as np

# 画像の読み込み
img = cv2.imread("image.jpg")

# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 局所的にしきい値を決定し二値化
img_binary = cv2.adaptiveThreshold(
    img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2
)

# オープニング処理の回数を設定
iterations = 1

# カーネル(矩形)
kernel = np.array(
    [
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
    ],
    np.uint8,
)

# オープニング処理
img_opening = cv2.morphologyEx(
    img_binary, cv2.MORPH_OPEN, kernel, iterations=iterations
)

# 白黒反転
img_opening = cv2.bitwise_not(img_opening)

# 輪郭抽出
contours, hierarchy = cv2.findContours(
    img_opening, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE
)


# 輪郭描画
img_contour = cv2.drawContours(img, contours, -1, (0, 255, 0), 2)

# 二値化画像、オープニング画像、輪郭画像を表示
plt.figure(figsize=(18, 4))
plt.subplot(1, 3, 1)
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.title("Binary")
plt.subplot(1, 3, 2)
plt.imshow(cv2.cvtColor(img_opening, cv2.COLOR_BGR2RGB))
plt.title(f"オープニング処理\n回数: {iterations}")
plt.subplot(1, 3, 3)
plt.imshow(cv2.cvtColor(img_contour, cv2.COLOR_BGR2RGB))
plt.title(f"Contour\n物体の数:{len(contours)}")
plt.show()

やはり、思った通りノイズや⼩さな隙間が多いせいで、輪郭の抽出と個数の推定がうまく⾏っていないよう です...。そこで、検出した輪郭の中から⼀定以上の⾯積のものだけを残すことによって、正しい輪郭を抽出 することを⽬指してみます。


# contour.py
import cv2
import matplotlib.pyplot as plt
import numpy as np

# 画像の読み込み
img = cv2.imread("image.jpg")

# グレースケール変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 局所的にしきい値を決定し二値化
img_binary = cv2.adaptiveThreshold(
    img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 2
)

# オープニング処理の回数を設定
iterations = 1

# カーネル(矩形)
kernel = np.array(
    [
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 1, 1, 0],
    ],
    np.uint8,
)

# オープニング処理
img_opening = cv2.morphologyEx(
    img_binary, cv2.MORPH_OPEN, kernel, iterations=iterations
)

# 白黒反転
img_opening = cv2.bitwise_not(img_opening)

# 輪郭抽出
contours, hierarchy = cv2.findContours(
    img_opening, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE
)

#一定以上の面積の輪郭のみ残す
area=1000
contours = [cnt for cnt in contours if cv2.contourArea(cnt) > area]

# 輪郭描画
img_contour = cv2.drawContours(img, contours, -1, (0, 255, 0), 2)

# 二値化画像、オープニング画像、輪郭画像を表示
plt.figure(figsize=(18, 4))
plt.subplot(1, 3, 1)
plt.imshow(cv2.cvtColor(img_binary, cv2.COLOR_BGR2RGB))
plt.title("Binary")
plt.subplot(1, 3, 2)
plt.imshow(cv2.cvtColor(img_opening, cv2.COLOR_BGR2RGB))
plt.title(f"オープニング処理\n回数: {iterations}")
plt.subplot(1, 3, 3)
plt.imshow(cv2.cvtColor(img_contour, cv2.COLOR_BGR2RGB))
plt.title(f"Contour\n物体の数:{len(contours)}")
plt.show()

先程のコードに加え、

#⼀定以上の⾯積の輪郭のみ残す
area=1000
contours = [cnt for cnt in contours if cv2.contourArea(cnt) > area]

を追加しています。このコードは、抽出した輪郭の中から⼀定以上の⾯積のものだけを残す処理を⾏っています。ここでは1000という値を設定していますが、実際には適宜調整すると良いでしょう。このコードを追加して実⾏すると次のようになります。

どうでしょうか。輪郭の形状が少しいびつですが、ノイズの輪郭を拾うこともなく、物体の輪郭を抽出できているように⾒えますね。

3.5. まとめ

今回は画像の輝度値を⽤いて⼆値化し、輪郭の抽出を⾏いました。しかし、対象の画像によりRGBの値や⾊相などを使⽤して⼆値化したほうが精度が良い場合もあります。OpenCVのライブラリには、それらのパラメータを扱う関数も豊富に⽤意されているので、私も機会があればまた試してみたいと思います。ここまで読んでくださりありがとうございました。

追記: 今回記事内で使⽤したコードはGitHubにアップロードしておきますので、興味がある⽅は⾒てみてください。

GitHubリポジトリ



3.6. 参考文献

取得に失敗しました

2022年度 入部

Twitter GitHub