If Objective-C is valid, you can see my GPUImage and your GPUImageSobelEdgeDetectionFilter file. This applies to Sobel edge detection using OpenGL ES 2.0 shaders. You can see the result of this in the βsketchβ example in this answer .
If you don't want to dig Objective-C code, the critical work here is done by two sets of shaders. In the first pass, I reduce the image to its brightness and save this value in the red, green and blue channels. I do this using the following vertex shader:
attribute vec4 position; attribute vec4 inputTextureCoordinate; varying vec2 textureCoordinate; void main() { gl_Position = position; textureCoordinate = inputTextureCoordinate.xy; }
and fragment shader:
precision highp float; varying vec2 textureCoordinate; uniform sampler2D inputImageTexture; const highp vec3 W = vec3(0.2125, 0.7154, 0.0721); void main() { float luminance = dot(texture2D(inputImageTexture, textureCoordinate).rgb, W); gl_FragColor = vec4(vec3(luminance), 1.0); }
After that, I actually do a Sobel edge detection (in this case, the lighter pixels are edges) using this vertex shader:
attribute vec4 position; attribute vec4 inputTextureCoordinate; uniform highp float imageWidthFactor; uniform highp float imageHeightFactor; varying vec2 textureCoordinate; varying vec2 leftTextureCoordinate; varying vec2 rightTextureCoordinate; varying vec2 topTextureCoordinate; varying vec2 topLeftTextureCoordinate; varying vec2 topRightTextureCoordinate; varying vec2 bottomTextureCoordinate; varying vec2 bottomLeftTextureCoordinate; varying vec2 bottomRightTextureCoordinate; void main() { gl_Position = position; vec2 widthStep = vec2(imageWidthFactor, 0.0); vec2 heightStep = vec2(0.0, imageHeightFactor); vec2 widthHeightStep = vec2(imageWidthFactor, imageHeightFactor); vec2 widthNegativeHeightStep = vec2(imageWidthFactor, -imageHeightFactor); textureCoordinate = inputTextureCoordinate.xy; leftTextureCoordinate = inputTextureCoordinate.xy - widthStep; rightTextureCoordinate = inputTextureCoordinate.xy + widthStep; topTextureCoordinate = inputTextureCoordinate.xy + heightStep; topLeftTextureCoordinate = inputTextureCoordinate.xy - widthNegativeHeightStep; topRightTextureCoordinate = inputTextureCoordinate.xy + widthHeightStep; bottomTextureCoordinate = inputTextureCoordinate.xy - heightStep; bottomLeftTextureCoordinate = inputTextureCoordinate.xy - widthHeightStep; bottomRightTextureCoordinate = inputTextureCoordinate.xy + widthNegativeHeightStep; }
and this fragmented shader:
precision highp float; varying vec2 textureCoordinate; varying vec2 leftTextureCoordinate; varying vec2 rightTextureCoordinate; varying vec2 topTextureCoordinate; varying vec2 topLeftTextureCoordinate; varying vec2 topRightTextureCoordinate; varying vec2 bottomTextureCoordinate; varying vec2 bottomLeftTextureCoordinate; varying vec2 bottomRightTextureCoordinate; uniform sampler2D inputImageTexture; void main() { float i00 = texture2D(inputImageTexture, textureCoordinate).r; float im1m1 = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r; float ip1p1 = texture2D(inputImageTexture, topRightTextureCoordinate).r; float im1p1 = texture2D(inputImageTexture, topLeftTextureCoordinate).r; float ip1m1 = texture2D(inputImageTexture, bottomRightTextureCoordinate).r; float im10 = texture2D(inputImageTexture, leftTextureCoordinate).r; float ip10 = texture2D(inputImageTexture, rightTextureCoordinate).r; float i0m1 = texture2D(inputImageTexture, bottomTextureCoordinate).r; float i0p1 = texture2D(inputImageTexture, topTextureCoordinate).r; float h = -im1p1 - 2.0 * i0p1 - ip1p1 + im1m1 + 2.0 * i0m1 + ip1m1; float v = -im1m1 - 2.0 * im10 - im1p1 + ip1m1 + 2.0 * ip10 + ip1p1; float mag = length(vec2(h, v)); gl_FragColor = vec4(vec3(mag), 1.0); }
imageWidthFactor and imageHeightFactor are simply the inverse of the input image in pixels.
You may have noticed that this two-pass approach is more complex than the one in the answer above. This is due to the fact that the initial implementation was not the most effective when working on mobile GPUs (at least for a variety of PowerVR on iOS devices). Having removed all the dependent texture readings and preliminarily calculated the brightness, so I only need to try from the red channel in the final shader, this method of detecting the adjusted edge in my tests is 20 times faster than the naive one, which does it all in one pass.