¿Cómo definir los marcadores de Cuenca Hidrográfica en OpenCV?


Estoy escribiendo para Android con OpenCV. Estoy segmentando una imagen similar a la de abajo usando una cuenca controlada por marcadores, sin que el usuario marque manualmente la imagen. Estoy planeando usar los máximos regionales como marcadores.

minMaxLoc() me daría el valor, pero ¿cómo puedo restringirlo a las manchas que es lo que me interesa? ¿Puedo utilizar los resultados de findContours() o los blobs cvBlob para restringir el ROI y aplicar máxima a cada blob?

imagen de entrada

Author: Harriv, 2012-07-02

3 answers

En primer lugar: la función minMaxLoc encuentra solo el mínimo global y el máximo global para una entrada dada, por lo que es casi inútil para determinar mínimos regionales y/o máximos regionales. Pero su idea es correcta, extraer marcadores basados en mínimos/máximos regionales para realizar una Transformación de cuenca basada en marcadores está totalmente bien. Permítanme tratar de aclarar lo que es la Transformación de la Cuenca y cómo se debe utilizar correctamente la implementación presente en OpenCV.

Una cantidad decente de documentos ese trato con watershed lo describe de manera similar a lo que sigue (podría perder algún detalle, si no está seguro: pregunte). Considere la superficie de alguna región que conoce, contiene valles y picos (entre otros detalles que son irrelevantes para nosotros aquí). Supongamos que debajo de esta superficie todo lo que tienes es agua, agua coloreada. Ahora, haga agujeros en cada valle de su superficie y luego el agua comienza a llenar toda el área. En algún momento, las aguas de diferentes colores se reunirán, y cuando esto suceda, se construye una dam tal que no se toquen. Al final tienes una colección de presas, que es la cuenca que separa todas las aguas de diferentes colores.

Ahora, si haces demasiados agujeros en esa superficie, terminas con demasiadas regiones: sobre-segmentación. Si haces muy pocos, obtienes una segmentación insuficiente. Por lo tanto, prácticamente cualquier documento que sugiere el uso de cuencas en realidad presenta técnicas para evitar estos problemas para la aplicación que el documento está tratando.

Escribí todo esto (lo cual es posiblemente demasiado ingenuo para cualquiera que sepa lo que es la Transformación de la Cuenca Hidrográfica) porque refleja directamente cómo debe usar las implementaciones de la cuenca hidrográfica (lo que la respuesta aceptada actual está haciendo de una manera completamente equivocada). Comencemos con el ejemplo de OpenCV ahora, usando los enlaces de Python.

La imagen presentada en la pregunta se compone de muchos objetos que en su mayoría están demasiado cerca y en algunos casos se superponen. La utilidad de watershed aquí es separar correctamente estos objetos, no para agruparlos en un solo componente. Por lo tanto, necesita al menos un marcador para cada objeto y buenos marcadores para el fondo. Como ejemplo, primero binarice la imagen de entrada por Otsu y realice una abertura morfológica para eliminar objetos pequeños. El resultado de este paso se muestra a continuación en la imagen de la izquierda. Ahora con la imagen binaria considere aplicar la transformada de distancia a ella, resultado a la derecha.

introduzca la descripción de la imagen aquíintroduzca la descripción de la imagen aquí

Con el resultado de la transformación de distancia, podemos considerar algún umbral tal que consideremos solo las regiones más distantes al fondo (imagen izquierda abajo). Haciendo esto, podemos obtener un marcador para cada objeto etiquetando las diferentes regiones después del umbral anterior. Ahora, también podemos considerar el borde de una versión dilatada de la imagen izquierda de arriba para componer nuestro marcador. El marcador completo se muestra abajo a la derecha (algunos marcadores son demasiado oscuros para ser vistos, pero cada región blanca en la imagen de la izquierda se representa a la derecha imagen).

introduzca la descripción de la imagen aquíintroduzca la descripción de la imagen aquí

Este marcador que tenemos aquí tiene mucho sentido. Cada colored water == one marker comenzará a llenar la región, y la transformación de la cuenca construirá represas para impedir que los diferentes "colores" se fusionen. Si hacemos la transformación, obtenemos la imagen a la izquierda. Considerando solo las presas componiéndolas con la imagen original, obtenemos el resultado a la derecha.

introduzca la descripción de la imagen aquíintroduzca la descripción de la imagen aquí

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)
 96
Author: mmgp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-10 17:50:13

Me gustaría explicar un código simple sobre cómo usar watershed aquí. Estoy usando OpenCV-Python, pero espero que no tengas ninguna dificultad para entenderlo.

