5.25. Deteção de manchas

A limiarização transformou o fotograma capturado numa máscara binária: cada pixel ou passa no teste de limiar ou não passa. Isso responde à pergunta que cores relevantes para a aplicação aparecem na cena, mas não onde – a máscara é apenas um conjunto de 1s e 0s. O passo seguinte é a deteção de manchas: percorrer a máscara, encontrar regiões contíguas de pixels que passaram o teste e devolver cada uma como um objeto com uma posição, um tamanho, uma orientação e outras propriedades sobre as quais a aplicação pode actuar.

find_blobs() é o método central para esse passo e é o ponto de entrada mais comum no mundo dos objetos de resultado do módulo de imagem. Rastrear uma bola colorida, seguir uma linha pintada no chão, contar quantos pontos brilhantes um sensor térmico deteta, decidir se um LED azul está ligado ou desligado – a mesma chamada cobre todos estes casos. As entradas mudam (os limiares, a região pesquisada, os filtros aplicados ao resultado), mas o padrão de chamada é o mesmo.

5.25.1. A chamada básica

find_blobs recebe uma lista de limiares e devolve uma lista de objetos de resultado de mancha:

thresholds = [(30, 100, 15, 127, 15, 127)]  # LAB threshold for red
blobs = img.find_blobs(thresholds)

for b in blobs:
    img.draw_rectangle(b.rect, color=(255, 0, 0))
    img.draw_cross(b.cx, b.cy, color=(255, 0, 0))

Cada tuplo de limiar tem a mesma forma que os limiares passados a binary() – seis entradas (l_lo, l_hi, a_lo, a_hi, b_lo, b_hi) para uma imagem RGB565 (os limites estão em LAB), duas entradas (lo, hi) para uma imagem em escala de cinzentos. Podem ser fornecidos até 32 limiares numa única chamada, o que torna find_blobs() tão flexível: balizas vermelha, verde e azul podem ser rastreadas simultaneamente, cada uma contribuindo com as suas próprias manchas para a lista devolvida, e a propriedade code de cada mancha identifica qual o limiar que correspondeu.

As chamadas a draw_rectangle() e draw_cross() acima anotam o fotograma capturado para a pré-visualização no IDE. O resultado da mancha já contém b.rect (a caixa delimitadora como um tuplo de 4 elementos) e b.cx / b.cy (o centróide em número inteiro), portanto desenhar a deteção de volta no fotograma resume-se a duas chamadas de método.

5.25.2. O que o resultado contém

Cada Blob é um tuplo de atributos que agrupa tudo o que o detetor mediu sobre a região. As propriedades dividem-se em quatro grupos.

O grupo de caixa delimitadora e centróidex, y, w, h, rect, cx, cy, cxf, cyf – descreve a posição da mancha. rect é o tuplo (x, y, w, h) de 4 elementos que os métodos de desenho esperam; cx e cy são o centróide em coordenadas de pixel inteiras; cxf e cyf são o centróide em coordenadas float de sub-pixel, úteis quando uma calibração a montante necessita de posições fracionadas.

Os descritores de formapixels, area, density, perimeter, roundness, elongation, compactness, rotation – descrevem a aparência da mancha. pixels é o número de pixels que passaram o teste; area é a área da caixa delimitadora alinhada com os eixos (w * h); density é a razão entre os dois, que se aproxima de 1.0 para um rectângulo sólido e desce para 0.0 para um traço diagonal fino. roundness e compactness avaliam ambos a redondeza da mancha, a partir de diferentes pontos de vista geométricos (roundness a partir dos momentos de segunda ordem, compactness a partir da razão perímetro-área); elongation é 1.0 - roundness por conveniência. rotation é a orientação do eixo principal em radianos, mais precisa em manchas alongadas e com maior ruído em manchas quase redondas (um eixo ambíguo não tem direcção bem definida).

Os metadados de limiar e fusãocode, count – identificam qual o limiar que correspondeu e quantas manchas de origem foram fundidas na devolvida. code é um mapa de bits de 32 bits com um bit definido por limiar correspondente (um único limiar dá code == 1; uma mancha multi-cor fundida pode ter vários bits definidos); count é 1 a menos que merge=True tenha combinado várias deteções numa só.

O grupo de cantoscorners, min_corners – fornece a geometria rotada da mancha. corners é o tuplo de 4 extremos (x, y) extraídos do contorno da mancha, ordenados no sentido horário a partir do canto superior esquerdo; min_corners é o tuplo de 4 cantos para o rectângulo rotado de área mínima que envolve a mancha. O rectângulo de área mínima é o ajuste apertado; o rect alinhado com os eixos é o ajuste largo alinhado com a grelha de pixels. Ambos são úteis consoante uma fase a jusante necessite de uma caixa orientada ou de uma caixa simples.

A blob detection illustrated against a binary threshold mask. The left panel shows a tilted oval mask of passing pixels. The right panel shows the same mask annotated with the axis-aligned bounding box drawn around it, the centroid marked with a cross in the middle, a dashed minimum-area rotated rectangle hugging the oval at its true angle, and the major-axis line through the centroid pointing along the oval's long direction.

Uma mancha contém a caixa delimitadora alinhada com os eixos (rect, x, y, w, h), o centróide (cx, cy ou sub-pixel cxf, cyf), o rectângulo rotado de área mínima (min_corners mais rotation), e as linhas dos eixos maior/menor opcionais calculadas pelas funções auxiliares ao nível do módulo descritas abaixo.

