5.26. Encontrar linhas e segmentos

Algumas características de cenas não são regiões conectadas de cor mas arestas rectas orientadas: uma linha pintada no chão, a emenda entre duas superfícies, o lado de um rectângulo impresso, a aresta de uma porta. Pedir ao detector de manchas que as encontre é a pergunta errada – a aresta tem um pixel de largura, o algoritmo de manchas quer área-com-cor, e a resposta volta vazia ou com ruído.

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

5.26.1. Como funciona a transformada de Hough

Ambos os detectores partilham a mesma ideia central, pelo que vale a pena entendê-la uma vez. O módulo image começa por executar um filtro de aresta ao estilo Sobel na entrada para pontuar cada pixel pela probabilidade de estar sobre uma aresta orientada. Cada pixel de aresta vota então em todas as linhas em que pode estar. As linhas que acumulam mais votos ganham.

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

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

5.26.2. Linhas infinitas

find_lines() executa a transformada de Hough e devolve 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))

threshold é o total mínimo de votos para uma linha ser aceite. O total de votos soma as magnitudes de aresta Sobel de cada pixel contribuinte, pelo que valores de threshold maiores exigem arestas mais longas ou mais fortes para passar – o que faz com que o valor correcto dependa da resolução da imagem (uma linha mais longa a uma resolução mais elevada acumula mais votos) bem como da cena, pelo que tem de ser ajustado para a aplicação em particular. Como pontos de partida aproximados para ajuste: 1000 para uma linha modesta numa imagem clara, 500 ou abaixo para contraste fraco ou linhas curtas, 2000 ou mais para cenas movimentadas onde linhas de falsos positivos se formam através de grupos de ruído de aresta.

theta_margin e rho_margin controlam a fusão de máximos próximos. Uma única aresta física produz um pequeno conjunto de bins de alto-voto em torno do seu verdadeiro (theta, rho), e o detector colapsa cada conjunto no seu pico antes de devolver. theta_margin=25 (graus) funde quaisquer picos dentro de 25 graus de orientação; rho_margin=25 (pixels) funde picos dentro de 25 pixels de distância. Os valores predefinidos são razoáveis; aumentá-los devolve menos linhas, mais distintas, e diminuí-los devolve mais linhas, por vezes duplicadas.

x_stride e y_stride avançam pelos pixels de aresta durante a votação, da mesma forma que avançam pelos pixels em find_blobs(). Os valores predefinidos de 2 e 1 funcionam para o caso comum; aumentá-los acelera a pesquisa ao custo da resolução. roi restringe a pesquisa a uma região do fotograma, o que tanto restringe as linhas devolvidas como reduz o trabalho.

Cada linha devolvida é desenhável directamente: o objecto Line passa directamente para draw_line(), que lê os campos de extremidades (x1, y1, x2, y2) da sua frente. l.theta é o ângulo em graus, que classifica a linha como horizontal, vertical ou diagonal numa única comparação. l.magnitude é o total de votos, que ordena as linhas devolvidas da mais forte para a mais fraca.

5.26.3. Segmentos de linha

find_lines() é o detector correcto para arestas que abrangem todo o fotograma, mas muitas arestas reais – o lado esquerdo de um código de barras impresso, a aresta superior de uma etiqueta, o lado visível de uma régua – apenas percorrem parte da imagem. find_line_segments() devolve segmentos finitos cujas extremidades estão dentro do fotograma:

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 percorre directamente os pixels de aresta orientados, em vez de votar no espaço de Hough, e o resultado é uma colecção de curtos percursos rectos. merge_distance define o espaçamento máximo de pixels que dois curtos percursos colineares podem ter e ainda se fundir num único segmento devolvido; max_theta_difference define quantos graus de orientação o fusionador tolera entre percursos adjacentes. Uma fusão generosa (merge_distance=10, max_theta_difference=15) devolve um pequeno número de segmentos longos ao custo de por vezes unir arestas genuinamente separadas; uma fusão estrita (merge_distance=0, max_theta_difference=5) devolve muitos segmentos curtos e deixa a aplicação ordenar em Python.

Os objectos de resultado são do mesmo tipo Line que find_lines() devolve, com as mesmas propriedades, pelo que um pipeline pode processar qualquer tipo de deteção pelo mesmo caminho de código subsequente. A única diferença prática é que as extremidades dos segmentos são as extremidades reais da linha na imagem, enquanto que as extremidades das linhas infinitas são onde a linha cruza a fronteira da imagem.

5.26.4. Quando usar cada um

A escolha entre os dois métodos resume-se a uma única questão: a aplicação precisa de saber onde a linha termina?

find_lines() é a ferramenta certa quando a resposta é não. Um robô seguidor de linha precisa de saber em que direcção a linha vai e onde cruza a parte inferior do fotograma; a própria linha vai até ao horizonte e além. Um detector de horizonte quer a aresta orientada mais forte na imagem; não precisa de saber onde o horizonte termina.

find_line_segments() é a ferramenta certa quando a resposta é sim. Identificar os quatro lados de um rectângulo impresso necessita de quatro segmentos com extremidades conhecidas. Seguir um dedo a apontar para um ecrã significa seguir um segmento curto cujas extremidades são a ponta e a base do dedo. Medir o comprimento de um risco visível necessita da extensão real do segmento em pixels.

Ambos os detectores partilham uma limitação comum: precisam de contraste. O filtro de aresta Sobel em que se baseiam responde a gradientes de brilho; uma aresta colorida contra um fundo igualmente brilhante (uma linha vermelha numa parede verde com a mesma luminância) não produz gradiente nem deteção. Quando esse caso aparece na prática, a solução é extrair um único canal LAB como uma imagem em escala de cinzentos com o contraste adequado antes de pesquisar – to_grayscale() com o canal b seleccionado isola o vermelho contra o verde onde o canal de luminância por si só é plano – e passar essa imagem de canal ao detector de linhas.