¿Cómo definir los marcadores para la cuenca hidrográfica en OpenCV?

Estoy escribiendo para Android con OpenCV. Estoy segmentando una imagen similar a la siguiente utilizando una cuenca hidrográfica controlada, sin que el usuario marque manualmente la imagen. Planeo usar los máximos regionales como marcadores.

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

imagen de entrada

En primer lugar, la función minMaxLoc solo encuentra el máximo global y mínimo global para una entrada determinada, por lo que es prácticamente inútil para determinar los mínimos regionales y / o los máximos regionales. Pero su idea es correcta, extraer marcadores basados ​​en mínimos / máximos regionales para realizar una Transformada de Cuenca basada en marcadores es totalmente correcta. Permítanme intentar aclarar qué es la Transformada de la Cuenca y cómo debe usar correctamente la implementación presente en OpenCV.

Una cantidad decente de documentos que tratan sobre cuencas hidrográficas lo describe de manera similar a lo que sigue (podría pasar por alto algunos detalles, si no está seguro: pregunte). Considere la superficie de alguna región que conozca, contiene valles y picos (entre otros detalles que son irrelevantes para nosotros aquí). Supongamos que debajo de esta superficie todo lo que tiene 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 encontrarán, y cuando esto suceda, se construye una represa tal que no se toquen entre sí. Al final, tienes una colección de presas, que es la cuenca que separa todas las aguas de diferentes colores.

Ahora, si hace demasiados agujeros en esa superficie, terminará con demasiadas regiones: exceso de segmentación. Si hace muy pocos, obtendrá una subsegmentación. Por lo tanto, prácticamente cualquier documento que sugiera el uso de una cuenca hidrográfica presenta técnicas para evitar estos problemas en la aplicación que trata el documento.

Escribí todo esto (que posiblemente sea demasiado ingenuo para cualquiera que sepa lo que es la Transformada de Cuencas hidrográficas) porque se refleja directamente en cómo se deben usar implementaciones de cuencas hidrográficas (lo que la respuesta aceptada actualmente está haciendo de una manera completamente incorrecta). 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 son demasiado cercanos y, en algunos casos, superpuestos. La utilidad de la cuenca aquí es separar correctamente estos objetos, no 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 apertura 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 considera aplicar la transformación de distancia a ella, el resultado es a la derecha.

enter image description hereenter image description here

Con el resultado de la transformación de distancia, podemos considerar un umbral tal que consideremos solo las regiones más distantes al fondo (imagen izquierda a continuación). Al hacer 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 de la 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 izquierda está representada en la imagen de la derecha).

enter image description hereenter image description here

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 hidrográfica construirá represas para impedir que los diferentes “colores” se fusionen. Si hacemos la transformación, obtenemos la imagen de la izquierda. Considerando solo las presas al componerlas con la imagen original, obtenemos el resultado a la derecha.

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) 

Me gustaría explicar un código simple sobre cómo usar la cuenca hidrográfica aquí. Estoy usando OpenCV-Python, pero espero que no tengas ninguna dificultad para entender.

En este código, utilizaré la cuenca hidrográfica como una herramienta para la extracción de fondos en primer plano. (Este ejemplo es la contraparte de Python del código C ++ en el libro de cocina OpenCV). Este es un caso simple para entender la cuenca hidrográfica. Aparte de eso, puede usar la cuenca hidrográfica para contar la cantidad de objetos en esta imagen. Esa será una versión ligeramente avanzada de este código.

1 – Primero cargamos nuestra imagen, la convertimos a escala de grises y la configuramos con un valor adecuado. Tomé la binarización de Otsu , por lo que encontraría el mejor valor de 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) 

Debajo está el resultado que obtuve:

enter image description here

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

2 – Ahora tenemos que crear el marcador. Marker es la imagen con el mismo tamaño que la imagen original que es 32SC1 (canal simple de 32 bits).

Ahora habrá algunas regiones en la imagen original donde simplemente está seguro, esa parte pertenece al primer plano. Marque dicha región con 255 en la imagen del marcador. Ahora la región en la que está seguro que será el fondo está marcada con 128. La región de la que no está seguro 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, de modo que estamos seguros de que la región restante pertenece al primer plano.

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

fg :

enter image description here

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

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

Ahora obtenemos bg de la siguiente manera:

enter image description here

C – Ahora agregamos tanto fg como bg :

 marker = cv2.add(fg,bg) 

Debajo está lo que obtenemos:

enter image description here

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

Luego lo convertimos en 32SC1:

 marker32 = np.int32(marker) 

3 – Finalmente, aplicamos la cuenca hidrográfica y volvemos el resultado a la imagen uint8 :

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

m:

enter image description here

4Lo bitwise_and correctamente para obtener la máscara y lo bitwise_and en modo 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:

enter image description here

¡¡¡Espero eso ayude!!!

ARCA

Prefacio

Me refiero principalmente porque encontré el tutorial de la línea divisoria de aguas en la documentación de OpenCV (y el ejemplo de C ++ ), así como la respuesta de mmgp anterior, para ser bastante confuso. Revisé un enfoque de cuenca varias veces para finalmente rendirme por la frustración. Finalmente me di cuenta de que necesitaba al menos darle una oportunidad a este enfoque y verlo en acción. Esto es lo que se me ocurrió después de ordenar todos los tutoriales que he encontrado.

