home | blog | art | now | git gpg | email | rss

morph

Generate xresources colors from an image.
git clone https://pollux.codes/git/morph.git
Log | Files | Refs | README | LICENSE
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:
Mmorph.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); }