Segmentação com Envoltória Convexa e OpenCV

Segmentando e interpretando imagens com OpenCV.

Toni Esteves
6 min readMar 9, 2020
Photo by Brandon Mowinkel on Unsplash

A Envoltória Convexa (Convex Hull) de um conjunto de pontos é definido como o menor polígono convexo, que inclui todos os pontos no conjunto. Convexo significa que o polígono não tem canto dobrado para dentro.

Esquerda: Polígono Convexo. Direita: Polígno não Convexo

As bordas vermelhas no polígono direito incluem o canto onde a forma é côncava, o oposto de convexo.

Uma maneira útil de pensar sobre o envoltória convexa é a analogia do elástico:

Suponha que os pontos do conjunto fossem pregos, saindo de uma superfície plana. Imagine agora, o que aconteceria se você pegasse um elástico e o esticasse ao redor desses pregos. Tentando se contrair de volta ao seu comprimento original, o elástico envolvia os pregos, tocando àqueles que ficavam mais afastadas do centro.

A analogia do elástico, imagem da Wikipedia.

Definindo a Envoltória Convexa

Em um espaço euclidiano, uma região convexa é uma região onde, para cada par de pontos dentro da região, cada ponto no segmento de reta que une o par também está dentro da região. De forma geral, em geometria convexa, um conjunto convexo é um subconjunto de um espaço afim que é fechado sob combinações convexas.

O limite de um conjunto convexo é sempre uma curva convexa. A interseção de todos os conjuntos convexos contendo um determinado subconjunto A do espaço euclidiano é chamada de invólucro convexo ou envoltória convexa de A. Assim, a Envoltória Convexa é o menor conjunto convexo contendo A.

Por exemplo, A figura a seguir apresenta duas formulações distintas P1 e P2 para o conjunto X = {(1,3), (2,1), (2,2), (2,3), (2,4), (3,1), (3,2), (3,3),(4,2)}

Um conjunto X ⊂ R é chamado convexo se para qualquer x₁, x₂ ∈ X e todo número real 𝜶, 0 < 𝜶 < 1, o ponto 𝜶x₁ +(1­-𝜶)x₂ X. A interpretação geométrica desta definição é que, se um conjunto é convexo, então, dados dois pontos no conjunto, todo ponto no segmento de reta que liga esses dois pontos também é um elemento do conjunto.

Assim, dado um conjunto X ⊂ R, a envoltória convexa de X, representada por conv(X), é definida como a interseção de todos os conjuntos convexos que contêm X, isto é, o menor conjunto convexo que contém X.

Envoltória Convexa do exemplo anterior.

A envoltória convexa corresponde à combinação convexa dos pontos x₁=(1,3), x₂ =(2,1), x₃ =(2,4), x₄ =(3,1), x₅ =(4,2), isto é:

Abordagem

De forma prática para a nossa bordagem vamos definir uma área de interesse calculando o valor médio de execução para uma quantidade de quadros. Depois que exibirmos a mão, podemos detectar uma alteração e identificar os limites.Quando a mão entrar no ROI, usaremos um envoltório convexo para desenhar um polígono ao redor da mão.

Contorno de uma mão

No nosso caso, esse conjunto de pontos é na verdade apenas a imagem do limiar de uma mão (e as informações externas de contorno). Usando um pouco de matemática, calcularemos o centro da mão em relação ao ângulo dos pontos externos para inferir a contagem de dedos.

Observe que precisaremos contabilizar as linhas do pulso (a). Em seguinda, calcularemos os pontos mais extremos (superior, inferior, esquerdo e direito)(b). Finalmente, podemos então calcular sua interseção e estimar isso como o centro da mão(c).

Esquerda: a. Centro: b. Direita: c

Em seguida, calcularemos a distância para o ponto mais distante do centro(a). Então, usando uma razão dessa distância, criamos um círculo(b). Finalmente, podemos supor que quaisquer pontos fora deste círculo e suficientemente distantes do fundo devem ser dedos abertos(c).

Esquerda: a. Centro: b. Direita: c

Implementação

Nesta vou destacar apenas os pontos mais relevantes em relação a implementação, dado que todo código encontra-se disponível no Github.

Inicialmente precisaremos definir algumas variáveis globais referentes ao nosso background e área de interesse.

background = None
accumulated_weight = .5
roi_top = 20
roi_bottom = 400
roi_right = 800
roi_left = 1200

O background será atualizado constantemente a partir de uma cópia do frame inicial.

cv2.accumulateWeighted(frame, background, accumulated_weight)

O propósito é calculamos a média ponderada dos pesos e atualizarmos o background. O processo de segmentação da nossa entrada, é realizado através de uma extração de background, além da definição um limiar para calcularmos a diferença absoluta entre o background e o frame anterior.

diff = cv2.absdiff(background.astype(“uint8”), frame)
_, thresholded = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY)

Assim, os contornos da nossa entrada são definidos da seguinte forma:

contours, hierarchy = cv2.findContours(thresholded.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

A contagem dos dedos fica por conta da função convexHull

conv_hull = cv2.convexHull(hand_segment)

Vamos capturar os limites na nossa imagem segmentada tomando o cuidado de torna-los imutáveis, para isso definimos tuplas. Posteriormente precisaremos definir o ponto central da mão para o calculo da envoltória convexa. Em teoria o ponto central da mão é a metade entre a margem superior e margem inferior, e a metade entre a margem esquerda e a margem direita.

top = tuple(conv_hull[conv_hull[:, :, 1].argmin()][0])
bottom = tuple(conv_hull[conv_hull[:, :, 1].argmax()][0])
left = tuple(conv_hull[conv_hull[:, :, 0].argmin()][0])
right = tuple(conv_hull[conv_hull[:, :, 0].argmax()][0])
cX = (left[0] + right[0]) // 2
cY = (top[1] + bottom[1]) // 2

Encontraremos a distancia entre o centro da mão e as extremidades através do cálculo da distancia euclidiana.

distance = pairwise.euclidean_distances([(cX, cY)], Y=[left, right, top, bottom])[0]

Vamos então utilizar a função bitwise_and para criar uma máscara e retornar o corte obtido.

circular_roi = cv2.bitwise_and(thresholded, thresholded, mask=circular_roi)

Por fim, contaremos o total de dedos que serão exibidos baseando-se em duas condições:

  1. A região contornada não é inferior a área da mão (pulso).
out_of_wrist = ((cY + (cY * 0.25)) > (y + h))

2. O número de pontos ao longo do contorno não excede 25% da circunferência(nossa máscara), caso contrário, estamos contando pontos fora da mão.

limit_points = ((circumference * 0.25) > cnt.shape[0])

Resultado

Conclusão

Finalmente, espero que esse material tenha sido útil e faça sentido pra você, principalmente aos iniciantes. De forma elementar apresentamos conceitos relacionados a envoltória convexa e um pouco de segmentação de imagens utilizando OpenCV. Além disso nas referências do artigo é possível encontrar um material muito útil utilizado para elaboração desse artigo que pode te ajudar a ampliar seus conhecimentos na área.

Lembrando que qualquer feedback, seja positivo ou negativo é só entrar em contato através do meu twitter, linkedin ou nos comentários aqui em baixo :)

Referências

--

--

Toni Esteves

I’m a Computer Scientist and Quantitative researcher. This is my notepad for applied ML / DS / Deep Learning topics. Follow me on Twitter for more!