Opencv の基礎 60 - Watershed アルゴリズム cv2. distanceTransform() を使用して画像のセグメンテーションと抽出を実現する原理と例

画像処理の過程では、画像から前景の物体を対象画像として切り出したり、抽出したりすることが必要となる場合が多い。例えば、ビデオ監視では、私たちが観察するのは固定された背景の下でのビデオコンテンツですが、私たちは背景そのものには興味がなく、背景に映る車両や歩行者などの物体に興味があります。これらのオブジェクトをビデオから抽出し、オブジェクトが背景に入らないビデオ コンテンツを無視したいと考えています。

前の章では、画像の形態学的変換、しきい値処理アルゴリズム、画像ピラミッド、画像の輪郭、エッジ検出などの方法を使用して画像をセグメント化する方法について説明しました。このセクションでは、ウォーターシェッド アルゴリズムと GrabCut アルゴリズムを使用して画像をセグメント化して抽出する方法を紹介します。

アルゴリズム原理

Gonzalez は、書籍「Digital Image Processing」の中で分水界アルゴリズムの詳細な分析と紹介を行っています。OpenCV の公式 Web サイトでは、MINES ParisTech の画像処理研究所の CMM (Centre for Mathematical Morphology) Web サイトにある分水嶺アルゴリズムの紹介とアニメーションのデモンストレーションを学習者に読むことを推奨しています。

以下は、流域アルゴリズムの関連コンテンツの簡単な紹介です。

グレースケール画像はすべて地理的な地形面とみなすことができ、グレースケール値が高い領域は山の頂上、グレースケール値が低い領域は谷とみなすことができます。図 17-1 に示すように、左側の画像は元の画像であり、右側の画像はそれに対応する「地形表面」です。

それぞれの谷に異なる色の水を「注入」するとします (ここでは OpenCV 公式 Web サイトが使用されています)。ゴンザレスは注入を、谷に穴を開け、その穴から均一な速度で水を上昇させることと表現しています)。その後、水位が上昇し続けると、さまざまな谷からの水が集まります。この過程では、異なる谷からの水が合流するのを防ぐために、水の流れが合流する場所に堤防を築く必要があります。このプロセスにより、画像が集水域と流域線という 2 つの異なるセットに分割されます。私たちが構築する堤防は分水嶺であり、元の画像を分割したものです。これが分水界アルゴリズムです。

ここに画像の説明を挿入
図 17-2 では、左側の画像が元の画像、右側の画像が分水界アルゴリズムを使用して取得された画像セグメンテーションの結果です。CMM の Web サイトではサンプル画像だけでなく、アニメーションのデモンストレーション効果も提供されています。興味のある読者は Web サイトにアクセスしてご覧ください。

ここに画像の説明を挿入
ノイズやその他の要因の影響により、上記の基本的な分水界アルゴリズムではオーバーセグメンテーションが発生することがよくあります。過度にセグメンテーションすると、画像が密な独立した小さなブロックに分割され、セグメンテーションが無意味になってしまいます。

図 17-3 は、過剰にセグメント化されたイメージを示しています。左の画像は電気泳動の画像、右の画像は過分割の結果画像であり、過分割の現象が非常に深刻であることがわかります。

ここに画像の説明を挿入

画像セグメンテーションの効果を改善するために,マスクに基づく改良された分水界アルゴリズムが提案された。改良された分水界アルゴリズムにより、ユーザーは同じセグメント化された領域とみなされるものをマークアウトすることができます (マークされた部分はマスクと呼ばれます)。このようにして、流域アルゴリズムが処理する際、ラベル付けされた部分を同じセグメント化された領域として処理します。Microsoft PowerPoint
の「背景の削除」機能を使用して、この改良されたアルゴリズムについての理解を深めてみてください。

図 17-4 では、左側の画像はマークを付けた元の画像であり、暗い色としてマークされた 3 つの小さなカラー ブロックは、マスク ウォーターシェッド アルゴリズムを使用すると、これらの部分に含まれる色が同じ色に分割されることを示しています。エリア。マスク流域アルゴリズムを使用して得られたセグメンテーションの結果を図 17-4 の右の図に示します。

