?️ CONTANDO DEDOS ✌️ (Usando defectos de convexidad) | Python – OpenCV

Por Administrador

En el post de hoy veremos como identificar cuantos dedos están siendo levantados dentro de cierta área de la imagen. Para ello estaremos usando defectos convexos, quiero aclarar que para esta aplicación no se ha realizado ningún tipo de entrenamiento, mas bien se han aplicado operaciones sobre contornos. ¿Quieres conocer el proceso?, te invito a que te quedes hasta el final.

IMPORTANTE: El código y video usados en este tutorial lo tendrás en mi respositorio en github.

CONTENIDO:

  • Convex Hull y defectos de convexidad
    • cv2.convexHull
    • cv2.convexityDefects
  • ¿Cuántos dedos he levantado?
    • Leer / Capturar un video y obtener el fondo de las escena
    • Sustracción de imágenes y obtención de la imagen binaria
    • Encontrar el contorno de la mano
    • Determinar el centro del contorno y el punto más alto
    • Mediante cv2.convexHull encontramos el casco convexo
    • Defectos de convexidad
      • Ángulo que forma el punto inicial al punto más alejado, y del punto más alejado al punto final.
      • Distancia mínima entre el punto inicial – final y distancia aproximada al punto más alejado.
    • Visualizando el conteo de dedos
  • Programa completo
  • Resultado final

Convex Hull y defectos de convexidad

Para realizar este tutorial necesitaremos prestarle atención a convex hull o casco convexo y a convexity defects o defectos de convexidad. Para ello vamos a analizar la siguiente imagen:

Figura 1: Imagen donde se muestra una mano, casco convexo y los defectos de convexidad. (Fuente)

Debo decir que he encontrado un post buenísimo de pyimagesearch donde Adrian Rosebrock explica estos temas, por lo que te recomiendo que le des  vistazo. Volviendo a este post, en la figura 1 tenemos el dibujo de una mano, aquel contorno rojo que rodea la mano es llamado casco convexo o convex hull, el cual da un efecto de banda elástica que está rodeando el objeto, como nos lo cuenta Adrian en su explicación.

Si prestamos atención a este contorno de color rojo dibujado, vamos a darnos cuenta existen áreas en donde no está topando a la mano (en la figura 1 están representadas con flechas bidireccionales negras), todas estas cavidades son consideradas defectos de convexidad que son desviaciones máximas locales, y en ellas nos apoyaremos para construir la aplicación de hoy, el conteo de dedos levantados, debido a que existirán defectos de convexidad.

cv2.convexHull

Esta es la función que emplea OpenCV para construir un casco convexo/convex hull que rodee un objeto, por lo tanto nos permitirá conseguir la línea roja que podemos ver en la figura 1, además nos ayudará a determinar los defectos de convexidad de un objeto.

Parámetros que usaremos:

  • Points, que es el contorno encontrado.
  • returnPoints, por defecto es True que nos devolverá las coordenadas del casco convexo. Mientas que si es False devolverá los índices de los puntos del casco convexo, lo cual permitirá encontrar los defectos de convexidad.

Para más información sobre otros parámetros que requiere esta función por favor dirigirse a la documentanción de OpenCV.

Pero, ¿qué te parece si usamos mi mano como ejemplo? a continuación podremos ver la imagen de entrada, y luego aplicando cv2.convexHull.

cv2.convexityDefects

Una vez que se ha calculado el casco convexo y tenemos False en returnPoints, vamos a aplicar la función cv2.convexityDefects para obtener los defectos de convexidad.

Parámetros que usaremos:

  • Contorno de entrada.
  • Casco convexo obtenido através de cv2.convexHull y cuyo parámetro returnPoints debió estar en False. Este debe contener los índices de los puntos que forman el casco convexo.

De esta función obtendremos un array en donde cada columna tendrá estos valores: [ punto inicial, punto final, punto más alejado, distancia aproximada al punto más alejado ].

Y de igual manera, vamos aplicarlo a la imagen de la figura 2.

Como podemos ver en la figura 4, aquí he etiquetado el punto inicial, final y más alejado del defecto de convexidad de entre el pulgar y el índice, mientras que en la figura 5 está la totalidad de puntos que corresponden a los defectos de convexidad encontrados (como podemos ver son muchísimos defectos encontrados y algunos parecen no aportar mucha información).

