5.25. Encontrando blobs

A limiarização transformou o quadro capturado em uma máscara binária: cada pixel passa no teste de limiar ou não. Isso responde quais cores que interessam à aplicação aparecem na cena, mas não onde – a máscara é apenas um mar de 1s e 0s. O próximo passo é a detecção de blobs: percorrer a máscara, encontrar regiões contíguas de pixels aprovados e retornar cada uma como um objeto com uma posição, um tamanho, uma orientação e as demais propriedades sobre as quais a aplicação pode atuar.

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

5.25.1. A chamada básica

find_blobs recebe uma lista de limiares e retorna uma lista de objetos-resultado de blob:

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 tupla de limiar tem a mesma forma que os limiares passados para 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 cinza. Até 32 limiares podem ser fornecidos em uma única chamada, o que é o que torna find_blobs() tão flexível: balizas vermelhas, verdes e azuis podem ser rastreadas simultaneamente, cada uma contribuindo com seus próprios blobs para a lista retornada, e a propriedade code de cada blob identifica qual limiar ele correspondeu.

As chamadas draw_rectangle() e draw_cross() acima anotam o quadro capturado para a pré-visualização da IDE. O resultado do blob já carrega b.rect (a caixa delimitadora como uma tupla de 4 elementos) e b.cx / b.cy (o centroide inteiro), de modo que desenhar a detecção de volta no quadro são duas chamadas de método.

5.25.2. O que o resultado contém

Cada Blob é uma tupla de atributos que reúne tudo o que o detector mediu sobre a região. As propriedades se dividem em quatro grupos.

O grupo de caixa delimitadora e centroidex, y, w, h, rect, cx, cy, cxf, cyf – descreve a posição do blob. rect é a tupla de 4 elementos (x, y, w, h) que os métodos de desenho esperam; cx e cy são o centroide em coordenadas inteiras de pixel; cxf e cyf são o centroide em coordenadas de ponto flutuante sub-pixel, úteis quando uma calibração a montante se importa com posições fracionárias.

Os descritores de formapixels, area, density, perimeter, roundness, elongation, compactness, rotation – descrevem a aparência do blob. pixels é a contagem de pixels aprovados; area é a área da caixa delimitadora alinhada aos eixos (w * h); density é a razão entre as duas, que se aproxima de 1.0 para um retângulo sólido e cai em direção a 0.0 para um traço diagonal fino. roundness e compactness ambos pontuam o quão redondo é o blob, 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 maior em radianos, que é mais precisa em blobs alongados e fica ruidosa em blobs quase redondos (um eixo ambíguo não tem direção bem definida).

Os metadados de limiar e mesclagemcode, count – identificam qual limiar correspondeu e quantos blobs de origem foram mesclados no blob retornado. code é um mapa de bits de 32 bits com um bit definido por limiar correspondente (um único limiar resulta em code == 1; um blob multicolorido mesclado pode ter vários bits definidos); count é 1 a menos que merge=True tenha combinado várias detecções em uma.

O grupo de cantoscorners, min_corners – fornece a geometria rotacionada do blob. corners é a tupla de 4 elementos de extremos (x, y) extraídos do contorno do blob, ordenados no sentido horário a partir do canto superior esquerdo; min_corners é a tupla de 4 cantos do retângulo rotacionado de área mínima que envolve o blob. O retângulo de área mínima é o ajuste justo; o rect alinhado aos eixos é o ajuste folgado alinhado à grade de pixels. Ambos são úteis dependendo de o estágio seguinte precisar de uma caixa orientada ou de uma simples.

Uma detecção de blob ilustrada contra uma máscara binária de limiar. O painel esquerdo mostra uma máscara oval inclinada de pixels aprovados. O painel direito mostra a mesma máscara anotada com a caixa delimitadora alinhada aos eixos desenhada ao redor dela, o centroide marcado com uma cruz no meio, um retângulo rotacionado de área mínima tracejado abraçando o oval em seu ângulo verdadeiro, e a linha do eixo maior através do centroide apontando ao longo da direção longa do oval.

Um blob carrega a caixa delimitadora alinhada aos eixos (rect, x, y, w, h), o centroide (cx, cy ou sub-pixel cxf, cyf), o retângulo rotacionado de área mínima (min_corners mais rotation) e as linhas opcionais de eixo maior / menor calculadas pelos auxiliares de nível de módulo abaixo.

5.25.4. Mesclando blobs sobrepostos

