? MIDIENDO la DISTANCIA entre 2 objetos | Python – OpenCV

Por Administrador

Si necesitáramos medir una distancia pequeña entre dos objetos (círculos verdes), podríamos simplemente tomar una regla y usar esta para determinar dicha distancia, pero en el tutorial de hoy me he planteado medir la distancia en centímetros que separa a dos objetos en el eje x, mediante técnicas de visión por computador, para ello como en previos tutoriales usaremos OpenCV con Python. 

Para este proceso usaremos un área de referencia dada por una hoja blanca A4, por lo que para alinearla usaremos el código de los post anteriores: Transformación de perspectiva | OpenCV con Python? Escáner de documentos + ? reconocimiento de texto (OCR) ?| OpenCV con Python.

CONTENIDO

  • Extraer la región de interés (hoja A4)
  • Detección de los círculos verdes
  • Calculando la distancia entre dos objetos
  • Pruebas de funcionamiento

Extraer la región de interés (hoja A4)

Figura 1: Captura del video de entrada.

Dado que vamos a tomar como referencia a una imagen correspondiente a una hoja blanca A4, procederemos a extraerla mediante transformación de perspectiva. Para ello usaremos parte del código el tutorial anterior, por lo que te recomiendo darle un vistazo.

Creamos un script llamado distancia_dos_objetos.py, y continuamos con el código:

import cv2
import numpy as np
import imutils

def ordenar_puntos(puntos):
    n_puntos = np.concatenate([puntos[0], puntos[1], puntos[2], puntos[3]]).tolist()

    y_order = sorted(n_puntos, key=lambda n_puntos: n_puntos[1])

    x1_order = y_order[:2]
    x1_order = sorted(x1_order, key=lambda x1_order: x1_order[0])

    x2_order = y_order[2:4]
    x2_order = sorted(x2_order, key=lambda x2_order: x2_order[0])
    
    return [x1_order[0], x1_order[1], x2_order[0], x2_order[1]]

def roi(image, ancho, alto):
    imagen_alineada = None

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, th = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    cnts = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
    cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:1]

    for c in cnts:
        epsilon = 0.01*cv2.arcLength(c,True)
        approx = cv2.approxPolyDP(c,epsilon,True)
        
        if len(approx) == 4:
            puntos = ordenar_puntos(approx)			
            pts1 = np.float32(puntos)
            pts2 = np.float32([[0,0], [ancho,0], [0,alto], [ancho,alto]])
            M = cv2.getPerspectiveTransform(pts1, pts2)
            imagen_alineada = cv2.warpPerspective(image, M, (ancho,alto))

    return imagen_alineada

Línea 1 a 3: Importamos los paquetes necesarios openCV, numpy e imutils.