Cabe destacar que también necesitaremos de otras funciones, aquellas que ya hemos visto en otros posts, por lo tanto cuando las usemos te dejaré el link a cada una de ellas para que aclares algún dato en caso de ser necesario.

¿Cuántos dedos estoy levantando?

Para realizar esta aplicación tendremos que seguir algunos pasos tales como, obtención del fondo de la imagen, sustracción de imágenes (primer plano y fondo), encontrar el contorno de la mano en la imagen, extraer el casco convexo y los defectos de convexidad, diferenciar los casos en que estén levantados:  0, 1, 2, 3, 4 y 5 dedos.

A continuación podrás ver algunas imágenes del video de entrada (además de distintos dedos levantados) sobre el cual se va a aplicar todos estos procedimientos:

El que usemos un video de entrada para poder trabajar no quiere decir, que no puedas realizar un video streaming y probar el programa, claro que puedes hacerlo. En este caso uso este video para poder mostrarte la entrada y al final el resultado obtenido.

NOTA: En esta aplicación estaré usando la sustracción de dos imágenes para poder realizar la detección de la mano. Tú podrías tomar además de este otros criterios para conseguir diferenciar la mano del fondo tales como: discriminar la mano dependiendo del color de la piel, sustracción de fondo, o simplemente umbralización simple, etc.

¡Empecemos!

Leer/Capturar un video y obtener el fondo de la escena

import cv2
import numpy as np
import imutils

#cap = cv2.VideoCapture(0,cv2.CAP_DSHOW)
cap = cv2.VideoCapture('videoEntrada.mp4')
bg = None

# COLORES PARA VISUALIZACIÓN
color_start = (204,204,0)
color_end = (204,0,204)
color_far = (255,0,0)

color_start_far = (204,204,0)
color_far_end = (204,0,204)
color_start_end = (0,255,255)

color_contorno = (0,255,0)
color_ymin = (0,130,255) # Punto más alto del contorno
#color_angulo = (0,255,255)
#color_d = (0,255,255)
color_fingers = (0,255,255)

while True:
  ret, frame = cap.read()
  if ret == False: break

  # Redimensionar la imagen para que tenga un ancho de 640
  frame = imutils.resize(frame,width=640) 	
  frame = cv2.flip(frame,1)
  frameAux = frame.copy()

Línea 1 a 3: Importamos OpenCV, numpy e imutils, este último lo emplearemos para redimensionar cada fotograma, ya lo veremos.

Línea 5: (En caso de realizar un video streaming o video en vivo). Iniciamos con cv2.VideoCapture para realizar un video streaming. He usado cv2.CAP_DSHOW en Windows para que la visualización del video aparezca completamente en la ventana de visualización. Podrías omitir esto en caso de no ser necesario.

Línea 6: (En caso de leer algún video, como el que vamos a trabar en este post). Para ello debemos especificar el nombre del video en caso de que este se encuentre en la misma carperta que la del script.

Línea 7: Establecemos bg (backgroun o fondo) como None, ya que luego con ayuda de una tecla (i) podremos capturar el fondo de la escena (esto lo veremos en la línea 138 más tarde).

if k == ord('i'):
  bg = cv2.cvtColor(frameAux,cv2.COLOR_BGR2GRAY)

Línea 10 a 22: Aquí únicamente se está asignando ciertos colores a variables. Esto nos servirá para poder cambiar los colores de la visualización para los defectos convexos, contornos, dedos levantados, entre otros.

Línea 25 y 26: Vamos a leer cada fotograma, entonces cada imagen se almacenará en frame, en la línea 26 en caso de que ret sea falso, es decir que no se haya leído una imagen, el ciclo se romperá.

Línea 29: Redimensionamos la imagen almacenada en frame para que tenga un ancho de 640, esto nos servirá en caso de que el video sea muy grande en su alto y ancho. El uso de imutils para el redimensionamiento ya lo hemos visto en el post anterior.

Línea 30 y 31: Vamos a usar la funcióncv2.flip para voltear frame para que nos permita visualizar el video streaming/video como si fuera un espejo. Podrías omitir esta línea en caso de ser necesario. En la línea 31 vamos a obtener una copia de frame que nos servirá para capturar el fondo de la imagen sin que esta sea alterada por las funciones de dibujo que usemos luego.

