- start with an equal split
- with each iteration, pick a palette and adjust its line ranges by a small random amount
- if the proposed palette is accepted, continue to apply the same delta
- if not, revert the adjustment and pick a different one
In addition, often there will be palettes that are entirely unused by
the image. For such palettes:
- find the palette with the largest line range. If > 20, then
subdivide this range and assign half each to both palettes
- if not, then pick a random line range for the unused palette
This helps to refine and explore more of the parameter space.
space but continue to use CAM16-UCS for distances and updating
centroid positions, before mapping back to the nearest legal 12-bit
RGB position.
Needs some more work to deal with the fact that now that there are
discrete distances (but no fixed minimum) between allowed centroid
positions, the previous notion of convergence doesn't apply. Actually
the centroids can oscillate between positions.
There is room for optimization but this is already reasonably
performant, and the image quality is much higher \o/
all palettes. This will be useful for Total Replay which does an
animation effect when displaying the image (first set palettes, then
transition in pixels)
- this requires us to go back to computing k-means ourself instead of
using sklearn, since it can't keep some centroids fixed
- try to be more careful about //gs RGB values, which are in the
Rec.601 colour space. This isn't quite right yet - the issue seems
to be that since we dither in linear RGB space but quantize in the
nonlinear space, small differences may lead to a +/- 1 in the 4-bit
//gs RGB value, which is quite noticeable. Instead we need to be
clustering and/or dithering with awareness of the quantized palette
space.
depend on the width of the palette sampling.
Note the potential issue that since we are clustering in CAM space but
then quantizing a (much coarser) 4-bit RGB value we could end up
picking multiple centroids that will be represented by the same RGB
value. This doesn't seem to be a major issue though (e.g. 3-4 lost
colours per typical image)
to mutate our source image!
Fix another bug introduced in the previous commit: convert from linear
rgb before quantizing //gs RGB palette since //gs RGB values are in
Rec.601 colour space.
Switch to double for colour_squared_distance and related variables,
not sure if it matters though.
When iterating palette clustering, reject the new palettes if they
would increase the total image error. This prevents accepting changes
that are local improvements to one palette but which would introduce
more net errors elsewhere when this palette is reused.
This now seems to give monotonic improvements in image quality so no need
to write out intermediate images any more.
- Repeatedly refit palettes since k-means is only a local
optimization. This can produce incremental improvements in image
quality but may also overfit, especially on complex images.
- use pygame to render incremental images
- Fix off-by-one in palette striping
- When fitting palettes, first cluster a 16-colour palette for the
entire image and use this to initialize the centroids for individual
palettes. This improves quality when fitting images with large
blocks of colour, since they will otherwise be fit separately and
may have slight differences. With a global initializer these will
tend to be the same. This also improves performance.
- switch to pyclustering for kmedians
- allow choosing the same palette as previous line, with a multiplicative penalty to distance in case it's much better
- iterate kmedians multiple times and choose the best, since it's only a local optimum
when dithering with two limitations:
- cannot choose the same palette as the previous line (this avoids banding)
- must be within +/- 1 of the "base" palette for the line number
This gives pretty good results!
direction. Otherwise, errors can accumulate in an RGB channel if
there are no palette colours with an extremal value, and then when
we introduce a new palette the error all suddenly discharges in a
spurious horizontal line. This now gives quite good results!
* Switch to using L1-norm for k-means, per suggestion of Lucas
Scharenbroich: "A k-medians effectively uses an L1 distance metric
instead of L2 for k-means. Using a squared distance metric causes
the fit to "fall off" too quickly and allows too many of the k
centroids to cluster around areas of high density, which results in
many similar colors being selected. A linear cost function forces
the centroids to spread out since the error influence has a broader
range."
Avoids the banding but not clear if it's overall better
Also implement my own k-means clustering which is able to keep some
centroids fixed, e.g. to be able to retain some fixed palette entries
while swapping out others. I was hoping this would improve colour
blending across neighbouring palettes but it's also not clear if it
does.