2

I'm trying to implement the Atkinson dithering algorithm in a fragment shader in GLSL using our own Brad Larson's GPUImage framework. (This might be one of those things that is impossible but I don't know enough to determine that yet so I'm just going ahead and doing it anyway.)

The Atkinson algo dithers grayscale images into pure black and white as seen on the original Macintosh. Basically, I need to investigate a few pixels around my pixel and determine how far away from pure black or white each is and use that to calculate a cumulative "error;" that error value plus the original value of the given pixel determines whether it should be black or white. The problem is that, as far as I could tell, the error value is (almost?) always zero or imperceptibly close to it. What I'm thinking might be happening is that the texture I'm sampling is the same one that I'm writing to, so that the error ends up being zero (or close to it) because most/all of the pixels I'm sampling are already black or white.

Is this correct, or are the textures that I'm sampling from and writing to distinct? If the former, is there a way to avoid that? If the latter, then might you be able to spot anything else wrong with this code? 'Cuz I'm stumped, and perhaps don't know how to debug it properly.

varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;

uniform highp vec3 dimensions;

void main()
{
    highp vec2 relevantPixels[6];

    relevantPixels[0] = vec2(textureCoordinate.x, textureCoordinate.y - 2.0);
    relevantPixels[1] = vec2(textureCoordinate.x - 1.0, textureCoordinate.y - 1.0);
    relevantPixels[2] = vec2(textureCoordinate.x, textureCoordinate.y - 1.0);
    relevantPixels[3] = vec2(textureCoordinate.x + 1.0, textureCoordinate.y - 1.0);
    relevantPixels[4] = vec2(textureCoordinate.x - 2.0, textureCoordinate.y);
    relevantPixels[5] = vec2(textureCoordinate.x - 1.0, textureCoordinate.y);

    highp float err = 0.0;

    for (mediump int i = 0; i < 6; i++) {
        highp vec2 relevantPixel = relevantPixels[i];
        // @todo Make sure we're not sampling a pixel out of scope. For now this
        // doesn't seem to be a failure (?!).
        lowp vec4 pointColor = texture2D(inputImageTexture, relevantPixel);
        err += ((pointColor.r - step(.5, pointColor.r)) / 8.0);
    }

    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    lowp float hue = step(.5, textureColor.r + err);

    gl_FragColor = vec4(hue, hue, hue, 1.0);
}
Community
  • 1
  • 1
Garrett Albright
  • 2,844
  • 3
  • 26
  • 45

2 Answers2

2

There are a few problems here, but the largest one is that Atkinson dithering can't be performed in an efficient manner within a fragment shader. This kind of dithering is a sequential process, being dependent on the results of fragments above and behind it. A fragment shader can only write to one fragment in OpenGL ES, not neighboring ones like is required in that Python implementation you point to.

For potential shader-friendly dither implementations, see the question "Floyd–Steinberg dithering alternatives for pixel shader."

You also normally can't write to and read from the same texture, although Apple did add some extensions in iOS 6.0 that let you write to a framebuffer and read from that written value in the same render pass.

As to why you're seeing odd error results, the coordinate system within a GPUImage filter is normalized to the range 0.0 - 1.0. When you try to offset a texture coordinate by adding 1.0, you're reading past the end of the texture (which is then clamped to the value at the edge by default). This is why you see me using texelWidth and texelHeight values as uniforms in other filters that require sampling from neighboring pixels. These are calculated as a fraction of the overall image width and height.

I'd also not recommend doing texture coordinate calculation within the fragment shader, as that will lead to a dependent texture read and really slow down the rendering. Move that up to the vertex shader, if you can.

Finally, to answer your title question, usually you can't modify a texture as it is being read, but the iOS texture cache mechanism sometimes allows you to overwrite texture values as a shader is working its way through a scene. This leads to bad tearing artifacts usually.

Community
  • 1
  • 1
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • You know, I started to think about the 0.0 - 1.0 range thing while lying in bed after posting my question, as it made other things I had seen in your code make sense, but I wrote it off because it was too insane an idea and couldn't possibly be true. Huh. At any rate, thanks for the dose of reality. Any ideas how [1-Bit Camera](http://lindecrantz.com/onebitcamera/) pulls off this kind of dithering then? – Garrett Albright Feb 12 '13 at 07:39
  • @GarrettAlbright - I believe that ordered Bayer dithering can be done on a shader: http://devlog-martinsh.blogspot.com/2011/03/glsl-dithering.html , but I'm not sure about their claims of Atkinson dithering. Perhaps they are just using lower-resolution images and iterating over them on the CPU with the assistance of Accelerate. They might also have something that looks like this kind of dithering, but isn't strictly the same. – Brad Larson Feb 12 '13 at 15:57
  • Actually, once I had my head wrapped around the 0.0 - 1.0 range thing, I was able to write an implementation of the Bayer algo using the 4x4 matrix from Wikipedia's "[Ordered dithering](http://en.wikipedia.org/wiki/Ordered_dithering)" article. My results are a bit off, though; compare [my result](https://www.evernote.com/shard/s252/sh/2c1ea8a7-594f-42ef-9d91-63f9cc4c0930/60630bc0730137fe1e1518980e985376) with [Wikipedia's](http://en.wikipedia.org/wiki/File:Michelangelo%27s_David_-_Bayer.png). Getting closer, though! :P – Garrett Albright Feb 13 '13 at 08:26
  • I found the stupid obvious bug in my implementation and am [much closer now](https://www.evernote.com/shard/s252/sh/43c4ea96-217d-4f1a-a5f9-352494578b4c/4917f1d4e3da3eaf5e8b415880123963) (too late to edit my previous comment). – Garrett Albright Feb 13 '13 at 08:38
  • @GarrettAlbright - If you do get this working and want to submit it as a pull request, I'd gladly take it. – Brad Larson Feb 13 '13 at 17:15
  • Well, I have a couple more questions (one about this Bayer dithering and another one about the Atkinson attempt, which might be insane but I still don't want to give up on yet) that maybe only you can answer. What would be the best way? New questions here on SO? Posts on your forum on your site? Or can I be so bold as to email you directly? – Garrett Albright Feb 14 '13 at 14:12
  • @GarrettAlbright - If they're generally applicable, you can ask them here. If they're very specific to your problems, feel free to send me an email (my address is listed on the front page of the GPUImage project). If they're specific to the GPUImage project, you could also ask on the GitHub issues page for that. – Brad Larson Feb 14 '13 at 18:11
1

@GarrettAlbright For the 1-Bit Camera app I ended up with simply iterating over the image data using raw memory pointers and (rather) tightly optimized C code. I looked into NEON intrisics and the Accelerate framework, but any parallelism really screws up an algorithm of this nature so I didn't use it.

I also toyed around with the idea to do a decent enough aproximation of the error distribution on the GPU first, and then do the thresholding in another pass, but I never got anything but a rather ugly noise dither from those experiments. There are some papers around covering other ways of approaching diffusion dithering on the GPU.

Optiroc
  • 61
  • 6
  • Thanks for your feedback. I've shelved my idea for now due to this and [other stumbling-blocks](https://github.com/BradLarson/GPUImage/issues/847), but should I come back to it, I'll give the approaches you mentioned some consideration. – Garrett Albright May 09 '13 at 09:52