ここに画像の説明を挿入

改良された分水界アルゴリズムを使用して、図 17-5 の左側の電気泳動画像をマスクし、右側のセグメンテーション結果を取得します。セグメンテーションの結果が大幅に改善されていることがわかります。

ここに画像の説明を挿入

関数 cv2.watershed() の概要

OpenCV では、関数 cv2.watershed() を使用して分水界アルゴリズムを実装できます。
特定の実装プロセスでは、形態学的関数と距離変換関数 cv2. distanceTransform() および cv2.connectedComponents() を使用して画像のセグメンテーションを完了することも必要です。以下は、分水界アルゴリズムで使用される関数の簡単な説明です。

1. 形態学的機能のレビュー

分水界アルゴリズムを使用して画像をセグメント化する前に、画像に対して単純な形態学的処理を実行する必要があります。まず、形態学の基本的な操作を確認します。
(1) オープニング操作
オープニング操作は、収縮、膨張の順で行う操作であり、画像のノイズを除去することができます。たとえば、図 17-6 では、左側の画像が最初に腐食されると、中央の画像が取得され、次に中央の画像が拡張されて右の画像が取得されます。左の画像を開くと (最初に腐食、次に拡張)、右の画像が得られます。

観察すると、左の画像を開封操作後に右の画像にすると、バリ(ノイズ情報)が除去されていることがわかります。

ここに画像の説明を挿入
画像を開くと画像内のノイズを除去できます。ウォーターシェッド アルゴリズムを使用して画像を処理する前に、画像セグメンテーションに対するノイズによって引き起こされる可能性のある干渉を回避するために、オープン操作を使用して画像内のノイズを除去する必要があります。

(2) 画像の境界を求める 画像
の境界は、モルフォロジー演算と減算演算によって求めることができます。

たとえば、図 17-7 では、左側の画像が元の画像、中央の画像がそれを腐食した画像であり、この 2 つを減算すると、右側の画像が得られます。観察すると、右の画像が左の画像の境界であることがわかります。

ここに画像の説明を挿入

例: 形態学的変換を使用して画像の境界情報を取得し、その効果を観察します。

元の画像:

ここに画像の説明を挿入

import cv2
import numpy as np
import matplotlib.pyplot as plt
o=cv2.imread("rice.png",cv2.IMREAD_UNCHANGED)
#构造一个5*5的结构元素
k=np.ones((5,5),np.uint8)
#腐蚀
e=cv2.erode(o,k)
#膨胀
b=cv2.subtract(o,e)

plt.subplot(131)
plt.imshow(o)
plt.axis('off')
plt.subplot(132)
plt.imshow(e)
plt.axis('off')
plt.subplot(133)
plt.imshow(b)
plt.axis('off')
plt.show()

操作結果:

ここに画像の説明を挿入
左の画像は元の画像、中央の画像は
それを腐食した画像、右の画像は元の画像から腐食した画像を差し引いた境界画像です。右の画像は、左の画像の前景オブジェクトの境界情報をより正確に表示していることがわかります。

上記の分析から、画像の境界情報はモルフォロジー演算と減算演算を使用して取得できることがわかります。ただし、形態学的操作は比較的単純な画像にのみ適用できます。画像の前景オブジェクトが接続されている場合、形態学的演算を使用して各サブ画像の境界を正確に取得することはできません。

2. 距離変換関数 distanceTransform

画像内のサブグラフが接続されていない場合、形態的浸食操作を使用して前景オブジェクトを直接決定できますが、画像内のサブグラフが互いに接続されている場合、前景オブジェクトを決定することは困難です。この時点で、
前景オブジェクトは距離変換関数 cv2. distanceTransform() を使用して簡単に抽出できます。

距離変換は、函数 cv2.distanceTransform()バイナリ イメージ内の任意の点から最も近い背景点までの距離を計算します。一般に、この関数は、イメージ内の非ゼロ値ピクセルから最も近いゼロ値ピクセルまでの距離を計算します。つまり、バイナリ イメージ内のすべてのピクセルと、値が 0 の最も近いピクセルの間の距離を計算します。もちろん、ピクセル自体の値が 0 であれば、この距離も 0 になります。

