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 centroide – x, 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 forma – pixels, 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 mesclagem – code, 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 cantos – corners, 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.
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.3. Filtrando a busca¶
Um quadro capturado normalmente contém pixels que correspondem ao limiar por motivos diferentes do objeto que interessa à aplicação: reflexos especulares, objetos de fundo distantes, pixels de ruído de imagem que por acaso caem na faixa LAB. Os argumentos nomeados para find_blobs() são a primeira linha de defesa.
roi restringe a busca a uma região do quadro, do mesmo jeito que todo outro método do módulo image faz. Uma aplicação que sabe que o objeto só pode aparecer na metade inferior do campo de visão passa roi=(0, h//2, w, h//2) e ignora tudo acima; o tempo economizado retorna como taxa de quadros.
area_threshold e pixels_threshold ambos filtram blobs pequenos demais para serem relevantes. area_threshold descarta blobs cuja caixa delimitadora tem menos pixels de área que esse valor (bom para filtrar ruído disperso); pixels_threshold descarta blobs que têm menos pixels aprovados que esse valor (bom para filtrar blobs grandes mas esparsos, como um padrão pontilhado limiarizado com um ou dois pixels correspondendo aqui e ali). Ambos têm padrão 10; aumentá-los para centenas para um alvo em primeiro plano de alguns centímetros descarta cada partícula de pequeno ruído.
x_stride e y_stride definem o passo em pixels que o varredor dá enquanto procura um blob para começar a rastrear. O passo (stride) não é a resolução do rastreamento – o rastreamento sempre segue a borda real do blob com detalhe de pixel único – mas ele controla com que rapidez a varredura encontra um pixel inicial. Quando se sabe que os blobs são grandes (um alvo colorido do tamanho de um punho a cerca de trinta centímetros da câmera, facilmente com uma centena de pixels de largura), x_stride=4, y_stride=4 reduz o tempo de varredura em dezesseis vezes sem perda prática na detecção. Quando os blobs são pequenos (uma baliza de LED distante, com alguns pixels de largura), os passos têm que permanecer em 1 para evitar pulá-los por completo. invert inverte o teste de limiar: corresponder torna-se não-corresponder e a rotina retorna blobs de pixels reprovados em vez disso.
threshold_cb é um callback Python invocado em cada blob após a limiarização, mas antes de a lista final de resultados ser montada. O callback recebe o blob e retorna True para mantê-lo ou False para descartá-lo. Este é o lugar para aplicar filtros arbitrários de nível Python em propriedades que os argumentos nomeados não expõem diretamente – uma densidade mínima, uma faixa específica de rotação, uma combinação personalizada de bits de código após a mesclagem. Os argumentos nomeados são filtros em código nativo e rodam rápido; o callback roda em Python e é mais lento, porém ilimitado no que pode expressar.
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 de1.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()eimage.get_minor_axis_line()retornam objetosLineao longo dos eixos maior e menor do blob, derivados do retângulo rotacionado de área mínima.image.get_enclosing_circle()retorna umCircleque 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 diretamentedraw_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)