Dithering soft gradients

I have a procedural algorithm that generates pictures pixel by pixel and sometimes produces soft, curving gradients which show banding.

To fix the banding I’m rendering into an array of Singles then dither that to the pixels [0-255] range. I’ve implemented Floyd-Steinberg and Sierra-3 dithering and they do an ok job smoothing linear gradients but curving gradients still look banded.

Does anyone know a better dithering algorithm for shallow gradients? Do I need a larger error distribution range or Riemersma dithering or ordered dithering or…? I’m lost in a world of algorithms and not sure which to pursue.

grey gradient from 30 to 50
top: set directly
middle: Floyd-Steinberg
bottom: Sierra-3

line

circle

I do wonder if some of this is due to the limitation of drawing colors in Xojo that are limited to 8bpc. On OS X, if you declares to draw gradients (CGGradient, NSGradient or CIGradient sub classes) they can come out really smooth, but then all three of these classes take colors which use floating point precision. Even if most screens are only 8bpc.

I looked into testing a CGGradient but see it requires a CGColorSpace and that’s something I haven’t quite wrapped my head around yet.

Thinking about color space though got me thinking it’s my display. When I draw columns of grey increasing 1 by 1 I get a perceptual grouping of columns. The columns increase linearly within, but between groups the step seems to be bigger, stand out in some way.

In this picture I marked off the groupings I see then looked at it on my phone (identical grouping) and on another laptop (some groupings were different). Does everyone see these groups?

Oddly, measuring the grey values with the OS’s pipette yields inconsistent results. Two visually distinct columns give the value 34 while another pair of columns that straddle the group discontinuity go up by just 1.

This got me thinking colors really are floating point and the [0-255] values are just for display purposes.

Then I remembered a javascript dithering demo. It sets pixels as [0-255] int values yet shows a perfectly smooth dithered gradient and most importantly the undithered gradient has no column groupings!

fullsize example
http://rectangleworld.com/demos/DitheredGradient/DitheredGradientExample.html
reference
http://rectangleworld.com/blog/archives/713

So in javascript you can get visually linear grey steps but in a Xojo Canvas with this paint code I get the following image. Plus drawing to a Picture then DrawPicturing that to the Canvas has slightly different grouping.

[code]Sub Paint(g As Graphics, areas() As REALbasic.Rect)

dim last As integer = g.Width / 10

for i As integer = 0 to last
g.ForeColor = RGB(i, i, i)
g.FillRect i * 10, 0, 10, g.Height
next

End Sub[/code]

Somethings going on but I’m still not sure what. Dithering isn’t the problem, it’s this magical shifting of color values that creates those column groupings. Do I have to set the colors only through CG functions? Do I have to create a CGImage/Context too or will the context from a Xojo Picture or Graphics work? Is the fix just a simple change to the Xojo given CGContext?

This conversation might relate, about NSGradient drawing banded. The solution was to create the NSGradient once instead of every draw call. Unfortunately it’s unknown why it works.

https://forum.xojo.com/20548-nsgradient-looks-ugly-why

This started as just trying to implement Floyd-Steinberg :slight_smile:

Jim Mckay’s code here makes a CGColor in DeviceRGB color space.
https://forum.xojo.com/conversation/post/53488

Using those colors to fill there’s no column grouping anymore, a nice even step!

[code]Sub Paint(g As Graphics, areas() As REALbasic.Rect)
declare function CGColorSpaceCreateDeviceRGB lib “Cocoa” () as ptr
declare function CGColorCreate lib “Cocoa” (colorspace as ptr, val as ptr) as ptr
declare sub CGColorRelease lib “Cocoa” (c as ptr)
declare sub CGColorSpaceRelease lib “Cocoa” (c as ptr)
declare sub CGContextFillRect lib “Cocoa” (cntxt As Ptr, rect As CGRect)
declare sub CGContextSetFillColorWithColor lib “Cocoa” (cntxt As Ptr, clr As Ptr)

dim cgcm As MemoryBlock, cgcolor As ptr, r As CGRect
dim cgcspace as ptr=CGColorSpaceCreateDeviceRGB
dim cntxt As Ptr = Ptr( g.Handle(g.HandleTypeCGContextRef) )

dim last As integer = g.Width / 10
for i As integer = 0 to last

cgcm= new MemoryBlock(16)
cgcm.SingleValue(0)=i/255
cgcm.SingleValue(4)=i/255
cgcm.SingleValue(8)=i/255
cgcm.SingleValue(12)=1'(255-c.Alpha)/255
cgcolor=CGColorCreate(cgcspace,cgcm)
CGContextSetFillColorWithColor(cntxt, cgcolor)

