Como detectar cantos em imagens binárias com o OpenGL?


13

Tenho imagens binárias de 160 x 120, como:

imagem original

Eu gostaria de detectar os cantos dessas bolhas brancas. Eles são previamente fechados por morfologia matemática, portanto, não deve haver cantos internos. Nesse caso específico, eu gostaria de 16 cantos, como:

exemplo de detecção de cantos

Minha primeira tentativa foi usar algumas funções do OpenCV como goodFeaturesToTrack ou FAST, mas elas são particularmente lentas (mais o FAST é muito instável). Minha idéia seria fazer esse cálculo na GPU, pois minha imagem de origem vem dela. Procurei idéias na web sobre como escrever esses shaders (estou usando o OpenGL ES 2.0), mas não encontrei nada concreto. Alguma idéia de como eu poderia iniciar um algoritmo desse tipo?


2
RÁPIDO é lento? :)
endolith

1
sim engraçado né? na verdade, é mais rápido do que algoritmos precedentes como SURF ou SIFT, mas é menos precisa, bastante instável a partir de uma imagem para outra e ainda não rápido o suficiente para ser feito na CPU
Stéphane Pechard

Qual é a importância de detectá-las com precisão em todos os quadros? Com que rapidez os retângulos se movem? É correto detectar os cantos na maioria dos quadros e interpolar nos quadros em que o algoritmo está ausente?
Justis

@justis bem, a maneira como faço agora (através do uso das funções cvFindContours () e cvApproxPoly () do OpenCV) não é muito estável ao longo do tempo, então filtrei o resultado com um filtro passa-baixo, introduzindo o atraso. Você acha que posso obter um resultado mais estável com uma interpolação?
Stéphane Péchard

Respostas:


3

Em que tamanho de imagem você está operando? A que taxa de quadros? Em que hardware? RÁPIDO é bonito, erm, rápido na minha experiência.

Também vi o FAST usado como um detector de ROI com goodFeaturesToTrack executado nas ROIs identificadas para fornecer melhor estabilidade sem executar a penalidade do gFTT em toda a imagem.

O detector de canto "Harris" também é potencialmente muito rápido, pois é composto de operações muito simples (sem sqrt () por pixel, por exemplo!) - não tão estável quanto o gFTT, mas possivelmente mais do que o FAST.

(Em termos de implementação de GPU, o Google gpu cornerparece apresentar muitos links, mas não tenho idéia de quão adequados eles sejam - tenho a tendência de implementar no FPGA.)


Minhas imagens são de 160x120, supostamente a 30fps, em um iPhone, mas é claro, o aplicativo tem muito mais a fazer :-) Eu vi um aplicativo implementando o RÁPIDO rapidamente em um dispositivo assim, mas era apenas uma demonstração fazendo isso ... É por isso que estou procurando soluções baseadas em gpu.
Stéphane Péchard

15

Por acaso, eu estava implementando algo assim no OpenGL ES 2.0 usando a detecção de canto Harris e, embora ainda não tenha terminado completamente, pensei em compartilhar a implementação baseada em shader que tenho até agora. Fiz isso como parte de uma estrutura de código-fonte aberto baseada no iOS , para que você possa conferir o código se estiver curioso para saber como funciona uma etapa específica.