En este código, usaré watershed como una herramienta para la extracción de fondo de primer plano. (Este ejemplo es la contraparte python del código C++ en OpenCV cookbook). Este es un caso sencillo para entender watershed. Aparte de eso, puede usar watershed para contar el número de objetos en esta imagen. Eso será un versión ligeramente avanzada de este código.

1 - Primero cargamos nuestra imagen, la convertimos a escala de grises y la umbral con un valor adecuado. Tomé La binarización de Otsu , para que encontrara el mejor valor umbral.

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)

A continuación se muestra el resultado que obtuve:

introduzca la descripción de la imagen aquí

(incluso ese resultado es bueno, porque hay un gran contraste entre las imágenes de primer plano y de fondo)

2 - Ahora tenemos que crear el marcador. El marcador es la imagen con el mismo tamaño como la de la imagen original que es 32SC1 (32 bits firmado solo canal).

Ahora habrá algunas regiones en la imagen original donde simplemente está seguro de que esa parte pertenece al primer plano. Marque dicha región con 255 en la imagen del marcador. Ahora la región donde está seguro de ser el fondo está marcada con 128. La región que no está segura está marcada con 0. Eso es lo que vamos a hacer a continuación.

A-Región de primer plano: - Ya tenemos una imagen de umbral donde las píldoras son de color blanco. Los erosionamos un poco, por lo que estamos seguros de que la región restante pertenece al primer plano.

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

Fg :

introduzca la descripción de la imagen aquí

B-Región de fondo :- Aquí dilatamos la imagen umbral para que la región de fondo se reduzca. Pero estamos seguros de que la región negra restante es 100% fondo. Lo pusimos en 128.

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

Ahora obtenemos bg de la siguiente manera:

introduzca la descripción de la imagen aquí

C - Ahora añadimos fg y bg :

marker = cv2.add(fg,bg)

A continuación se muestra lo que obtenemos:

introduzca la descripción de la imagen aquí

Ahora podemos entender claramente desde la imagen superior, que la región blanca es 100% primer plano, la región gris es 100% fondo, y la región negra no estamos seguros.

Luego lo convertimos en 32SC1:

marker32 = np.int32(marker)

3-Finalmente aplicamos watershed y convertimos el resultado de nuevo en uint8 imagen:

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

M :

introduzca la descripción de la imagen aquí

4 - Nos umbral correctamente para obtener la máscara y realizar bitwise_and con la imagen de entrada:

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

Res :

introduzca la descripción de la imagen aquí

Espero que ayude!!!

ARK

 43
Author: Abid Rahman K,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-08 15:40:00

Prólogo

Estoy interviniendo principalmente porque encontré tanto el tutorial de watershed en la documentación de OpenCV (y el ejemplo de C++ ) como la respuesta de mmgp anterior bastante confusa. Revisé un enfoque decisivo varias veces para finalmente rendirme por frustración. Finalmente me di cuenta de que necesitaba al menos probar este enfoque y verlo en acción. Esto es lo que he llegado con después de ordenar todos los tutoriales que he acércate.

Aparte de ser un novato en visión artificial, la mayoría de mis problemas probablemente tuvieron que ver con mi requisito de usar la biblioteca OpenCVSharp en lugar de Python. C# no tiene operadores de matriz de alta potencia como los que se encuentran en NumPy (aunque me doy cuenta de que esto ha sido portado a través de IronPython), así que tuve problemas para comprender e implementar estas operaciones en C#. Además, para que conste, realmente desprecio los matices y las inconsistencias en la mayoría de estos llamadas a funciones. OpenCVSharp es una de las bibliotecas más frágiles con las que he trabajado. Pero bueno, es un puerto, ¿qué esperaba? Lo mejor de todo, sin embargo it es gratis.

Sin más preámbulos, hablemos de mi implementación OpenCVSharp de la cuenca, y esperemos aclarar algunos de los puntos más pegajosos de la implementación de la cuenca en general.

Solicitud

En primer lugar, asegúrese de que watershed es lo que desea y comprenda su uso. Yo he utilizado placas de células manchadas, como esta:

introduzca la descripción de la imagen aquí

Me tomó un buen tiempo descubrir que no podía hacer una llamada para diferenciar cada célula en el campo. Por el contrario, primero tuve que aislar una porción del campo, luego llamar a watershed en esa pequeña porción. Aislé mi región de interés (ROI) a través de una serie de filtros, que explicaré brevemente aquí:

introduzca la descripción de la imagen aquí

  1. Comience con la imagen de origen (izquierda, recortada para fines de demostración)
  2. Aislar el canal rojo (centro izquierdo)
  3. Aplicar umbral adaptativo (medio derecho)
  4. Encuentra contornos y elimina aquellos con áreas pequeñas (derecha)

