Merge branch 'vftdan-palette-k-means'

This commit is contained in:
Casey 2024-10-03 15:35:35 +03:00
commit 022cb0cde8
Signed by: hkc
GPG Key ID: F0F6CFE11CDB0960
6 changed files with 230 additions and 31 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
img2cpi
img2cpi.o
wsvpn
wsvpn.o

6
.gitmodules vendored Normal file
View File

@ -0,0 +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

9
Makefile Normal file
View File

@ -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

1
dependencies/mongoose vendored Submodule

@ -0,0 +1 @@
Subproject commit 3525f044f551816dc1469f445fc16b94d51a1e78

1
dependencies/stb vendored Submodule

@ -0,0 +1 @@
Subproject commit f75e8d1cad7d90d72ef7a4661f1b994ef78b4e31

240
img2cpi.c
View File

@ -13,6 +13,7 @@
#include <stdbool.h> #include <stdbool.h>
#define MAX_COLOR_DIFFERENCE 768 #define MAX_COLOR_DIFFERENCE 768
#define K_MEANS_ITERATIONS 4
struct rgba { uint8_t r, g, b, a; }; struct rgba { uint8_t r, g, b, a; };
union color { union color {
@ -23,9 +24,15 @@ struct cc_char {
unsigned char character; unsigned char character;
unsigned char bg, fg; 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 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 { struct arguments {
bool fast_mode; bool fast_mode;
@ -76,8 +83,22 @@ struct image {
struct image_pal { struct image_pal {
int w, h; int w, h;
uint8_t *pixels; uint8_t *pixels;
const union color *palette; const struct palette *palette;
size_t palette_size; };
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;
union color closest_present_item;
float closest_present_distance;
} *centroid_intermediate;
size_t item_count;
}; };
bool parse_cmdline(int argc, char **argv); bool parse_cmdline(int argc, char **argv);
@ -85,7 +106,7 @@ void show_help(const char *progname, bool show_all, FILE *fp);
struct image *image_load(const char *fp); struct image *image_load(const char *fp);
struct image *image_new(int w, int h); struct image *image_new(int w, int h);
struct image *image_resize(struct image *original, int new_w, int new_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_difference(union color a, union color b);
float get_color_brightness(union color clr); float get_color_brightness(union color clr);
void image_unload(struct image *img); void image_unload(struct image *img);
@ -94,6 +115,15 @@ 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_2x3(const struct image_pal *img, struct cc_char *characters);
void convert_8x11(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);
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[] = { const char *known_file_extensions[] = {
".png", ".jpg", ".jpeg", ".jfif", ".jpg", ".gif", ".png", ".jpg", ".jpeg", ".jfif", ".jpg", ".gif",
".tga", ".bmp", ".hdr", ".pnm", 0 ".tga", ".bmp", ".hdr", ".pnm", 0
@ -163,11 +193,11 @@ int main(int argc, char **argv) {
} }
// TODO: load palette, maybe calculate it too? k-means? // 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) { switch (args.palette_type) {
case PALETTE_DEFAULT: palette = DEFAULT_PALETTE; break; case PALETTE_DEFAULT: palette = &DEFAULT_PALETTE; break;
case PALETTE_DEFAULT_GRAY: palette = DEFAULT_GRAY_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_LIST: assert(0 && "Not implemented"); break;
case PALETTE_PATH: assert(0 && "Not implemented"); break; case PALETTE_PATH: assert(0 && "Not implemented"); break;
default: assert(0 && "Unreachable"); default: assert(0 && "Unreachable");
@ -198,7 +228,7 @@ int main(int argc, char **argv) {
// TODO: actually do stuff // TODO: actually do stuff
struct cc_char *characters = calloc(args.width * args.height, sizeof(struct cc_char)); 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) { if (!quantized_image) {
fprintf(stderr, "Error: failed to open the file\n"); fprintf(stderr, "Error: failed to open the file\n");
return EXIT_FAILURE; return EXIT_FAILURE;
@ -217,9 +247,9 @@ int main(int argc, char **argv) {
fputc(args.height, fp); fputc(args.height, fp);
fputc(0x00, fp); fputc(0x00, fp);
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
fputc(palette[i].rgba.r, fp); fputc(palette->colors[i].rgba.r, fp);
fputc(palette[i].rgba.g, fp); fputc(palette->colors[i].rgba.g, fp);
fputc(palette[i].rgba.b, fp); fputc(palette->colors[i].rgba.b, fp);
} }
for (int i = 0; i < args.width * args.height; i++) { for (int i = 0; i < args.width * args.height; i++) {
fputc(characters[i].character, fp); fputc(characters[i].character, fp);
@ -486,19 +516,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)); struct image_pal *out = calloc(1, sizeof(struct image_pal));
out->w = original->w; out->w = original->w;
out->h = original->h; out->h = original->h;
out->pixels = calloc(original->w, original->h); out->pixels = calloc(original->w, original->h);
out->palette = colors; out->palette = palette;
out->palette_size = n_colors;
for (int i = 0; i < out->w * out->h; i++) { for (int i = 0; i < out->w * out->h; i++) {
int closest_color = 0; int closest_color = 0;
float closest_distance = 1e20; float closest_distance = 1e20;
for (int color = 0; color < n_colors; color++) { for (int color = 0; color < palette->count; color++) {
float dist = get_color_difference(colors[color], original->pixels[i]); float dist = get_color_difference(palette->colors[color], original->pixels[i]);
if (dist <= closest_distance) { if (dist <= closest_distance) {
closest_distance = dist; closest_distance = dist;
closest_color = color; closest_color = color;
@ -531,7 +560,7 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) {
for (int oy = 0; oy < 3; oy++) { for (int oy = 0; oy < 3; oy++) {
for (int ox = 0; ox < 2; ox++) { for (int ox = 0; ox < 2; ox++) {
unsigned char pix = img->pixels[ox + (x + (y * 3 + oy) * w) * 2]; 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) { if (brightness >= brightest_diff) {
brightest_i = pix; brightest_i = pix;
brightest_diff = brightness; brightest_diff = brightness;
@ -549,8 +578,8 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) {
for (int ox = 0; ox < 2; ox++) { for (int ox = 0; ox < 2; ox++) {
if (ox == 1 && oy == 2) continue; if (ox == 1 && oy == 2) continue;
unsigned char pix = img->pixels[ox + (x + (y * 3 + oy) * w) * 2]; 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_bg = get_color_difference(img->palette->colors[darkest_i], img->palette->colors[pix]);
float diff_fg = get_color_difference(img->palette[brightest_i], img->palette[pix]); float diff_fg = get_color_difference(img->palette->colors[brightest_i], img->palette->colors[pix]);
if (diff_fg < diff_bg) { if (diff_fg < diff_bg) {
bitmap |= pixel_bits[oy][ox]; bitmap |= pixel_bits[oy][ox];
} }
@ -559,8 +588,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]; 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_bg = get_color_difference(img->palette->colors[darkest_i], img->palette->colors[pix]);
float diff_fg = get_color_difference(img->palette[brightest_i], img->palette[pix]); float diff_fg = get_color_difference(img->palette->colors[brightest_i], img->palette->colors[pix]);
if (diff_fg < diff_bg) { if (diff_fg < diff_bg) {
bitmap ^= 31; bitmap ^= 31;
unsigned char tmp = darkest_i; unsigned char tmp = darkest_i;
@ -578,8 +607,27 @@ void convert_2x3(const struct image_pal *img, struct cc_char *characters) {
void convert_8x11(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; 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->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]);
}
}
for (int y = 0; y < h; y++) { for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) { 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++) {
uint8_t pixel_unresolved = img->pixels[
ox + (x + (y * 11 + oy) * w) * 8
];
for (int color = 0x0; color < 0x10 && color < img->palette->count; color++) {
chunk_palette_diffs[ox][oy][color] = palette_self_diffs[pixel_unresolved][color];
}
}
}
float min_diff = 0xffffff; float min_diff = 0xffffff;
char closest_sym = 0x00, closest_color = 0xae; char closest_sym = 0x00, closest_color = 0xae;
for (int sym = 0x01; sym <= 0xFF; sym++) { for (int sym = 0x01; sym <= 0xFF; sym++) {
@ -587,17 +635,12 @@ void convert_8x11(const struct image_pal *img, struct cc_char *characters) {
continue; continue;
} }
for (int color = 0x00; color <= 0xff; color++) { for (int color = 0x00; color <= 0xff; color++) {
union color cell_bg = img->palette[color & 0xF],
cell_fg = img->palette[color >> 4];
float difference = 0; float difference = 0;
for (int oy = 0; oy < 11; oy++) { for (int oy = 0; oy < 11; oy++) {
unsigned char sym_line = font_atlas[sym][oy]; unsigned char sym_line = font_atlas[sym][oy];
for (int ox = 0; ox < 8; ox++) { for (int ox = 0; ox < 8; ox++) {
bool lit = sym_line & (0x80 >> ox); bool lit = sym_line & (0x80 >> ox);
union color pixel = img->palette[img->pixels[ difference += chunk_palette_diffs[ox][oy][lit ? color >> 4 : color & 0xF];
ox + (x + (y * 11 + oy) * w) * 8
]];
difference += get_color_difference(pixel, lit ? cell_fg : cell_bg);
} }
} }
if (difference <= min_diff) { if (difference <= min_diff) {
@ -614,7 +657,142 @@ void convert_8x11(const struct image_pal *img, struct cc_char *characters) {
} }
} }
const union color DEFAULT_PALETTE[16] = { 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;
}
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(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;
}
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++) {
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) {
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) {
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
// 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, .closest_present_item = state->clusters->colors[i], .closest_present_distance = 1e20 };
}
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 } }, { { 0xf0, 0xf0, 0xf0, 0xff } },
{ { 0xf2, 0xb2, 0x33, 0xff } }, { { 0xf2, 0xb2, 0x33, 0xff } },
{ { 0xe5, 0x7f, 0xd8, 0xff } }, { { 0xe5, 0x7f, 0xd8, 0xff } },
@ -631,7 +809,7 @@ const union color DEFAULT_PALETTE[16] = {
{ { 0x57, 0xa6, 0x4e, 0xff } }, { { 0x57, 0xa6, 0x4e, 0xff } },
{ { 0xcc, 0x4c, 0x4c, 0xff } }, { { 0xcc, 0x4c, 0x4c, 0xff } },
{ { 0x11, 0x11, 0x11, 0xff } } { { 0x11, 0x11, 0x11, 0xff } }
}, DEFAULT_GRAY_PALETTE[16] = { ), DEFAULT_GRAY_PALETTE = PALETTE(
{ { 0xf0, 0xf0, 0xf0, 0xff } }, { { 0xf0, 0xf0, 0xf0, 0xff } },
{ { 0x9d, 0x9d, 0x9d, 0xff } }, { { 0x9d, 0x9d, 0x9d, 0xff } },
{ { 0xbe, 0xbe, 0xbe, 0xff } }, { { 0xbe, 0xbe, 0xbe, 0xff } },
@ -648,7 +826,7 @@ const union color DEFAULT_PALETTE[16] = {
{ { 0x6e, 0x6e, 0x6e, 0xff } }, { { 0x6e, 0x6e, 0x6e, 0xff } },
{ { 0x76, 0x76, 0x76, 0xff } }, { { 0x76, 0x76, 0x76, 0xff } },
{ { 0x11, 0x11, 0x11, 0xff } } { { 0x11, 0x11, 0x11, 0xff } }
}; );
const char font_atlas[256][11] = { const char font_atlas[256][11] = {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },