5.26. Búsqueda de líneas y segmentos

Algunas características de la escena no son regiones conexas de color, sino bordes rectos orientados: una línea pintada en el suelo, la junta entre dos superficies, el lado de un rectángulo impreso, el borde de una puerta. Pedirle al detector de manchas que las encuentre es la pregunta equivocada: el borde tiene un píxel de ancho, el algoritmo de manchas busca área con color, y la respuesta llega vacía o con ruido.

El detector adecuado para los bordes orientados es la transformada de líneas de Hough. El módulo image la expone en dos variantes: find_lines() devuelve líneas infinitas (cada línea se extiende por toda la imagen); find_line_segments() devuelve segmentos finitos (cada línea tiene puntos finales dentro del fotograma). Cuál necesita la aplicación depende de si los bordes de interés son continuos a lo largo de todo el fotograma o solo abarcan una parte de él.

5.26.1. Cómo funciona la transformada de Hough

Ambos detectores comparten la misma idea central, así que vale la pena entenderla una vez. El módulo image primero ejecuta un filtro de bordes de tipo Sobel sobre la entrada para puntuar cada píxel según la probabilidad de que se encuentre sobre un borde orientado. Cada uno de estos píxeles de borde luego vota por todas las líneas sobre las que podría encontrarse. Las líneas que reúnen más votos ganan.

Una línea se parametriza en el espacio de Hough mediante dos números: theta, el ángulo de la línea (0 – 179 grados), y rho, la distancia perpendicular desde el origen de la imagen hasta la línea (con signo, en píxeles). Cada línea que contiene la imagen es un punto en el espacio (theta, rho). Cada píxel de borde de la entrada contribuye con un voto a cada combinación (theta, rho) coherente con su posición; conceptualmente, una curva a través del espacio de Hough. Donde se cruzan muchas de estas curvas, muchos píxeles de borde coinciden en la misma línea, y ese cruce es una detección.

El detector devuelve los máximos locales en el espacio de Hough cuyos totales de votos superan un umbral. Cada Line devuelta lleva ambas representaciones: x1, y1, x2, y2 para la forma de puntos finales (recortada a los límites de la imagen en el caso infinito), theta, rho para la forma de Hough, y length y magnitude para el tamaño y el recuento de votos respectivamente.

5.26.2. Líneas infinitas

find_lines() ejecuta la transformada de Hough y devuelve las líneas más fuertes, cada una extendida por toda la imagen:

lines = img.find_lines(threshold=1500, theta_margin=25, rho_margin=25)

for l in lines:
    img.draw_line(l, color=(255, 0, 0))

El threshold es el total mínimo de votos para que una línea sea aceptada. El total de votos suma las magnitudes de borde de Sobel de cada píxel contribuyente, por lo que valores de threshold mayores exigen bordes más largos o más fuertes para pasar; esto hace que el valor adecuado dependa de la resolución de la imagen (una línea más larga a mayor resolución acumula más votos) además de la escena, por lo que debe ajustarse para la aplicación concreta. Como puntos de partida aproximados para ajustar: 1000 para una línea modesta en una imagen clara, 500 o menos para contraste débil o líneas cortas, 2000 o más para escenas abarrotadas donde se forman líneas de falso positivo a través de grupos de ruido de borde.

theta_margin y rho_margin controlan la fusión de máximos cercanos. Un único borde físico produce un pequeño grupo de bins con muchos votos alrededor de su verdadero (theta, rho), y el detector colapsa cada grupo a su pico antes de devolver. theta_margin=25 (grados) fusiona cualquier pico dentro de 25 grados de orientación; rho_margin=25 (píxeles) fusiona picos dentro de 25 píxeles de distancia. Los valores predeterminados son razonables; aumentarlos devuelve menos líneas, más distintas, y reducirlos devuelve más líneas, a veces duplicadas.

