web-dev-qa-db-ja.com

OpenCVでWatershedのマーカーを定義する方法は?

AndroidでOpenCVを使用しています。ユーザーが手動で画像にマークを付けることなく、マーカー制御の流域を使用して以下のような画像を分割しています。地域の最大値を使用する予定ですマーカーとして。

minMaxLoc()は値を提供しますが、どのように私が興味のあるブロブに制限できますか? findContours()またはcvBlobブロブの結果を利用してROIを制限し、各ブロブに最大値を適用できますか?

input image

66
Tru

まず、関数minMaxLocは、指定された入力のグローバルな最小値とグローバルな最大値のみを検索するため、地域の最小値および/または地域の最大値の決定にはほとんど役に立ちません。しかし、あなたの考えは正しいです。マーカーに基づいて流域変換を実行するために、地域の最小値/最大値に基づいてマーカーを抽出することはまったく問題ありません。 Watershed Transformとは何か、OpenCVに存在する実装をどのように正しく使用すべきかを明確にしてみましょう。

流域を扱ったまともな量の論文は、それに続くものと同様にそれを説明します(確信がないなら、私はいくらかの詳細を見逃すかもしれません:ask)あなたが知っているいくつかの地域の表面を考えてみましょう。そこには谷と山頂が含まれています(ここでは私たちにとって重要ではない他の詳細の中でも)。この表面の下にあるのは、水、色付きの水だけだとします。さて、表面の各谷に穴を開けると、水がすべての領域を満たし始めます。ある時点で、異なる色の水が出会うでしょう。そして、これが起こるとき、あなたはそれらが互いに触れないようにダムを建設します。最後に、ダムのコレクションがあります。これは、すべての異なる色の水を分ける流域です。

さて、その表面にあまりにも多くの穴を開けると、過度に多くの領域ができてしまいます:オーバーセグメンテーション。少なすぎると、セグメンテーションが不十分になります。したがって、事実上、流域の使用を提案するすべての紙は、紙が扱っているアプリケーションのこれらの問題を回避するための技術を提示します。

私はこれをすべて書きました(Watershed Transformが何であるかを知っている人にはおそらくあまりにもナイーブです)それはwatershed実装の使用方法に直接反映するためです(現在受け入れられている答えは完全に間違った方法でしています)。 Pythonバインディングを使用して、OpenCVの例から始めましょう。

質問で提示される画像は、ほとんどが近すぎて、場合によっては重なっている多くのオブジェクトで構成されています。ここでの流域の有用性は、これらのオブジェクトを1つのコンポーネントにグループ化するのではなく、正しく分離することです。そのため、オブジェクトごとに少なくとも1つのマーカーと、背景に適切なマーカーが必要です。例として、最初に入力画像をOtsuで2値化し、小さなオブジェクトを削除するための形態学的オープニングを実行します。このステップの結果を左の画像に示します。ここで、バイナリイメージで距離変換を適用することを検討してください。結果は右になります。

enter image description hereenter image description here

距離変換の結果では、背景から最も離れた領域のみを考慮するように、いくつかのしきい値を考慮することができます(下の左の画像)。これを行うと、以前のしきい値の後に異なる領域にラベルを付けることにより、各オブジェクトのマーカーを取得できます。マーカーを構成するために、上の左画像の拡張バージョンの境界線を考慮することもできます。完全なマーカーは右下に表示されます(一部のマーカーは表示するには暗すぎますが、左の画像の各白い領域は右の画像で表されます)。

enter image description hereenter image description here

ここにあるこのマーカーは非常に理にかなっています。各colored water == one markerは領域の塗りつぶしを開始し、分水界変換はダムを構築して、異なる「色」のマージを妨げます。変換を行うと、左の画像が得られます。元の画像でダムを構成することにより、ダムのみを考慮すると、正しい結果が得られます。

enter image description hereenter image description here

import sys
import cv2
import numpy
from scipy.ndimage import label

