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:
Os seguintes pontos são identificados como cantos com base na supressão e limiares não máximos:
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.