Para fazer isso, eu uso as seguintes etapas:

  • Reduza a imagem para seus valores de luminância usando um produto escalar dos valores RGB com o vetor (0,2125, 0,7154, 0,0721).
  • Calcule as derivadas X e Y subtraindo os valores do canal vermelho dos pixels esquerdo e direito e acima e abaixo do pixel atual. Em seguida, armazeno a derivada x ao quadrado no canal vermelho, a derivada Y ao quadrado no canal verde e o produto das derivadas X e Y no canal azul. O sombreador de fragmento para isso se parece com o seguinte:

    precision highp float;
    
    varying vec2 textureCoordinate;
    varying vec2 leftTextureCoordinate;
    varying vec2 rightTextureCoordinate;
    
    varying vec2 topTextureCoordinate; 
    varying vec2 bottomTextureCoordinate;
    
    uniform sampler2D inputImageTexture;
    
    void main()
    {
     float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;
     float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;
     float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;
     float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;
    
     float verticalDerivative = abs(-topIntensity + bottomIntensity);
     float horizontalDerivative = abs(-leftIntensity + rightIntensity);
    
     gl_FragColor = vec4(horizontalDerivative * horizontalDerivative, verticalDerivative * verticalDerivative, verticalDerivative * horizontalDerivative, 1.0);
    }
    

    onde as variações são apenas as coordenadas de textura de deslocamento em cada direção. Eu pré-calculo isso no sombreador de vértices para eliminar leituras de textura dependentes, que são notoriamente lentas nessas GPUs móveis.

  • Aplique um desfoque gaussiano a esta imagem derivada. Usei um desfoque horizontal e vertical separado e aproveite a filtragem de textura de hardware para fazer um desfoque de nove hits com apenas cinco leituras de textura em cada passagem. Eu descrevo esse shader nesta resposta do Stack Overflow .

  • Execute o cálculo real da detecção de canto de Harris usando os valores derivados da entrada desfocada. Nesse caso, na verdade, estou usando o cálculo descrito por Alison Noble em seu doutorado. dissertação "Descrições de superfícies de imagem". O sombreador que lida com isso se parece com o seguinte:

    varying highp vec2 textureCoordinate;
    
    uniform sampler2D inputImageTexture;
    
    const mediump float harrisConstant = 0.04;
    
    void main()
    {
     mediump vec3 derivativeElements = texture2D(inputImageTexture, textureCoordinate).rgb;
    
     mediump float derivativeSum = derivativeElements.x + derivativeElements.y;
    
     // This is the Noble variant on the Harris detector, from 
     // Alison Noble, "Descriptions of Image Surfaces", PhD thesis, Department of Engineering Science, Oxford University 1989, p45.     
     mediump float harrisIntensity = (derivativeElements.x * derivativeElements.y - (derivativeElements.z * derivativeElements.z)) / (derivativeSum);
    
     // Original Harris detector
     //     highp float harrisIntensity = derivativeElements.x * derivativeElements.y - (derivativeElements.z * derivativeElements.z) - harrisConstant * derivativeSum * derivativeSum;
    
     gl_FragColor = vec4(vec3(harrisIntensity * 10.0), 1.0);
    }
    
  • Execute a supressão não máxima local e aplique um limite para destacar os pixels que passam. Eu uso o seguinte fragmento shader para amostrar os oito pixels na vizinhança de um pixel central e identificar se é o máximo nesse agrupamento:

    uniform sampler2D inputImageTexture;
    
    varying highp vec2 textureCoordinate;
    varying highp vec2 leftTextureCoordinate;
    varying highp vec2 rightTextureCoordinate;
    
    varying highp vec2 topTextureCoordinate;
    varying highp vec2 topLeftTextureCoordinate;
    varying highp vec2 topRightTextureCoordinate;
    
    varying highp vec2 bottomTextureCoordinate;
    varying highp vec2 bottomLeftTextureCoordinate;
    varying highp vec2 bottomRightTextureCoordinate;
    
    void main()
    {
        lowp float bottomColor = texture2D(inputImageTexture, bottomTextureCoordinate).r;
        lowp float bottomLeftColor = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;
        lowp float bottomRightColor = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;
        lowp vec4 centerColor = texture2D(inputImageTexture, textureCoordinate);
        lowp float leftColor = texture2D(inputImageTexture, leftTextureCoordinate).r;
        lowp float rightColor = texture2D(inputImageTexture, rightTextureCoordinate).r;
        lowp float topColor = texture2D(inputImageTexture, topTextureCoordinate).r;
        lowp float topRightColor = texture2D(inputImageTexture, topRightTextureCoordinate).r;
        lowp float topLeftColor = texture2D(inputImageTexture, topLeftTextureCoordinate).r;
    
        // Use a tiebreaker for pixels to the left and immediately above this one
        lowp float multiplier = 1.0 - step(centerColor.r, topColor);
        multiplier = multiplier * 1.0 - step(centerColor.r, topLeftColor);
        multiplier = multiplier * 1.0 - step(centerColor.r, leftColor);
        multiplier = multiplier * 1.0 - step(centerColor.r, bottomLeftColor);
    
        lowp float maxValue = max(centerColor.r, bottomColor);
        maxValue = max(maxValue, bottomRightColor);
        maxValue = max(maxValue, rightColor);
        maxValue = max(maxValue, topRightColor);
    
        gl_FragColor = vec4((centerColor.rgb * step(maxValue, centerColor.r) * multiplier), 1.0);
    }
    

