Commit Graph

197 Commits

Author SHA1 Message Date
kris
870c008827 Parametrize quantization error decay and minimum value. The latter
helps with images where there are large solid colour fields that
sometimes cause uneven dithering because of colours that cannot be
matched with the //gs palette, but it's not a viable solution in
general since it reduces overall quality (sometimes substantially,
e.g. in case of vertical colour gradients)
2021-11-25 09:09:40 +00:00
kris
8b5c3dc6c1 Fix bool flags 2021-11-24 16:03:55 +00:00
kris
9a77af37aa Add a --show-final-score to output the final image quality score.
This is useful when used as part of an image repository build
pipeline, to avoid replacing existing images if the new score is
higher.

Hide intermediate output behind --verbose
2021-11-24 15:49:56 +00:00
kris
0036ee9522 Add default values to help 2021-11-24 15:44:37 +00:00
kris
8d3ab4f50e Add the ability to disable saving preview images. Also rename --gamma_correct to --gamma-correct for consistency 2021-11-24 15:41:32 +00:00
kris
8175dcb052 Add --fixed-colours to control how many colours will be kept identical
across all 16 SHR palettes.
2021-11-24 15:27:34 +00:00
kris
5fefd0b0bb Don't initialize pygame if --no-show-output 2021-11-24 15:24:58 +00:00
kris
e77e7abd43 Rename 2021-11-24 15:24:45 +00:00
kris
d645cc5964 Tidy 2021-11-24 15:21:50 +00:00
kris
c36de2b76b When initializing centroids for fitting the SHR palettes, only use the
reserved colours from the global palette, and pick unique random
points from the samples for the rest.  This encourages a larger range
of colours in the resulting images and may improve quality.

Iterate a max number of times without improvement in the outer loop as
well.

Save intermediate preview outputs.
2021-11-24 14:57:24 +00:00
kris
3b8767782b Each run seems to converge fairly quickly but there is a lot of variation across runs. Run in a loop and keep the running best. 2021-11-24 11:47:39 +00:00
kris
de8a303de2 Initial attempt at fitting palettes to arbitrary lines instead of line ranges.
Works OK but isn't converging as well as I hoped.
2021-11-24 10:41:25 +00:00
kris
50c71d3a35 Whitespace 2021-11-24 09:19:35 +00:00
kris
04fd4f7427 Move reassigning palettes back to after fitting, otherwise it does the
wrong thing the first time.

Fix an off by one when splitting palette ranges
2021-11-24 09:18:59 +00:00
kris
62f23ff910 Don't mutate initial_centroids 2021-11-24 09:10:03 +00:00
kris
7179d009e1 Refactor
Reassign palettes before computing new ones instead of after
2021-11-23 15:09:12 +00:00
kris
e488955c23 Reorder 2021-11-23 14:58:46 +00:00
kris
0b985a66b9 Reorder and tidy 2021-11-23 14:58:09 +00:00
kris
c78f731cd7 Refactor 2021-11-23 14:55:45 +00:00
kris
0323b80e68 Refactor 2021-11-23 14:51:04 +00:00
kris
6988b19b43 Tidy 2021-11-23 14:00:57 +00:00
kris
1ce5c25764 Fix a bug where _fit_global_palette would crash if there were fewer
than 16 global colours computed.
2021-11-23 13:59:48 +00:00
kris
6e52680cf1 Dynamically tune the line ranges used to fit the 16 SHR palettes:
- 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.
2021-11-23 13:01:50 +00:00
kris
189b4655ad Since fixing the bug in the previous commit there is no longer a need
to limit to neighbouring palettes (which was unaware of the dynamic
line splits anyway)
2021-11-23 12:49:37 +00:00
kris
be55fb859d - Fix a serious bug in best_palette_for_line which was not actually computing the palette with lowest per-row error, rather the lowest per-pixel error!
- Tidy a bit
2021-11-23 12:46:36 +00:00
kris
b78c42e287 Fix rounding 2021-11-18 22:35:15 +00:00
kris
b1d3488182 Actually use equal-sized palette splits. With the previous version
the first and last were smaller.
2021-11-18 22:27:19 +00:00
kris
9e46ca48a0 Refactor to extract palette splits in preparation for tuning them dynamically 2021-11-18 22:08:09 +00:00
kris
cfc150ed13 Remove some dead code 2021-11-18 22:03:18 +00:00
kris
c608f6b961 Optimize calling _convert_cam16ucs_to_rgb12_iigs since it has
significant overhead
2021-11-18 21:50:39 +00:00
kris
3159a09c27 Uncomment 2021-11-18 20:33:21 +00:00
kris
7609297f0d Optimize a bit 2021-11-18 17:34:27 +00:00
kris
d7969f50ba Remove cython checks and obsolete TODO 2021-11-18 17:24:12 +00:00
kris
e53c085a91 Remove debugging prints 2021-11-17 22:55:47 +00:00
kris
ed2082344a Working version! Quantize the k-means centroids in 12-bit //gs RGB
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/
2021-11-17 22:49:06 +00:00
kris
0009ce8913 - allow reserving a number of colours which are to be shared across
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.
2021-11-17 17:09:42 +00:00
kris
f2f07ddc04 Refactor and add comments 2021-11-16 23:45:11 +00:00
kris
bb70eea7b0 Cleanup 2021-11-16 21:07:13 +00:00
kris
613a36909c Suppress pygame message at startup
Keep iterating until N iterations without quality improvement
2021-11-16 17:23:31 +00:00
kris
5111696d5c Compute number of unique colours. This does not seem to strongly
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)
2021-11-16 16:57:44 +00:00
kris
91e4fd7cba Add comment 2021-11-16 15:50:19 +00:00
kris
83b047b73f Whoops, fix a major bug with the iterated image fitting: we don't want
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.
2021-11-16 15:44:04 +00:00
kris
8694ab364e Perform conversions in linear RGB space 2021-11-16 12:38:53 +00:00
kris
7ad560247b Clean up 2021-11-16 12:24:43 +00:00
kris
10c829906b Checkpoint
- 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.
2021-11-16 11:21:53 +00:00
kris
b363d60754 Checkpoint
- 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
2021-11-15 09:19:44 +00:00
kris
643e50349e Optimize more 2021-11-13 17:29:13 +00:00
kris
0596aefe0b Use pyclustering for kmedians instead of hand-rolled
Optimize cython code
2021-11-13 17:18:34 +00:00
kris
52af982159 k-means should be using median with L1 norm, otherwise it may not converge
Also optimize a tiny bit
2021-11-13 16:10:33 +00:00
kris
5cab854269 Fit palettes from overlapping line ranges, and map line to palette
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!
2021-11-11 16:10:03 +00:00