def segment_on_dt(a, img):
    border = cv2.dilate(img, None, iterations=5)
    border = border - cv2.erode(border, None)

    dt = cv2.distanceTransform(img, 2, 3)
    dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)
    _, dt = cv2.threshold(dt, 180, 255, cv2.THRESH_BINARY)
    lbl, ncc = label(dt)
    lbl = lbl * (255 / (ncc + 1))
    # Completing the markers now. 
    lbl[border == 255] = 255

    lbl = lbl.astype(numpy.int32)
    cv2.watershed(a, lbl)

    lbl[lbl == -1] = 0
    lbl = lbl.astype(numpy.uint8)
    return 255 - lbl


img = cv2.imread(sys.argv[1])

# Pre-processing.
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)    
_, img_bin = cv2.threshold(img_gray, 0, 255,
        cv2.THRESH_OTSU)
img_bin = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN,
        numpy.ones((3, 3), dtype=int))

result = segment_on_dt(img, img_bin)
cv2.imwrite(sys.argv[2], result)

result[result != 255] = 0
result = cv2.dilate(result, None)
img[result == 255] = (0, 0, 255)
cv2.imwrite(sys.argv[3], img)
108
mmgp

ここで流域の使用方法に関する簡単なコードを説明したいと思います。 OpenCV-Pythonを使用していますが、理解するのに苦労しないことを願っています。

このコードでは、フォアグラウンドバックグラウンド抽出のツールとしてwatershedを使用します(この例はpython OpenCVクックブックのC++コードの対応物です)。これは分水界を理解するための簡単なケースであり、それとは別に、分水界を使用してこの画像内のオブジェクトの数を数えることができます。

1-最初に画像を読み込み、グレースケールに変換し、適切な値でしきい値を設定します。 Otsuの2値化を使用したため、最適なしきい値が見つかります。

import cv2
import numpy as np

