5.26. Encontrando linhas e segmentos

Algumas características de uma cena não são regiões conectadas de cor, mas bordas retas orientadas: uma linha pintada no chão, a junção entre duas superfícies, o lado de um retângulo impresso, a borda de um vão de porta. Pedir ao detector de blobs que as encontre é a pergunta errada – a borda tem um pixel de largura, o algoritmo de blob quer área-com-cor, e a resposta volta vazia ou ruidosa.

O detector certo para bordas orientadas é a transformada de linha de Hough. O módulo image a expõe em duas variantes: find_lines() retorna linhas infinitas (cada linha se estende por toda a imagem); find_line_segments() retorna segmentos finitos (cada linha tem extremidades dentro do quadro). Qual delas a aplicação precisa depende de as bordas de interesse serem contínuas por todo o quadro ou abrangerem apenas parte dele.

5.26.1. Como a transformada de Hough funciona

Ambos os detectores compartilham a mesma ideia central, então vale a pena entendê-la uma vez. O módulo image primeiro executa um filtro de borda no estilo Sobel sobre a entrada para pontuar cada pixel por quão provável é que ele esteja sobre uma borda orientada. Cada um desses pixels de borda então vota em todas as linhas sobre as quais poderia estar. As linhas que acumulam mais votos vencem.

Uma linha é parametrizada no espaço de Hough por dois números: theta, o ângulo da linha (0 – 179 graus), e rho, a distância perpendicular da origem da imagem até a linha (com sinal, em pixels). Toda linha que a imagem contém é um ponto no espaço (theta, rho). Cada pixel de borda na entrada contribui com um voto para toda combinação (theta, rho) consistente com sua posição – conceitualmente, uma curva através do espaço de Hough. Onde muitas dessas curvas se cruzam, muitos pixels de borda concordam com a mesma linha, e esse cruzamento é uma detecção.

O detector retorna os máximos locais no espaço de Hough cujos totais de votos excedem um limiar. Cada Line retornada carrega ambas as representações: x1, y1, x2, y2 para a forma de extremidades (recortada aos limites da imagem no caso infinito), theta, rho para a forma de Hough, e length e magnitude para o tamanho e a contagem de votos, respectivamente.

5.26.2. Linhas infinitas

find_lines() executa a transformada de Hough e retorna as linhas mais fortes, cada uma estendida por toda a imagem:

lines = img.find_lines(threshold=1500, theta_margin=25, rho_margin=25)

for l in lines:
    img.draw_line(l, color=(255, 0, 0))

O threshold é o total mínimo de votos para que uma linha seja aceita. O total de votos soma as magnitudes de borda Sobel de cada pixel contribuinte, de modo que valores maiores de threshold exigem bordas mais longas ou mais fortes para passar – o que faz o valor correto depender da resolução da imagem (uma linha mais longa em uma resolução maior acumula mais votos) bem como da cena, então ele precisa ser ajustado para a aplicação específica. Como pontos de partida aproximados a partir dos quais ajustar: 1000 para uma linha modesta em uma imagem clara, 500 ou abaixo para contraste fraco ou linhas curtas, 2000 ou mais para cenas movimentadas onde linhas de falso positivo se formam por meio de agrupamentos de ruído de borda.

theta_margin e rho_margin controlam a fusão de máximos próximos. Uma única borda física produz um pequeno agrupamento de bins com muitos votos ao redor de seu (theta, rho) verdadeiro, e o detector colapsa cada agrupamento em seu pico antes de retornar. theta_margin=25 (graus) mescla quaisquer picos dentro de 25 graus de orientação; rho_margin=25 (pixels) mescla picos dentro de 25 pixels de distância. Os padrões são razoáveis; aumentá-los retorna menos linhas, mais distintas, e diminuí-los retorna mais linhas, às vezes duplicadas.

x_stride e y_stride percorrem os pixels de borda durante a votação, da mesma forma que percorrem os pixels em find_blobs(). Os padrões de 2 e 1 funcionam para o caso comum; aumentá-los acelera a busca ao custo da resolução. roi restringe a busca a uma região do quadro, o que tanto reduz as linhas retornadas quanto diminui o trabalho.

Cada linha retornada é diretamente desenhável: o objeto Line é passado diretamente para draw_line(), que lê os campos de extremidades (x1, y1, x2, y2) logo no início dele. l.theta é o ângulo em graus, que classifica a linha como horizontal, vertical ou diagonal em uma única comparação. l.magnitude é o total de votos, que ordena as linhas retornadas da mais forte para a mais fraca.

5.26.3. Segmentos de linha

find_lines() é o detector certo para bordas que abrangem todo o quadro, mas muitas bordas reais – o lado esquerdo de um código de barras impresso, a borda superior de um rótulo, o lado visível de uma régua – só percorrem parte da imagem. find_line_segments() retorna segmentos finitos cujas extremidades estão dentro do quadro:

segments = img.find_line_segments(merge_distance=5, max_theta_difference=10)

for s in segments:
    img.draw_line(s, color=(0, 255, 0))

O detector de segmentos rastreia diretamente ao longo dos pixels de borda orientados, em vez de votar no espaço de Hough, e o resultado é uma coleção de trechos retos curtos. merge_distance define o intervalo máximo em pixels que dois trechos curtos colineares podem abranger e ainda assim se fundir em um único segmento retornado; max_theta_difference define quantos graus de orientação o mesclador tolera entre trechos adjacentes. Uma fusão generosa (merge_distance=10, max_theta_difference=15) retorna um pequeno número de segmentos longos ao custo de às vezes interligar bordas genuinamente separadas; uma fusão estrita (merge_distance=0, max_theta_difference=5) retorna muitos segmentos curtos e deixa a aplicação resolvê-los em Python.

Os objetos de resultado são do mesmo tipo Line que find_lines() retorna, com as mesmas propriedades, de modo que um pipeline pode processar qualquer um dos tipos de detecção pelo mesmo caminho de código a jusante. A única diferença prática é que as extremidades dos segmentos são as extremidades reais da linha na imagem, ao passo que as extremidades das linhas infinitas ficam onde quer que a linha cruze a borda da imagem.

5.26.4. Quando usar cada um

A escolha entre os dois métodos se resume a uma única pergunta: a aplicação se importa com onde a linha termina?

find_lines() é a ferramenta certa quando a resposta é não. Um robô seguidor de linha precisa saber para que lado a linha vai e onde ela cruza a parte inferior do quadro; a linha em si segue até o horizonte e além. Um detector de horizonte quer a borda orientada mais forte da imagem; ele não precisa saber onde o horizonte termina.

find_line_segments() é a ferramenta certa quando a resposta é sim. Identificar os quatro lados de um retângulo impresso requer quatro segmentos com extremidades conhecidas. Rastrear um dedo apontando para um display significa seguir um segmento curto cujas extremidades são a ponta e a base do dedo. Medir o comprimento de um arranhão visível requer a extensão real do segmento em pixels.

Ambos os detectores compartilham uma limitação comum: eles precisam de contraste. O filtro de borda Sobel sobre o qual se baseiam responde a gradientes de brilho; uma borda colorida contra um fundo igualmente claro (uma linha vermelha sobre uma parede verde de mesma luminância) não produz gradiente nem detecção. Quando esse caso surge na prática, a solução é extrair um único canal LAB como imagem em escala de cinza com o contraste correto antes de buscar – to_grayscale() com o canal b selecionado isola o vermelho contra o verde onde o canal de luminância sozinho fica plano – e entregar essa imagem de canal ao detector de linhas.