x_stride y y_stride recorren los píxeles de borde durante la votación, del mismo modo que recorren los píxeles en find_blobs(). Los valores predeterminados de 2 y 1 funcionan para el caso común; aumentarlos acelera la búsqueda a costa de la resolución. roi restringe la búsqueda a una región del fotograma, lo que tanto reduce las líneas devueltas como disminuye el trabajo.

Cada línea devuelta es dibujable directamente: el objeto Line se pasa tal cual a draw_line(), que lee los campos de puntos finales (x1, y1, x2, y2) del comienzo de él. l.theta es el ángulo en grados, que clasifica la línea como horizontal, vertical o diagonal en una sola comparación. l.magnitude es el total de votos, que ordena las líneas devueltas de la más fuerte a la más débil.

5.26.3. Segmentos de línea

find_lines() es el detector adecuado para bordes que abarcan todo el fotograma, pero muchos bordes reales (el lado izquierdo de un código de barras impreso, el borde superior de una etiqueta, el lado visible de una regla) solo recorren parte de la imagen. find_line_segments() devuelve segmentos finitos cuyos puntos finales están dentro del fotograma:

segments = img.find_line_segments(merge_distance=5, max_theta_difference=10)

for s in segments:
    img.draw_line(s, color=(0, 255, 0))

El detector de segmentos traza directamente a lo largo de los píxeles de borde orientados, en lugar de votar en el espacio de Hough, y el resultado es una colección de tramos rectos cortos. merge_distance establece el hueco máximo en píxeles que dos tramos cortos colineales pueden abarcar y aun así fusionarse en un único segmento devuelto; max_theta_difference establece cuántos grados de orientación tolera el fusionador entre tramos adyacentes. Una fusión generosa (merge_distance=10, max_theta_difference=15) devuelve un número pequeño de segmentos largos a costa de a veces unir bordes genuinamente separados; una fusión estricta (merge_distance=0, max_theta_difference=5) devuelve muchos segmentos cortos y deja que la aplicación los resuelva en Python.

Los objetos resultantes son del mismo tipo Line que devuelve find_lines(), con las mismas propiedades, por lo que una canalización puede procesar cualquiera de los dos tipos de detección a través de la misma ruta de código posterior. La única diferencia práctica es que los puntos finales de los segmentos son los extremos reales de la línea en la imagen, mientras que los puntos finales de las líneas infinitas están donde la línea cruza el borde de la imagen.

5.26.4. Cuándo usar cada uno

La elección entre los dos métodos se reduce a una sola pregunta: ¿le importa a la aplicación dónde se detiene la línea?

find_lines() es la herramienta adecuada cuando la respuesta es no. Un robot seguidor de líneas necesita saber hacia dónde va la línea y dónde cruza la parte inferior del fotograma; la línea en sí se extiende hasta el horizonte y más allá. Un detector de horizonte busca el borde orientado más fuerte de la imagen; no necesita saber dónde termina el horizonte.

find_line_segments() es la herramienta adecuada cuando la respuesta es sí. Identificar los cuatro lados de un rectángulo impreso requiere cuatro segmentos con puntos finales conocidos. Seguir un dedo que apunta a una pantalla implica seguir un segmento corto cuyos puntos finales son la punta y la base del dedo. Medir la longitud de un rasguño visible requiere la extensión real del segmento en píxeles.

Ambos detectores comparten una limitación común: necesitan contraste. El filtro de bordes de Sobel sobre el que se construyen responde a gradientes de brillo; un borde de color contra un fondo igualmente brillante (una línea roja sobre una pared verde de la misma luminancia) no produce gradiente ni detección. Cuando ese caso aparece en la práctica, la solución es extraer un único canal LAB como imagen en escala de grises con el contraste adecuado antes de buscar: to_grayscale() con el canal b seleccionado aísla el rojo contra el verde donde el canal de luminancia por sí solo es plano, y entregar esa imagen de canal al detector de líneas.