img = cv2.imread('sofwatershed.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

以下は私が得た結果です:

enter image description here

(前景と背景画像のコントラストが大きいため、その結果でさえ良いです)

2-マーカーを作成する必要があります。 マーカーは、32SC1(32ビット符号付きシングルチャネル)である元の画像と同じサイズの画像です。

これで、元の画像にいくつかの領域が存在することがわかります。その部分は前景に属します。マーカー画像でそのような領域を255でマークします。これで、背景になりそうな領域には128のマークが付けられます。不明な領域には0のマークが付けられます。それが次に実行されます。

A-前景領域:-ピルが白い色のしきい値画像をすでに取得しています。残りの領域がフォアグラウンドに属することが確実になるように、それらを少し侵食します。

fg = cv2.erode(thresh,None,iterations = 2)

fg

enter image description here

B-背景領域:-ここでは、背景領域が縮小されるように、しきい値処理された画像を拡張します。しかし、残りの黒い領域は100%バックグラウンドであると確信しています。 128に設定します。

bgt = cv2.dilate(thresh,None,iterations = 3)
ret,bg = cv2.threshold(bgt,1,128,1)

次のようにbgを取得します。

enter image description here

C-fgとbgの両方を追加します

marker = cv2.add(fg,bg)

以下が得られるものです。

enter image description here

上記の画像から、白い領域が100%の前景、灰色の領域が100%の背景、黒の領域がわからないことが明確に理解できます。

次に、それを32SC1に変換します。

marker32 = np.int32(marker)

3-最後にwatershedを適用に変換し、結果をint8に変換します。

cv2.watershed(img,marker32)
m = cv2.convertScaleAbs(marker32)

m:

enter image description here

4-マスクを取得してbitwise_and入力画像:

ret,thresh = cv2.threshold(m,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
res = cv2.bitwise_and(img,img,mask = thresh)

res:

enter image description here

それが役に立てば幸い!!!

アーク

42
Abid Rahman K

まえがき

OpenCVドキュメントの流域チュートリアル (および C++の例 )と mmgpの答え の両方を見つけたことが主な理由ですかなり混乱します。最終的にフラストレーションをfruめるために、私は何度も流域アプローチを再訪しました。私はついに、少なくともこのアプローチを試してみて、実際にそれを見る必要があることに気付きました。これは、私が出会ったすべてのチュートリアルを整理した後に思いついたものです。

コンピュータービジョンの初心者であることは別として、私の問題のほとんどは、PythonではなくOpenCVSharpライブラリを使用するという私の要件に関係していると思われます。 C#には、NumPyにあるような強力な配列演算子が組み込まれていません(IronPythonを介して移植されたことに気づいています)また、記録のために、これらの関数呼び出しのほとんどのニュアンスと矛盾を軽inしています。 OpenCVSharpは、私がこれまで取り組んだ中で最も脆弱なライブラリの1つです。しかし、ちょっと、それは港なので、私は何を期待していましたか?何よりも素晴らしいのは、無料です。

これ以上苦労することなく、OpenCVSharpでの流域の実装についてお話しします。

アプリケーション

まず第一に、分水界があなたが望むものであることを確認し、その使用を理解してください。私はこのような染色細胞プレートを使用しています:

enter image description here

フィールド内のすべてのセルを区別するために、1つの集水域の呼び出しを行うことができないことを理解するのにかなり時間がかかりました。それどころか、私は最初に畑の一部を隔離し、その小さな部分を分水界と呼ぶ必要がありました。いくつかのフィルターを使用して関心領域(ROI)を分離しました。ここで簡単に説明します。

enter image description here

  1. ソース画像から開始(左、デモ用にトリミング)
  2. 赤いチャネルを分離します(左中央)
  3. 適応しきい値を適用(右中央)
  4. 輪郭を見つけて、小さな領域の輪郭を除去します(右)

上記のしきい値設定操作の結果として輪郭をきれいにしたら、分水界の候補を見つける時が来ました。私の場合、特定の領域よりも大きいすべての輪郭を繰り返し処理しました。

コード

上記のフィールドからこの輪郭をROIとして分離したとします。

enter image description here

流域をどのようにコーディングするかを見てみましょう。

空白のマットから始めて、ROIを定義する輪郭のみを描画します。

_var isolatedContour = new Mat(source.Size(), MatType.CV_8UC1, new Scalar(0, 0, 0));
Cv2.DrawContours(isolatedContour, new List<List<Point>> { contour }, -1, new Scalar(255, 255, 255), -1);
_

流域の呼び出しが機能するためには、ROIに関するいくつかの「ヒント」が必要になります。あなたが私のような完全な初心者なら、クイック入門のために CMM流域ページ をチェックすることをお勧めします。右側のシェイプを作成して、左側のROIに関するヒントを作成します。

enter image description here

この「ヒント」形状の白い部分(または「背景」)を作成するには、次のように分離した形状をDilateします。

_var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(2, 2));
var background = new Mat();
Cv2.Dilate(isolatedContour, background, kernel, iterations: 8);
_

中央(または「前景」)に黒い部分を作成するには、距離変換とそれに続くしきい値を使用します。これにより、左側の図形から右側の図形に移動します。

enter image description here

これにはいくつかの手順が必要です。必要な結果を得るには、しきい値の下限を調整する必要がある場合があります。

_var foreground = new Mat(source.Size(), MatType.CV_8UC1);
Cv2.DistanceTransform(isolatedContour, foreground, DistanceTypes.L2, DistanceMaskSize.Mask5);
Cv2.Normalize(foreground, foreground, 0, 1, NormTypes.MinMax); //Remember to normalize!

foreground.ConvertTo(foreground, MatType.CV_8UC1, 255, 0);
Cv2.Threshold(foreground, foreground, 150, 255, ThresholdTypes.Binary);
_

次に、これら2つのマットを減算して、「ヒント」形状の最終結果を取得します。

_var unknown = new Mat(); //this variable is also named "border" in some examples
Cv2.Subtract(background, foreground, unknown);
_

繰り返しますが、_Cv2.ImShow_unknownの場合、次のようになります。

enter image description here

いいね!これは私の頭を包み込むのは簡単でした。しかし、次の部分は私をかなり困惑させました。 「ヒント」をWatershed関数で使用できるものに変えてみましょう。このためには、ConnectedComponentsを使用する必要があります。これは基本的に、インデックスのおかげでグループ化されたピクセルの大きなマトリックスです。たとえば、「HI」という文字のマットがある場合、ConnectedComponentsは次の行列を返します。

_0 0 0 0 0 0 0 0 0
0 1 0 1 0 2 2 2 0
0 1 0 1 0 0 2 0 0 
0 1 1 1 0 0 2 0 0
0 1 0 1 0 0 2 0 0
0 1 0 1 0 2 2 2 0
0 0 0 0 0 0 0 0 0
_

したがって、0は背景、1は文字「H」、2は文字「I」です。 (この時点でマトリックスを視覚化する場合は、 この有益な答え を確認することをお勧めします。)ここで、ConnectedComponentsを使用してマーカーを作成します(またはラベル)集水域用:

_var labels = new Mat(); //also called "markers" in some examples
Cv2.ConnectedComponents(foreground, labels);
labels = labels + 1;

//this is a much more verbose port of numpy's: labels[unknown==255] = 0
for (int x = 0; x < labels.Width; x++)
{
    for (int y = 0; y < labels.Height; y++)
    {
        //You may be able to just send "int" in rather than "char" here:
        var labelPixel = (int)labels.At<char>(y, x);    //note: x and y are inexplicably 
        var borderPixel = (int)unknown.At<char>(y, x);  //and infuriatingly reversed

        if (borderPixel == 255)
            labels.Set(y, x, 0);
    }
}
_

Watershed関数では、境界領域を0でマークする必要があることに注意してください。したがって、ラベル/マーカー配列で境界ピクセルを0に設定しました。

この時点で、Watershedを呼び出すように設定する必要があります。ただし、特定のアプリケーションでは、この呼び出し中にソースイメージ全体のごく一部を視覚化するだけで便利です。これはオプションの場合もありますが、最初に、ソースを少し拡張してマスクします。

_var mask = new Mat();
Cv2.Dilate(isolatedContour, mask, new Mat(), iterations: 20);
var sourceCrop = new Mat(source.Size(), source.Type(), new Scalar(0, 0, 0));
source.CopyTo(sourceCrop, mask);
_

そして、魔法の呼び出しを行います:

_Cv2.Watershed(sourceCrop, labels);
_

結果

上記のWatershed呼び出しは、labelsin placeを変更します。 ConnectedComponentsから生じる行列について思い出すことに戻る必要があります。ここでの違いは、分水界が分水界間にダムを見つけた場合、そのマトリックスで「-1」としてマークされることです。 ConnectedComponentsの結果と同様に、さまざまな分水界が同様の方法で数値をインクリメントしてマークされます。私の目的のために、これらを別々の輪郭に保存したかったので、これらを分割するためにこのループを作成しました。

_var watershedContours = new List<Tuple<int, List<Point>>>();

for (int x = 0; x < labels.Width; x++)
{
    for (int y = 0; y < labels.Height; y++)
    {
        var labelPixel = labels.At<Int32>(y, x); //note: x, y switched 

        var connected = watershedContours.Where(t => t.Item1 == labelPixel).FirstOrDefault();
        if (connected == null)
        {
            connected = new Tuple<int, List<Point>>(labelPixel, new List<Point>());
            watershedContours.Add(connected);
        }
        connected.Item2.Add(new Point(x, y));

        if (labelPixel == -1)
            sourceCrop.Set(y, x, new Vec3b(0, 255, 255));

    }
}
_

次に、これらの輪郭をランダムな色で印刷したいので、次のマットを作成しました。

_var watershed = new Mat(source.Size(), MatType.CV_8UC3, new Scalar(0, 0, 0));
foreach (var component in watershedContours)
{
    if (component.Item2.Count < (labels.Width * labels.Height) / 4 && component.Item1 >= 0)
    {
        var color = GetRandomColor();
        foreach (var point in component.Item2)
            watershed.Set(point.Y, point.X, color);
    }
}
_

表示されると、次のようになります。

enter image description here

ソースイメージに、以前に-1でマークされたダムを描画すると、次のようになります。

enter image description here

編集:

忘れてしまったのは、マットを使い終わったら、必ずクリーンアップすることです。それらはメモリ内にとどまり、OpenCVSharpが理解できないエラーメッセージを表示する場合があります。上記のusingを実際に使用する必要がありますが、mat.Release()もオプションです。

また、上記のmmgpの回答には次の行が含まれています:dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)。これは、距離変換の結果に適用されるヒストグラムストレッチングステップです。いくつかの理由でこの手順を省略しました(主に、最初に見たヒストグラムが狭すぎるとは思わなかったため)が、走行距離は異なる場合があります。

5
Daniel