OpenCVで画像のエッジ検出から背景除去を行う

はじめに

以前にOpenCVから画像を二値化処理を行ってから輪郭を抽出し、背景除去を行いましたが、 今回はアプローチを変えて、背景と前景の画素の差を利用しエッジを抽出してから輪郭を抽出して背景除去を行います。 この方法は背景の光加減や色加減などによって二値化がうまくいかない場合の代替手法として、背景と前景の境界が分かりやすい場合に有効です。 しかし前景と一緒に対象物以外の余計なものが重なっていたり、境界または背景が複雑で境界のエッジの抽出が困難であれば失敗する可能性があります。 また、エッジ抽出時には輝度の勾配の大きさによる閾値設定のパラメータが必要になります。 エッジ抽出にはSobelフィルタやLaplacianフィルタを適用する方法がありますが、今回はcanny法を使用します。

流れとしては以下になります

  • Canny法を使って、エッジを抽出する
  • 膨張と収縮を繰り返して、前景エッジ部分の黒穴を埋める(クロージング処理)
  • 輪郭を抽出して白塗りする
  • 背景除去

この手法は画像のセマンティックセグメンテーション(Semantic Segmentation)からtrimapを用いた手法 (https://tk87s.blogspot.com/2022/03/openccv-deeplearningsemantic.html) よりもロバスト性は低く精度も少し低い傾向にありますが、 segmentationの学習の必要がないのは大きな利点です。

Canny法によるエッジを抽出

Canny法は画素の輝度値の勾配が閾値以上であればエッジとみなす手法です。 また勾配の大きいエッジと連結している勾配が小さめの部分もエッジとして採用することができます。 これはノイズとエッジの区別を助けるためのヒステリシス機能(Hysteresis Thresholding)です。 またノイズを前もって除去し余計なエッジの検出を抑止するためにフィルター処理を前処理として行うこともできます。 OpenCVのcv.Cannyでは5x5のガウシアンフィルターを使用してノイズの除去を行っています。

cv.Canny関数を使用すれば、閾値を設定するだけでエッジを抽出することができます。 ヒステリシス機能のために小さいの勾配側の閾値と大きい勾配側のエッジ閾値を引数を設定します。 この引数の数値によってはエッジの抽出のされ方は変わります。 画像の質にもよりますがノイズを過度に拾わない範囲で閾値幅が大きい方がいい感じで抽出してくれると思います。

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

file_path = "../src_data/bird.jpg"

img = cv2.imread(file_path)
img = cv2.Canny(img,100,200)

plt.imshow(img, cmap="gray")
    

クロージング処理

輪郭を抽出するには対象の輪郭が連結である必要があります。 エッジが途中で途切れていては輪郭を取得するときに失敗してしまいますので、それを埋めてしまう必要があります。 この対処には白色領域を膨張・収縮を繰り返すことによるクロージング処理を用い、黒穴を埋めてしまうことでエッジの連結確率を高めます。

    kernel = np.ones((5,5),np.uint8)
img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel, iterations= 3)
    

輪郭抽出

以前の時と同じ方法で輪郭の抽出を行います。 画像に対して小さいまたは大きすぎる面積の輪郭は前景ではなくノイズとして無視するようにしています。 輪郭を検出したらmask画像にcv2.fillConvexPolyで輪郭を白色に塗り、前景を白色、背景を黒色としたmask画像の作成を行います。

    r=(0.05, 0.80)

height, width = img.shape[:2]
base_area = height * width

contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

mask = np.zeros(img.shape, dtype=np.uint8)
for cont in contours:
    cont_area = cv2.contourArea(cont)
    if cont_area >= base_area * r[0] and cont_area <= base_area * r[1]:
        cv2.fillConvexPoly(mask, cont, color=255)
    

背景除去

mask画像においては白色部分が前景部分となっているはずなので、 黒色部分は元画像のalpha値を0、白色部分はalpha値を255にすることで背景の除去を行います。

    img = cv2.imread(file_path)
img = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA)
mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGBA)
img[:, :, 3] = np.where(np.all(mask == [0, 0, 0, 255], axis=-1), 0, 255)
cv2.imwrite('./bird.png', img)
    

うまく背景が除去できていることが確認できました。 これは背景と前景の画素の輝度差を利用してエッジを抽出しそこから前景の輪郭を得ているので、背景と前景の境界の状態によっては取得が難しいこともあります。 この画像では鳥だけではなく土台も映り込んでいますので鳥と土台を区別するには、あらかじめ画像のsegmentationなどで識別する必要があるでしょう。

まとめ

前回は二値化のみで背景除去を行いましたが、前回より今回のエッジを利用する方が背景の状態をある程度無視できるのでいい感じで除去することができました。 ただ多種多様な大量の画像を一斉に処理をする場合は、失敗するものが出てくると思われるので、 画像に合わせて個別に閾値などのパラメータの対応する必要があります。 使い分けとしては以下のような感じでしょうか?

  • 背景の色が単純→二値化から背景除去
  • 背景と前景の境界が鮮明→エッジ抽出から背景除去
  • その他→画像のsemantic segmentationを行って背景除去

Reference

  1. https://docs.opencv.org/4.x/da/d22/tutorial_py_canny.html
Next Post Previous Post
No Comment
Add Comment
comment url