Esse processo gera um mapa de cantos de seus objetos que se parece com isso:

Mapa Cornerness

Os seguintes pontos são identificados como cantos com base na supressão e limiares não máximos:

Cantos identificados

Com os limites adequados definidos para esse filtro, ele pode identificar todos os 16 cantos nessa imagem, embora tenda a colocar os cantos com um pixel ou mais dentro das bordas reais do objeto.

Em um iPhone 4, essa detecção de canto pode ser executada a 20 FPS em quadros de vídeo de 640x480 provenientes da câmera, e um iPhone 4S pode processar facilmente vídeos desse tamanho em mais de 60 FPS. Isso deve ser muito mais rápido que o processamento vinculado à CPU para uma tarefa como essa, embora, no momento, o processo de leitura dos pontos seja vinculado à CPU e um pouco mais lento do que deveria ser.

Se você quiser ver isso em ação, poderá pegar o código da minha estrutura e executar o exemplo do FilterShowcase que vem com ela. O exemplo de detecção de canto da Harris é executado em vídeo ao vivo a partir da câmera do dispositivo, embora, como mencionei, a leitura dos pontos dos cantos ocorra atualmente na CPU, o que está realmente diminuindo a velocidade. Também estou migrando para um processo baseado em GPU.


1
Muito agradável! Eu sigo sua estrutura no github, parece realmente interessante, parabéns!
Stéphane Péchard

Você tem algum exemplo em algum lugar de como obter as coordenadas dos cantos de volta à CPU? Existe alguma maneira inteligente de GPU ou requer uma readback e depois faz um loop na CPU através do bitmap retornado procurando por pixels marcados?
Quasimondo

@Quasimondo - Estou trabalhando no uso de pirâmides de histograma para extração de pontos: tevs.eu/files/vmv06.pdf , a fim de evitar a iteração vinculada à CPU sobre os pixels para a detecção de canto. Ultimamente fiquei um pouco distraído, então ainda não terminei isso, mas gostaria de fazê-lo em breve.
Brad Larson

Oi @BradLarson, sei que esse é um tópico muito antigo e obrigado por sua resposta. Acabei de verificar o KGPUImageHarrisCornerDetection.m na estrutura GPUImage. Para extrair a localização do canto da imagem, você usou o glReadPixels para ler a imagem no buffer e, em seguida, fez um loop no buffer para armazenar pontos com colotByte> 0 em uma matriz. Existe alguma maneira de fazer isso tudo na GPU em que não precisamos ler a imagem no buffer e no loop?
precisa

1
@SahilBajaj - Uma técnica que eu vi (e ainda não tive tempo de implementar) é usar pirâmides de histograma para fazer a extração rápida de pontos de imagens esparsas como esta. Isso aceleraria significativamente isso.
Brad Larson

3

Detectores de esquina "robustos" como Shi-Tomasi e Moravec são notoriamente lentos. verifique-os aqui - http://en.wikipedia.org/wiki/Corner_detection O FAST provavelmente é o único detector de canto leve o suficiente. Você pode melhorar o FAST fazendo a supressão não máxima - escolha a saída FAST com a melhor pontuação de "facilidade" (existem várias maneiras intuitivas de calculá-la, incluindo Shi-Tomasi e Moravec como pontuação de facilidade). Você também pode escolher entre vários detectores FAST - do FAST-5 ao FAST-12 e FAST_ER (o último provavelmente é grande demais para dispositivos móveis) Outra maneira é gerar o FAST - obtenha o gerador de código FAST no site do autor e treine-o no conjunto de imagens prováveis. http://www.edwardrosten.com/work/fast.html


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.