Hasta este punto tenemos los fotogramas y una vez que se digite «i» se almacenará una imagen (fondo) en bg.

Sustracción de imágenes y obtención de la imagen binaria

Ahora será necesario restar los fotogramas con la imagen de fondo almacenada:

if bg is not None:

  # Determinar la región de interés
  ROI = frame[50:300,380:600]
  cv2.rectangle(frame,(380-2,50-2),(600+2,300+2),color_fingers,1)
  grayROI = cv2.cvtColor(ROI,cv2.COLOR_BGR2GRAY)

  # Región de interés del fondo de la imagen
  bgROI = bg[50:300,380:600]

  # Determinar la imagen binaria (background vs foreground)
  dif = cv2.absdiff(grayROI, bgROI)
  _, th = cv2.threshold(dif, 30, 255, cv2.THRESH_BINARY)
  th = cv2.medianBlur(th, 7)

Línea 33: Una vez que se haya almacenado una imagen en bg entramos en esta condicional.

Línea 36: Determinamos el área de interés donde deseemos que la mano se pueda ubicar y pueda contar los dedos.

Línea 37: Dibujamos un rectángulo en dicha área de interés, puedes notar que he restado y sumado 2, esto es para dar un margen al rectángulo dibujado.

Línea 38: Transformamos de BGR a GRAY a la variable grayROI.

Línea 41: Ahora tomamos la misma área de interés pero ahora en bg que es el fondo de la imagen.

Línea 44 a 46: Vamos a restar la imagen de primer plano y el fondo que se almacenará en dif,  y esta a su vez aplicamos umbralización simple, finalmente para mejorar la imagen binaria he usado cv2.medianBlur.

NOTA: Tanto la sustracción de imágenes como umbralización simple lo hemos visto en videos anteriores. Es más habíamos usado estas técnicas para realizar la detección simple de movimiento.

Si visualizamos obtendremos las siguientes imágenes:

Figura 6: Visualización del proceso de obtención de la imagen binaria. (Arriba: ROI, grayROI, bgROI) (Abajo: dif,th)

Como te pudiste haber dado cuenta, el objetivo del proceso que hemos realizado es obtener la imagen binaria, en donde en blanco aparecerá la mano, mientras que el fondo debe mantenerse en negro. Es importante que elijas un fondo que contraste con el color de la mano para que se pueda obtener una imagen binaria óptima, de otro modo puede presentar problemas al contar los dedos en procesos posteriores.

Encontrar el contorno de la mano

Una vez que hemos obtenido  la imagen binaria en donde se muestra la mano en blanco necesitamos encontrar su contorno.

# Encontrando los contornos de la imagen binaria
cnts, _ = cv2.findContours(th,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:1]

Línea 49: Encontramos los contornos externos de la imagen binaria (si obtienes algún problema prueba con _, cnts, _).

Línea 50: Escogemos el contorno más grande que se muestre en la imagen. Podrías cambiar este criterio y por ejemplo determinar el contorno de la mano mediante su área.

Determinar el centro del contorno y el punto más alto del mismo

Estos pasos nos servirán para los casos en que no se haya levantado ningún dedo o se haya levantado uno, esto lo veremos más adelante:

for cnt in cnts:

  # Encontrar el centro del contorno
  M = cv2.moments(cnt)
  if M["m00"] == 0: M["m00"]=1
  x = int(M["m10"]/M["m00"])
  y = int(M["m01"]/M["m00"])
  cv2.circle(ROI,tuple([x,y]),5,(0,255,0),-1)

  # Punto más alto del contorno
  ymin = cnt.min(axis=1)
  cv2.circle(ROI,tuple(ymin[0]),5,color_ymin,-1)

Línea 55 a 59: A través de estas líneas encontramos los puntos x e y que corresponden al centro del contorno. En la línea 59 estaremos dibujando el punto obtenido.

Figura 7: Centro del contorno encontrado representado por un círculo verde.

Línea 62 y 63: Encontramos el punto más alto del contorno. Esto nos servirá en caso de que no se haya levantado ningún dedo o se haya levantado un dedo.

Mediante cv2.convexHull encontramos el casco convexo