Además de ser un novicio de visión por computadora, la mayoría de mis problemas probablemente tenían que ver con mi requisito de usar la biblioteca OpenCVSharp en lugar de Python. C # no tiene operadores de array 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 entender e implementar estas operaciones en C #. Además, para el registro, realmente desprecio los matices y las inconsistencias en la mayoría de estas llamadas a funciones. OpenCVSharp es una de las bibliotecas más frágiles con las que he trabajado. Pero bueno, es un puerto, entonces ¿qué esperaba? Pero lo mejor de todo es que es gratis.

Sin más preámbulos, hablemos sobre la implementación de OpenCVSharp de la cuenca y, con suerte, aclaremos algunos de los puntos más complicados de la implementación de cuencas en general.

Solicitud

En primer lugar, asegúrese de que la cuenca hidrográfica sea la que desea y comprenda su uso. Estoy usando placas de células teñidas, como esta:

enter image description here

Me tomó un buen tiempo darme cuenta de que no podía hacer una sola llamada para diferenciar cada célula en el campo. Por el contrario, primero tuve que aislar una parte del campo y luego llamar cuenca 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í:

enter image description here

  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 (centro derecho)
  4. Encuentre contornos y luego elimine aquellos con áreas pequeñas (derecha)

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

Código

Supongamos que hemos aislado este contorno del campo anterior como nuestro ROI:

enter image description here

Echemos un vistazo a cómo codificaremos una línea divisoria de aguas.

Comenzaremos con una alfombra 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> { contour }, -1, new Scalar(255, 255, 255), -1); 

Para que la llamada a la cuenca funcione, necesitará un par de “sugerencias” sobre el ROI. Si eres un principiante como yo, te recomiendo que visites la página de la cuenca del CMM para obtener una guía rápida. Baste decir que vamos a crear sugerencias sobre el ROI a la izquierda creando la forma a la derecha:

enter image description here

Para crear la parte blanca (o “fondo”) de esta forma de “sugerencia”, simplemente Dilate la forma aislada de la siguiente manera:

 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 transformación de distancia seguida de un umbral, que nos lleva de la forma de la izquierda a la de la derecha:

enter image description here

Esto requiere unos pocos pasos, y es posible que deba 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 “sugerencia”:

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

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

enter image description here

¡Bonito! Esto fue fácil para mí envolver mi cabeza. La siguiente parte, sin embargo, me dejó bastante perplejo. Veamos cómo podemos convertir nuestra “pista” en algo que la función de Watershed pueda 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 carpeta 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 llega a este punto y desea visualizar su matriz, le recomiendo que revise esta respuesta instructiva ). Ahora, así es como utilizaremos ConnectedComponents para crear los marcadores (o tags) para la 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(y, x); //note: x and y are inexplicably var borderPixel = (int)unknown.At(y, x); //and infuriatingly reversed if (borderPixel == 255) labels.Set(y, x, 0); } } 

Tenga en cuenta que la función Cuenca requiere que el área del borde esté marcada con 0. Por lo tanto, hemos establecido los píxeles del borde en 0 en la matriz etiqueta / marcador.

En este punto, deberíamos estar listos para llamar a Watershed . Sin embargo, en mi aplicación particular, es útil simplemente visualizar una pequeña porción de toda la imagen de origen durante esta llamada. Esto puede ser opcional para ti, pero primero solo enmascaré 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 haz la llamada mágica:

 Cv2.Watershed(sourceCrop, labels); 

Resultados

La llamada de Watershed anterior modificará las labels en su lugar . Tendrá que volver a recordar acerca de la matriz resultante de ConnectedComponents . La diferencia aquí es que si la cuenca encontrara represas entre las cuencas hidrográficas, se marcarán como “-1” en esa matriz. Al igual que el resultado de ConnectedComponents , las diferentes cuencas hidrográficas se marcarán de una manera similar en la que se incrementarán los números. Para mis propósitos, quería almacenarlos en contornos separados, así que creé este ciclo para dividirlos:

 var watershedContours = new List>>(); for (int x = 0; x < labels.Width; x++) { for (int y = 0; y < labels.Height; y++) { var labelPixel = labels.At(y, x); //note: x, y switched var connected = watershedContours.Where(t => t.Item1 == labelPixel).FirstOrDefault(); if (connected == null) { connected = new Tuple>(labelPixel, new List()); watershedContours.Add(connected); } connected.Item2.Add(new Point(x, y)); if (labelPixel == -1) sourceCrop.Set(y, x, new Vec3b(0, 255, 255)); } } 

Luego, quería imprimir estos contornos con colores aleatorios, así que creé la siguiente estera:

 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:

enter image description here

Si dibujamos en la imagen de origen las represas marcadas con -1 antes, obtenemos esto:

enter image description here

Ediciones:

Olvidé tomar nota: asegúrate de que estás limpiando tus esteras una vez que hayas terminado con ellas. Permanecerán en la memoria y OpenCVSharp puede presentar un mensaje de error ininteligible. Realmente debería usar el using anterior, pero mat.Release() 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 histogtwig paso de estiramiento aplicado a los resultados de la transformación de distancia. He omitido este paso por varias razones (sobre todo porque no creía que los histogtwigs que vi fueran demasiado estrechos para empezar), pero su kilometraje puede variar.