Как определить маркеры для водораздела в OpenCV?

Я пишу для Android с OpenCV. Я сегментирую изображение, подобное приведенному ниже, используя управляемый маркером водораздел, без того, чтобы пользователь вручную отмечал изображение. Я планирую использовать региональные максимумы в качестве маркеров.

minMaxLoc() дал бы мне значение, но как я могу ограничить его каплями, которые меня интересуют? Могу ли я использовать результаты от findContours() или cvblob blobs для ограничения ROI и применения максимумов к каждому blob?

input image

3 ответов


прежде всего: функция minMaxLoc находит только глобальный минимум и глобальный максимум для данного ввода, поэтому в основном бесполезно определять региональные минимумы и/или региональные максимумы. Но ваша идея верна, извлечение маркеров на основе региональных минимумов / максимумов для выполнения преобразования водораздела на основе маркеров абсолютно нормально. Позвольте мне попытаться прояснить, что такое преобразование водораздела и как вы должны правильно использовать реализацию, присутствующую в OpenCV.

приличные количество документов, которые касаются водораздела, описывают его аналогично тому, что следует (я могу пропустить некоторые детали, если вы не уверены: спросите). Рассмотрим поверхность некоторого региона, который вы знаете, он содержит долины и вершины (среди других деталей, которые не имеют значения для нас здесь). Предположим, под этой поверхностью у вас есть только вода, окрашенная вода. Теперь сделайте отверстия в каждой долине вашей поверхности, а затем вода начнет заполнять всю область. В какой-то момент разные по цвету воды встретятся, и когда это произойдет, вы строите плотину так, чтобы они не касались друг друга. В конце концов у вас есть коллекция плотин, которая является водоразделом, отделяющим все разные цветные воды.

Теперь, если вы сделаете слишком много отверстий в этой поверхности, вы получите слишком много областей: чрезмерная сегментация. Если вы делаете слишком мало, вы получаете недостаточную сегментацию. Таким образом, практически любая статья, которая предлагает использовать водораздел, фактически представляет методы, чтобы избежать этих проблем для приложения, с которым имеет дело с.

Я написал все это (что, возможно, слишком наивно для тех, кто знает, что такое преобразование водораздела), потому что это непосредственно отражает, как вы должны использовать реализации водораздела (что текущий принятый ответ делает совершенно неправильно). Давайте начнем с примера OpenCV, используя привязки Python.

изображение, представленное в вопросе, состоит из многих объектов, которые в основном слишком близки и в некоторых случаях перекрываются. Этот полезность водораздела здесь состоит в том, чтобы правильно разделить эти объекты, а не сгруппировать их в один компонент. Поэтому вам нужен хотя бы один маркер для каждого объекта и хорошие маркеры для фона. В качестве примера, первое преобразование входного изображения на Оцу и выполнить морфологический отверстие для удаления небольших объектов. Результат этого шага показан ниже на левом изображении. Теперь с двоичным изображением рассмотрим применение к нему преобразования расстояния, результат при право.

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)

Я хотел бы объяснить простой код о том, как использовать водораздел здесь. Я использую OpenCV-Python, но я надеюсь, что вам не составит труда понять.

в этом коде я буду использовать watershed в качестве инструмента для передний план-извлечение фона. (этот пример является аналогом python кода C++ в кулинарной книге OpenCV). Это простой случай, чтобы понять водораздел. Кроме того, вы можете использовать водораздел для подсчета количества объектов на этом изображении. Что будет немного продвинутая версия этого кода.

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)

ниже результат, который я получил:

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 - Наконец-то мы!--11-->применить водоразделом и преобразовать результат обратно в тип uint8 изображение:

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

надеюсь, что это помогает!!!

ковчег


предисловие

я вмешиваюсь в основном потому, что я нашел оба водораздел учебник в документации OpenCVв C++ пример), а также ответ mmgp выше чтобы быть довольно запутанным. Я несколько раз пересматривал подход к водоразделу, чтобы в конечном итоге отказаться от разочарования. Я, наконец, понял, что мне нужно, по крайней мере, попробовать этот подход и увидеть его в действии. Вот что я придумал после сортировки из всех учебников, с которыми я сталкивался.

помимо того, что я новичок в компьютерном зрении, большая часть моих проблем, вероятно, была связана с моим требованием использовать библиотеку OpenCVSharp, а не Python. C# не имеет запеченных в мощных операторах массива, таких как найденные в NumPy (хотя я понимаю, что это было портировано через IronPython), поэтому я немного боролся за понимание и реализацию этих операций на C#. Кроме того, для протокола, я действительно презираю нюансы, и несоответствия в большинстве этих вызовов функций. OpenCVSharp - одна из самых хрупких библиотек, с которыми я когда-либо работал. Но это же портвейн, так чего же я ожидал? Но лучше всего то, что это бесплатно.

без дальнейших церемоний, давайте поговорим о моей реализации OpenCVSharp водораздела и, надеюсь, проясним некоторые из более липких точек реализации водораздела в целом.

приложение

прежде всего, убедитесь, что водораздел-это то, что вы хотите и понимаете его использование. Я использую окрашенные клеточные пластины, как этот:

enter image description here

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);

затем мы вычитаем эти два коврика, чтобы получить конечный результат нашей "намек" форма:

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

еще раз, если мы Cv2.ImShow неизвестный, это будет выглядеть так:

enter image description here

приятно! Это было легко для меня, чтобы обернуть мою голову вокруг. Однако следующая часть меня озадачила. Давайте посмотрим, как превратить наш "намек" во что-то Watershed функция можно использовать. Для этого нам нужно использовать ConnectedComponents, который в основном представляет собой большую матрицу пикселей, сгруппированных в силу их индекса. Например, если бы мы коврик с буквами "привет", 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". (Если вы дошли до этого момента и хотите визуализировать свою матрицу, я рекомендую проверить этот поучительный ответ!--30-->.) Вот как мы будем использовать 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);
    }
}

обратите внимание, что функция водораздела требует, чтобы пограничная область была отмечена 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);
    }
}

, который дает следующее когда показано ниже:

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), который является шагом растяжения гистограммы, примененным к результаты преобразования расстояния. Я опустил этот шаг по ряду причин (в основном потому, что я не думал, что гистограммы, которые я видел, были слишком узкими для начала), но ваш пробег может отличаться.