merge=True pós-processa a lista de resultados para combinar blobs cujos retângulos delimitadores se sobrepõem. O uso natural é detectar um alvo cuja cor a câmera vê como múltiplas regiões limiarizadas por causa de reflexos especulares, linhas de sombra ou iluminação desigual sobre o objeto: uma única bola vermelha pode retornar como três ou quatro pequenos blobs vermelhos que, em conjunto, traçam a bola. Com merge=True, os três blobs se tornam um grande blob, o rect cobre a união, o code é o OU bit a bit dos códigos dos blobs mesclados (de modo que uma mesclagem multicolorida identifica quais cores contribuíram), e count informa quantos blobs de origem foram combinados.

margin aumenta ou encolhe os retângulos delimitadores antes do teste de sobreposição. Com margin=2, blobs cujos retângulos delimitadores chegam a 2 pixels um do outro ainda se mesclam; com margin=-2, apenas blobs cujos retângulos delimitadores se sobrepõem por pelo menos 2 pixels se mesclam. O ajuste natural: margem positiva para lidar com blobs que o limiar quebrou em pedaços adjacentes; margem negativa para manter separados objetos distintos agrupados de perto.

merge_cb roda em cada par candidato antes de a mesclagem acontecer. O callback recebe os dois blobs e retorna True para permitir a mesclagem ou False para impedi-la. Esta é a ferramenta certa para verificações cruzadas de mesclagens que a regra geométrica deixa passar – por exemplo, recusar a mesclagem de dois blobs cujos ângulos de rotation divergem por mais do que um limiar, ou recusar mesclar um blob pequeno em um muito maior se o pequeno for apenas ruído pontual.

5.25.5. Histogramas de projeção

x_hist_bins_max e y_hist_bins_max anexam histogramas de projeção opcionais a cada blob. Um histograma de projeçã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 do blob, e o histograma do eixo Y totaliza por linha. Ambos têm padrão zero – os histogramas não são calculados a menos que um max diferente de zero seja fornecido, já que, caso contrário, adicionariam trabalho a cada detecção.

Quando são calculados, os histogramas fornecem um sinal 1-D barato sobre o qual uma aplicação pode rodar análises adicionais: detectar a posição de uma faixa vertical dentro do blob, encontrar o ponto de quebra de um alvo bicolor, contar quantas lacunas aparecem ao longo do eixo longo. Eles são preenchidos como as propriedades x_hist_bins e y_hist_bins em cada Blob.

5.25.6. Auxiliares geométricos extras

Um punhado de outras medidas geométricas existem como funções de nível de módulo que recebem um blob e retornam a medição solicitada:

  • image.get_solidity() retorna a solidez do blob – pixels divididos pela área do fecho convexo. Uma região sólida preenchida fica perto de 1.0; um blob com concavidades (uma ferradura, uma mão com os dedos abertos) cai bem abaixo disso.

  • image.get_convexity() retorna a convexidade – o perímetro do fecho convexo dividido pelo perímetro do blob. Um blob perfeitamente convexo é 1.0; blobs irregulares ou entalhados são menores.

  • image.get_major_axis_line() e image.get_minor_axis_line() retornam objetos Line ao longo dos eixos maior e menor do blob, derivados do retângulo rotacionado de área mínima.

  • image.get_enclosing_circle() retorna um Circle que envolve o blob – útil quando um estágio seguinte quer um círculo para desenhar ou testar.

  • image.get_enclosed_ellipse() retorna a tupla de 5 elementos (cx, cy, rx, ry, rotation) para uma elipse inscrita no retângulo de área mínima do blob. Os valores alimentam diretamente draw_ellipse().

5.25.7. Aprendendo um limiar automaticamente

Um detector de blobs é tão bom quanto os limiares com que é executado, e o trabalho de encontrar o limiar certo para uma cor-alvo é um problema à parte. Dois padrões comuns reduzem esse trabalho.

O primeiro é a seleção interativa na IDE: capture um quadro, arraste um retângulo ao redor de um exemplo da cor-alvo e deixe o editor de limiar da IDE informar os limites LAB que ele enxerga. Esses limites entram no script como os limiares de find_blobs() e o detector está pronto.

O segundo é o auto-aprendizado programático: uma rotina de calibração rodando na câmera captura um quadro, faz um histograma de um trecho conhecido onde o alvo está (get_histogram() com roi=), e lê a faixa de valores do trecho a partir do histograma com get_percentile(). O 5º percentil define o limite inferior de cada canal e o 95º o limite superior, ignorando pixels discrepantes nas duas extremidades. Em uma imagem RGB565 uma chamada de percentil informa os três canais LAB de uma vez, de modo 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)