距離変換関数 cv2. distanceTransform() の計算結果は、
各ピクセルと背景 (値が 0 のピクセル) との距離関係を反映します。いつもの:

  • 前景オブジェクトの中心 (重心) が値 0 のピクセルから離れるほど、より大きな値が取得されます。
  • 前景オブジェクトのエッジが値 0 のピクセルに近い場合、より小さな値が取得されます。

上記の計算結果を二値化すると、画像中の部分画像の中心や骨格などの情報が得られます。距離変換関数 cv2. distanceTransform() は、オブジェクトの中心を計算することができ、輪郭を洗練したり、画像の前景を取得したりすることもでき、多くの機能を持っています。
距離変換関数 cv2. distanceTransform() の構文形式は次のとおりです。

dst=cv2. distanceTransform(src, distanceType, MaskSize[, dstType]])

式では次のようになります。

  • src は、8 ビットのシングル チャネル バイナリ イメージです。
  • distanceType は距離タイプのパラメータであり、その具体的な値と意味を表 17-1 に示します。

ここに画像の説明を挿入
ここに画像の説明を挿入

  • MaskSize はマスクのサイズであり、その可能な値を表 17-2 に示します。distanceType = cv2.DIST_L1 または cv2.DIST_C の場合、maskSize は強制的に 3 になることに注意してください (3 に設定することと 5 以上に設定することの間に違いはないため)。

ここに画像の説明を挿入

  • dstType はターゲット画像のタイプで、デフォルト値は CV_32F です。
  • dst は計算されたターゲット イメージを表し、8 ビットまたは 32 ビットの浮動小数点数にすることができ、そのサイズは src のサイズと同じです。

例: 距離変換関数 cv2. distanceTransform() を使用して、画像の特定の前景を計算し、その効果を観察します。

import numpy as np
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('water_coins.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
ishow=img.copy()
#对图像阈值化处理,得到二值图像,黑色背景,白色前景,前景为我们要提取的目标,背景为0,前景为1
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
#定义一个3*3的卷积核,卷积核的作用是将前景与背景分离
kernel = np.ones((3,3),np.uint8)
#对二值图像进行开运算,去除噪声,开运算是先腐蚀再膨胀的过程,它可以去除小的干扰块,平滑较大的对象边界,同时并不明显改变其面积
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
#距离变换,得到每一个前景像素点到最近的背景像素点的距离,然后对其进行阈值化处理,得到的结果就是前景
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
#对膨胀后的图像进行阈值化处理,得到前景,前景为我们要提取的目标,背景为0,前景为1,这样就得到了我们要提取的目标,但是目标并不连续
ret, fore = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)

plt.subplot(131)
plt.imshow(ishow)
plt.axis('off')
plt.subplot(132)
plt.imshow(dist_transform)
plt.axis('off')
plt.subplot(133)
plt.imshow(fore)
plt.axis('off')
plt.show()

結果は次のとおりです。

ここに画像の説明を挿入

  • 左の画像が元の画像です。
  • 中央は、距離変換関数 cv2. distanceTransform() で計算された距離画像です。
  • 右側の画像は、距離画像を閾値処理した後の画像です。

右側の画像は、左側の画像の「決定された前景」をより正確に示しています。ここで決定される前景とは、通常、
前景オブジェクトの中心を指します。これらの点が前景であるとみなされる理由は、これらの点が背景点から十分に離れており、その距離が十分に大きな固定しきい値 (0.7*dist_transform.max()) よりも大きいすべての点であるためです。

3. 未知の領域を特定する

画像内の前景は、形態学的拡張操作を使用して「拡張」できます。画像の前景を拡大すると背景が「圧縮」されるため、このとき得られる背景情報は実際の背景よりも小さくなければならず、前景の「確定した背景」は含まれません。