r.x = i * 10
r.y = 0
r.w = 10
r.h = g.Height

CGContextFillRect(cntxt, r)

CGColorRelease(cgcolor)

next

CGColorSpaceRelease(cgcspace)

End Sub[/code]

So I knocked up a CGGradient class, which you can download from:
http://www.ohanaware.com/xojo/CGGradientTester.xojo_binary_project

Which produces the following result on a 2015 MacBook:

I can still see banding, but not as much as before. Also to use this class, avoid creating the gradient class in the paint event as creating a gradient is not a cheap operation.

Thanks Sam, I’ll include that in my CGKit. It’s weird that it’s banded though, I wonder if it’s color space.

So I was wrong above when I said CGColors in DeviceRGB space solved the column grouping problem. This afternoon, with sunlight illuminating the room, I looked at those pictures again, the javascript demo and my pic that I said were even steps, and I could see column groupings. Not as bad as before but they were there.

What? It was late and my eyes must have been weak so I didn’t notice. Then the sun set and I worked in the dark for a bit and checked the images again. Now they look even. With the room light on for a time they still look like even steps.

When the sun comes up again I’ll check this really really closely. It’d be wierd to have a solution that works at night but not day time.

Right now my suspicions are some or all of these factors…

color space
rendering intent
display profile calibration
environmental lighting
my eyes

Also, OpenGL is giving nice even steps but I haven’t looked at it with sunlight yet.

Most displays are only 8bpc, so I wonder if this is what we’re seeing. Before you pointed out this banding, I never noticed it so bad with CGGradients until I looked for it. It also probably is compounded because the OS screen grabs a 8 bpc PNG, so when looking again at the screen capture it’s further reduced.

If we had access to a 10bpc display and could screen grab in tiff float (or exr float), I wonder if we could see it so bad then?

You could also compare to a gradient in Photoshop or similar. I never noticed the banding before, too.

Thanks Sam and Beatrix. Putting these together it’s starting to make sense

I have an old copy of Photoshop which I fiddled with to produce 2 pictures of the same gradient. One with ‘Untagged RGB’ profile (which I think is really no profile) and another with ‘Color LCD’ profile. The untagged one is nice and linear while the one with color lcd has wild column groupings.

Combine that with the notes from CGColorSpaceCreateDeviceRGB used above to make linear columns…

[quote]DeviceRGB
Colors in a device-dependent color space are not transformed or otherwise modified when displayed on an output device—that is, there is no attempt to maintain the visual appearance of a color. As a consequence, colors in a device color space often appear different when displayed on different output devices. For this reason, device color spaces are not recommended when color preservation is important.[/quote]

When there’s no profile or using DeviceRGB the values set for a pixel are the same values the physical lcd pixel receives. Otherwise the lcd is recieving shifted values.

There’s a tradeoff here. With DeviceRGB I can get rid of banding but overall the image won’t look the same on other displays, with a profile/color space there’s banding but overall preservation of the image.

Our eyes are most sensitive to this profile banding effect in dark regions. I wonder if there’s a way to use calibrated colors but modify the values in dark regions so they end up shifting to the value I want. But then that’s not calibrated and will look different on other screens. Oh well, maybe in 4 years we’ll all have 10bpc screens and this won’t matter anymore :stuck_out_tongue:

There’s a mountain of docs to read about CGContext, CGBitmapContext, CGImage, CGColorSpace, CGColor, etc before I can really work out how to approach my rendering algorithm.

Interesting… I would then suggest perhaps experimenting with CGColorSpaceCreateWithName. From here you can create several variants of RGB.

https://developer.apple.com/library/prerelease/mac/documentation/GraphicsImaging/Reference/CGColorSpace/index.html#//apple_ref/c/func/CGColorSpaceCreateWithName

I wish it were that easy. There seems to be so many other factors at play.

In a horribly inefficient and unusable way I’ve managed to draw a totally smooth shape (at least on my display :slight_smile: ).

before

after

I had to use the technique above of creating CGColors in DeviceRGB space and filling 1 pixel rects in the Canvas’s Graphics parameter context.

Using a Pictures Graphics context didn’t work. Creating a CGBitmapContext in DeviceRGB didn’t work. Setting RenderingIntent didn’t work. Setting the Windows color space to DeviceRGB almost worked.

One of the challenges is I haven’t found a way to tell what the given color spaces are. I mean, I can get the CGContext from the Graphics parameter or a Picture but there’s no function to get what color space it has. The only color space option is setting the fill or stroke color space via a color in that color space. Plus drawing a CGImage to a context has it’s own separate color space shenanigans.

I haven’t figured out where exactly the color shifting happens. Working through the behemoth Quartz 2D Programming Guide will hopefully reveal that.