Una vez que hayamos limpiado los contornos resultantes de las operaciones de umbral anteriores, es hora de encontrar candidatos para la cuenca hidrográfica. En mi caso, simplemente iteré a través de todos los contornos mayores que un área determinada.

Código

Digamos que hemos aislado esto contorno del campo anterior como nuestro ROI:

introduzca la descripción de la imagen aquí

Echemos un vistazo a cómo codificaremos una cuenca.

Comenzaremos con una estera en blanco y dibujaremos solo el contorno que define nuestro 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);

Para que la llamada watershed funcione, necesitará un par de "pistas" sobre el ROI. Si eres un principiante completo como yo, te recomiendo que eches un vistazo a la página CMM watershed para obtener un primer rápido. Basta con decir que vamos a crear pistas sobre el ROI a la izquierda creando la forma a la derecha:

introduzca la descripción de la imagen aquí

Para crear la parte blanca (o "fondo") de esta forma "indirecta", simplemente Dilate la forma aislada así:

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

Para crear la parte negra en el medio (o" primer plano"), usaremos una transformada de distancia seguida de umbral, que nos lleva de la forma de la izquierda a la forma de la derecha:

introduzca la descripción de la imagen aquí

Esto toma algunos pasos, y usted es posible que tenga que jugar con el límite inferior de su umbral para obtener resultados que funcionen para usted:

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

Luego restaremos estas dos esteras para obtener el resultado final de nuestra forma de "pista":

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

De nuevo, si Cv2.ImShow desconocido , se vería así:

introduzca la descripción de la imagen aquí

Agradable! Esto fue fácil para mí envolver mi cabeza alrededor. La siguiente parte, sin embargo, me dejó bastante perplejo. Veamos cómo convertir nuestra "pista" en algo la función Watershed puede usar. Para esto necesitamos usar ConnectedComponents, que es básicamente una gran matriz de píxeles agrupados por la virtud de su índice. Por ejemplo, si tuviéramos una estera con las letras "HI", ConnectedComponents podría devolver esta matriz:

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

Entonces, 0 es el fondo, 1 es la letra "H", y 2 es la letra "I". (Si llegas a este punto y quieres visualizar tu matriz, te recomiendo revisar esta instructiva respuesta.) Ahora, así es como utilizaremos ConnectedComponents para crear los marcadores (o etiquetas) para cuenca hidrográfica:

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

Tenga en cuenta que la función de cuenca requiere que el área fronteriza esté marcada por 0. Por lo tanto, hemos establecido cualquier píxel de borde a 0 en el array etiqueta/marcador.

En este punto, deberíamos estar listos para llamar Watershed. Sin embargo, en mi aplicación en particular, es útil solo visualizar una pequeña porción de toda la imagen de origen durante esta llamada. Esto puede ser opcional para usted, pero primero solo enmascare un poco de la fuente dilatándola:

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

Y luego haga la llamada mágica:

Cv2.Watershed(sourceCrop, labels);

Resultados

La llamada anterior Watershed modificará labels in place . Usted tendrá que volver a recordar acerca de la matriz resultante de ConnectedComponents. La diferencia aquí es, si watershed encontró alguna presa entre las cuencas hidrográficas, se marcarán como " -1 " en esa matriz. Al igual que el resultado ConnectedComponents, diferentes cuencas se marcarán de una manera similar de aumentar los números. Para mis propósitos, quería guardar estos en separado contornos, así que creé este bucle para dividirlos:

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

    }
}

Entonces, quería imprimir estos contornos con colores aleatorios, así que creé la siguiente estera:{[27]]}

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

Que produce lo siguiente cuando se muestra:

introduzca la descripción de la imagen aquí

Si dibujamos en la imagen de origen las presas que fueron marcadas por a -1 anteriormente, obtenemos esto:

introduzca la descripción de la imagen aquí

Ediciones:

Me olvidé de tener en cuenta: asegúrese de que está limpiando sus esteras después ya terminaste con ellos. Permanecerán en la memoria y OpenCVSharp puede presentar algún mensaje de error ininteligible. Realmente debería usar using arriba, pero mat.Release() también es una opción.

Además, la respuesta de mmgp anterior incluye esta línea: dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8), que es un paso de estiramiento del histograma aplicado a los resultados de la transformada de distancia. Omití este paso por varias razones (principalmente porque no pensé que los histogramas que vi fueran demasiado estrechos para comenzar), pero su kilometraje puede variar.

 1
Author: Daniel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-22 22:13:07