OpenCV에서 Watershed의 마커를 정의하는 방법은 무엇입니까?
OpenCV로 Android 용으로 작성 중입니다. 사용자가 수동으로 이미지를 표시하지 않고 마커 제어 유역을 사용하여 아래와 유사한 이미지를 분할하고 있습니다. 지역 최대 값을 마커로 사용할 계획입니다.
minMaxLoc()
나에게 가치를 줄 것이지만 내가 관심있는 블롭으로 어떻게 제한 할 수 있습니까? findContours()
또는 cvBlob Blob 의 결과를 활용하여 ROI를 제한하고 각 Blob에 최대 값을 적용 할 수 있습니까?
우선 :이 함수 minMaxLoc
는 주어진 입력에 대해 전역 최소값과 전역 최대 값 만 찾습니다. 따라서 지역 최소값 및 / 또는 지역 최대 값을 결정하는 데는 대부분 쓸모가 없습니다. 그러나 귀하의 아이디어는 옳습니다. 마커를 기반으로 유역 변환을 수행하기 위해 지역 최소값 / 최대 값을 기반으로 마커를 추출하는 것은 완전히 괜찮습니다. Watershed Transform이 무엇이며 OpenCV에있는 구현을 올바르게 사용해야하는 방법을 명확히하겠습니다.
유역을 다루는 몇 가지 적절한 양의 논문은 다음과 유사하게 설명합니다 (확실하지 않은 경우 세부 사항을 놓칠 수 있습니다 : 질문). 여러분이 알고있는 일부 지역의 표면을 고려하십시오. 여기에는 계곡과 봉우리가 포함되어 있습니다 (여기에서 우리와 관련이없는 기타 세부 사항 중). 이 표면 아래에있는 모든 것이 물, 착색 된 물이라고 가정합니다. 이제 표면의 각 계곡에 구멍을 뚫 으면 물이 모든 영역을 채우기 시작합니다. 언젠가는 다른 색의 물이 만나고, 이런 일이 발생하면 서로 닿지 않도록 댐을 건설합니다. 결국 당신은 모든 다른 색깔의 물을 분리하는 유역 인 댐 모음을 가지고 있습니다.
이제 그 표면에 너무 많은 구멍을 만들면 너무 많은 영역이 생깁니다. 너무 적게 만들면 세분화되지 않습니다. 따라서 유역 사용을 제안하는 거의 모든 논문은 실제로 해당 논문이 다루는 응용 프로그램에서 이러한 문제를 방지하는 기술을 제시합니다.
나는이 모든 것을 작성했습니다 (유역 변환이 무엇인지 아는 사람에게는 너무 순진한 일입니다). 유역 구현을 사용해야하는 방법을 직접 반영하기 때문입니다 (현재 허용되는 답변은 완전히 잘못된 방식으로 수행됩니다). 이제 Python 바인딩을 사용하여 OpenCV 예제를 시작하겠습니다.
질문에 제시된 이미지는 대부분 너무 가깝고 경우에 따라 겹치는 많은 물체로 구성됩니다. 여기서 유역의 유용성은 이러한 개체를 단일 구성 요소로 그룹화하는 것이 아니라 올바르게 분리하는 것입니다. 따라서 각 개체에 대해 하나 이상의 마커와 배경에 적합한 마커가 필요합니다. 예를 들어, 먼저 Otsu에 의해 입력 이미지를 이진화하고 작은 물체를 제거하기위한 형태 학적 열기를 수행합니다. 이 단계의 결과는 아래 왼쪽 이미지에 나와 있습니다. 이제 이진 이미지로 거리 변환을 적용하는 것을 고려하십시오. 결과는 오른쪽에 있습니다.
거리 변환 결과를 통해 배경에서 가장 먼 영역 만 고려하도록 임계 값을 고려할 수 있습니다 (아래 왼쪽 이미지). 이렇게하면 이전 임계 값 이후에 다른 영역에 레이블을 지정하여 각 개체에 대한 마커를 얻을 수 있습니다. 이제 우리는 마커를 구성하기 위해 위의 왼쪽 이미지의 확장 된 버전의 테두리를 고려할 수도 있습니다. 전체 마커는 아래 오른쪽에 표시됩니다 (일부 마커는 너무 어둡기 때문에 보이지 않지만 왼쪽 이미지의 각 흰색 영역은 오른쪽 이미지에 표시됩니다).
여기에있는이 마커는 많은 의미가 있습니다. 각각 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)
여기서 유역을 사용하는 방법에 대한 간단한 코드를 설명하고 싶습니다. OpenCV-Python을 사용하고 있지만 이해하는 데 어려움이 없길 바랍니다.
이 코드에서는 전경-배경 추출을 위한 도구로 유역을 사용할 것 입니다. (이 예제는 OpenCV 쿡북의 C ++ 코드에 해당하는 Python 코드입니다). 이것은 유역을 이해하는 간단한 사례입니다. 그 외에도 유역을 사용하여이 이미지의 개체 수를 계산할 수 있습니다. 이 코드는 약간 고급 버전이 될 것입니다.
1- 먼저 이미지를로드하고 그레이 스케일로 변환 한 다음 적절한 값으로 임계 값을 지정합니다. 나는 Otsu의 이진화를 취 했기 때문에 최상의 임계 값을 찾을 것입니다.
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 %, 검은 영역은 확실하지 않다는 것을 명확하게 이해할 수 있습니다.
그런 다음 32SC1로 변환합니다.
marker32 = np.int32(marker)
3-마지막으로 유역 을 적용 하고 결과를 uint8 이미지 로 다시 변환 합니다.
cv2.watershed(img,marker32)
m = cv2.convertScaleAbs(marker32)
미디엄 :
4 - 우리는 임계 값을 제대로 마스크를 얻고 수행하는 bitwise_and
입력 이미지 :
ret,thresh = cv2.threshold(m,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
res = cv2.bitwise_and(img,img,mask = thresh)
입술 :
도움이 되었기를 바랍니다 !!!
방주
머리말
나는 OpenCV 문서 (및 C ++ 예제 ) 의 유역 자습서 와 위의 mmgp 답변 이 매우 혼란 스럽기 때문에 주로 차밍 하고 있습니다. 궁극적으로 좌절감에서 포기하기 위해 분수령 접근 방식을 여러 번 재검토했습니다. 나는 마침내이 접근 방식을 시도해보고 실제로 실행되어야한다는 것을 깨달았다. 이것이 제가 접한 모든 튜토리얼을 정리 한 후 생각 해낸 것입니다.
컴퓨터 비전 초보자 인 것 외에도 대부분의 문제는 Python이 아닌 OpenCVSharp 라이브러리를 사용하라는 요구 사항과 관련이있을 것입니다. C #에는 NumPy에서 볼 수있는 것과 같은 고출력 배열 연산자가 포함되어 있지 않으므로 (IronPython을 통해 이식되었음을 알고 있지만) C #에서 이러한 작업을 이해하고 구현하는 데 상당히 어려움을 겪었습니다. 또한 기록을 위해 이러한 함수 호출 대부분의 뉘앙스와 불일치를 정말로 경멸합니다. OpenCVSharp는 제가 지금까지 사용 해본 라이브러리 중 가장 취약한 라이브러리 중 하나입니다. 하지만 어이, 그것은 항구입니다. 그래서 내가 무엇을 기대하고 있었습니까? 하지만 무엇보다도 무료입니다.
더 이상 고민하지 않고 유역의 OpenCVSharp 구현에 대해 이야기하고 일반적으로 유역 구현의 더 까다로운 점을 명확히하겠습니다.
신청
우선, 유역이 당신이 원하는 것인지 확인하고 그 용도를 이해하십시오. 다음과 같이 염색 된 세포 판을 사용하고 있습니다.
현장의 모든 세포를 구별하기 위해 한 번의 분수령 호출을 할 수 없다는 것을 알아내는 데 시간이 오래 걸렸습니다. 반대로 나는 먼저 밭의 일부를 분리 한 다음 그 작은 부분에 대해 유역을 호출해야했습니다. 여러 필터를 통해 관심 영역 (ROI)을 분리했습니다. 여기에서 간단히 설명하겠습니다.
- 소스 이미지로 시작 (왼쪽, 데모 용으로 잘림)
- 빨간색 채널 분리 (왼쪽 가운데)
- 적응 형 임계 값 적용 (오른쪽 가운데)
- 윤곽선을 찾은 다음 작은 영역이있는 윤곽선 제거 (오른쪽)
위의 임계 값 작업으로 인한 윤곽선을 정리했으면 이제 유역 후보를 찾을 때입니다. 제 경우에는 특정 영역보다 큰 모든 윤곽선을 간단히 반복했습니다.
암호
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);
그런 다음이 두 매트를 빼서 "힌트"모양의 최종 결과를 얻습니다.
var unknown = new Mat(); //this variable is also named "border" in some examples
Cv2.Subtract(background, foreground, unknown);
다시 말하지만, Cv2.ImShow
알 수없는 경우 다음과 같이 표시됩니다.
Nice! This was easy for me to wrap my head around. The next part, however, got me quite puzzled. Let's look at turning our "hint" into something the Watershed
function can use. For this we need to use ConnectedComponents
, which is basically a big matrix of pixels grouped by the virtue of their index. For example, if we had a mat with the letters "HI", ConnectedComponents
might return this matrix:
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
So, 0 is the background, 1 is the letter "H", and 2 is the letter "I". (If you get to this point and want to visualize your matrix, I recommend checking out this instructive answer.) Now, here's how we'll utilize ConnectedComponents
to create the markers (or labels) for watershed:
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);
}
}
Note that the Watershed function requires the border area to be marked by 0. So, we've set any border pixels to 0 in the label/marker array.
At this point, we should be all set to call Watershed
. However, in my particular application, it is useful just to visualize a small portion of the entire source image during this call. This may be optional for you, but I first just mask off a small bit of the source by dilating it:
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);
And then make the magic call:
Cv2.Watershed(sourceCrop, labels);
Results
The above Watershed
call will modify labels
in place. You'll have to go back to remembering about the matrix resulting from ConnectedComponents
. The difference here is, if watershed found any dams between watersheds, they will be marked as "-1" in that matrix. Like the ConnectedComponents
result, different watersheds will be marked in a similar fashion of incrementing numbers. For my purposes, I wanted to store these into separate contours, so I created this loop to split them up:
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));
}
}
Then, I wanted to print these contours with random colors, so I created the following mat:
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);
}
}
Which yields the following when shown:
If we draw on the source image the dams that were marked by a -1 earlier, we get this:
Edits:
메모하는 것을 잊었습니다. 매트를 다 사용한 후에는 매트를 청소하고 있는지 확인하십시오. 그들은 메모리에 남아 있으며 OpenCVSharp는 이해할 수없는 오류 메시지와 함께 나타날 수 있습니다. using
위의 내용을 실제로 사용해야 하지만 mat.Release()
옵션이기도합니다.
또한 위의 mmgp 답변에는 dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)
거리 변환 결과에 적용되는 히스토그램 스트레칭 단계 인 다음 줄이 포함됩니다 . 여러 가지 이유로이 단계를 생략했지만 (대부분 내가 본 히스토그램이 시작하기에 너무 좁다 고 생각하지 않았기 때문에) 마일리지가 다를 수 있습니다.
참고 URL : https://stackoverflow.com/questions/11294859/how-to-define-the-markers-for-watershed-in-opencv
'Program Tip' 카테고리의 다른 글
IF a == true OR b == true 문 (0) | 2020.11.13 |
---|---|
MySQL에서`REPLACE`와`INSERT… ON DUPLICATE KEY UPDATE`의 실질적인 차이점은 무엇입니까? (0) | 2020.11.13 |
Git 병합은 기본 병합 메시지를 사용하지 않고 기본 메시지로 편집기를 엽니 다. (0) | 2020.11.13 |
Git을 사용할 때 Visual Studio에서 분기 (로컬 / 원격)를 어떻게 새로 고치나요? (0) | 2020.11.13 |
이“react-scripts eject”명령은 무엇을합니까? (0) | 2020.11.13 |