Este paso es netamente para visualización.

# Contorno encontrado a través de cv2.convexHull
hull1 = cv2.convexHull(cnt)
cv2.drawContours(ROI,[hull1],0,color_contorno,2)

Línea 66 y 67: Vamos con el casco convexo del contorno más grande encontrado, luego lo visualizaremos en la línea 67. Tendríamos lo siguiente:

Figura 10: Visualización del casco convexo.

Defectos de convexidad

# Defectos convexos
hull2 = cv2.convexHull(cnt,returnPoints=False)
defects = cv2.convexityDefects(cnt,hull2)

# Seguimos con la condición si es que existen defectos convexos
if defects is not None:

  inicio = [] # Contenedor en donde se almacenarán los puntos iniciales de los defectos convexos
  fin = [] # Contenedor en donde se almacenarán los puntos finales de los defectos convexos
  fingers = 0 # Contador para el número de dedos levantados

  for i in range(defects.shape[0]):
    
    s,e,f,d = defects[i,0]
    start = cnt[s][0]
    end = cnt[e][0]
    far = cnt[f][0]

Líneas 70 y 71: Como vimos en un principio, con estas líneas vamos a encontrar los defectos de convexidad. Los datos que nos devolverá defects serán escenciales ya que con ellos vamos a calcular el número de dedos levantados.

Línea 76 a 78: Estas variables las emplearemos más tarde para visualizar información en el resultado final.

Línea 80 a 85: Procedemos a desempaquetar la información dada por defects es decir el punto inicial, punto final, punto más alejado, distancia aproximada al punto más alejado. Si visualizamos estos puntos, obtendremos algunas imágenes como estas:

Figura 11: Puntos obtenidos al  haber encontrado defectos de convexidad en distintas posiciones de la mano.

En la figura 11 podemos ver los puntos iniciales en color celeste, puntos finales en violeta mientras que los puntos más alejados en azul. Son muchos puntos ¿verdad?, pues necesitamos deshacernos de algunos de ellos que no nos aportan mucha ayuda.

Para poder tomar puntos que nos ayuden a solucionar esta aplicación he tomado 3 criteriorios:

  • Ángulo que forma el punto inicial al punto más alejado, y del punto más alejado al punto final.
  • Distancia mínima entre el punto inicial y final.
  • Distancia aproximada al punto más alejado.

Ángulo que forma el punto inicial al punto más alejado, y del punto más alejado al punto final.

Voy a  empezar explicando el procedimiento que necesita un poco más de líneas de código. Si tomamos en cuenta los ángulos que pueden formar los dedos consecutivamente, tenemos que estos podrán formar hasta unos 90 grados aproximadamente (Por el ángulo que forma el pulgar y el índice).

Para poder calcular los ángulos que forme cada par de dedos, podremos aplicar trigonometría, de tal modo que dados los lados de un triángulo formados por los segmentos del punto inicial al punto más alejado, del punto más alejado al punto final y del punto final al punto inicial, tendríamos lo siguiente:

 

Figura 12: Ejemplo de ángulo formado por el dedo pulgar e índice.

Para obtener el ángulo entonces tendríamos lo siguiente:

Esto para en el pograma sería:

# Encontrar el triángulo asociado a cada defecto convexo para determinar ángulo					
a = np.linalg.norm(far-end)
b = np.linalg.norm(far-start)
c = np.linalg.norm(start-end)

angulo = np.arccos((np.power(a,2)+np.power(b,2)-np.power(c,2))/(2*a*b))
angulo = np.degrees(angulo)
angulo = int(angulo)

Línea 88 a 90: Aplicamos la distancia entre dos puntos con la ayuda de numpy, con ello encontraremos los lados a, b y c.

Línea 92 a 94: Encontramos el ángulo C de la figura 12 con ayuda de la fórmula que habíamos visto recientemente. Como el resultado está en radianes lo convertiremos en grados y finalmente a enteros.

Según varias pruebas realizadas por mi parte, encontré que se puede obtener hasta 90 grados en los ángulos entre los dedos (para esta aplicación), por lo tanto este es el valor que me  estaré basando para filtrar de otros ángulos obtenidos.

Distancia mínima entre el punto inicial – final y distancia aproximada al punto más alejado.

