OpenCV

Tutorial OpenCV: Detección de bordes y contornos

OpenCV: Guía completa para implementar detección de bordes con Canny. Aprende a ajustar parámetros y visualizar resultados precisos en Python.

Aprende OpenCV GRATIS y certifícate

Detección de bordes con canny

La detección de bordes es una técnica fundamental en el procesamiento de imágenes, ya que permite identificar las regiones donde hay cambios bruscos de intensidad. El algoritmo de Canny es uno de los métodos más utilizados debido a su capacidad para detectar bordes con precisión y reducir el ruido.

El algoritmo de Canny sigue una serie de pasos para identificar los bordes más significativos:

  1. Suavizado con filtro Gaussiano: se aplica un filtro Gaussiano para reducir el ruido y suavizar la imagen.
  2. Cálculo del gradiente de intensidad: se calculan la magnitud y dirección del gradiente utilizando operadores como Sobel.
  3. Supresión de no máximos: se eliminan los píxeles que no representan un máximo local en la dirección del gradiente, afinando así los bordes.
  4. Umbralización con histéresis: se utilizan dos umbrales, uno alto y otro bajo, para definir bordes fuertes y débiles, conectando los débiles a los fuertes si están próximos.

A continuación, se muestra cómo implementar la detección de bordes con Canny utilizando OpenCV en Python:

import cv2
import numpy as np

# Cargar la imagen en escala de grises
imagen = cv2.imread('ruta_a_la_imagen.jpg', cv2.IMREAD_GRAYSCALE)

# Aplicar el algoritmo de Canny
bordes = cv2.Canny(imagen, 50, 150)

# Mostrar la imagen original y los bordes detectados
cv2.imshow('Imagen Original', imagen)
cv2.imshow('Bordes Detectados', bordes)
cv2.waitKey(0)
cv2.destroyAllWindows()

En este código, se utiliza la función cv2.Canny() de OpenCV, que recibe la imagen de entrada y dos umbrales: 50 y 150. Estos valores determinan los límites para clasificar un píxel como borde fuerte, borde débil o no borde. Es crucial ajustar estos umbrales para obtener resultados óptimos en la detección de bordes.

Para mejorar la precisión, es común aplicar un suavizado previo utilizando un filtro Gaussiano:

# Suavizar la imagen con un filtro Gaussiano
imagen_suavizada = cv2.GaussianBlur(imagen, (5, 5), 1.4)

# Aplicar Canny a la imagen suavizada
bordes = cv2.Canny(imagen_suavizada, 50, 150)

El filtro Gaussiano reduce el impacto del ruido y las variaciones bruscas no deseadas, lo que facilita la detección de bordes reales. El parámetro (5, 5) define el tamaño del kernel, y 1.4 es la desviación estándar de la distribución Gaussiana.

Es posible ajustar más parámetros en la función cv2.Canny() para personalizar el proceso:

bordes = cv2.Canny(imagen, 50, 150, apertureSize=3, L2gradient=True)
  • apertureSize=3 especifica el tamaño del kernel utilizado en el cálculo del gradiente de Sobel.
  • L2gradient=True indica que se debe utilizar la norma L2 para calcular la magnitud del gradiente, proporcionando una mayor precisión en la detección de bordes.

Comprender cómo los parámetros afectan el resultado es esencial. Un umbral bajo elevado puede perder bordes débiles, mientras que un umbral alto bajo puede incluir demasiado ruido. El suavizado excesivo puede difuminar detalles importantes, pero insuficiente puede dejar pasar ruido. Se recomienda experimentar con diferentes valores y analizar su impacto.

La detección de bordes con Canny es ampliamente utilizada en aplicaciones como la segmentación de objetos, reconocimiento de formas y visión por computadora. Su implementación en OpenCV facilita su integración en proyectos y su combinación con otras técnicas de procesamiento de imágenes.

Encontrar y dibujar contornos

La identificación y representación de contornos es fundamental en el análisis de imágenes, ya que permite delimitar formas y objetos dentro de una imagen. OpenCV proporciona funciones eficaces para encontrar y dibujar contornos basados en binarizaciones o detecciones previas.

Para encontrar contornos en una imagen, se utiliza la función cv2.findContours(). Esta función analiza la imagen binaria y devuelve una lista de contornos encontrados y su jerarquía.

import cv2
import numpy as np

# Cargar la imagen en escala de grises
imagen = cv2.imread('ruta_a_la_imagen.jpg', cv2.IMREAD_GRAYSCALE)

# Aplicar un umbral para binarizar la imagen
_, umbral = cv2.threshold(imagen, 127, 255, cv2.THRESH_BINARY)

