75

私はOpenCVでAndroid用に書いています。ユーザーが手動で画像にマークを付けることなく、マーカー制御の流域を使用して、以下のような画像をセグメント化しています。地域の最大値をマーカーとして使用することを計画しています。

minMaxLoc()私に価値を与えるだろうが、どうすれば私が興味を持っているブロブにそれを制限することができますか?またはcvBlobブロブからの結果を利用しfindContours()てROIを制限し、各ブロブに最大値を適用できますか?

入力画像

4

3 に答える 3

128

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

流域を扱っているかなりの量の論文は、それを次のように説明しています(不明な場合は、詳細を見逃す可能性があります:質問してください)。あなたが知っているいくつかの地域の表面を考えてみてください。そこには谷と山が含まれています(ここでは私たちには関係のない他の詳細の中でも)。この表面の下にあるのは水、着色された水だけだとしましょう。次に、表面の各谷に穴を開けると、水がすべての領域を満たし始めます。ある時点で、異なる色の水が出会うでしょう、そしてこれが起こるとき、あなたはそれらが互いに接触しないようにダムを建設します。最後に、ダムのコレクションがあります。これは、すべての異なる色の水を分離する流域です。

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

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

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

ここに画像の説明を入力してください ここに画像の説明を入力してください

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

ここに画像の説明を入力してください ここに画像の説明を入力してください

ここにあるこのマーカーは非常に理にかなっています。それぞれcolored water == one markerがその地域を埋め始め、流域の変化は、異なる「色」が融合するのを妨げるダムを建設します。変換を行うと、左の画像が表示されます。ダムだけを元の画像で構成して考えると、正しい結果が得られます。

ここに画像の説明を入力してください ここに画像の説明を入力してください

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)
于 2013-01-31T02:15:01.920 に答える
46

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

このコードでは、前景と背景を抽出するためのツールとして流域を使用します。(この例は、OpenCVクックブックのC ++コードのPython版です)。これは、流域を理解するための簡単なケースです。それとは別に、流域を使用して、この画像内のオブジェクトの数を数えることができます。これは、このコードの少し高度なバージョンになります。

1-まず、画像をロードしてグレースケールに変換し、適切な値でしきい値を設定します。大津の二値化を採用したので、最適なしきい値が見つかります。

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)

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

ここに画像の説明を入力してください

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

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

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

A-前景領域:-ピルが白色であるしきい値画像をすでに取得しています。残りの領域が前景に属していることを確認できるように、それらを少し侵食します。

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

fg

ここに画像の説明を入力してください

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

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

今、私たちは次のようにbgを取得します:

ここに画像の説明を入力してください

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

marker = cv2.add(fg,bg)

以下は私たちが得るものです:

ここに画像の説明を入力してください

これで、上の画像から、白い領域が100%前景、灰色の領域が100%背景、黒い領域が100%前景であることがはっきりとわかります。

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

marker32 = np.int32(marker)

3-最後に、流域を適用し、結果をuint8イメージに変換し直します。

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

m:

ここに画像の説明を入力してください

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:

ここに画像の説明を入力してください

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

アーク

于 2012-07-11T17:30:18.520 に答える
8

序文

私は主に、OpenCVドキュメント(およびC ++の例)の流域チュートリアルと上記のmmgpの回答の両方が非常に混乱していることに気付いたためにチャイムを鳴らしています。私は、最終的に欲求不満をあきらめるために、流域アプローチを何度も再検討しました。私はついに、少なくともこのアプローチを試してみて、実際に動作することを確認する必要があることに気づきました。これは私が出くわしたすべてのチュートリアルを整理した後に私が思いついたものです。

コンピュータービジョンの初心者であることを除けば、私の問題のほとんどは、PythonではなくOpenCVSharpライブラリを使用するという私の要件に関係している可能性があります。C#にはNumPyに見られるような組み込みの高出力配列演算子がないため(これはIronPythonを介して移植されていることはわかっていますが)、C#でのこれらの操作の理解と実装の両方でかなり苦労しました。また、記録のために、私はこれらの関数呼び出しのほとんどのニュアンスと矛盾を本当に軽蔑しています。OpenCVSharpは、私がこれまでに使用した中で最も壊れやすいライブラリの1つです。でもねえ、それは港なので、私は何を期待していましたか?何よりも、それは無料です。

さらに面倒なことはせずに、私のOpenCVSharpによる流域の実装について話しましょう。うまくいけば、流域の実装の一般的な厄介な点のいくつかを明確にすることができます。

応用

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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

上記のしきい値処理の結果として得られた等高線をクリーンアップしたら、流域の候補を見つけるときが来ました。私の場合、特定の領域よりも大きいすべての輪郭を単純に繰り返しました。

コード

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

ここに画像の説明を入力してください

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

空白のマットから始めて、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に関するヒントを作成すると言えば十分です。

ここに画像の説明を入力してください

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

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

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

ここに画像の説明を入力してください

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

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 不明な場合は、次のようになります。

ここに画像の説明を入力してください

良い!これは私にとって頭を包み込むのは簡単でした。しかし、次の部分は私をかなり困惑させました。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呼び出しは、その場で変更labels されます。から生じる行列について覚えておく必要があります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);
    }
}

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

編集:

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

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

于 2018-06-22T20:03:16.487 に答える