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

morph.c (13425B)


      1 
      2 /* See LICENSE file for copyright and license details. */
      3 
      4 #include <errno.h>
      5 #include <math.h>
      6 #include <stdarg.h>
      7 #include <stdint.h>
      8 #include <stdio.h>
      9 #include <stdlib.h>
     10 #include <string.h>
     11 #include <unistd.h>
     12 
     13 #include <Imlib2.h>
     14 
     15 #define PRIMARY_COLOR_COUNT 32
     16 #define PI 3.1415926535
     17 
     18 #define WORKING_IMG_WIDTH 500
     19 #define WORKING_IMG_HEIGHT 500
     20 
     21 #define min(a, b) ((a) < (b) ? (a) : (b))
     22 #define max(a, b) ((a) > (b) ? (a) : (b))
     23 
     24 #define TARGET_COLOR(name, cr, cg, cb, cl) \
     25 	color = match_color(palette, palette_weights, PRIMARY_COLOR_COUNT*4,\
     26 			rgb_to_lab((col_rgb_t){ .r = cr, .g = cg, .b = cb}));\
     27 	color.l = cl;\
     28 	export_color(name, color);
     29 
     30 /// Type to represent a color in the srgb color space. Use the functions
     31 /// rgb_to_lab and lab_to_rgb to convert between different color spaces.
     32 typedef struct {
     33 	float           r;
     34 	float           g;
     35 	float           b;
     36 } col_rgb_t;
     37 
     38 /// Type to represent a color in the OkLab color space. Use the functions
     39 /// rgb_to_lab and lab_to_rgb to convert between different color spaces.
     40 typedef struct {
     41 	float           l;
     42 	float           a;
     43 	float           b;
     44 } col_lab_t;
     45 
     46 typedef struct {
     47 	col_lab_t      *first;
     48 	size_t          len;
     49 } col_cluster_t;
     50 
     51 /// Exit the program with an error message.
     52 void
     53 die(const char *fmt, ...) {
     54 	va_list         ap;
     55 	int             saved_errno;
     56 
     57 	saved_errno = errno;
     58 
     59 	va_start(ap, fmt);
     60 	vfprintf(stderr, fmt, ap);
     61 	va_end(ap);
     62 
     63 	if(fmt[0] && fmt[strlen(fmt) - 1] == ':')
     64 		fprintf(stderr, " %s", strerror(saved_errno));
     65 	fputc('\n', stderr);
     66 	exit(1);
     67 }
     68 
     69 void
     70 usage() {
     71 	printf("morph [-hV] <image>\n");
     72 	exit(0);
     73 }
     74 
     75 /// Compare the luminances of the two given colors. Returns 1 if the first has
     76 /// a larger luminance, returns -1 otherwise.
     77 int
     78 compare_l(const void *a, const void *b) {
     79 	const col_lab_t *ca = (const col_lab_t *)a;
     80 	const col_lab_t *cb = (const col_lab_t *)b;
     81 
     82 	if(ca->l > cb->l) {
     83 		return 1;
     84 	} else {
     85 		return -1;
     86 	}
     87 }
     88 
     89 /// Compare the 'a' of the two given colors. Returns 1 if the first has a
     90 /// larger 'a', returns -1 otherwise.
     91 int
     92 compare_a(const void *a, const void *b) {
     93 	const col_lab_t *ca = (const col_lab_t *)a;
     94 	const col_lab_t *cb = (const col_lab_t *)b;
     95 
     96 	if(ca->a > cb->a) {
     97 		return 1;
     98 	} else {
     99 		return -1;
    100 	}
    101 }
    102 
    103 /// Compare the 'b' of the two given colors. Returns 1 if the first has a
    104 /// larger 'b', returns -1 otherwise.
    105 int
    106 compare_b(const void *a, const void *b) {
    107 	const col_lab_t *ca = (const col_lab_t *)a;
    108 	const col_lab_t *cb = (const col_lab_t *)b;
    109 
    110 	if(ca->b > cb->b) {
    111 		return 1;
    112 	} else {
    113 		return -1;
    114 	}
    115 }
    116 
    117 /// Returns the distance between the two given colors in the OkLab color space.
    118 float
    119 get_color_distance(const col_lab_t c1, const col_lab_t c2) {
    120 	return sqrtf((c1.l - c2.l) * (c1.l - c2.l) +
    121 	             (c1.a - c2.a) * (c1.a - c2.a) + (c1.b - c2.b) * (c1.b -
    122 	                                                              c2.b));
    123 }
    124 
    125 /// Convert a color in the srgb color space, used for color IO, to the OkLab
    126 /// perceptual color space.
    127 col_lab_t
    128 rgb_to_lab(col_rgb_t rgb) {
    129 
    130 	col_lab_t       lab;
    131 	float           lin_r, lin_g, lin_b;
    132 	float           l, m, s;
    133 
    134 	lin_r = rgb.r >= 0.04045 ? powf((rgb.r + 0.055) / 1.055,
    135 	                                2.4) : rgb.r / 12.92;
    136 	lin_g = rgb.g >= 0.04045 ? powf((rgb.g + 0.055) / 1.055,
    137 	                                2.4) : rgb.g / 12.92;
    138 	lin_b = rgb.b >= 0.04045 ? powf((rgb.b + 0.055) / 1.055,
    139 	                                2.4) : rgb.b / 12.92;
    140 
    141 	l = cbrtf(0.4122214708 * lin_r + 0.5363325363 * lin_g +
    142 	          0.0514459929 * lin_b);
    143 	m = cbrtf(0.2119034982 * lin_r + 0.6806995451 * lin_g +
    144 	          0.1073969566 * lin_b);
    145 	s = cbrtf(0.0883024619 * lin_r + 0.2817188376 * lin_g +
    146 	          0.6299787005 * lin_b);
    147 
    148 	lab.l = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
    149 	lab.a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
    150 	lab.b = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
    151 
    152 	return lab;
    153 }
    154 
    155 /// Convert a color in the OkLab perceptual color space to the srgb color space
    156 /// for IO.
    157 col_rgb_t
    158 lab_to_rgb(col_lab_t lab) {
    159 
    160 	col_rgb_t       rgb;
    161 	float           l, m, s;
    162 	float           lin_r, lin_g, lin_b;
    163 
    164 	l = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
    165 	m = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
    166 	s = lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b;
    167 
    168 	l = l * l * l;
    169 	m = m * m * m;
    170 	s = s * s * s;
    171 
    172 	lin_r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
    173 	lin_g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
    174 	lin_b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
    175 
    176 	rgb.r = lin_r >= 0.0031308 ? 1.055 * powf(lin_r,
    177 	                                          1 / 2.4) -
    178 	        0.055 : 12.92 * lin_r;
    179 	rgb.g = lin_g >= 0.0031308 ? 1.055 * powf(lin_g,
    180 	                                          1 / 2.4) -
    181 	        0.055 : 12.92 * lin_g;
    182 	rgb.b = lin_b >= 0.0031308 ? 1.055 * powf(lin_b,
    183 	                                          1 / 2.4) -
    184 	        0.055 : 12.92 * lin_b;
    185 
    186 	return rgb;
    187 }
    188 
    189 /// Loads the image located at the given path and extracts the colors in the
    190 /// pixels into an array of col_rgb_t. The caller is responsible for freeing
    191 /// the return value. Returns NULL if the image failed to load.
    192 col_rgb_t      *
    193 get_image_pixel_data(const char *path, int *pixel_count) {
    194 
    195 	int             source_width, source_height;
    196 	col_rgb_t      *image_pixels;
    197 	DATA32         *image_data, c;
    198 	Imlib_Image     source, image;
    199 
    200 	source = imlib_load_image(path);
    201 
    202 	if(source == NULL)
    203 		return NULL;
    204 
    205 	imlib_context_set_image(source);
    206 
    207 	source_width = imlib_image_get_width();
    208 	source_height = imlib_image_get_height();
    209 
    210 	image = imlib_create_image(WORKING_IMG_WIDTH, WORKING_IMG_HEIGHT);
    211 	imlib_context_set_image(image);
    212 
    213 	imlib_blend_image_onto_image(source, 0, 0, 0, source_width,
    214 	                             source_height, 0, 0, WORKING_IMG_WIDTH,
    215 	                             WORKING_IMG_HEIGHT);
    216 
    217 	*pixel_count = WORKING_IMG_WIDTH * WORKING_IMG_HEIGHT;
    218 
    219 	image_pixels = malloc(*pixel_count * sizeof(col_rgb_t));
    220 
    221 	image_data = imlib_image_get_data_for_reading_only();
    222 
    223 	for(int pixel = 0; pixel < *pixel_count; pixel++) {
    224 
    225 		c = *(image_data + pixel);
    226 
    227 		image_pixels[pixel].r = ((c & 0x00ff0000) >> 16) / 255.0f;
    228 		image_pixels[pixel].g = ((c & 0x0000ff00) >> 8) / 255.0f;
    229 		image_pixels[pixel].b = (c & 0x000000ff) / 255.0f;
    230 	}
    231 
    232 	imlib_free_image();
    233 
    234 	imlib_context_set_image(source);
    235 	imlib_free_image();
    236 
    237 	return image_pixels;
    238 }
    239 
    240 /// Extract the dominant colors from among a list of colors in lab colorspace
    241 col_lab_t      *
    242 cluster_image_colors(col_lab_t *colors, int color_count, int cluster_count) {
    243 
    244 	col_cluster_t  *clusters;
    245 	col_lab_t      *cluster_colors;
    246 	col_lab_t       color;
    247 
    248 	float           max_dim_size;
    249 	float           max_l, min_l, max_a, min_a, max_b, min_b;
    250 	int             largest_cluster, largest_dimension;
    251 	int             cc, c, i;
    252 
    253 	clusters = malloc(cluster_count * sizeof(col_cluster_t));
    254 	cluster_colors = malloc(cluster_count * sizeof(col_lab_t));
    255 
    256 	clusters[0].first = colors;
    257 	clusters[0].len = color_count;
    258 
    259 	for(cc = 1; cc < cluster_count; cc++) {
    260 
    261 		max_dim_size = 0;
    262 		largest_cluster = 0;
    263 		largest_dimension = 0;
    264 
    265 		for(c = 0; c < cc; c++) {
    266 
    267 			min_l = 1;
    268 			max_l = -1;
    269 			min_a = 1;
    270 			max_a = -1;
    271 			min_b = 1;
    272 			max_b = -1;
    273 
    274 			for(i = 0; i < clusters[c].len; i++) {
    275 				color = clusters[c].first[i];
    276 
    277 				min_l = min(min_l, color.l);
    278 				max_l = max(max_l, color.l);
    279 
    280 				min_a = min(min_a, color.a);
    281 				max_a = max(max_a, color.a);
    282 
    283 				min_b = min(min_b, color.b);
    284 				max_b = max(max_b, color.b);
    285 			}
    286 
    287 			if(max_l - min_l > max_dim_size) {
    288 				max_dim_size = max_l - min_l;
    289 				largest_cluster = c;
    290 				largest_dimension = 0;
    291 			}
    292 
    293 			if(max_a - min_a > max_dim_size) {
    294 				max_dim_size = max_a - min_a;
    295 				largest_cluster = c;
    296 				largest_dimension = 1;
    297 			}
    298 
    299 			if(max_b - min_b > max_dim_size) {
    300 				max_dim_size = max_b - min_b;
    301 				largest_cluster = c;
    302 				largest_dimension = 2;
    303 			}
    304 		}
    305 
    306 		// TODO OPTIMIZE v0.2: Replace with quickselect followed by partition
    307 
    308 		switch (largest_dimension) {
    309 		case 0:
    310 			qsort(clusters[largest_cluster].first,
    311 			      clusters[largest_cluster].len, sizeof(col_lab_t),
    312 			      compare_l);
    313 			break;
    314 		case 1:
    315 			qsort(clusters[largest_cluster].first,
    316 			      clusters[largest_cluster].len, sizeof(col_lab_t),
    317 			      compare_a);
    318 			break;
    319 		case 2:
    320 			qsort(clusters[largest_cluster].first,
    321 			      clusters[largest_cluster].len, sizeof(col_lab_t),
    322 			      compare_b);
    323 			break;
    324 		}
    325 
    326 		clusters[cc].first =
    327 		        clusters[largest_cluster].first +
    328 		        clusters[largest_cluster].len / 2;
    329 		clusters[cc].len = (clusters[largest_cluster].len + 1) / 2;
    330 		clusters[largest_cluster].len /= 2;
    331 	}
    332 
    333 	// TODO OPTIMIZE v0.2: Optimize this using quickselect
    334 
    335 	for(int c = 0; c < cluster_count; c++) {
    336 
    337 		qsort(clusters[c].first, clusters[c].len, sizeof(col_lab_t),
    338 		      compare_l);
    339 		cluster_colors[c].l =
    340 		        (clusters[c].first + clusters[c].len / 2)->l;
    341 
    342 		qsort(clusters[c].first, clusters[c].len, sizeof(col_lab_t),
    343 		      compare_a);
    344 		cluster_colors[c].a =
    345 		        (clusters[c].first + clusters[c].len / 2)->a;
    346 
    347 		qsort(clusters[c].first, clusters[c].len, sizeof(col_lab_t),
    348 		      compare_b);
    349 		cluster_colors[c].b =
    350 		        (clusters[c].first + clusters[c].len / 2)->b;
    351 	}
    352 
    353 	free(clusters);
    354 
    355 	return cluster_colors;
    356 }
    357 
    358 /// Rotates the hue of the given Lab color by the given angle in radians.
    359 col_lab_t
    360 hue_shift(col_lab_t in, float angle) {
    361 
    362 	col_lab_t       out;
    363 
    364 	float           cos = cosf(angle);
    365 	float           sin = sinf(angle);
    366 
    367 	out.l = in.l;
    368 	out.a = cos * in.a - sin * in.b;
    369 	out.b = sin * in.a + cos * in.b;
    370 
    371 	return out;
    372 }
    373 
    374 /// Given a palette and a target color, finds the color in the palette closest
    375 /// to the target color in Lab color space. The palette weights can be used to
    376 /// biase the result towards specific colors; colors with larger weights are
    377 /// more likely to be picked.
    378 col_lab_t
    379 match_color(col_lab_t *palette, float *palette_weights, size_t palette_size,
    380 	    col_lab_t target_color) {
    381 
    382 	float           score, best_score = 0;
    383 	col_lab_t       best_color;
    384 
    385 	for(int c = 0; c < palette_size; c++) {
    386 		score = palette_weights[c] / get_color_distance(palette[c],
    387 		                                                target_color);
    388 
    389 		if(score > best_score) {
    390 			best_score = score;
    391 			best_color = palette[c];
    392 		}
    393 	}
    394 
    395 	return best_color;
    396 }
    397 
    398 /// Prints the given color in the X resources format, e.g. "name: #aabbcc"
    399 void
    400 export_color(const char *name, col_lab_t color) {
    401 
    402 	int             red, green, blue;
    403 	col_rgb_t       rgb;
    404 
    405 	rgb = lab_to_rgb(color);
    406 
    407 	red = min(1, max(0, rgb.r)) * 255;
    408 	green = min(1, max(0, rgb.g)) * 255;
    409 	blue = min(1, max(0, rgb.b)) * 255;
    410 
    411 	printf("%s: #%02x%02x%02x\n", name, red, green, blue);
    412 }
    413 
    414 void
    415 parse_argv(int argc, char **argv, char **filepath) {
    416 
    417 	int             c;
    418 
    419 	opterr = 0;
    420 
    421 	while((c = getopt(argc, argv, "hV")) != -1) {
    422 		switch (c) {
    423 		case 'V':
    424 			printf("morph-" VERSION "\n");
    425 			exit(0);
    426 		default:
    427 			usage();
    428 		}
    429 	}
    430 
    431 	if(optind >= argc)
    432 		usage();
    433 
    434 	*filepath = argv[optind];
    435 }
    436 
    437 int
    438 main(int argc, char **argv) {
    439 
    440 	char           *filepath;
    441 	int             pixel_count;
    442 	int             i;
    443 	col_rgb_t      *image_pixels_rgb;
    444 	col_lab_t      *image_pixels_lab;
    445 
    446 	col_lab_t      *primary_colors;
    447 
    448 	col_lab_t       palette[PRIMARY_COLOR_COUNT * 4];
    449 	float           palette_weights[PRIMARY_COLOR_COUNT * 4];
    450 
    451 	col_lab_t       color;
    452 
    453 	parse_argv(argc, argv, &filepath);
    454 
    455 	image_pixels_rgb = get_image_pixel_data(filepath, &pixel_count);
    456 
    457 	if(image_pixels_rgb == NULL)
    458 		die("Image failed to load.");
    459 
    460 	image_pixels_lab = malloc(pixel_count * sizeof(col_lab_t));
    461 
    462 	for(i = 0; i < pixel_count; i++) {
    463 		image_pixels_lab[i] = rgb_to_lab(image_pixels_rgb[i]);
    464 	}
    465 
    466 	primary_colors =
    467 	        cluster_image_colors(image_pixels_lab, pixel_count,
    468 	                             PRIMARY_COLOR_COUNT);
    469 
    470 	for(i = 0; i < PRIMARY_COLOR_COUNT; i++) {
    471 		palette[4 * i] = primary_colors[i];
    472 		palette_weights[4 * i] = 1;
    473 		palette[4 * i + 1] = hue_shift(primary_colors[i], PI);
    474 		palette_weights[4 * i + 1] = 0.8;
    475 		palette[4 * i + 2] = hue_shift(primary_colors[i], PI / 6);
    476 		palette_weights[4 * i + 2] = 0.4;
    477 		palette[4 * i + 3] = hue_shift(primary_colors[i], -PI / 6);
    478 		palette_weights[4 * i + 3] = 0.4;
    479 	}
    480 
    481 	TARGET_COLOR("*.color0", 0, 0, 0, 0.25);
    482 	TARGET_COLOR("*.color1", 1, 0, 0, 0.7);
    483 	TARGET_COLOR("*.color2", 0, 1, 0, 0.7);
    484 	TARGET_COLOR("*.color3", 1, 1, 0, 0.7);
    485 	TARGET_COLOR("*.color4", 0, 0, 1, 0.7);
    486 	TARGET_COLOR("*.color5", 1, 0, 1, 0.7);
    487 	TARGET_COLOR("*.color6", 0, 1, 1, 0.7);
    488 	TARGET_COLOR("*.color7", 0, 0, 0, 0.9);
    489 	TARGET_COLOR("*.color8", 0, 0, 0, 0.4);
    490 	TARGET_COLOR("*.color9", 1, 0, 0, 0.8);
    491 	TARGET_COLOR("*.color10", 0, 1, 0, 0.8);
    492 	TARGET_COLOR("*.color11", 1, 1, 0, 0.8);
    493 	TARGET_COLOR("*.color12", 0, 0, 1, 0.8);
    494 	TARGET_COLOR("*.color13", 1, 0, 1, 0.8);
    495 	TARGET_COLOR("*.color14", 0, 1, 1, 0.8);
    496 	TARGET_COLOR("*.color15", 0, 0, 0, 0.9);
    497 
    498 	TARGET_COLOR("*.base", 0, 0, 0, 0.15);
    499 
    500 	TARGET_COLOR("*.surface0", 0, 0, 0, 0.2);
    501 	TARGET_COLOR("*.surface1", 0, 0, 0, 0.3);
    502 
    503 	TARGET_COLOR("*.text2", 0, 0, 0, 0.7);
    504 	TARGET_COLOR("*.text1", 0, 0, 0, 0.8);
    505 	TARGET_COLOR("*.text0", 0, 0, 0, 0.95);
    506 
    507 	TARGET_COLOR("*.highlight", 0.7, 0.7, 0.7, 0.9);
    508 
    509 	printf("*.wallpaper: %s\n", filepath);
    510 
    511 	free(image_pixels_rgb);
    512 	free(image_pixels_lab);
    513 	free(primary_colors);
    514 }