5.27. Encontrar círculos e rectângulos

As linhas e segmentos cobrem as arestas rectas do fotograma capturado, mas muitas das características do mundo real que a câmara procura não são rectas. Uma moeda pousada numa secretária é um círculo. Uma etiqueta impressa, um post-it, o topo de uma caixa vista de lado é um quadrilátero. O módulo image expõe um detector dedicado para cada caso: uma pesquisa ao estilo Hough para círculos e uma pesquisa derivada do AprilTag para formas de quatro lados.

Ambos os métodos seguem o mesmo modelo que os detectores de linhas – threshold controla quantos votos uma deteção necessita, roi restringe a pesquisa, e os objectos devolvidos transportam tanto uma posição como uma magnitude de confiança – mas os espaços de parâmetros e os valores predefinidos adequados diferem o suficiente para merecerem tratamento explícito.

5.27.1. Círculos de Hough

find_circles() executa a variante circular da transformada de Hough. Cada pixel de aresta da pré-passagem de Sobel vota em todos os círculos que podem passar por ele; os círculos que acumulam votos suficientes são devolvidos.

circles = img.find_circles(threshold=3500,
                            x_margin=10, y_margin=10, r_margin=10,
                            r_min=10, r_max=80, r_step=2)

for c in circles:
    img.draw_circle((c.x, c.y, c.r), color=(255, 0, 0))

threshold é a soma mínima das magnitudes das arestas de Sobel ao longo do círculo candidato. Círculos maiores percorrem mais pixels e por isso precisam de limiares mais elevados para passar; um valor que encontra uma moeda de raio 20 pixels também dispara sobre o ruído em torno de uma aresta de 100 pixels, enquanto que um valor ajustado para a moeda grande irá perder a pequena. Quando o intervalo de raio alvo é conhecido, o valor correcto do limiar escala com a circunferência – aproximadamente threshold = 50 * 2 * pi * r constitui um ponto de partida razoável e o valor certo resulta de uma breve passagem de ajuste.

r_min, r_max, e r_step definem a pesquisa de raio. Sem limites, o detector pesquisaria todos os raios desde alguns pixels até metade da largura da imagem, o que é simultaneamente lento e uma receita para falsos positivos. Definir r_min e r_max para delimitar o tamanho do alvo esperado com uma margem generosa (por exemplo, r_min=15, r_max=25 para uma moeda com cerca de 20 pixels) reduz substancialmente o trabalho e melhora o rácio sinal-ruído dos votos. r_step controla a granularidade da pesquisa; passos maiores correm mais rápido e podem perder um círculo cujo raio verdadeiro caia entre dois valores amostrados. O valor predefinido r_step=2 é um compromisso razoável.

x_margin, y_margin, e r_margin controlam a fusão de detecções próximas, da mesma forma que theta_margin e rho_margin fazem para a deteção de linhas. Um único círculo físico na imagem vota para um conjunto de círculos candidatos cujos centros e raios concordam até alguns pixels; as margens colapsam cada conjunto no seu pico antes de a lista de resultados ser construída. Margens maiores devolvem menos detecções mais confiantes; margens menores devolvem mais detecções com possíveis quase-duplicados.

x_stride e y_stride avançam a varredura de votação, da mesma forma que fazem nos outros detectores. O valor predefinido de 2 e 1 é adequado para a maioria das imagens; aumentar ambos para 4 é o compromisso de velocidade padrão para uma imagem conhecida por conter círculos grandes.

Cada Circle devolvido transporta x, y, r (o centro e raio) e magnitude (o total de votos, útil como pontuação de confiança para ordenação ou filtragem). Desenhar a deteção de volta no fotograma é uma única chamada – draw_circle() recebe o 3-tuplo (x, y, r), disponível como (c.x, c.y, c.r) directamente no resultado.

5.27.2. Rectângulos

find_rects() empresta o detector de quadriláteros do pipeline AprilTag – a mesma rotina que localiza o quadrado negro em torno de uma etiqueta é exposta por si só como um localizador de rectângulos de uso geral.

rects = img.find_rects(threshold=12000)

for r in rects:
    img.draw_rectangle(r.rect, color=(0, 255, 0))
    for corner in r.corners:
        img.draw_circle((corner[0], corner[1], 4),
                        color=(0, 255, 0))

threshold é a soma mínima das magnitudes de aresta em torno do perímetro do rectângulo. Um rectângulo preto sobre branco impresso num fotograma bem iluminado ultrapassa facilmente 10000; um rectângulo ténue sobre um fundo com textura pode necessitar descer para 2000 – trocando falsos positivos por sensibilidade. Tal como o detector de círculos, o valor correcto resulta de uma passagem rápida de ajuste com os alvos pretendidos em vista.

O detector é projectivo – encontra quadriláteros cujos lados são rectos mas não necessariamente paralelos ou alinhados com os eixos. Uma etiqueta vista de lado parece um trapézio na imagem, e o detector de rectângulos encontra-o correctamente; um rectângulo alinhado com os eixos é apenas o caso degenerado em que os quatro cantos formam uma caixa de ângulo recto. roi restringe a pesquisa; os restantes argumentos de palavra-chave assumem os valores predefinidos do pipeline AprilTag e raramente necessitam de ajuste.

Cada Rect devolvido transporta a caixa delimitadora alinhada com os eixos – x, y, w, h, mais o 4-tuplo rect que draw_rectangle() espera – e os quatro cantos detectados como corners. A caixa delimitadora é o que a aplicação usa para posição e tamanho aproximados; os cantos descrevem o próprio quadrilátero projectivo. Quando a câmara está a visualizar um alvo plano de um ângulo e a aplicação precisa de desfazer o efeito de perspectiva – ler texto numa etiqueta, amostrar cor de uma superfície plana – os cantos alimentam directamente rotation_corr() com a palavra-chave corners= (veja correcção de lente e perspectiva), e o resultado é o rectângulo rectificado pronto para qualquer análise subsequente.

Aviso

Como o detector está ajustado para o que o pipeline AprilTag necessita – quadriláteros com bordas fortes e de alto contraste, como o contorno negro de uma etiqueta sobre papel branco – não é uma passagem que encontra todos os rectângulos. Rectângulos com contraste suave, arestas com textura ou ambientes movimentados podem não ser detectados de todo. O seu funcionamento depende da situação: teste-o com os alvos reais cedo, antes de construir um pipeline em torno dele.

5.27.3. Quando o detector falha

Os círculos em particular beneficiam de um pré-filtro na entrada. Um fotograma com ruído produz muitos pixels de aresta dispersos que todos votam, e o espaço de Hough resultante tem picos largos e difusos que o fusionador tem dificuldade em separar. Uma passagem gaussian() ou mean() antes de find_circles() suaviza o ruído e deixa as arestas verdadeiras intactas; o detector devolve picos mais nítidos em menos tempo.

Para os rectângulos, o modo de falha comum é o oposto: baixo contraste entre o rectângulo e o seu fundo significa que a soma das magnitudes de aresta nunca ultrapassa o threshold. Uma passagem histeq() para redistribuir a gama de brilho por todo o intervalo de 0 a 255 restaura o contraste de que o detector necessita. (O contraste tem de existir algures na imagem; a equalização de histograma só pode amplificar o que já lá está.)