From bbbe20f9417ad0c9e6db6cc37ec471ac0702a68e Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 18:23:18 +0200 Subject: [PATCH 01/13] Add stb submodule as dependency --- .gitmodules | 3 +++ dependencies/stb | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 dependencies/stb diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b49ed62 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dependencies/stb"] + path = dependencies/stb + url = https://github.com/nothings/stb diff --git a/dependencies/stb b/dependencies/stb new file mode 160000 index 0000000..f75e8d1 --- /dev/null +++ b/dependencies/stb @@ -0,0 +1 @@ +Subproject commit f75e8d1cad7d90d72ef7a4661f1b994ef78b4e31 From 2f80e5327d6852826d972f3ddce1ee50ef026e14 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 18:45:37 +0200 Subject: [PATCH 02/13] Added mongoose.ws dependency submodule --- .gitmodules | 3 +++ dependencies/mongoose | 1 + 2 files changed, 4 insertions(+) create mode 160000 dependencies/mongoose diff --git a/.gitmodules b/.gitmodules index b49ed62..09e1df5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "dependencies/stb"] path = dependencies/stb url = https://github.com/nothings/stb +[submodule "dependencies/mongoose"] + path = dependencies/mongoose + url = https://github.com/cesanta/mongoose diff --git a/dependencies/mongoose b/dependencies/mongoose new file mode 160000 index 0000000..3525f04 --- /dev/null +++ b/dependencies/mongoose @@ -0,0 +1 @@ +Subproject commit 3525f044f551816dc1469f445fc16b94d51a1e78 From a65943a6bdd9a9f519924a20a457e17f9e108cef Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 18:47:09 +0200 Subject: [PATCH 03/13] Added Makefile for img2cpi and wsvpn FIXME: downgrade mongoose submodule or fix wsvpn --- Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0ec1f65 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +CPPFLAGS += -Idependencies -Idependencies/mongoose +LDLIBS += -lm + +all: img2cpi wsvpn + +wsvpn: wsvpn.o dependencies/mongoose/mongoose.o + $(CC) $(LDFLAGS) "$<" $(LOADLIBES) $(LDLIBS) -o "$@" + +.PHONY: all From 1eb767d0d28037a1aae59808c1bc773d930d7769 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 18:48:47 +0200 Subject: [PATCH 04/13] Added gitignore with C build artifacts --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8fb39c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +img2cpi +img2cpi.o +wsvpn +wsvpn.o From d765e88679f60971ebf7dd9bf78acec218b23dd5 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 19:11:57 +0200 Subject: [PATCH 05/13] =?UTF-8?q?Only=20calculate=20difference=20between?= =?UTF-8?q?=20each=20pixel=20=C3=97=20each=20palette=20entry=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- img2cpi.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/img2cpi.c b/img2cpi.c index 62fff89..87c4d86 100644 --- a/img2cpi.c +++ b/img2cpi.c @@ -580,6 +580,18 @@ void convert_8x11(const struct image_pal *img, struct cc_char *characters) { int w = img->w / 8, h = img->h / 11; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { + float chunk_palette_diffs[8][11][0x10] = {{{(float) 0xffffff}}}; + for (int ox = 0; ox < 8; ox++) { + for (int oy = 0; oy < 11; oy++) { + union color pixel = img->palette[img->pixels[ + ox + (x + (y * 11 + oy) * w) * 8 + ]]; + for (int color = 0x0; color < 0x10 && color < img->palette_size; color++) { + chunk_palette_diffs[ox][oy][color] = get_color_difference(pixel, img->palette[color]); + } + } + } + float min_diff = 0xffffff; char closest_sym = 0x00, closest_color = 0xae; for (int sym = 0x01; sym <= 0xFF; sym++) { @@ -587,17 +599,12 @@ void convert_8x11(const struct image_pal *img, struct cc_char *characters) { continue; } for (int color = 0x00; color <= 0xff; color++) { - union color cell_bg = img->palette[color & 0xF], - cell_fg = img->palette[color >> 4]; float difference = 0; for (int oy = 0; oy < 11; oy++) { unsigned char sym_line = font_atlas[sym][oy]; for (int ox = 0; ox < 8; ox++) { bool lit = sym_line & (0x80 >> ox); - union color pixel = img->palette[img->pixels[ - ox + (x + (y * 11 + oy) * w) * 8 - ]]; - difference += get_color_difference(pixel, lit ? cell_fg : cell_bg); + difference += chunk_palette_diffs[ox][oy][lit ? color >> 4 : color & 0xF]; } } if (difference <= min_diff) { From be21d42fa06da6f1993e1ae8680541d5e2b6f56c Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 19:43:30 +0200 Subject: [PATCH 06/13] Now only calculate color difference once for each palette entry pair Not clear whether it is more optimal --- img2cpi.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/img2cpi.c b/img2cpi.c index 87c4d86..4f66bc3 100644 --- a/img2cpi.c +++ b/img2cpi.c @@ -578,16 +578,23 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) { void convert_8x11(const struct image_pal *img, struct cc_char *characters) { int w = img->w / 8, h = img->h / 11; + float palette_self_diffs[0x100][0x10] = {{(float) 0xffffff}}; + for (int input_color = 0x0; input_color < 0x100 && input_color < img->palette_size; input_color++) { + for (int output_color = 0x0; output_color < 0x10 && output_color < img->palette_size; output_color++) { + palette_self_diffs[input_color][output_color] = get_color_difference(img->palette[input_color], img->palette[output_color]); + } + } + for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { float chunk_palette_diffs[8][11][0x10] = {{{(float) 0xffffff}}}; for (int ox = 0; ox < 8; ox++) { for (int oy = 0; oy < 11; oy++) { - union color pixel = img->palette[img->pixels[ + uint8_t pixel_unresolved = img->pixels[ ox + (x + (y * 11 + oy) * w) * 8 - ]]; + ]; for (int color = 0x0; color < 0x10 && color < img->palette_size; color++) { - chunk_palette_diffs[ox][oy][color] = get_color_difference(pixel, img->palette[color]); + chunk_palette_diffs[ox][oy][color] = palette_self_diffs[pixel_unresolved][color]; } } } From e8c53b1f9b1b33c11cd6da3b91f22d696a303a32 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 21:22:19 +0200 Subject: [PATCH 07/13] Fix linking recipe for wsvpn Only the first object was being added --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0ec1f65..21e8cf0 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,6 @@ LDLIBS += -lm all: img2cpi wsvpn wsvpn: wsvpn.o dependencies/mongoose/mongoose.o - $(CC) $(LDFLAGS) "$<" $(LOADLIBES) $(LDLIBS) -o "$@" + $(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o "$@" .PHONY: all From d9f43626158219302a80a114b79ff45722d281f4 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 23:17:29 +0200 Subject: [PATCH 08/13] Dedicated palette type that also stores own length --- img2cpi.c | 60 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/img2cpi.c b/img2cpi.c index 4f66bc3..f2da4e4 100644 --- a/img2cpi.c +++ b/img2cpi.c @@ -23,9 +23,15 @@ struct cc_char { unsigned char character; unsigned char bg, fg; }; +struct palette { + const uint8_t count; + union color colors[] __attribute__((counted_by(count))); +}; +#define LENGTHOF(...) (sizeof(__VA_ARGS__) / sizeof(*(__VA_ARGS__))) +#define PALETTE(...) { .count = LENGTHOF((union color[]){__VA_ARGS__}), .colors = {__VA_ARGS__} } const extern char font_atlas[256][11]; -const extern union color DEFAULT_PALETTE[16], DEFAULT_GRAY_PALETTE[16]; +const extern struct palette DEFAULT_PALETTE, DEFAULT_GRAY_PALETTE; struct arguments { bool fast_mode; @@ -76,8 +82,7 @@ struct image { struct image_pal { int w, h; uint8_t *pixels; - const union color *palette; - size_t palette_size; + const struct palette *palette; }; bool parse_cmdline(int argc, char **argv); @@ -85,7 +90,7 @@ void show_help(const char *progname, bool show_all, FILE *fp); struct image *image_load(const char *fp); struct image *image_new(int w, int h); struct image *image_resize(struct image *original, int new_w, int new_h); -struct image_pal *image_quantize(struct image *original, const union color *colors, size_t n_colors); +struct image_pal *image_quantize(struct image *original, const struct palette *palette); float get_color_difference(union color a, union color b); float get_color_brightness(union color clr); void image_unload(struct image *img); @@ -163,10 +168,10 @@ int main(int argc, char **argv) { } // TODO: load palette, maybe calculate it too? k-means? - const union color *palette = DEFAULT_PALETTE; + const struct palette *palette = &DEFAULT_PALETTE; switch (args.palette_type) { - case PALETTE_DEFAULT: palette = DEFAULT_PALETTE; break; - case PALETTE_DEFAULT_GRAY: palette = DEFAULT_GRAY_PALETTE; break; + case PALETTE_DEFAULT: palette = &DEFAULT_PALETTE; break; + case PALETTE_DEFAULT_GRAY: palette = &DEFAULT_GRAY_PALETTE; break; case PALETTE_AUTO: assert(0 && "Not implemented"); break; case PALETTE_LIST: assert(0 && "Not implemented"); break; case PALETTE_PATH: assert(0 && "Not implemented"); break; @@ -198,7 +203,7 @@ int main(int argc, char **argv) { // TODO: actually do stuff struct cc_char *characters = calloc(args.width * args.height, sizeof(struct cc_char)); - struct image_pal *quantized_image = image_quantize(canvas, palette, 16); + struct image_pal *quantized_image = image_quantize(canvas, palette); if (!quantized_image) { fprintf(stderr, "Error: failed to open the file\n"); return EXIT_FAILURE; @@ -217,9 +222,9 @@ int main(int argc, char **argv) { fputc(args.height, fp); fputc(0x00, fp); for (int i = 0; i < 16; i++) { - fputc(palette[i].rgba.r, fp); - fputc(palette[i].rgba.g, fp); - fputc(palette[i].rgba.b, fp); + fputc(palette->colors[i].rgba.r, fp); + fputc(palette->colors[i].rgba.g, fp); + fputc(palette->colors[i].rgba.b, fp); } for (int i = 0; i < args.width * args.height; i++) { fputc(characters[i].character, fp); @@ -486,19 +491,18 @@ void get_size_keep_aspect(int w, int h, int dw, int dh, int *ow, int *oh) } } -struct image_pal *image_quantize(struct image *original, const union color *colors, size_t n_colors) { +struct image_pal *image_quantize(struct image *original, const struct palette *palette) { struct image_pal *out = calloc(1, sizeof(struct image_pal)); out->w = original->w; out->h = original->h; out->pixels = calloc(original->w, original->h); - out->palette = colors; - out->palette_size = n_colors; + out->palette = palette; for (int i = 0; i < out->w * out->h; i++) { int closest_color = 0; float closest_distance = 1e20; - for (int color = 0; color < n_colors; color++) { - float dist = get_color_difference(colors[color], original->pixels[i]); + for (int color = 0; color < palette->count; color++) { + float dist = get_color_difference(palette->colors[color], original->pixels[i]); if (dist <= closest_distance) { closest_distance = dist; closest_color = color; @@ -531,7 +535,7 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) { for (int oy = 0; oy < 3; oy++) { for (int ox = 0; ox < 2; ox++) { unsigned char pix = img->pixels[ox + (x + (y * 3 + oy) * w) * 2]; - float brightness = get_color_brightness(img->palette[pix]); + float brightness = get_color_brightness(img->palette->colors[pix]); if (brightness >= brightest_diff) { brightest_i = pix; brightest_diff = brightness; @@ -549,8 +553,8 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) { for (int ox = 0; ox < 2; ox++) { if (ox == 1 && oy == 2) continue; unsigned char pix = img->pixels[ox + (x + (y * 3 + oy) * w) * 2]; - float diff_bg = get_color_difference(img->palette[darkest_i], img->palette[pix]); - float diff_fg = get_color_difference(img->palette[brightest_i], img->palette[pix]); + float diff_bg = get_color_difference(img->palette->colors[darkest_i], img->palette->colors[pix]); + float diff_fg = get_color_difference(img->palette->colors[brightest_i], img->palette->colors[pix]); if (diff_fg < diff_bg) { bitmap |= pixel_bits[oy][ox]; } @@ -559,8 +563,8 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) { { unsigned char pix = img->pixels[1 + (x + (y * 3 + 2) * w) * 2]; - float diff_bg = get_color_difference(img->palette[darkest_i], img->palette[pix]); - float diff_fg = get_color_difference(img->palette[brightest_i], img->palette[pix]); + float diff_bg = get_color_difference(img->palette->colors[darkest_i], img->palette->colors[pix]); + float diff_fg = get_color_difference(img->palette->colors[brightest_i], img->palette->colors[pix]); if (diff_fg < diff_bg) { bitmap ^= 31; unsigned char tmp = darkest_i; @@ -579,9 +583,9 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) { void convert_8x11(const struct image_pal *img, struct cc_char *characters) { int w = img->w / 8, h = img->h / 11; float palette_self_diffs[0x100][0x10] = {{(float) 0xffffff}}; - for (int input_color = 0x0; input_color < 0x100 && input_color < img->palette_size; input_color++) { - for (int output_color = 0x0; output_color < 0x10 && output_color < img->palette_size; output_color++) { - palette_self_diffs[input_color][output_color] = get_color_difference(img->palette[input_color], img->palette[output_color]); + for (int input_color = 0x0; input_color < 0x100 && input_color < img->palette->count; input_color++) { + for (int output_color = 0x0; output_color < 0x10 && output_color < img->palette->count; output_color++) { + palette_self_diffs[input_color][output_color] = get_color_difference(img->palette->colors[input_color], img->palette->colors[output_color]); } } @@ -593,7 +597,7 @@ void convert_8x11(const struct image_pal *img, struct cc_char *characters) { uint8_t pixel_unresolved = img->pixels[ ox + (x + (y * 11 + oy) * w) * 8 ]; - for (int color = 0x0; color < 0x10 && color < img->palette_size; color++) { + for (int color = 0x0; color < 0x10 && color < img->palette->count; color++) { chunk_palette_diffs[ox][oy][color] = palette_self_diffs[pixel_unresolved][color]; } } @@ -628,7 +632,7 @@ void convert_8x11(const struct image_pal *img, struct cc_char *characters) { } } -const union color DEFAULT_PALETTE[16] = { +const struct palette DEFAULT_PALETTE = PALETTE( { { 0xf0, 0xf0, 0xf0, 0xff } }, { { 0xf2, 0xb2, 0x33, 0xff } }, { { 0xe5, 0x7f, 0xd8, 0xff } }, @@ -645,7 +649,7 @@ const union color DEFAULT_PALETTE[16] = { { { 0x57, 0xa6, 0x4e, 0xff } }, { { 0xcc, 0x4c, 0x4c, 0xff } }, { { 0x11, 0x11, 0x11, 0xff } } -}, DEFAULT_GRAY_PALETTE[16] = { +), DEFAULT_GRAY_PALETTE = PALETTE( { { 0xf0, 0xf0, 0xf0, 0xff } }, { { 0x9d, 0x9d, 0x9d, 0xff } }, { { 0xbe, 0xbe, 0xbe, 0xff } }, @@ -662,7 +666,7 @@ const union color DEFAULT_PALETTE[16] = { { { 0x6e, 0x6e, 0x6e, 0xff } }, { { 0x76, 0x76, 0x76, 0xff } }, { { 0x11, 0x11, 0x11, 0xff } } -}; +); const char font_atlas[256][11] = { { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, From 849563285c6dd433aae72198531c3403856d0efd Mon Sep 17 00:00:00 2001 From: Vftdan Date: Wed, 2 Oct 2024 23:57:12 +0200 Subject: [PATCH 09/13] Global resizeable palette --- img2cpi.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/img2cpi.c b/img2cpi.c index f2da4e4..ee81cf3 100644 --- a/img2cpi.c +++ b/img2cpi.c @@ -99,6 +99,10 @@ void get_size_keep_aspect(int w, int h, int dw, int dh, int *ow, int *oh); void convert_2x3(const struct image_pal *img, struct cc_char *characters); void convert_8x11(const struct image_pal *img, struct cc_char *characters); +// Only one global custom palette is maintained +struct palette *custom_palette_resize(uint8_t size); +struct palette *custom_palette_from(const struct palette *orig); + const char *known_file_extensions[] = { ".png", ".jpg", ".jpeg", ".jfif", ".jpg", ".gif", ".tga", ".bmp", ".hdr", ".pnm", 0 @@ -632,6 +636,24 @@ void convert_8x11(const struct image_pal *img, struct cc_char *characters) { } } +struct { + uint8_t count; + union color colors[256]; +} custom_palette_data; + +struct palette *custom_palette_resize(uint8_t size) { + custom_palette_data.count = size; + return (struct palette*)&custom_palette_data; +} + +struct palette *custom_palette_from(const struct palette *orig) { + custom_palette_data.count = orig->count; + for (int i = 0; i < custom_palette_data.count; i++) { + custom_palette_data.colors[i] = orig->colors[i]; + } + return (struct palette*)&custom_palette_data; +} + const struct palette DEFAULT_PALETTE = PALETTE( { { 0xf0, 0xf0, 0xf0, 0xff } }, { { 0xf2, 0xb2, 0x33, 0xff } }, From c5af524dac5839e6db6bc2a92576d7f3b43e42c4 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Thu, 3 Oct 2024 01:27:30 +0200 Subject: [PATCH 10/13] Implement k-means palette generation --- img2cpi.c | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/img2cpi.c b/img2cpi.c index ee81cf3..be9c3fa 100644 --- a/img2cpi.c +++ b/img2cpi.c @@ -13,6 +13,7 @@ #include #define MAX_COLOR_DIFFERENCE 768 +#define K_MEANS_ITERATIONS 4 struct rgba { uint8_t r, g, b, a; }; union color { @@ -85,6 +86,19 @@ struct image_pal { const struct palette *palette; }; +struct k_means_state { + const struct image *items; + struct palette *clusters; + uint8_t *predicted_cluster; + struct k_means_centroid_intermediate { + struct { + float r, g, b; + } sums; + size_t count; + } *centroid_intermediate; + size_t item_count; +}; + bool parse_cmdline(int argc, char **argv); void show_help(const char *progname, bool show_all, FILE *fp); struct image *image_load(const char *fp); @@ -103,6 +117,11 @@ void convert_8x11(const struct image_pal *img, struct cc_char *characters); struct palette *custom_palette_resize(uint8_t size); struct palette *custom_palette_from(const struct palette *orig); +struct k_means_state k_means_init(const struct image *image, struct palette *starting_palette); +bool k_means_iteration(struct k_means_state *state); +void k_means_end(struct k_means_state *state); +struct palette *palette_k_means(const struct image *image, const struct palette *prototype); + const char *known_file_extensions[] = { ".png", ".jpg", ".jpeg", ".jfif", ".jpg", ".gif", ".tga", ".bmp", ".hdr", ".pnm", 0 @@ -176,7 +195,7 @@ int main(int argc, char **argv) { switch (args.palette_type) { case PALETTE_DEFAULT: palette = &DEFAULT_PALETTE; break; case PALETTE_DEFAULT_GRAY: palette = &DEFAULT_GRAY_PALETTE; break; - case PALETTE_AUTO: assert(0 && "Not implemented"); break; + case PALETTE_AUTO: palette = palette_k_means(src_image, &DEFAULT_PALETTE); break; case PALETTE_LIST: assert(0 && "Not implemented"); break; case PALETTE_PATH: assert(0 && "Not implemented"); break; default: assert(0 && "Unreachable"); @@ -654,6 +673,98 @@ struct palette *custom_palette_from(const struct palette *orig) { return (struct palette*)&custom_palette_data; } +struct k_means_state k_means_init(const struct image *image, struct palette *starting_palette) { + size_t item_count = image->w * image->h; + struct k_means_state state = { + .items = image, + .clusters = starting_palette, + .predicted_cluster = calloc(image->w, image->h), + .centroid_intermediate = calloc(item_count, sizeof(struct k_means_centroid_intermediate)), + .item_count = item_count, + }; + return state; +} + +bool k_means_iteration(struct k_means_state *state) { + if (!state->predicted_cluster || !state->centroid_intermediate) { + return false; + } + bool changed = false; + + // Find closest cluster + for (int i = 0; i < state->item_count; i++) { + int closest_cluster = 0; + float closest_distance = 1e20; + for (int cluster = 0; cluster < state->clusters->count; cluster++) { + float dist = get_color_difference(state->clusters->colors[cluster], state->items->pixels[i]); + if (dist <= closest_distance) { + closest_distance = dist; + closest_cluster = cluster; + } + } + if (!changed) { + changed = state->predicted_cluster[i] != closest_cluster; + } + state->predicted_cluster[i] = closest_cluster; + state->centroid_intermediate[closest_cluster].count += 1; + state->centroid_intermediate[closest_cluster].sums.r += state->items->pixels[i].rgba.r; + state->centroid_intermediate[closest_cluster].sums.g += state->items->pixels[i].rgba.g; + state->centroid_intermediate[closest_cluster].sums.b += state->items->pixels[i].rgba.b; + } + + // Update centroids + for (int i = 0; i < state->clusters->count; ++i) { + struct k_means_centroid_intermediate intermediate = state->centroid_intermediate[i]; + if (intermediate.count) { + union color centroid = { + .rgba = { + .r = intermediate.sums.r / intermediate.count, + .g = intermediate.sums.g / intermediate.count, + .b = intermediate.sums.b / intermediate.count, + .a = 0xff, + } + }; + if (!changed) { + changed = state->clusters->colors[i].v != centroid.v; + } + state->clusters->colors[i] = centroid; + } else { + // No pixels are closest to this color + // TODO: wiggle the centroid? + } + state->centroid_intermediate[i] = (struct k_means_centroid_intermediate) { .sums = {0, 0, 0}, .count = 0 }; + } + + return changed; +} + +void k_means_end(struct k_means_state *state) { + if (state->predicted_cluster) { + free(state->predicted_cluster); + } + if (state->centroid_intermediate) { + free(state->centroid_intermediate); + } +} + +struct palette *palette_k_means(const struct image *image, const struct palette *prototype) { + if (!prototype) { + prototype = &DEFAULT_PALETTE; + } + struct palette *palette = custom_palette_from(prototype); + + struct k_means_state state = k_means_init(image, palette); + for (int i = 0; i < K_MEANS_ITERATIONS; i++) { + if (!k_means_iteration(&state)) { + fprintf(stderr, "early k-means stop at iteration %d\n", i); + break; + } + } + k_means_end(&state); + + return palette; +} + const struct palette DEFAULT_PALETTE = PALETTE( { { 0xf0, 0xf0, 0xf0, 0xff } }, { { 0xf2, 0xb2, 0x33, 0xff } }, From c91f4d79bd758283572930f350b61a91506de5a2 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Thu, 3 Oct 2024 10:16:57 +0200 Subject: [PATCH 11/13] Allocate the correct amount of intermediate centroid data --- img2cpi.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/img2cpi.c b/img2cpi.c index be9c3fa..af339bc 100644 --- a/img2cpi.c +++ b/img2cpi.c @@ -675,11 +675,12 @@ struct palette *custom_palette_from(const struct palette *orig) { struct k_means_state k_means_init(const struct image *image, struct palette *starting_palette) { size_t item_count = image->w * image->h; + uint8_t cluster_count = starting_palette->count; struct k_means_state state = { .items = image, .clusters = starting_palette, .predicted_cluster = calloc(image->w, image->h), - .centroid_intermediate = calloc(item_count, sizeof(struct k_means_centroid_intermediate)), + .centroid_intermediate = calloc(cluster_count, sizeof(struct k_means_centroid_intermediate)), .item_count = item_count, }; return state; From 5f95f895d245200c33b92ba8978b4ae30dec4657 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Thu, 3 Oct 2024 10:18:02 +0200 Subject: [PATCH 12/13] Try to avoid having unused palette items when the source image only uses a limited gamut region Warp each empty cluster's centroid onto the closest dataset item --- img2cpi.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/img2cpi.c b/img2cpi.c index af339bc..960bb54 100644 --- a/img2cpi.c +++ b/img2cpi.c @@ -95,6 +95,8 @@ struct k_means_state { float r, g, b; } sums; size_t count; + union color closest_present_item; + float closest_present_distance; } *centroid_intermediate; size_t item_count; }; @@ -683,6 +685,12 @@ struct k_means_state k_means_init(const struct image *image, struct palette *sta .centroid_intermediate = calloc(cluster_count, sizeof(struct k_means_centroid_intermediate)), .item_count = item_count, }; + if (state.centroid_intermediate) { + for (size_t i = 0; i < cluster_count; i++) { + state.centroid_intermediate[i].closest_present_item = starting_palette->colors[i]; + state.centroid_intermediate[i].closest_present_distance = 1e20; + } + } return state; } @@ -697,11 +705,16 @@ bool k_means_iteration(struct k_means_state *state) { int closest_cluster = 0; float closest_distance = 1e20; for (int cluster = 0; cluster < state->clusters->count; cluster++) { - float dist = get_color_difference(state->clusters->colors[cluster], state->items->pixels[i]); + union color item = state->items->pixels[i]; + float dist = get_color_difference(state->clusters->colors[cluster], item); if (dist <= closest_distance) { closest_distance = dist; closest_cluster = cluster; } + if (dist <= state->centroid_intermediate[cluster].closest_present_distance) { + state->centroid_intermediate[cluster].closest_present_item = item; + state->centroid_intermediate[cluster].closest_present_distance = dist; + } } if (!changed) { changed = state->predicted_cluster[i] != closest_cluster; @@ -731,9 +744,10 @@ bool k_means_iteration(struct k_means_state *state) { state->clusters->colors[i] = centroid; } else { // No pixels are closest to this color - // TODO: wiggle the centroid? + // Warp the centroid onto the closest item + state->clusters->colors[i] = intermediate.closest_present_item; } - state->centroid_intermediate[i] = (struct k_means_centroid_intermediate) { .sums = {0, 0, 0}, .count = 0 }; + state->centroid_intermediate[i] = (struct k_means_centroid_intermediate) { .sums = {0, 0, 0}, .count = 0, .closest_present_item = state->clusters->colors[i], .closest_present_distance = 1e20 }; } return changed; From 6028bb419e702950454757637e389ccb9b8c9ac5 Mon Sep 17 00:00:00 2001 From: Vftdan Date: Thu, 3 Oct 2024 11:26:45 +0200 Subject: [PATCH 13/13] Avoid duplicate palette entries Don't make an item an empty cluster warp candidate, if it is already warp candidate for another cluster --- img2cpi.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/img2cpi.c b/img2cpi.c index 960bb54..9b3fb11 100644 --- a/img2cpi.c +++ b/img2cpi.c @@ -711,9 +711,21 @@ bool k_means_iteration(struct k_means_state *state) { closest_distance = dist; closest_cluster = cluster; } - if (dist <= state->centroid_intermediate[cluster].closest_present_distance) { - state->centroid_intermediate[cluster].closest_present_item = item; - state->centroid_intermediate[cluster].closest_present_distance = dist; + if (dist < state->centroid_intermediate[cluster].closest_present_distance) { + bool can_update = true; + for (int other_cluster = 0; other_cluster < state->clusters->count; other_cluster++) { + if (other_cluster == cluster) { + continue; + } + if (state->centroid_intermediate[other_cluster].closest_present_item.v == item.v) { + can_update = false; + break; + } + } + if (can_update) { + state->centroid_intermediate[cluster].closest_present_item = item; + state->centroid_intermediate[cluster].closest_present_distance = dist; + } } } if (!changed) {