photomosaics

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

photomosaics.c (19649B)


      1 #include <assert.h>
      2 #include <errno.h>
      3 #include <getopt.h>
      4 #include <limits.h>
      5 #include <locale.h>
      6 #include <math.h>
      7 #include <stdbool.h>
      8 #include <stdio.h>
      9 #include <stdlib.h>
     10 #include <string.h>
     11 #include <sys/stat.h>
     12 #include <time.h>
     13 #include <unistd.h>
     14 #include <MagickCore/MagickCore.h>
     15 
     16 #define DIE(fmt, ...)                        { fprintf(stderr, "FATAL: "fmt"\n", __VA_ARGS__); exit(EXIT_FAILURE); }
     17 #define WARN(fmt, ...)                       { fprintf(stderr, "WARN: " fmt"\n", __VA_ARGS__); }
     18 #define assert_error(expression, s)          if(!expression) { perror(s); abort(); }
     19 #define assert_call_succeeds(exit_code, s)   if(exit_code != EXIT_SUCCESS) { perror(s); abort(); }
     20 
     21 typedef struct {
     22     unsigned int r, g, b;
     23 } Pixel;
     24 typedef enum { L, UL, XU, XUL, F } NUM_TYPES;
     25 
     26 #define MAX_FN_LEN 150
     27 #define IMG_LIST_MAX_SIZE 5091
     28 #define DELETABLES_INITIAL_SIZE 50
     29 
     30 static const char *cache_filename = "/home/wilson/.cache/photomosaics/avgs";
     31 static char *cache_buf = NULL;
     32 static size_t cache_max_size = 0;
     33 static ssize_t initial_cache_size = 1;
     34 static ssize_t cache_size = 0;
     35 static time_t cache_mtime;
     36 static long *deletables;
     37 static size_t deletables_ind = 0;
     38 static char temp_dirname[] = "/tmp/photomosaics-XXXXXX";
     39 static char **inner_cache_tmp_files;
     40 static char **files_inner_cached = NULL;
     41 static size_t files_inner_cached_ind = 0;
     42 
     43 static size_t slen(const char *s, size_t maxlen) {
     44     char *pos = memchr(s, '\0', maxlen);
     45     return pos ? (size_t)(pos - s) : maxlen;
     46 }
     47 static size_t indof(const char *s, char ch, size_t maxlen) {
     48     char *pos = memchr(s, ch, maxlen);
     49     return pos ? (size_t)(pos - s) : maxlen;
     50 }
     51 
     52 static bool parse_num(const char *str, NUM_TYPES type, void *out) {
     53     char *endptr;
     54     const char *old_locale = setlocale(LC_ALL, NULL);
     55     setlocale(LC_ALL|~LC_NUMERIC, "");
     56     int my_errno = 0;
     57     errno = 0;
     58 
     59     switch(type) {
     60     case L:
     61         *((long *)out) = strtol(str, &endptr, 10);
     62         break;
     63     case UL:
     64         *((unsigned long *)out) = strtoul(str, &endptr, 10);
     65         break;
     66     case XU: {
     67             unsigned long tmp = strtoul(str, &endptr, 16);
     68             if(tmp > UINT_MAX) my_errno = ERANGE;
     69             else *((unsigned int *)out) = tmp;
     70         }
     71         break;
     72     case XUL:
     73         *((unsigned long *)out) = strtoul(str, &endptr, 16);
     74         break;
     75     case F:
     76         *((float *)out) = strtof(str, &endptr);
     77         break;
     78     }
     79 
     80     int their_errno = errno;
     81     setlocale(LC_ALL, old_locale);
     82     if((errno=their_errno)) return false;
     83     if(my_errno) { 
     84         errno = my_errno;
     85         return false;
     86     }
     87     /*N.B. fails on "partial" conversions or if str is empty*/
     88     return *str != '\0' && *endptr == '\0';
     89 }
     90 static bool parse_hex_tou(char *str, unsigned int *out) {
     91     return parse_num(str, XU, out);
     92 }
     93 static bool parse_ulong(char *str, unsigned long *out) {
     94     return parse_num(str, UL, out);
     95 }
     96 
     97 static Pixel hexstr_top(const char *hs) {
     98     char rstr[3] = {hs[0], hs[1],};
     99     char gstr[3] = {hs[2], hs[3],};
    100     char bstr[3] = {hs[4], hs[5],};
    101     Pixel p;
    102     parse_hex_tou(rstr, &p.r);
    103     parse_hex_tou(gstr, &p.g);
    104     parse_hex_tou(bstr, &p.b);
    105     return p;
    106 }
    107 
    108 static bool cache_init() {
    109     errno = 0;
    110     FILE *cache_file = fopen(cache_filename, "r");
    111     struct stat cache_st;
    112     if(!cache_file) {
    113         WARN("Couldn't open cache file '%s'. "
    114             "Please ensure the directory exists.", cache_filename);
    115         perror("fopen");
    116     }
    117     else {
    118         if(stat(cache_filename, &cache_st) == 0)
    119             errno = 0;
    120         else if(errno == ENOENT) {
    121             /* create the file and try again just in case that's the only error */
    122             errno = 0;
    123             FILE *tmp_cache_file = fopen(cache_filename, "a");
    124             if(!tmp_cache_file) {
    125                 WARN("Couldn't open cache file '%s'. "
    126                     "Please ensure the directory exists.", cache_filename);
    127                 perror("fopen");
    128             }
    129             else {
    130                 assert_call_succeeds(fclose(tmp_cache_file), "fclose");
    131                 if(stat(cache_filename, &cache_st) != 0) {
    132                     WARN("Could not stat cache file '%s'", cache_filename);
    133                     perror("stat");
    134                 }
    135             }
    136         }
    137     }
    138 
    139     if(errno) {
    140         WARN("Will stop attempting to cache to '%s' for the remainder of execution.", cache_filename);
    141         cache_size = -1;
    142         return false;
    143     }
    144 
    145     /* No errors, proceed to populate cache_buf */
    146 
    147     cache_mtime = cache_st.st_mtime;
    148     long cache_file_size = cache_st.st_size;
    149     /* The following 2 mallocs are guesses; will realloc later if needed */
    150     cache_max_size = (cache_file_size < 5822 ? 5822 : cache_file_size) + 5 * MAX_FN_LEN;
    151     cache_buf = malloc(cache_max_size);
    152     deletables = malloc(DELETABLES_INITIAL_SIZE * sizeof(long));
    153     initial_cache_size = cache_size = fread(cache_buf, 1, cache_file_size, cache_file);
    154     cache_buf[cache_size] = 0; /* For the initial strncat later */
    155 
    156     assert(cache_size == cache_file_size);
    157     assert_call_succeeds(fclose(cache_file), "fclose");
    158     return true;
    159 }
    160 static ssize_t cache_grep(char *key) {
    161     if(cache_size == -1)
    162         return -1;
    163     if(!cache_buf) {
    164         if(!cache_init()) return -1;
    165     }
    166     if(cache_size == 0) return -1;
    167 
    168     char filename[MAX_FN_LEN];
    169     struct stat file_st;
    170 
    171     for(ssize_t i=0; i < cache_size; i += indof(cache_buf + i, '\n', cache_size - i) + 1) {
    172         /* If we already marked it for deletion, we want the image's cache entry which
    173            we put at the bottom of the buffer, in case the avg color has changed. */
    174         bool skip = false;
    175         for(size_t j=0; j < deletables_ind; j++) {
    176             if(deletables[j] == i) {
    177                 skip = true;
    178                 break;
    179             }
    180         }
    181         if(skip) continue;
    182         size_t fn_len = 0;
    183         size_t fn_begin = i;
    184         for(; i < cache_size; i++) {
    185             if((filename[i-fn_begin]=cache_buf[i]) == '\t') {
    186                 fn_len = i - fn_begin;
    187                 filename[fn_len] = '\0';
    188                 i++;
    189                 break;
    190             }
    191         }
    192         assert(fn_len);
    193         if(!strncmp(filename, key, fn_len)) {
    194             //Already exists in cache
    195             assert_call_succeeds(stat(filename, &file_st), "stat");
    196             //The sole use of `initial_...`. Prevents the caller from re- and recaching newly-added files
    197             if(i > initial_cache_size - 1 || file_st.st_mtime < cache_mtime) {
    198                 /* Cache entry is up to date */
    199                 return i;
    200             }
    201             /* Not up to date. Caller will create a new cache entry,
    202                then we will delete this line at the end of the program */
    203             if(deletables_ind >= DELETABLES_INITIAL_SIZE) {
    204                 deletables = realloc(deletables, (deletables_ind + 1) * sizeof(deletables[0]));
    205                 assert_error(deletables, "realloc");
    206             }
    207             deletables[deletables_ind++] = fn_begin;
    208             return -1;
    209         }
    210     }
    211     return -1;
    212 }
    213 static bool cache_fetch(char *key, Pixel *value) {
    214     ssize_t i = cache_grep(key);
    215     if(i == -1) return false;
    216     char hexstr[7];
    217     assert(indof(cache_buf + i, '\n', cache_size - i) == 6);
    218     hexstr[0] = 0;
    219     strncat(hexstr, cache_buf + i, 6);
    220     *value = hexstr_top(hexstr);
    221     return true;
    222 }
    223 static bool cache_put(char *key, Pixel value) {
    224     if(!cache_buf) return false;
    225     char entry[MAX_FN_LEN + 9];
    226     int entry_length = sprintf(entry, "%s\t%02x%02x%02x\n", key, value.r, value.g, value.b);
    227     size_t new_size_of_cache = cache_size + entry_length + 1;
    228     if(new_size_of_cache > cache_max_size) {
    229         cache_buf = realloc(cache_buf, new_size_of_cache);
    230         assert_error(cache_buf, "realloc");
    231         cache_max_size = new_size_of_cache;
    232     }
    233     strncat(cache_buf, entry, entry_length);
    234     cache_size = new_size_of_cache - 1;
    235     return true;
    236 }
    237 
    238 
    239 static Pixel get_avg_color(unsigned char *pixels, const size_t pixels_column_cnt, const ssize_t x, const ssize_t y, const size_t width, const size_t height) {
    240     Pixel p = {0};
    241     int i = y * pixels_column_cnt + x * 3;
    242     for(unsigned long c=0; c < width*height;) {
    243         p.r += pixels[i++];
    244         p.g += pixels[i++];
    245         p.b += pixels[i++];
    246         if(++c % width == 0)
    247             i += (pixels_column_cnt - width) * 3; //next row ...
    248     }
    249 
    250     p.r /= width*height;
    251     p.g /= width*height;
    252     p.b /= width*height;
    253     return p;
    254 }
    255 
    256 static bool get_resized_pixel_info(const char *filename, const size_t width, const size_t height, unsigned char *pixels_out, ExceptionInfo *exception) {
    257     if(!files_inner_cached) {
    258         inner_cache_tmp_files = malloc(IMG_LIST_MAX_SIZE * sizeof(char*));
    259         files_inner_cached = malloc(IMG_LIST_MAX_SIZE * sizeof(char*));
    260     }
    261     const size_t pixels_arr_size = width * height * 3;
    262     bool file_is_cached = false;
    263     size_t i;
    264     for(i=0; i < files_inner_cached_ind; i++) {
    265         if(!strcmp(files_inner_cached[i], filename)) {
    266             file_is_cached = true;
    267             break;
    268         }
    269     }
    270 
    271     if(file_is_cached) {
    272         FILE *inner_cache = fopen(inner_cache_tmp_files[i], "rb");
    273         assert_error(inner_cache, "fopen");
    274         size_t z = fread(pixels_out, 1, pixels_arr_size, inner_cache);
    275         fclose(inner_cache);
    276         return z == pixels_arr_size;
    277     }
    278     else {
    279         if(files_inner_cached_ind == 0)
    280             mkdtemp(temp_dirname);
    281         const size_t filename_len = strlen(filename);
    282         const size_t dirname_len = strlen(temp_dirname);
    283         char *temp_name = malloc(filename_len);
    284         char *temp_path = malloc(filename_len + dirname_len + 2);
    285         temp_name[0] = 0;
    286         strncat(temp_name, filename, filename_len);
    287         /* TODO gracefully handle slashes (?) and percents in the filenames themselves */
    288         for(size_t c=0; c < filename_len; ++c) if(temp_name[c] == '/') temp_name[c] = '%';
    289         temp_path[0] = 0;
    290         strncat(temp_path, temp_dirname, dirname_len);
    291         temp_path[dirname_len] = '/';
    292         temp_path[dirname_len+1] = 0;
    293         strncat(temp_path, temp_name, filename_len);
    294         free(temp_name);
    295 
    296         FILE *inner_cache = fopen(temp_path, "wb");
    297         assert_error(inner_cache, "fopen");
    298         ImageInfo *image_info = CloneImageInfo((ImageInfo *)NULL);
    299         image_info->filename[0] = 0;
    300         strncat(image_info->filename, filename, filename_len);
    301         Image *src_img = ReadImage(image_info, exception);
    302         Image *src_img_r = ResizeImage(src_img, width, height, LanczosFilter, exception);
    303 
    304         if(!src_img_r) MagickError(exception->severity, exception->reason, exception->description);
    305         ExportImagePixels(src_img_r, 0, 0, width, height, "RGB", CharPixel, pixels_out, exception);
    306         if(exception->severity != UndefinedException) CatchException(exception);
    307         DestroyImage(src_img);
    308         DestroyImage(src_img_r);
    309         DestroyImageInfo(image_info);
    310         assert(fwrite(pixels_out, 3, pixels_arr_size / 3, inner_cache) == pixels_arr_size / 3);
    311         fclose(inner_cache);
    312         inner_cache_tmp_files[files_inner_cached_ind] = malloc(strlen(temp_path) + 1);
    313         strcpy(inner_cache_tmp_files[files_inner_cached_ind], temp_path);
    314         files_inner_cached[files_inner_cached_ind] = malloc(strlen(filename) + 1);
    315         files_inner_cached[files_inner_cached_ind][0] = 0;
    316         strncat(files_inner_cached[files_inner_cached_ind++], filename, filename_len);
    317         free(temp_path);
    318         return true;
    319     }
    320 }
    321 
    322 static unsigned char *get_img_with_closest_avg(char *img_list, size_t img_list_size, Pixel p, const size_t width, const size_t height, ExceptionInfo *exception) {
    323     const size_t pixels_arr_size = width * height * 3;
    324     unsigned char *pixels_of_closest = malloc(pixels_arr_size);
    325     unsigned char *pixels = malloc(pixels_arr_size);
    326     float distance_of_closest = sqrtf(powf(0xff, 2) * 3); //max diff value
    327     bool test_pxofcls_populated = false;
    328 
    329     for(size_t c=0; c < img_list_size;) {
    330         Pixel avg;
    331         bool fetched_avg_from_cache = cache_fetch(&img_list[c], &avg);
    332         if(!fetched_avg_from_cache) {
    333             assert(get_resized_pixel_info(&img_list[c], width, height, pixels, exception));
    334             avg = get_avg_color(pixels, width, 0, 0, width, height);
    335             assert(cache_put(&img_list[c], avg));
    336         }
    337         long rdiff = (long)avg.r - p.r;
    338         long gdiff = (long)avg.g - p.g;
    339         long bdiff = (long)avg.b - p.b;
    340         float new_distance = sqrtf(powf(rdiff, 2) + powf(gdiff, 2) + powf(bdiff, 2));
    341         if(new_distance < distance_of_closest) {
    342             distance_of_closest = new_distance;
    343             if(fetched_avg_from_cache)
    344                 assert(get_resized_pixel_info(&img_list[c], width, height, pixels, exception));
    345             // For now, return any perfect match
    346             if(new_distance < FLT_EPSILON) {
    347                free(pixels_of_closest);
    348                return pixels;
    349             }
    350             memcpy(pixels_of_closest, pixels, pixels_arr_size);
    351             test_pxofcls_populated = true;
    352         }
    353         c += slen(&img_list[c], img_list_size - c) + 1;
    354     }
    355     assert(test_pxofcls_populated);
    356     free(pixels);
    357     return pixels_of_closest;
    358 }
    359 
    360 static Image *photomosaic(Image *image, const size_t each_width, const size_t each_height, ExceptionInfo *exception) {
    361     const size_t pixel_cnt = image->columns * image->rows;
    362     unsigned char *pixels = malloc(pixel_cnt * 3);
    363     FILE *f = popen("find $(find ~/pics -type d | grep -vE 'redacted|not_real') -maxdepth 1 -type f -print0", "r");
    364     char buf[IMG_LIST_MAX_SIZE];
    365     size_t bytes_read = fread(buf, 1, IMG_LIST_MAX_SIZE, f);
    366     assert_call_succeeds(pclose(f), "pclose");
    367 
    368     assert(ExportImagePixels(image, 0, 0, image->columns, image->rows, "RGB", CharPixel, pixels, exception));
    369 
    370     for(size_t i=0, j=0; i < pixel_cnt;) {
    371         /*Specifying 0 for y allows us to automatically use i to "roll over" into next row*/
    372         Pixel p = get_avg_color(pixels, image->columns, i, 0, each_width, each_height);
    373         unsigned char *new_pixels = get_img_with_closest_avg(buf, bytes_read, p, each_width, each_height, exception);
    374         for(size_t c=0; c < each_width*each_height;) {
    375             pixels[j] = new_pixels[c*3];
    376             pixels[j+1] = new_pixels[c*3+1];
    377             pixels[j+2] = new_pixels[c*3+2];
    378             j += 3;
    379             if(++c % each_width == 0)
    380                 j += (image->columns - each_width) * 3; //next row ...
    381         }
    382         i += each_width; //next splotch
    383         /*If this row is done, skip over all the rows we just splotched*/
    384         if(i % image->columns == 0)
    385             i += image->columns * (each_height - 1);
    386         j = i * 3;
    387         free(new_pixels);
    388     }
    389     Image *new_image = ConstituteImage(image->columns, image->rows, "RGB", CharPixel, pixels, exception);
    390     free(pixels);
    391     if(!new_image)
    392         MagickError(exception->severity, exception->reason, exception->description);
    393     return new_image;
    394 }
    395 
    396 void usage(char *progname) {
    397     fprintf(stderr,
    398         "Usage: %s (-h | (-i <input_file> -o <output_file> -w <width> -l <length>))\n"
    399         "\t-h\tPrint this help message and exit.\n"
    400         "\tThis program creates a photomosaic by replacing each block\n\t"
    401         "of 'input_file' of size 'width' x 'length' by the resized\n\t"
    402         "version of some image with a similar average color.\n\t"
    403         "Writes the new image to the filename specified by 'output_file'.\n"
    404         "\nExit status:\n"
    405         "\t0\tSpecified operation succeeded\n"
    406         "\t1\tError reading or performing some operation on an image\n"
    407         "\t2\tError parsing command line arguments\n"
    408         , progname);
    409 }
    410 
    411 int main(int argc, char **argv) {
    412     ExceptionInfo *exception;
    413     Image *input_img, *output_img = NULL;
    414     const size_t max_fn_len = 400;
    415     char input_img_filename[max_fn_len];
    416     input_img_filename[0] = 0;
    417     char output_img_filename[max_fn_len];
    418     output_img_filename[0] = 0;
    419     ImageInfo *image_info, *new_image_info = NULL;
    420     size_t length = 1, width = 1;
    421 
    422     int opt;
    423     while((opt=getopt(argc, argv, "hi:o:l:w:")) > -1) {
    424         switch(opt) {
    425         case 'h':
    426             usage(argv[0]);
    427             return 0;
    428         case 'i':
    429             if(slen(optarg, max_fn_len) == max_fn_len) DIE("Argument \"%s\" to option -i should be less than %zu characters.", optarg, max_fn_len)
    430             strncat(input_img_filename, optarg, max_fn_len - 1);
    431             break;
    432         case 'l':
    433             if(!parse_ulong(optarg, &length))
    434                 DIE("Argument \"%s\" to option -l could not be parsed to an unsigned long int.", optarg);
    435             break;
    436         case 'o':
    437             if(slen(optarg, max_fn_len) == max_fn_len) DIE("Argument \"%s\" to option -o should be less than %zu characters.", optarg, max_fn_len)
    438             strncat(output_img_filename, optarg, max_fn_len - 1);
    439             break;
    440         case 'w':
    441             if(!parse_ulong(optarg, &width))
    442                 DIE("Argument \"%s\" to option -w could not be parsed to an unsigned long int.", optarg);
    443             break;
    444         }
    445     }
    446 
    447     if(slen(input_img_filename, max_fn_len) < 1)  DIE("No input image specified.%s", "");
    448     if(slen(output_img_filename, max_fn_len) < 1) DIE("No output image specified.%s", "");
    449 
    450     MagickCoreGenesis(*argv, MagickTrue);
    451     exception = AcquireExceptionInfo();
    452 
    453     image_info = CloneImageInfo((ImageInfo *)NULL);
    454     strcpy(image_info->filename, input_img_filename);
    455     input_img = ReadImage(image_info, exception);
    456     if(exception->severity != UndefinedException)
    457         CatchException(exception);
    458     if(!input_img)
    459         DIE("Input image %s could not be read.", input_img_filename);
    460 
    461     output_img = photomosaic(input_img, width, length, exception);
    462 
    463     if(exception->severity != UndefinedException)
    464         CatchException(exception);
    465 
    466 
    467     /* Teardown */
    468     if(files_inner_cached) {
    469         for(size_t i=0; i < files_inner_cached_ind; i++) {
    470             assert_call_succeeds(remove(inner_cache_tmp_files[i]), "remove");
    471             free(inner_cache_tmp_files[i]);
    472             free(files_inner_cached[i]);
    473         }
    474         assert_call_succeeds(remove(temp_dirname), "remove");
    475     }
    476     if(cache_buf) {
    477         FILE *cache = fopen(cache_filename, "w");
    478         if(!cache) {
    479             WARN("Failed to reopen the cache file '%s' for writing "
    480                 "in order to update the cache properly:", cache_filename);
    481             perror("fopen");
    482             WARN("The cache at '%s' may now contain duplicate entries.", cache_filename);
    483         }
    484         else for(ssize_t i=0; i < cache_size;) {
    485             bool keep = true;
    486             for(size_t j=0; j < deletables_ind; j++) {
    487                 if(deletables[j] == i) {
    488                     keep = false;
    489                     break;
    490                 }
    491             }
    492             size_t line_len = indof(cache_buf + i, '\n', cache_size - i);
    493             if(keep) for(size_t j=0; j <= line_len; j++) assert(fputc(cache_buf[i+j], cache) != EOF);
    494             i += line_len + 1;
    495         }
    496         if(cache) fclose(cache);
    497         free(deletables);
    498         free(cache_buf);
    499     }
    500 
    501     if(output_img) {
    502         new_image_info = CloneImageInfo((ImageInfo *)NULL);
    503         strcpy(output_img->filename, output_img_filename);
    504         WriteImage(new_image_info, output_img, exception);
    505         DestroyImage(output_img);
    506         DestroyImageInfo(new_image_info);
    507     }
    508     DestroyImage(input_img);
    509     DestroyImageInfo(image_info);
    510     DestroyExceptionInfo(exception);
    511     MagickCoreTerminus();
    512     return EXIT_SUCCESS;
    513 }