Línea 5 a 16: Esta función es la misma que habíamos usado en el tutorial anterior (http://omes-va.com/escaner-de-documentos-reconocimiento-de-texto/). Nos servirá para ordenar los 4 puntos de la hoja en: vértice superior izquierdo, superior derecho, inferior izquierdo e inferior derecho.

Línea 18 a 37: La segunda función que vamos a usar es una llamada roi, que nos servirá para obtener nuestra región de interés, en este caso la imagen correspondiente a la hoja A4 alineada. Este proceso también lo usamos en el tutorial anterior.

Los parámetros de esta función son: imagen de entrada, así como el ancho y alto que tendrá la imagen de salida.

Para discriminar la hoja A4 dentro de la imagen de entrada, transformaremos esta en escala de grises, y luego aplicaremos umbralización siemple, de este modo podremos obtener una imagen binaria (th) en la que el área en blanco repesentará a la hoja A4. A continuación aplicaremos cv2.findContours y luego seleccionamos el contorno más grande (aquí estoy asumiendo que el área más grande corresponderá a la hoja A4).

Figura 2: Visualización de th.

Finalmente controlamos que el contorno tenga 4 vértices y aplicamos la transformación de perspectiva, de tal modo que obtendremos una nueva imagen con un ancho y alto especificados por los parámetros que pide la función.

Antes de pasar con la siguiente parte del programa, debemos tener en cuenta el ancho y alto en centímetros de la hoja A4 . Esta posee 29,7cm (297mm) de alto y 21 cm (210mm) de ancho.

Figura 3: Medidas de una hoja formato A4

Si calculamos su relación de aspecto (aspect ratio), obtendríamos lo siguiente:

Figura 4: Cálculo de la relación de aspecto de la hoja A4.

El resultado de la relación de aspecto nos servirá mucho para establecer las medidas de la hoja A4 pero en pixeles. Por ejemplo quisiera obtener una imagen resultante con 720 pixeles de ancho, podría tomar el aspect ratio y obtener el alto correspondiente. ¡Hagamos los cálculos!.

Figura 5: Cálculo del alto de la imagen resultante a partir de la relación de aspecto de la hoja A4.

De este modo podemos establecer que la imagen de la hoja A4 tendrá 720 pixeles de ancho y 509 de alto. Entonces continuemos con con la programación:

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
#cap = cv2.VideoCapture('Video.mp4')

while True:

    ret, frame = cap.read()
    
    if ret == False: break
    #frame = imutils.resize(frame, width=720)
    imagen_A4 = roi(frame, ancho=1080, alto=509)

Línea 40 y 41: Con la línea 40 realizaremos un videostreaming, mientras que con la línea 41 podremos leer un video en específico.

Línea 45 a 49: Leemos los fotogramas y llamamos a la función roi previamente creada, en ella especificamos como primer argumento a frame, luego un ancho de 720 y alto de 509, como habíamos obtenidos en los cálculos anteriores.

Cabe aclarar que he establecido el valor de 720 y a través de la relación de aspecto pude establecer como alto a 509 pixeles, pero podrías establecer otros.

NOTA: La línea 48 puedes descomentarla en caso de que el video que estés leyendo sea muy grande. Claro que también podrías omitir esta línea o cambiar el valor de redimensionamiento.

Figura 6: Aplicación de la transformación de perspectiva sobre la hoja A4.

Detección de los círculos verdes

if imagen_A4 is not None:
    puntos = []
    imagenHSV = cv2.cvtColor(imagen_A4, cv2.COLOR_BGR2HSV)
    verdeBajo = np.array([36, 14, 0], np.uint8)
    verdeAlto = np.array([56, 120, 255], np.uint8)
    maskVerde = cv2.inRange(imagenHSV, verdeBajo, verdeAlto)

    cnts = cv2.findContours(maskVerde, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
    cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:2]

    for c in cnts:
        x, y, w, h = cv2.boundingRect(c)
        cv2.rectangle(imagen_A4, (x, y), (x+w, y+h), (255, 0, 0), 2)
        puntos.append([x, y, w, h])

Línea 51 a 56: Si existe una imagen almacenada en imagen_A4, entonces continuamos con la condición.

Declaramos un array vacío llamado puntos en el que más tarde almacenaremos los puntos correspondientes a cada círculo verde. Luego transformamos imagen_A4 de BGR a HSV, establecemos los límites bajo y alto del color verde en el formato de color HSV y luego en la línea 56 obtenemos una imagen binaria cuya región en blanco representará la presencia del color verde, mientras en negro la no presencia del mismo.

Figura 7: Detección del color verde de los círculos.

Línea 58 y 59: Buscamos los contornos presentes en maskVerde y luego seleccionamos los dos contornos más grandes (estoy asumiendo que estos contornos corresponden a los círculos verces).

Línea 61 a 64: Continuamos analizando los dos contornos seleccionados, y de cada uno de ellos extraemos las coordenadas x, y ancho y alto del contorno con ayuda de cv2.boundingRect.  Estos datos nos permitirán dibujar un rectángulo que rodeará a cada contorno. En la línea 64 almacenaremos los puntos encontrados (x, y ancho y alto).

Figura 8: Visualización de los círculos verdes detectados.

Calculando la distancia entre dos objetos

if len(puntos) == 2:
    x1, y1, w1, h1 = puntos[0]
    x2, y2, w2, h2 = puntos[1]
    
    if x1 < x2:
        distancia_pixeles = abs(x2 - (x1+w1)) 
        distancia_cm = (distancia_pixeles*29.7)/720
        cv2.putText(imagen_A4, "{:.2f} cm".format(distancia_cm), (x1+w1+distancia_pixeles//2, y1-30), 2, 0.8, (0,0,255), 1, cv2.LINE_AA)
        cv2.line(imagen_A4,(x1+w1,y1-20),(x2, y1-20),(0, 0, 255),2)
        cv2.line(imagen_A4,(x1+w1,y1-30),(x1+w1, y1-10),(0, 0, 255),2)
        cv2.line(imagen_A4,(x2,y1-30),(x2, y1-10),(0, 0, 255),2)
    else:
        distancia_pixeles = abs(x1 - (x2+w2))
        distancia_cm = (distancia_pixeles*29.7)/720
        cv2.putText(imagen_A4, "{:.2f} cm".format(distancia_cm), (x2+w2+distancia_pixeles//2, y2-30), 2, 0.8, (0,0,255), 1, cv2.LINE_AA)
        cv2.line(imagen_A4,(x2+w2,y2-20),(x1, y2-20),(0, 0, 255),2)
        cv2.line(imagen_A4,(x2+w2,y2-30),(x2+w2, y2-10),(0, 0, 255),2)
        cv2.line(imagen_A4,(x1,y2-30),(x1, y2-10),(0, 0, 255),2)

Línea 66 a 68: Vamos a verificar que tengamos 2 arrays con la información de los dos círculos verdes, si es así desempaquetamos estos puntos.

Para poder obtener la distancia en pixeles y luego trasformarla en distancia en centímetros vamos a necesitar conocer el objeto que se encuentre a la izquierda y el que esté a la derecha, para de este modo saber cuanto los separa.

Ya que no sabemos cual de ellos está a la izquierda o derecha, vamos a comparar sus coordenadas en el eje x.

Línea 70 a 76: En esta condición tenemos que el contorno con x1 es menor, es decir está a la izquierda, mientras que a x2 a la derecha. Entonces procedemos a aplicar la diferencia entre el punto más alejado (x2) y el punto del primer contorno (x1+w1).

Figura 9: Distancia entre dos puntos (pixeles).

En la línea 72 aplicaremos una regla de 3 simple, con la información del ancho de la hoja real y el de la imagen, además de la distancia en x obtenida en distancia_pixeles.

Figura 10: Distancia de pixeles a centímetros.

De la línea 73 a 76 visualizaremos la distancia en centímetros obtenida. Además dibujaremos una línea horizontal que cubre dicha distancia, y dos líneas verticales al inicio y fin de la línea.

Línes 77 a 83: Realizamos el mismo procedimiento de las líneas 70 a 76, con la diferencia que ahora x2 será menor a x1, es decir que x2 corresponde al contorno de la izquierda y x1 al de la derecha.

        cv2.imshow('imagen_A4',imagen_A4)

    cv2.imshow('frame',frame)	
    k = cv2.waitKey(1) & 0xFF
    if k == 27:
        break
cap.release()
cv2.destroyAllWindows()

Línea 85 y 87: Visualizamos las imánges contenidas en imagen_A4 y frame.

Línea 87 a 92: Si se preciona ESC, el proceso se romperá y terminará la captura del video.

Pruebas de funcionamiento

En un principio realicé pruebas con una webcam, que posee cierto efecto ojo de pez, entonces obtuve los siguientes resultados al medir a 1, 5, 10, 15 y 20 cm de distancia:

Figura 11: Pruebas de funcionamiento.

También hice pruebas con mi teléfono, en este los fotogramas no poseen este efecto ojo de pez, por lo que los resultados al medir las distancias fueron:

Figura 12: Pruebas de funcionamiento.

Como ves usando el teléfono, al no tener este efecto ojo de pez se pudo obtener mejores resultados, mientras que con la webcam que estaba usando en un principio el error al medir es más grande.

Cabe destacar que siempre vamos a tener cierto porcentaje de error al medir, no solo usando una cámara y visión articial como en este caso, sino incluso con una regla ya sea por la construcción de la misma o por error humano, por lo que debemos apuntar a obtener un error lo más bajo posible.