Otros aspectos que probé es la distancia entre dos puntos, el punto inicial y final ya que si están demasido cerca puede que no nos den buenos resultados al identificar los defectos convexos en esta aplciación, por ello deben cumplir con ser mayores a una distancia de 20.

Otra condición sería la distancia al punto más alejado, puesto que pueden aparecer identificaciones en los dedos que doblamos cuando extendemos otros, con esto nos aseguramos que haya una distancia mínima a cumplir.

Veamos estos  3 aspectos en el código:

# Se descartarán los defectos convexos encontrados de acuerdo a la distnacia
# entre los puntos inicial, final y más alelago, por el ángulo y d
if np.linalg.norm(start-end) > 20 and angulo < 90 and d > 12000:
  
  # Almacenamos todos los puntos iniciales y finales que han sido
  # obtenidos
  inicio.append(start)
  fin.append(end)
  
  # Visualización de distintos datos obtenidos
  #cv2.putText(ROI,'{}'.format(angulo),tuple(far), 1, 1.5,color_angulo,2,cv2.LINE_AA)
  #cv2.putText(ROI,'{}'.format(d),tuple(far), 1, 1.1,color_d,1,cv2.LINE_AA)
  cv2.circle(ROI,tuple(start),5,color_start,2)
  cv2.circle(ROI,tuple(end),5,color_end,2)
  cv2.circle(ROI,tuple(far),7,color_far,-1)
  #cv2.line(ROI,tuple(start),tuple(far),color_start_far,2)
  #cv2.line(ROI,tuple(far),tuple(end),color_far_end,2)
  #cv2.line(ROI,tuple(start),tuple(end),color_start_end,2)

Línea 98: Vamos con la condición que seleccionará a los puntos que representen a los dedos levantados.

Línea 102 y 103: Almacenamos los puntos iniciales y finales, lo que nos va a servir para visualizar los números sobre cada dedo levantado.

Línea 106 a 113: Estas líneas nos van a servir para visualizar la información de los puntos de los defectos convexos, además si descomentamos podemos visualizar las líneas que los unen o la información del ángulo o de la distancia en d por ejemplo.

Si visualizamos tendríamos lo siguiente:

Figura 13: Puntos iniciales, finales y más alejados en 2, 3, 4 y 5 dedos levantados.

Visualizando el conteo de dedos

Ya casi terminamos, ya prácticamente tenemos como contar 2, 3, 4 y 5 dedos. Pero aún no hemos realizado el proceso para contar 1 dedo levantado, veamos el procedimiento que se ha realizado para conseguirlo.

# Si no se han almacenado puntos de inicio (o fin), puede tratarse de
# 0 dedos levantados o 1 dedo levantado
if len(inicio)==0:
  minY = np.linalg.norm(ymin[0]-[x,y])
  if minY >= 110:
    fingers = fingers +1
    cv2.putText(ROI,'{}'.format(fingers),tuple(ymin[0]), 1, 1.7,(color_fingers),1,cv2.LINE_AA)

Línea 117 y 118: Si no se ha almacenado ningún punto inicial (esto sucederá cuando se hayan levantado 0 y 1 dedo), entonces procedemos a calcular la distancia entre el centro del contorno y el punto más alto del contorno que se almacenará en minY.

Línea 119 a 121:  Si la distancia minY es mayor o igual a 110 (valor que he determinado después de pruebas, tu podrías modificarlo), quiere decir que un dedo ha sido levantado, por lo tanto en la línea 121 se visualizará 1 en el la coordenada del punto más alto del contorno.

Figura 14: Visualización de un dedo levantado, con el punto más alto representado en naranja y el centro del contorno en verde.

        # Si se han almacenado puntos de inicio, se contará el número de dedos levantados
        for i in range(len(inicio)):
          fingers = fingers + 1
          cv2.putText(ROI,'{}'.format(fingers),tuple(inicio[i]), 1, 1.7,(color_fingers),1,cv2.LINE_AA)
          if i == len(inicio)-1:
            fingers = fingers + 1
            cv2.putText(ROI,'{}'.format(fingers),tuple(fin[i]), 1, 1.7,(color_fingers),1,cv2.LINE_AA)
        
        # Se visualiza el número de dedos levantados en el rectángulo izquierdo
        cv2.putText(frame,'{}'.format(fingers),(390,45), 1, 4,(color_fingers),2,cv2.LINE_AA)
        
    cv2.imshow('th',th)
  cv2.imshow('Frame',frame)

  k = cv2.waitKey(20)
  if k == ord('i'):
    bg = cv2.cvtColor(frameAux,cv2.COLOR_BGR2GRAY)
  if k == 27:
    break
  