# Encontrar contornos en la imagen umbralizada
contornos, jerarquia = cv2.findContours(umbral, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

En este código, la función cv2.threshold() binariza la imagen, esencial para la correcta detección de contornos. La función cv2.findContours() devuelve dos elementos:

  • contornos: Una lista de arrays de puntos que representan cada contorno encontrado.
  • jerarquia: Una matriz que describe la relación entre los contornos (padre-hijo).

Los parámetros principales de cv2.findContours() son:

  • La imagen binaria de entrada.
  • cv2.RETR_TREE: Modo de recuperación de contornos que construye una jerarquía completa.
  • cv2.CHAIN_APPROX_SIMPLE: Método de aproximación de contornos que reduce los puntos redundantes, almacenando solo los esenciales.

Para dibujar los contornos sobre la imagen original, se utiliza la función cv2.drawContours().

# Crear una copia de la imagen original en color
imagen_color = cv2.cvtColor(imagen, cv2.COLOR_GRAY2BGR)

# Dibujar todos los contornos en la imagen
cv2.drawContours(imagen_color, contornos, -1, (0, 255, 0), 2)

# Mostrar la imagen con los contornos dibujados
cv2.imshow('Contornos', imagen_color)
cv2.waitKey(0)
cv2.destroyAllWindows()

En este ejemplo, se dibujan todos los contornos sobre la imagen en color con un grosor de 2 píxeles y color verde (0, 255, 0). El parámetro -1 indica que se deben dibujar todos los contornos. Es importante convertir la imagen a color con cv2.cvtColor() para visualizar correctamente los contornos en color.

Para acceder y manipular contornos específicos, es posible iterar sobre la lista de contornos:

for i, contorno in enumerate(contornos):
    # Calcular el perímetro del contorno
    perimetro = cv2.arcLength(contorno, True)
    
    # Calcular el área del contorno
    area = cv2.contourArea(contorno)
    
    # Dibujar contorno individual si cumple cierta condición
    if area > 100:
        cv2.drawContours(imagen_color, [contorno], 0, (0, 0, 255), 2)

En este caso, se dibujan en rojo los contornos cuya área es mayor a 100 píxeles cuadrados. Las funciones cv2.arcLength() y cv2.contourArea() permiten calcular propiedades geométricas de los contornos.

Además, se pueden obtener aproximaciones poligonales de los contornos para simplificar su forma:

# Aproximar el contorno a un polígono
epsilon = 0.01 * cv2.arcLength(contorno, True)
aproximacion = cv2.approxPolyDP(contorno, epsilon, True)

# Dibujar el contorno aproximado
cv2.drawContours(imagen_color, [aproximacion], 0, (255, 0, 0), 2)

La función cv2.approxPolyDP() aproxima un contorno a una forma con menos vértices, basado en la precisión establecida por epsilon. Esto es útil para identificar formas geométricas específicas, como triángulos o rectángulos.

También es posible extraer información adicional de los contornos, como sus momentos para calcular el centroide:

# Calcular los momentos del contorno
M = cv2.moments(contorno)

# Calcular las coordenadas del centroide
if M['m00'] != 0:
    cx = int(M['m10'] / M['m00'])
    cy = int(M['m01'] / M['m00'])
    
    # Dibujar el centroide en la imagen
    cv2.circle(imagen_color, (cx, cy), 5, (0, 255, 255), -1)

Los momentos proporcionan medidas estadísticas del contorno que permiten calcular características como el centroide, área y orientación. Este ejemplo dibuja un círculo amarillo en el centroide de cada contorno.

Para mejorar la detección de contornos, es recomendable preprocesar la imagen con técnicas como suavizado, umbral adaptativo o detección de bordes.

# Aplicar suavizado Gaussiano
imagen_suavizada = cv2.GaussianBlur(imagen, (5, 5), 0)

# Aplicar detección de bordes con Canny
edges = cv2.Canny(imagen_suavizada, 50, 150)

# Encontrar contornos a partir de los bordes detectados
contornos, jerarquia = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

Al utilizar cv2.RETR_EXTERNAL, solo se recuperan los contornos externos, lo cual puede ser adecuado dependiendo de la aplicación.

Comprender cómo funcionan y se manipulan los contornos es esencial en tareas como segmentación, reconocimiento de objetos y análisis de formas. OpenCV ofrece herramientas poderosas para trabajar con contornos.

Jerarquía y propiedades de contornos

La jerarquía de contornos es un concepto crucial en el análisis de imágenes, ya que permite entender la relación entre diferentes contornos dentro de una imagen. OpenCV proporciona mecanismos para representar y manejar esta jerarquía, facilitando el procesamiento de imágenes complejas con múltiples objetos y niveles de anidación.

Cuando se detectan contornos utilizando la función cv2.findContours(), además de la lista de contornos, se obtiene información sobre la jerarquía que describe cómo estos contornos están relacionados entre sí. Esta jerarquía se representa mediante una matriz que contiene cuatro valores por cada contorno:

  1. Siguiente contorno (Next): Índice del contorno siguiente en el mismo nivel jerárquico.
  2. Contorno previo (Previous): Índice del contorno anterior en el mismo nivel.
  3. Primer hijo (First Child): Índice del primer contorno hijo anidado.
  4. Padre (Parent): Índice del contorno padre que contiene al contorno actual.

La estructura de la jerarquía permite navegar y procesar los contornos según sus relaciones de contención. Esto es especialmente útil en imágenes donde los objetos están anidados o cuando se necesita diferenciar entre contornos externos e internos.

Modos de recuperación de contornos

OpenCV ofrece diferentes modos de recuperación de contornos que afectan cómo se construye la jerarquía:

  • cv2.RETR_EXTERNAL: Recupera solo los contornos externos, ignorando los contornos anidados.
  • cv2.RETR_LIST: Recupera todos los contornos sin establecer ninguna jerarquía.
  • cv2.RETR_CCOMP: Organiza los contornos en dos niveles: externos e internos.
  • cv2.RETR_TREE: Recupera todos los contornos y construye una jerarquía completa, incluyendo todas las relaciones de anidación.

Por ejemplo, al utilizar cv2.RETR_TREE, se obtiene la jerarquía más detallada:

contornos, jerarquia = cv2.findContours(umbral, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

La variable jerarquia es un array de NumPy con forma (1, N, 4), donde N es el número de contornos detectados. Para acceder a la información de jerarquía de un contorno específico:

# Información de jerarquía del contorno i
[Next, Previous, First, Parent] = jerarquia[0][i]

Interpretación de la jerarquía

Comprender la jerarquía de contornos permite realizar operaciones selectivas sobre contornos de interés. Por ejemplo, para identificar los contornos externos y sus agujeros internos:

for i, contorno in enumerate(contornos):
    # Si el contorno no tiene padre, es un contorno externo
    if jerarquia[0][i][3] == -1:
        cv2.drawContours(imagen_color, contornos, i, (0, 255, 0), 2)
    # Si el contorno tiene padre, es un agujero o contorno interno
    else:
        cv2.drawContours(imagen_color, contornos, i, (0, 0, 255), 2)

En este código, los contornos externos se dibujan en verde y los internos en rojo. El valor -1 en la posición del padre indica que el contorno no tiene ningún contorno que lo contenga.

Propiedades geométricas de los contornos

Además de la jerarquía, es esencial analizar las propiedades geométricas de los contornos para extraer características relevantes de los objetos en la imagen. Algunas de las propiedades más comunes incluyen:

  • Momentos: Permiten calcular características como el centroide, área y orientación del contorno.
  • Área: Se calcula con cv2.contourArea(contorno) y representa el número de píxeles dentro del contorno.
  • Perímetro: Utilizando cv2.arcLength(contorno, True), se obtiene la longitud del contorno.
  • Rectángulo Delimitador: Encierra el contorno en un rectángulo, facilitando su localización.

Cálculo de momentos y centroide

Los momentos son medidas fundamentales en el análisis de formas. Para calcularlos:

M = cv2.moments(contorno)
if M['m00'] != 0:
    centro_x = int(M['m10'] / M['m00'])
    centro_y = int(M['m01'] / M['m00'])
    # Dibujar el centroide
    cv2.circle(imagen_color, (centro_x, centro_y), 5, (255, 0, 0), -1)

El centroide se utiliza a menudo en el seguimiento de objetos y en la alineación de formas.

Rectángulos delimitadores

Existen dos tipos principales de rectángulos delimitadores:

  1. Rectángulo envolvente recto: Alineado con los ejes X e Y.
   x, y, w, h = cv2.boundingRect(contorno)
   cv2.rectangle(imagen_color, (x, y), (x + w, y + h), (0, 255, 255), 2)
  1. Rectángulo envolvente rotado: Mínimo área, puede estar rotado.
   rect = cv2.minAreaRect(contorno)
   box = cv2.boxPoints(rect)
   box = np.int_(box)
   cv2.drawContours(imagen_color, [box], 0, (255, 0, 255), 2)

El rectángulo rotado es útil cuando se trabaja con objetos inclinados o para calcular la orientación.

Aproximación de contorno y forma convexa

La aproximación poligonal simplifica el contorno manteniendo su forma general:

epsilon = 0.02 * cv2.arcLength(contorno, True)
aproximacion = cv2.approxPolyDP(contorno, epsilon, True)
cv2.drawContours(imagen_color, [aproximacion], 0, (0, 165, 255), 2)

La envolvente convexa es el polígono convexo más pequeño que encierra al contorno:

hull = cv2.convexHull(contorno)
cv2.drawContours(imagen_color, [hull], 0, (127, 127, 127), 2)

Estas técnicas son esenciales para el reconocimiento de formas y la detección de defectos en objetos.

Identificación de formas y medidas avanzadas

Analizando el número de vértices de la aproximación poligonal, es posible identificar formas geométricas:

if len(aproximacion) == 3:
    forma = "Triángulo"
elif len(aproximacion) == 4:
    forma = "Cuadrilátero"
elif len(aproximacion) > 4:
    forma = "Círculo"
cv2.putText(imagen_color, forma, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

Además, se pueden calcular otras propiedades como:

  • Extensión: Relación entre el área del contorno y el área de su rectángulo delimitador.
  area = cv2.contourArea(contorno)
  area_rect = w * h
  extension = area / area_rect
  • Solidez: Relación entre el área del contorno y el área de su envolvente convexa.
  area_hull = cv2.contourArea(hull)
  solidez = area / area_hull

Estas medidas son útiles para diferenciar entre formas similares y analizar la morfología de los objetos.

Aplicación de máscaras basadas en contornos

Es posible crear máscaras para aislar regiones de interés utilizando los contornos:

# Crear una imagen en negro con las mismas dimensiones
mascara = np.zeros_like(imagen)

# Rellenar el contorno deseado en la máscara
cv2.drawContours(mascara, [contorno], -1, 255, -1)

# Aplicar la máscara a la imagen original
imagen_segmentada = cv2.bitwise_and(imagen, imagen, mask=mascara)

Esta técnica permite realizar operaciones como la segmentación y el análisis específico de regiones.

Uso de la jerarquía en procesamiento avanzado

La jerarquía de contornos es especialmente relevante en aplicaciones donde es necesario distinguir entre objetos externos e internos, como en la detección de objetos con agujeros o en textos impresos.

Por ejemplo, para contar cuántos objetos tienen agujeros:

contador_objetos_con_agujeros = 0
for i, contorno in enumerate(contornos):
    hijo = jerarquia[0][i][2]
    if hijo != -1:
        contador_objetos_con_agujeros += 1

Aquí se incrementa el contador si el contorno actual tiene un hijo, indicando la presencia de un agujero interno.

Consejos para trabajar con contornos y jerarquía

  • Preprocesamiento adecuado: Una buena binarización y reducción de ruido mejoran la precisión en la detección de contornos.
  • Selección del modo de recuperación: Elegir el modo adecuado (RETR_TREE, RETR_EXTERNAL, etc.) según la necesidad de la aplicación.
  • Validación de contornos: Filtrar contornos por área, forma o relación de aspecto para descartar detecciones no deseadas.
  • Visualización efectiva: Utilizar distintos colores y grosores al dibujar contornos para facilitar su análisis.

Comprender y utilizar la jerarquía y propiedades de contornos en OpenCV permite realizar análisis detallados y eficientes en el procesamiento de imágenes. Estas herramientas son fundamentales en campos como la visión por computadora, robótica y análisis biomédico, donde la precisión y robustez del procesamiento de imágenes son esenciales.

Aplicaciones en segmentación

La segmentación de imágenes es un proceso esencial en visión por computadora que consiste en dividir una imagen en regiones significativas para facilitar su análisis. La detección de bordes y contornos juega un papel fundamental en este proceso, permitiendo identificar y separar objetos del fondo de manera efectiva.

Una aplicación práctica es la extracción de objetos específicos utilizando contornos detectados. Al identificar los contornos de interés, es posible crear máscaras que segmenten los objetos deseados para posteriores análisis o procesamiento.

Por ejemplo, para segmentar monedas en una imagen, se pueden seguir los siguientes pasos:

import cv2
import numpy as np

# Cargar la imagen en color
imagen = cv2.imread('ruta_de_la_imagen.jpg')

# Convertir a escala de grises y aplicar desenfoque Gaussiano
gris = cv2.cvtColor(imagen, cv2.COLOR_BGR2GRAY)
gris = cv2.GaussianBlur(gris, (5, 5), 0)

# Aplicar umbralización inversa para binarizar la imagen
_, umbral = cv2.threshold(gris, 127, 255, cv2.THRESH_BINARY_INV)

# Encontrar contornos externos
contornos, jerarquia = cv2.findContours(umbral, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# Crear una máscara vacía
mascara = np.zeros_like(gris)

# Dibujar y rellenar los contornos en la máscara
cv2.drawContours(mascara, contornos, -1, 255, -1)

# Aplicar la máscara a la imagen original
resultado = cv2.bitwise_and(imagen, imagen, mask=mascara)

# Mostrar la imagen segmentada
cv2.imshow('Monedas Segmentadas', resultado)
cv2.waitKey(0)
cv2.destroyAllWindows()

En este ejemplo, se utiliza la máscara creada a partir de los contornos para aislar partes de la imagen. La función cv2.bitwise_and() permite aplicar la máscara y obtener solo las regiones de interés de la imagen original.

La segmentación basada en contornos es especialmente útil cuando los objetos tienen bordes bien definidos y contrastan con el fondo. Sin embargo, en imágenes con iluminación variable o ruido, es recomendable aplicar preprocesamientos adicionales como la binarización adaptativa.

# Aplicar binarización adaptativa
umbral_adaptativo = cv2.adaptiveThreshold(gris, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 2)

Los operadores morfológicos también mejoran la segmentación al eliminar imperfecciones y conectar regiones disjuntas. Operaciones como la apertura y el cierre refinan el resultado al aplicar erosión y dilatación.

# Definir elemento estructurante
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))

# Aplicar operación de cierre
umbral_morfo = cv2.morphologyEx(umbral_adaptativo, cv2.MORPH_CLOSE, kernel, iterations=2)

Una vez refinada la máscara, se pueden extraer propiedades de cada objeto segmentado, como el área o el perímetro, y utilizarlas para filtrar objetos no deseados.

# Filtrar contornos por área mínima
area_minima = 1000
nueva_mascara = np.zeros_like(gris)

for contorno in contornos:
    area = cv2.contourArea(contorno)
    if area > area_minima:
        cv2.drawContours(nueva_mascara, [contorno], -1, 255, -1)

# Aplicar la nueva máscara filtrada
objetos_segmentados = cv2.bitwise_and(imagen, imagen, mask=nueva_mascara)

La segmentación de objetos permite no solo aislar elementos sino también contar cuántos objetos hay en una imagen, una tarea común en aplicaciones industriales y biomédicas.

# Contar el número de objetos segmentados
numero_objetos = len(contornos)
print(f"Número de objetos detectados: {numero_objetos}")

Además, la segmentación es fundamental en el seguimiento de objetos en secuencias de imágenes o videos, donde es necesario identificar y rastrear elementos a lo largo del tiempo.

La técnica de watershed es otra aplicación avanzada en segmentación que utiliza la transformación de distancias y jerarquía de contornos para separar objetos pegados entre sí.

# Aplicar la transformación de distancia
distancia = cv2.distanceTransform(umbral_morfo, cv2.DIST_L2, 5)

# Normalizar y umbralizar la distancia
_, marcos = cv2.threshold(distancia, 0.7 * distancia.max(), 255, 0)
marcos = np.uint8(marcos)

# Encontrar marcadores para watershed
ret, marcadores = cv2.connectedComponents(marcos)

# Aplicar watershed
marcadores = marcadores + 1
marcadores[umbral_morfo == 0] = 0
marcadores = cv2.watershed(imagen, marcadores)

# Marcar bordes detectados por watershed
imagen[marcadores == -1] = [0, 0, 255]

La segmentación por watershed es efectiva para separar objetos que se tocan y no pueden ser separados por simples contornos. Esta técnica requiere un preprocesamiento cuidadoso y es más compleja, pero ofrece resultados precisos en escenarios complicados.

La combinación de detección de bordes, operaciones morfológicas y algoritmos avanzados permite abordar problemas de segmentación en diversas aplicaciones, desde el reconocimiento de patrones hasta la inspección automática.

Aprende OpenCV GRATIS online

Todas las lecciones de OpenCV

Accede a todas las lecciones de OpenCV y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a OpenCV y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  1. Aplicar el filtro Gaussiano para suavizar imágenes.
  2. Calcular el gradiente de intensidad con operadores como Sobel.
  3. Ejecutar la supresión de no máximos para refinar bordes.
  4. Implementar la umbralización con histéresis para clasificar bordes.
  5. Ajustar parámetros de Canny para mejorar la detección de bordes.
  6. Usar OpenCV para visualizar y guardar resultados.