commit baaf2c668823b6c94641ffb13c98ac078a09a185
parent c706757c33d05f5f4618038cbedf2634974bb147
Author: Pollux <pollux@pollux.codes>
Date: Fri, 18 Jul 2025 21:01:27 -0500
feat: Extract main colors from image
Signed-off-by: Pollux <pollux@pollux.codes>
Diffstat:
M | morph.c | | | 191 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
1 file changed, 188 insertions(+), 3 deletions(-)
diff --git a/morph.c b/morph.c
@@ -3,10 +3,14 @@
#include <math.h>
#include <stdint.h>
+#include <stdio.h>
#include <stdlib.h>
#include <Imlib2.h>
+#define min(a, b) ((a) < (b) ? (a) : (b))
+#define max(a, b) ((a) > (b) ? (a) : (b))
+
/// Type to represent a color in the srgb color space. Use the functions
/// rgb_to_lab and lab_to_rgb to convert between different color spaces.
typedef struct {
@@ -23,6 +27,47 @@ typedef struct {
float b;
} col_lab_t;
+typedef struct {
+ col_lab_t *first;
+ size_t len;
+} col_cluster_t;
+
+int
+compare_l(const void *a, const void *b) {
+ const col_lab_t *ca = (const col_lab_t *)a;
+ const col_lab_t *cb = (const col_lab_t *)b;
+
+ if(ca->l > cb->l) {
+ return 1;
+ } else {
+ return -1;
+ }
+}
+
+int
+compare_a(const void *a, const void *b) {
+ const col_lab_t *ca = (const col_lab_t *)a;
+ const col_lab_t *cb = (const col_lab_t *)b;
+
+ if(ca->a > cb->a) {
+ return 1;
+ } else {
+ return -1;
+ }
+}
+
+int
+compare_b(const void *a, const void *b) {
+ const col_lab_t *ca = (const col_lab_t *)a;
+ const col_lab_t *cb = (const col_lab_t *)b;
+
+ if(ca->b > cb->b) {
+ return 1;
+ } else {
+ return -1;
+ }
+}
+
/// Convert a color in the srgb color space, used for color IO, to the OkLab
/// perceptual color space.
col_lab_t
@@ -91,7 +136,7 @@ lab_to_rgb(col_lab_t lab) {
/// pixels into an array of col_rgb_t. The caller is responsible for freeing
/// the return value.
col_rgb_t *
-get_image_pixel_data(const char *path) {
+get_image_pixel_data(const char *path, int *pixel_count) {
int image_width, image_height;
col_rgb_t *image_pixels;
@@ -103,7 +148,9 @@ get_image_pixel_data(const char *path) {
image_width = imlib_image_get_width();
image_height = imlib_image_get_height();
- image_pixels = malloc(image_width * image_height * sizeof(col_rgb_t));
+ *pixel_count = image_width * image_height;
+
+ image_pixels = malloc(*pixel_count * sizeof(col_rgb_t));
DATA32 *image_data = imlib_image_get_data_for_reading_only();
@@ -120,10 +167,148 @@ get_image_pixel_data(const char *path) {
return image_pixels;
}
+col_lab_t *
+cluster_image_colors(col_lab_t *colors, int color_count, int cluster_count) {
+
+ col_cluster_t *clusters =
+ malloc(cluster_count * sizeof(col_cluster_t));
+ col_lab_t *cluster_colors =
+ malloc(cluster_count * sizeof(col_lab_t));
+ clusters[0].first = colors;
+ clusters[0].len = color_count;
+
+ for(int cc = 1; cc < cluster_count; cc++) {
+
+ float max_dim_size = 0;
+ int largest_cluster = 0;
+ int largest_dimension = 0;
+
+ for(int c = 0; c < cc; c++) {
+
+ col_cluster_t cluster = clusters[c];
+
+ // Find spread in L, a, and b channels.
+
+ float min_l = 1, max_l = 0, min_a = 1, max_a =
+ -1, min_b = 1, max_b = -1;
+
+ for(int i = 0; i < cluster.len; i++) {
+ col_lab_t color = *(cluster.first + i);
+
+ min_l = min(min_l, color.l);
+ max_l = max(max_l, color.l);
+
+ min_a = min(min_a, color.a);
+ max_a = max(max_a, color.a);
+
+ min_b = min(min_b, color.b);
+ max_b = max(max_b, color.b);
+ }
+
+ // Compare with max_dim_size and set if applicable
+
+ if(max_l - min_l > max_dim_size) {
+ max_dim_size = max_l - min_l;
+ largest_cluster = c;
+ largest_dimension = 0;
+ }
+
+ if(max_a - min_a > max_dim_size) {
+ max_dim_size = max_a - min_a;
+ largest_cluster = c;
+ largest_dimension = 1;
+ }
+
+ if(max_b - min_b > max_dim_size) {
+ max_dim_size = max_b - min_b;
+ largest_cluster = c;
+ largest_dimension = 2;
+ }
+ }
+
+ // Sort largest collection by largest dimension and split into two clusters
+ // TODO: Replace with quickselect followed by partition
+
+ switch (largest_dimension) {
+ case 0:
+ qsort(clusters[largest_cluster].first,
+ clusters[largest_cluster].len, sizeof(col_lab_t),
+ compare_l);
+ break;
+ case 1:
+ qsort(clusters[largest_cluster].first,
+ clusters[largest_cluster].len, sizeof(col_lab_t),
+ compare_a);
+ break;
+ case 2:
+ qsort(clusters[largest_cluster].first,
+ clusters[largest_cluster].len, sizeof(col_lab_t),
+ compare_b);
+ break;
+ }
+
+ int total_len = clusters[largest_cluster].len;
+
+ clusters[largest_cluster].len = total_len / 2;
+ clusters[cc].first =
+ clusters[largest_cluster].first +
+ clusters[largest_cluster].len;
+ clusters[cc].len = total_len - clusters[largest_cluster].len;
+ }
+
+ // Find the median color of each cluster
+ // TODO: Optimize this using quickselect
+
+ for(int c = 0; c < cluster_count; c++) {
+
+ qsort(clusters[c].first, clusters[c].len, sizeof(col_lab_t),
+ compare_l);
+ cluster_colors[c].l =
+ (clusters[c].first + clusters[c].len / 2)->l;
+
+ qsort(clusters[c].first, clusters[c].len, sizeof(col_lab_t),
+ compare_a);
+ cluster_colors[c].a =
+ (clusters[c].first + clusters[c].len / 2)->a;
+
+ qsort(clusters[c].first, clusters[c].len, sizeof(col_lab_t),
+ compare_b);
+ cluster_colors[c].b =
+ (clusters[c].first + clusters[c].len / 2)->b;
+ }
+
+ free(clusters);
+
+ return cluster_colors;
+}
+
int
main(int argc, char **argv) {
- col_rgb_t *image_pixels = get_image_pixel_data("test.jpg");
+ int pixel_count;
+ col_rgb_t *image_pixels =
+ get_image_pixel_data("test.jpg", &pixel_count);
+
+ // TODO: White-balance colors first using GIMP algorithm
+
+ col_lab_t *image_pixels_lab =
+ malloc(pixel_count * sizeof(col_lab_t));
+
+ for(int i = 0; i < pixel_count; i++) {
+ image_pixels_lab[i] = rgb_to_lab(image_pixels[i]);
+ }
+
+ col_lab_t *primary_colors =
+ cluster_image_colors(image_pixels_lab, pixel_count, 128);
+
+ for(int c = 0; c < 128; c++) {
+ col_rgb_t rgb = lab_to_rgb(primary_colors[c]);
+
+ printf("#%02x%02x%02x\n", (int)(rgb.r * 255),
+ (int)(rgb.g * 255), (int)(rgb.b * 255));
+ }
free(image_pixels);
+ free(image_pixels_lab);
+ free(primary_colors);
}