cap.release()
cv2.destroyAllWindows()

Línea 124 a 129: Vamos a usar los puntos alamacenados en inicio y fin, para poder visualizar los números sobre cada dedo, para ello emplearemos un for que recorrerá cada coordenada almacenada. Usamos fingers como contador de los dedos de la mano.

Línea 132: Visualizamos el número total de dedos encontrados.

Figura 15: Visualización del conteo de dedos.

Línea 134 y 135: Visualizamos tanto la imagen binaria th para asegurarnos que se esté identificando de forma correcta la mano y frame.

Línea 137 a 141: Cuando ‘i’ es presionada se guarda la imagen bg que corresponderá al fondo de la imagen, mientras que si presiona ‘Esc’ rompe el proceso.

Línea 143 y 144: Finalizamos la captura o video y cerramos todas las ventanas que se han abierto.

Programa completo

import cv2
import numpy as np
import imutils

#cap = cv2.VideoCapture(0,cv2.CAP_DSHOW)
cap = cv2.VideoCapture('videoEntrada.mp4')
bg = None

# COLORES PARA VISUALIZACIÓN
color_start = (204,204,0)
color_end = (204,0,204)
color_far = (255,0,0)

color_start_far = (204,204,0)
color_far_end = (204,0,204)
color_start_end = (0,255,255)

color_contorno = (0,255,0)
color_ymin = (0,130,255) # Punto más alto del contorno
#color_angulo = (0,255,255)
#color_d = (0,255,255)
color_fingers = (0,255,255)