以下、説明の便宜上、判定背景をBと呼ぶ。距離変換関数は、cv2.distanceTransform()画像の「中心」を取得して「決定された前景」を取得することができます。説明の便宜上、決定された前景をFと呼びます。画像内で前景Fと背景Bが決定された場合、残りの領域が未知領域UNとなる。
エリアのこの部分は、まさに流域アルゴリズムによってさらに定義されるエリアです。
画像 O の場合、未知領域 UN は次の関係から取得できます。

未知の領域 UN = 画像 O - 背景を決定 B - 前景 F を決定

上記の式を整理すると、次のようになります。

未知の領域 UN = (画像 O - 背景 B を決定) - 前景 F を決定

上式の「画像O - 背景Bを決定する」は、画像に対して形態的拡張演算を行うことで得られます。

例: 画像の特定の前景、特定の背景、および未知の領域に注釈を付けます。

import numpy as np
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('water_coins.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
ishow=img.copy()
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
#对图像进行膨胀操作,得到背景
bg = cv2.dilate(opening,kernel,iterations=3)

dist = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, fore = cv2.threshold(dist,0.7*dist.max(),255,0)
fore = np.uint8(fore)
un = cv2.subtract(bg,fore)
plt.subplot(221)
plt.imshow(ishow)
plt.axis('off')
plt.subplot(222)
plt.imshow(bg)
plt.axis('off')
plt.subplot(223)
plt.imshow(fore)
plt.axis('off')
plt.subplot(224)
plt.imshow(un)
plt.axis('off')
plt.show()

操作結果:

ここに画像の説明を挿入

  • 左上隅には元の画像が表示されます。
  • 右上は画像 isow をインフレートした後の画像 bg で、背景画像が確定背景、前景画像が「元画像 - 確定背景」です。
  • 左下隅は前景画像を決定するためのものです。
  • 画像右下の小さな円は、画像 bg と画像 fore を差し引いた未知領域画像 un です。つまり、未知領域画像unは、「原画像で決まる背景と決定される前景」から導出される。

4. 関数connectedComponents

決定された前景が決定された後、決定された前景画像にマークを付けることができる。OpenCVでは関数が利用可能です
cv2.connectedComponents()进行标注この関数は背景を 0 としてマークし、他のオブジェクトを 1 から始まる正の整数でマークします。
関数 cv2.connectedComponents() の構文形式は次のとおりです。

retval、ラベル = cv2.connectedComponents( image )

式では次のようになります。

  • image は、ラベル付けされる 8 ビットのシングルチャネル画像です。
  • retval は返されるラベルの数です。
  • ラベルはラベル付けされた結果イメージです。

例: 関数 cv2.connectedComponents() を使用して画像に注釈を付け、注釈の効果を観察します。

import numpy as np
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('water_coins.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
ishow=img.copy()
ret, thresh = cv2.threshold(gray,0,255,
cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
sure_bg = cv2.dilate(opening,kernel,iterations=3)
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, fore = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)
fore = np.uint8(fore)
ret, markers = cv2.connectedComponents(fore)
print(markers)
plt.subplot(131)
plt.imshow(ishow)
plt.axis('off')
plt.subplot(132)
plt.imshow(fore)
plt.axis('off')
plt.subplot(133)
plt.imshow(markers)
plt.axis('off')
print(ret)
plt.show()

操作結果:

  • 左の画像はショーの元の画像です。
  • 中央は、距離変換後に得られた前景画像の前方の中心点画像です。
  • 右側の画像は、前景画像の中心点画像にラベルを付けた後の結果の画像マーカーです。
    前景画像の中心点が異なってマークされることがわかります。ここに画像の説明を挿入
    関数 cv2.connectedComponents() が画像をマークすると、背景は 0 としてマークされ、
    他のオブジェクトは 1 から始まる正の整数でマークされます。具体的な対応関係は次のとおりです。
  • 値 0 は背景領域を表します。
  • 1 から始まる値は、異なる前景領域を表します。
    分水界アルゴリズムでは、ラベル値 0 は未知の領域を表します。したがって、関数 cv2.connectedComponents() でマークされた結果を調整する必要があります。マークされた結果に値 1 を追加します。上記の処理後のラベル付け結果は次のようになります。
  • 値 1 は背景領域を表します。
  • 2 から始まる値は、異なる前景領域を表します。

分水界アルゴリズムを使用できるようにするには、元の画像内の未知の領域をマークし、計算された未知の領域を 0 としてマークする必要もあります。
ここでのキーコードは次のとおりです。

ret, markers = cv2.connectedComponents(fore)
markers = markers+1
markers[未知区域] = 0

例: 関数 cv2.connectedComponents() を使用して画像をマークし、未知の領域が 0 としてマークされるように画像を修正し、マークの効果を観察します。

import numpy as np
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('water_coins.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
ishow=img.copy()
ret, thresh = cv2.threshold(gray,0,255,
cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
sure_bg = cv2.dilate(opening,kernel,iterations=3)
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, fore = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)
fore = np.uint8(fore)
ret, markers1 = cv2.connectedComponents(fore)
foreAdv=fore.copy()
unknown = cv2.subtract(sure_bg,foreAdv)
ret, markers2 = cv2.connectedComponents(foreAdv)
markers2 = markers2+1
markers2[unknown==255] = 0
plt.subplot(121)
plt.imshow(markers1)
plt.axis('off')
plt.subplot(122)
plt.imshow(markers2)
plt.axis('off')
plt.show()

操作結果:

ここに画像の説明を挿入

左右の画像を比較すると、右の画像は前景画像のエッジ(未知の領域)にマークが付けられているため、決定された各前景にはマークされた未知の領域である黒いエッジが存在することがわかります。

流域アルゴリズム関数 cv2.watershed()

上記の処理が完了すると、分水界アルゴリズムを使用して前処理結果画像をセグメント化できます。OpenCV では、ウォーターシェッド アルゴリズムを実装する関数は cv2.watershed() で、その構文形式は次のとおりです。

マーカー = cv2.watershed( 画像, マーカー )

式では次のようになります。

  • image は入力イメージであり、8 ビット 3 チャネル イメージである必要があります。画像上で cv2.watershed() 関数を使用する前に、画像内の目的のセグメント化された領域の輪郭を正の数値で大まかに描く必要があります。セグメント化された各領域には、1、2、3などのラベルが付けられます。決定されていない領域については、0 としてマークする必要があります。マークされた領域は、流域アルゴリズム セグメンテーションの「シード」領域として理解できます。
  • マーカーは 32 ビットの単一チャネルのアノテーション結果であり、画像と同じサイズにする必要があります。マーカーでは、各ピクセルは初期の「シード値」に設定されるか、境界の「-1」に設定されます。マーカーは省略可能です。

画像セグメンテーションに分水嶺アルゴリズムを使用する場合の基本的な手順は次のとおりです。

  1. 元の画像 O は、モルフォロジー オープニング操作によってノイズ除去されます。
  2. エッチング操作により「確定背景B」を取得します。ここでは「元の画像を取得する - 背景を決定する」だけで十分であることに注意してください。
  3. 距離変換関数 cv2. distanceTransform() を使用して元の画像を操作し、閾値処理を実行して
    「決定された前景 F」を取得します。
  4. 未知の領域 UN (UN = O –B – F) を計算します。
  5. 関数 cv2.connectedComponents() を使用して、元のイメージ O に注釈を付けます。
  6. 関数 cv2.connectedComponents() のアノテーション結果を修正します。
  7. 画像のセグメンテーションは、ウォーターシェッド関数を使用して行われます。

コード例: ウォーターシェッド アルゴリズムを使用して画像をセグメント化し、セグメンテーション効果を観察します。

import numpy as np
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('water_coins.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
ishow=img.copy()
ret, thresh = cv2.threshold(gray,0,255,
cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
sure_bg = cv2.dilate(opening,kernel,iterations=3)
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)
ret, markers = cv2.connectedComponents(sure_fg)
markers = markers+1
markers[unknown==255] = 0
markers = cv2.watershed(img,markers)
img[markers == -1] = [255,0,0]
plt.subplot(121)
plt.imshow(ishow)
plt.axis('off')
plt.subplot(122)
plt.imshow(img)
plt.axis('off')
plt.show()

実行結果:

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/hai411741962/article/details/132228072