5.25.4. Fundir manchas sobrepostas

merge=True pós-processa a lista de resultados para combinar manchas cujos rectângulos delimitadores se sobrepõem. O uso natural é detectar um alvo cuja cor a câmara vê como múltiplas regiões limiarizadas devido a reflexos especulares, linhas de sombra ou iluminação desigual sobre o objecto: uma bola vermelha pode ser devolvida como três ou quatro manchas vermelhas pequenas que, tomadas em conjunto, delineiam a bola. Com merge=True, as três manchas tornam-se uma única mancha grande, o rect cobre a união, o code é o OR bit-a-bit dos códigos das manchas fundidas (pelo que uma fusão multi-cor identifica quais as cores que contribuíram), e count relata quantas manchas de origem foram combinadas.

margin aumenta ou diminui os rectângulos delimitadores antes do teste de sobreposição. Com margin=2, manchas cujos rectângulos delimitadores ficam a 2 pixels um do outro ainda se fundem; com margin=-2, apenas se fundem manchas cujos rectângulos delimitadores se sobrepõem em pelo menos 2 pixels. A afinação natural: margem positiva para lidar com manchas que o limiar fragmentou em peças adjacentes; margem negativa para manter objectos distintos muito próximos separados.

merge_cb executa em cada par candidato antes de a fusão ocorrer. A callback recebe as duas manchas e devolve True para permitir a fusão ou False para a impedir. Esta é a ferramenta certa para verificar fusões que a regra geométrica falha – por exemplo, recusar fundir duas manchas cujos ângulos de rotation diferem mais do que um limiar, ou recusar fundir uma mancha pequena numa muito maior se a pequena for apenas ruído.

5.25.5. Histogramas de projecção

x_hist_bins_max e y_hist_bins_max associam histogramas de projecção opcionais a cada mancha. Um histograma de projecção é a contagem de pixels aprovados ao longo de um eixo: o histograma do eixo X totaliza os pixels aprovados por coluna dentro da caixa delimitadora da mancha, e o histograma do eixo Y totaliza por linha. Ambos têm valor predefinido de zero – os histogramas não são calculados a menos que seja fornecido um max diferente de zero, pois caso contrário adicionariam trabalho a cada deteção.

Quando são calculados, os histogramas fornecem um sinal 1-D económico sobre o qual uma aplicação pode efectuar análises adicionais: detectar a posição de uma faixa vertical dentro da mancha, encontrar o ponto de divisão de um alvo com duas cores, contar quantos hiatos aparecem ao longo do eixo longo. São preenchidos como as propriedades x_hist_bins e y_hist_bins em cada Blob.

5.25.6. Auxiliares geométricos adicionais

Um conjunto de medidas geométricas adicionais existe como funções ao nível do módulo que recebem uma mancha e devolvem a medição pedida:

  • image.get_solidity() devolve a solidez da mancha – pixels divididos pela área do fecho convexo. Uma região preenchida sólida está próxima de 1.0; uma mancha com concavidades (uma ferradura, uma mão com dedos abertos) desce bem abaixo.

  • image.get_convexity() devolve a convexidade – o perímetro do fecho convexo dividido pelo perímetro da mancha. Uma mancha perfeitamente convexa é 1.0; manchas irregulares ou com entalhes são inferiores.

  • image.get_major_axis_line() e image.get_minor_axis_line() devolvem objectos Line ao longo dos eixos maior e menor da mancha, derivados do rectângulo rotado de área mínima.

  • image.get_enclosing_circle() devolve um Circle que envolve a mancha – útil quando uma fase a jusante quer um círculo para desenhar ou testar.

  • image.get_enclosed_ellipse() devolve o tuplo de 5 elementos (cx, cy, rx, ry, rotation) para uma elipse inscrita no rectângulo de área mínima da mancha. Os valores são passados directamente a draw_ellipse().

5.25.7. Aprender automaticamente um limiar

Um detetor de manchas é tão bom quanto os limiares com que é executado, e o trabalho de encontrar o limiar correcto para uma cor alvo é um problema por si só. Dois padrões comuns reduzem esse trabalho.

O primeiro é a selecção interactiva no IDE: capturar um fotograma, arrastar um rectângulo à volta de um exemplo da cor alvo e deixar o editor de limiares do IDE reportar os limites LAB que vê. Esses limites entram no script como os limiares de find_blobs() e o detetor está pronto.

O segundo é a aprendizagem automática programática: uma rotina de calibração a correr na câmara captura um fotograma, calcula um histograma de uma região conhecida onde o alvo se encontra (get_histogram() com roi=), e lê o intervalo de valores da região a partir do histograma com get_percentile(). O percentil 5 define o limite inferior de cada canal e o percentil 95 o limite superior, ignorando pixels aberrantes em ambas as extremidades. Numa imagem RGB565, uma chamada ao percentil relata os três canais LAB de uma vez, pelo que as duas chamadas produzem os seis números que find_blobs() espera:

h = img.get_histogram(roi=patch)
lo = h.get_percentile(0.05)
hi = h.get_percentile(0.95)
threshold = (lo.l_value, hi.l_value,
             lo.a_value, hi.a_value,
             lo.b_value, hi.b_value)