while True:
  ret, frame = cap.read()
  if ret == False: break

  # Redimensionar la imagen para que tenga un ancho de 640
  frame = imutils.resize(frame,width=640) 	
  frame = cv2.flip(frame,1)
  frameAux = frame.copy()
  
  if bg is not None:

    # Determinar la región de interés
    ROI = frame[50:300,380:600]
    cv2.rectangle(frame,(380-2,50-2),(600+2,300+2),color_fingers,1)
    grayROI = cv2.cvtColor(ROI,cv2.COLOR_BGR2GRAY)

    # Región de interés del fondo de la imagen
    bgROI = bg[50:300,380:600]

    # Determinar la imagen binaria (background vs foreground)
    dif = cv2.absdiff(grayROI, bgROI)
    _, th = cv2.threshold(dif, 30, 255, cv2.THRESH_BINARY)
    th = cv2.medianBlur(th, 7)
    
    # Encontrando los contornos de la imagen binaria
    cnts, _ = cv2.findContours(th,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:1]

    for cnt in cnts:

      # Encontrar el centro del contorno
      M = cv2.moments(cnt)
      if M["m00"] == 0: M["m00"]=1
      x = int(M["m10"]/M["m00"])
      y = int(M["m01"]/M["m00"])
      cv2.circle(ROI,tuple([x,y]),5,(0,255,0),-1)

      # Punto más alto del contorno
      ymin = cnt.min(axis=1)
      cv2.circle(ROI,tuple(ymin[0]),5,color_ymin,-1)

      # Contorno encontrado a través de cv2.convexHull
      hull1 = cv2.convexHull(cnt)
      cv2.drawContours(ROI,[hull1],0,color_contorno,2)

      # Defectos convexos
      hull2 = cv2.convexHull(cnt,returnPoints=False)
      defects = cv2.convexityDefects(cnt,hull2)
      
      # Seguimos con la condición si es que existen defectos convexos
      if defects is not None:

        inicio = [] # Contenedor en donde se almacenarán los puntos iniciales de los defectos convexos
        fin = [] # Contenedor en donde se almacenarán los puntos finales de los defectos convexos
        fingers = 0 # Contador para el número de dedos levantados

        for i in range(defects.shape[0]):
    
          s,e,f,d = defects[i,0]
          start = cnt[s][0]
          end = cnt[e][0]
          far = cnt[f][0]

          # Encontrar el triángulo asociado a cada defecto convexo para determinar ángulo					
          a = np.linalg.norm(far-end)
          b = np.linalg.norm(far-start)
          c = np.linalg.norm(start-end)
          
          angulo = np.arccos((np.power(a,2)+np.power(b,2)-np.power(c,2))/(2*a*b))
          angulo = np.degrees(angulo)
          angulo = int(angulo)
          
          # Se descartarán los defectos convexos encontrados de acuerdo a la distnacia
          # entre los puntos inicial, final y más alelago, por el ángulo y d
          if np.linalg.norm(start-end) > 20 and angulo < 90 and d > 12000:
            
            # Almacenamos todos los puntos iniciales y finales que han sido
            # obtenidos
            inicio.append(start)
            fin.append(end)
            
            # Visualización de distintos datos obtenidos
            #cv2.putText(ROI,'{}'.format(angulo),tuple(far), 1, 1.5,color_angulo,2,cv2.LINE_AA)
            #cv2.putText(ROI,'{}'.format(d),tuple(far), 1, 1.1,color_d,1,cv2.LINE_AA)
            cv2.circle(ROI,tuple(start),5,color_start,2)
            cv2.circle(ROI,tuple(end),5,color_end,2)
            cv2.circle(ROI,tuple(far),7,color_far,-1)
            #cv2.line(ROI,tuple(start),tuple(far),color_start_far,2)
            #cv2.line(ROI,tuple(far),tuple(end),color_far_end,2)
            #cv2.line(ROI,tuple(start),tuple(end),color_start_end,2)

        # Si no se han almacenado puntos de inicio (o fin), puede tratarse de
        # 0 dedos levantados o 1 dedo levantado
        if len(inicio)==0:
          minY = np.linalg.norm(ymin[0]-[x,y])
          if minY >= 110:
            fingers = fingers +1
            cv2.putText(ROI,'{}'.format(fingers),tuple(ymin[0]), 1, 1.7,(color_fingers),1,cv2.LINE_AA)
          
        # Si se han almacenado puntos de inicio, se contará el número de dedos levantados
        for i in range(len(inicio)):
          fingers = fingers + 1
          cv2.putText(ROI,'{}'.format(fingers),tuple(inicio[i]), 1, 1.7,(color_fingers),1,cv2.LINE_AA)
          if i == len(inicio)-1:
            fingers = fingers + 1
            cv2.putText(ROI,'{}'.format(fingers),tuple(fin[i]), 1, 1.7,(color_fingers),1,cv2.LINE_AA)
        
        # Se visualiza el número de dedos levantados en el rectángulo izquierdo
        cv2.putText(frame,'{}'.format(fingers),(390,45), 1, 4,(color_fingers),2,cv2.LINE_AA)
        
    cv2.imshow('th',th)
  cv2.imshow('Frame',frame)

  k = cv2.waitKey(20)
  if k == ord('i'):
    bg = cv2.cvtColor(frameAux,cv2.COLOR_BGR2GRAY)
  if k == 27:
    break
  
cap.release()
cv2.destroyAllWindows()

Resultado final

A continuación podemos apreciar como se desempeñando el programa. Recuerda presionar ‘i’ para almacenar el fondo de la imagen, para que se puedan detectar los dedos. (Puedes visualizar el video del resultado final aquí)

Hemos llegado al final de este post, espero que te haya gustado, nos vemos en el siguiente video o post. ¡Cuídate mucho!.

REFERENCIAS:

  • https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html
  • https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_more_functions/py_contours_more_functions.html#convexity-defects
  • https://gurus.pyimagesearch.com/lesson-sample-advanced-contour-properties/
  • https://dsp.stackexchange.com/questions/26996/what-is-the-deffinition-of-convexity-defect-in-image-processing
  • https://docs.opencv.org/2.4/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html#convexitydefects
  • https://www.youtube.com/watch?v=v-XcmsYlzjA
  • https://medium.com/@soffritti.pierfrancesco/handy-hands-detection-with-opencv-ac6e9fb3cec1
  • https://www.superprof.es/apuntes/escolar/matematicas/trigonometria/4-resolver-un-triangulo-conociendo-los-tres-lados.html