htpp

htrdr-image post-processing
git clone git://git.meso-star.fr/htpp.git
Log | Files | Refs | README | LICENSE

commit aa49a2927171afb33a9949173de3a61bd9371a89
parent 99c23c41f8d08c95f4a9b842017200c0bbf29865
Author: Vincent Forest <vincent.forest@meso-star.com>
Date:   Thu,  2 Apr 2020 12:26:52 +0200

Merge branch 'feature_colormap' into develop

Diffstat:
MREADME.md | 12++++++------
Mcmake/CMakeLists.txt | 5+++--
Mdoc/htpp.1.txt | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/htpp.c | 669++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
4 files changed, 590 insertions(+), 203 deletions(-)

diff --git a/README.md b/README.md @@ -50,10 +50,10 @@ used formulae. ## License -htpp copyright (C) 2018-2019 Centre National de la Recherche -(CNRS), [|Meso|Star>](https://www.meso-star.com) <contact@meso-star.com>, -Université Paul Sabatier <contact-edstar@laplace.univ-tlse.fr>. It is free -software released under the GPL v3+ license: GNU GPL version 3 or later. You -are welcome to redistribute it under certain conditions; refer to the COPYING -file for details. +Copyright (C) 2018, 2019, 2029 [|Meso|Star>](https://www.meso-star.com) +<contact@meso-star.com>. Copyright (C) 2018-2019 Centre National de la +Recherche (CNRS), Université Paul Sabatier +<contact-edstar@laplace.univ-tlse.fr>. htpp is free software released under the +GPL v3+ license: GNU GPL version 3 or later. You are welcome to redistribute it +under certain conditions; refer to the COPYING file for details. diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt @@ -22,7 +22,8 @@ set(HTPP_SOURCE_DIR ${PROJECT_SOURCE_DIR}/../src) # Check dependencies ################################################################################ find_package(RCMake 0.3 REQUIRED) -find_package(RSys 0.6 REQUIRED) +find_package(RSys 0.9 REQUIRED) +find_package(StarCMap 0.0 REQUIRED) find_package(OpenMP 1.2 REQUIRED) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${RCMAKE_SOURCE_DIR}) @@ -50,7 +51,7 @@ rcmake_prepend_path(HTPP_FILES_SRC ${HTPP_SOURCE_DIR}) rcmake_prepend_path(HTPP_FILES_DOC ${PROJECT_SOURCE_DIR}/../) add_executable(htpp ${HTPP_FILES_SRC}) -target_link_libraries(htpp RSys) +target_link_libraries(htpp RSys StarCMap) set_target_properties(htpp PROPERTIES COMPILE_FLAGS "${OpenMP_C_FLAGS}") diff --git a/doc/htpp.1.txt b/doc/htpp.1.txt @@ -1,4 +1,5 @@ -// Copyright (C) 2018-2019 CNRS, |Meso|Star>, Université Paul Sabatier +// Copyright (C) 2018, 2019, 2020 |Meso|Star> (contact@meso-star.com) +// Copyright (C) 2018, 2019 CNRS, Université Paul Sabatier // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -28,12 +29,16 @@ htpp [_option_] ... [_input_] DESCRIPTION ----------- -*htpp* post-processes an *htrdr-image*(5) and convert it to a regular -PPM image [1]. If _input_ is not defined, the *htrdr-image*(5) is read from -standard input. +*htpp* post-processes a *htrdr-image*(5) and convert it to a regular PPM image +[1]. If _input_ is not defined, the *htrdr-image*(5) is read from standard +input. Two post-processing procedures are provided: the post-processing of the +image colors (option *-i*), and the mapping of a given pixel component onto a +color ramp (option "*-m*). By default, *htpp* post-process the image color. -*htpp* tone maps the high dynamic range colors of the _input_ image with the -following filmic tone mapping operator [2]: +To post-process the image colors (option *-i*) *htpp* assumes that the first, +the third and the fifth component of each *htrdr-image*(5) pixel encodes a +color with respect to the CIE 1931 XYZ color space. *htpp* first +tone maps these colors with the following filmic tone mapping operator [2]: out-color = f(in-color * exposure) / f(white-scale) @@ -48,41 +53,71 @@ with: E = 0.02 F = 0.30 -The _exposure_ term is an user defined parameter provided by the *-e* option. -If not defined, a default _exposure_ of 1 is used. The _white-scale_ factor is -either defined by the user through the *-w* option or automatically computed -as the luminance from which roughly all image pixels have a luminance less -than _white-scale_. Currently, *htpp* empirically defines _white-scale_ as the -luminance greater than the luminance of 99.5% of the pixels. Once tone mapped, -the pixels are transformed from the CIE 1931 XYZ color space to the sRGB -linear color space before to be gamma corrected. Finally, the resulting pixel -components are clamped to [0, 1] and encoded on 8-bits. +The _exposure_ term is an user defined parameter provided as the _exposure_ +image option. If not defined, a default _exposure_ of 1 is used. The +_white-scale_ factor is either defined by the user through the _white_ image +option or automatically computed as the luminance from which roughly all image +pixels have a luminance less than _white-scale_. Currently, *htpp* empirically +defines _white-scale_ as the luminance greater than the luminance of 99.5% of +the pixels. Once tone mapped, the pixels are transformed from the CIE 1931 XYZ +color space to the sRGB linear color space before to be gamma corrected. +Finally, the resulting pixel components are clamped to [0, 1] and encoded on +8-bits. + +The mapping of a pixel component onto a color ramp is controlled by the *-m* +option. The pixel component to post-process is defined by the _pixcpnt_ +mapping option. *htpp* normalized this component according to its range onto +the whole image or with respect to the _range_ mapping option if it is +defined. The resuling value is then mapped to a built-in color ramp whose name +is defined by the _palette_ mapping option. OPTIONS ------- -*-e* _exposure_:: - Pixel exposure. By default its value is 1. - *-f*:: Force overwrite of the _output_ file. *-h*:: List short help and exit. +*-i* <__sub-option__>[:<__sub-option__> ...]:: + Post-process the color of the *htrdr-image*(5). The first, the third and the + fifth pixel component are assumed to store the pixel color encoded into the + CIE 1931 XYZ color space. Available sub-options are: + + **default**;; + Use the default values of the sub-options. + + **exposure**=__real__;; + Pixel exposure. By default its value is 1. + + **white**=__white-scale__;; + Factor used to normalize input colors. If not defined, the white scale is + automatically computed from the luminance of the _input_ image. + +*-m* <__sub-option__>[:<__sub-option__> ...]:: + Map a pixel component to a regular color. Available sub options are: + + **default**;; + Use the default values of the sub-options. + + **palette**=__palette-name__;; + Color palette to use. Available palettes are the ones supported + by the Star-ColorMap library [3]. The default palette is inferno. + + **pixcpnt**=__pixel-component__;; + Index in [0, 7] of the pixel component to map. The default pixel component + is the first one, i.e. *pixcpnt*=0. + + **range**=__min__,__max__;; + Range of the values to map. A degenerated range (i.e. __min__ >= __max__) + means that this range is automatically computed from the boundaries of the + selected pixel component over the whole image. This is the default + comportment. + *-o* _output_:: File where the PPM image is written. If not defined, write _output_ to standard output. -*-u*:: - Generate an image of the uncertainties of the pixels rather of their - estimated radiance. The uncertainties are tone mapped as if they were - regular colors but no XYZ to sRGB conversion is applied on the tone mapped - values. - -*-T*:: - Generate an image of the per radiative path computation time rather than an - image of the estimated radiance. - *-t* _threads-count_:: Hint on the number of threads to use. By default use as many threads as CPU cores. @@ -110,24 +145,26 @@ Convert *img.htrdr* and visualise the resulting image by redirecting the standard output to the *feh*(1) image viewer. Use an _exposure_ of *0.5* and explicitly define the normalization factor to *0.0025*: - $ htpp -e 0.5 -w 0.0025 img.htrdr | feh - + $ htpp -i exposure=0.5:white=0.0025 img.htrdr | feh - -Use the *-u* option to visualise the uncertainty of the *img.htrdr* image -rather than its estimated radiance: +Use the *-m* option to map the values of the second pixel component clamped in +[0, 2] to the color ramp _magma_. - $ htpp -u img.htrdr | feh - + $ htpp -m pixcpnt=1:palette=magma:range=0 img.htrdr | feh - NOTES ----- 1. Portable PixMap - <http://netpbm.sourceforge.net/doc/ppm.html> 2. Filmic tone mapping operator - <http://filmicworlds.com/blog/filmic-tonemapping-operators/> +3. Star-ColorMap - <https://gitlab.com/meso-star/star-cmap> COPYRIGHT --------- -Copyright &copy; 2018-2019 CNRS, |Meso|Star> <contact@meso-star.com>, -Université Paul Sabatier <contact-edstar@laplace.univ-tlse.fr>. *htpp* is free -software released under the GPLv3+ license: GNU GPL version 3 or later +Copyright &copy; 2018, 2019, 2020 |Meso|Star> <contact@meso-star.com>. +Copyright &copy; 2018, 2019 CNRS, Université Paul Sabatier +<contact-edstar@laplace.univ-tlse.fr>. *htpp* is free software released under +the GPLv3+ license: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. You are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. diff --git a/src/htpp.c b/src/htpp.c @@ -20,8 +20,10 @@ #include <rsys/cstr.h> #include <rsys/double33.h> #include <rsys/mem_allocator.h> -#include <rsys/stretchy_array.h> #include <rsys/rsys.h> +#include <rsys/text_reader.h> + +#include <star/scmap.h> #include <errno.h> #include <fcntl.h> /* open */ @@ -30,24 +32,65 @@ #include <sys/stat.h> /* S_IRUSR & S_IWUSR */ #include <unistd.h> /* getopt */ -enum pixel_data { - PIXEL_RADIANCE, - PIXEL_UNCERTAINTY, - PIXEL_TIME +enum pixcpnt { + PIXCPNT_X, + PIXCPNT_R = PIXCPNT_X, + PIXCPNT_X_STDERR, + PIXCPNT_Y, + PIXCPNT_G = PIXCPNT_Y, + PIXCPNT_Y_STDERR, + PIXCPNT_Z, + PIXCPNT_B = PIXCPNT_Z, + PIXCPNT_Z_STDERR, + PIXCPNT_TIME, + PIXCPNT_TIME_STDERR, + PIXCPNTS_COUNT__ +}; + +enum pp_type { + PP_IMAGE, + PP_MAP }; struct args { const char* input; const char* output; - double exposure; - double white_scale; - enum pixel_data pixdata; + + enum pp_type pp_type; + + struct image_opt { + double exposure; /* In [0, inf) */ + double white; /* In ]0, inf) */ + } image; + + struct map_opt { + const struct scmap_palette* palette; + unsigned pixcpnt; /* In [0, PIXCPNTS_COUNT__[ */ + double range[2]; + } map; + int verbose; int force_overwrite; int nthreads; int quit; }; -#define ARGS_DEFAULT__ {NULL,NULL,1.0,-1.0,PIXEL_RADIANCE,0,0,INT_MAX,0} +#define ARGS_DEFAULT__ { \ + NULL, /* Input */ \ + NULL, /* Output */ \ + PP_IMAGE, /* Post process type */ \ + { \ + 1.0, /* Image exposure */ \ + -1.0, /* Image white scale */ \ + }, { \ + &scmap_palette_inferno, /* Map palette */ \ + 0, /* Map channel */ \ + {DBL_MAX,-DBL_MAX}, /* Range */ \ + }, \ + 0, /* Verbosity level */ \ + 0, /* Force overwrite? */ \ + INT_MAX, /* #threads */ \ + 0 /* Quit? */ \ +} static const struct args ARGS_DEFAULT = ARGS_DEFAULT__; struct img { @@ -56,10 +99,10 @@ struct img { size_t height; size_t pitch; /* #bytes of a row */ - double Yrange[2]; /* Luminance range */ + /* Ranges of the loaded value */ + double ranges[PIXCPNTS_COUNT__][2]; }; -#define IMG_NULL__ {NULL,0,0,0,{0,0}} -static const struct img IMG_NULL = IMG_NULL__; +static const struct img IMG_NULL; /******************************************************************************* * Helper functions @@ -70,38 +113,209 @@ print_help(const char* cmd) ASSERT(cmd); printf( -"Usage: %s [OPTIONS] [INPUT]\n" -"Tone map a htrdr-image(5) and convert it in a regular PPM image.\n\n", +"Usage: %s [options] [image]\n" +"Post process a htrdr-image(5) and convert the result in a regular PPM\n" +"image. If no image name is defined, read the image data from\n" +"standard input\n", cmd); - printf( -" -e EXPOSURE exposure of the pixel. Default value is 1.\n"); + printf("\n"); printf( " -f overwrite the OUTPUT file if it already exists.\n"); printf( " -h display this help and exit.\n"); printf( -" -o OUTPUT write PPM image to OUTPUT. If not defined, write results\n" -" to standard output.\n"); +" -i <sub-option>[:<sub-option> ... ]\n" +" post process the colors of the submitted image. The\n" +" first, third, and fifth pixel component are assumed\n" +" to store a color encoded in the CIE 1931 XYZ color\n" +" space ('man htpp' for the list of sub-options).\n"); printf( -" -u dump per channel uncertainties rather than radiance.\n"); +" -m <sub-option>[:<sub-option> ... ]\n" +" map a specific pixel component to a color.\n" +" ('man htpp' for the list of sub-options).\n"); printf( -" -T dump per realiastion time rather than radiance.\n"); +" -o <output> write PPM image to <output>. If not defined, write\n" +" results to standard output.\n"); printf( -" -t THREADS hint on the number of threads to use. By default use as\n" -" many threads as CPU cores.\n"); +" -t <threads-count>\n" +" hint on the number of threads to use.\n" +" By default use as many threads as CPU cores.\n"); printf( " -v make the program verbose.\n"); printf( -" -w WHITE-SCALE Factor used to normalize input colors. By default, it is\n" -" automatically computed from the luminance of the INPUT image.\n"); - printf( " --version display version information and exit.\n"); printf("\n"); printf( -"htpp (C) 2018-2019 CNRS, |Meso|Star> <contact@meso-star.com>, Université Paul\n" -"Sabatier <contact-edstar@laplace.univ-tlse.fr>. This is free software released\n" -"under the GNU GPL license, version 3 or later. You are free to change or\n" -"redistribute it under certain conditions <http://gnu.org/licenses/gpl.html>.\n"); +"Copyright (C) 2018, 2019, 2020 |Meso|Star> <contact@meso-star.com>.\n" +"Copyright (C) 2018, 2019 CNRS, Université Paul Sabatier\n" +"<contact-edstar@laplace.univ-tlse.fr>. htpp is free software released\n" +"under the GNU GPL license, version 3 or later. You are free to change\n" +"or redistribute it under certain conditions\n" +"<http://gnu.org/licenses/gpl.html>.\n"); +} + +static res_T +parse_multiple_options + (struct args* args, + const char* str, + res_T (*parse_option)(struct args* args, const char* str)) +{ + char buf[512]; + char* tk; + char* ctx; + res_T res = RES_OK; + ASSERT(args && str); + + if(strlen(str) >= sizeof(buf) - 1/*NULL char*/) { + fprintf(stderr, "Could not duplicate the option string `%s'.\n", str); + res = RES_MEM_ERR; + goto error; + } + strncpy(buf, str, sizeof(buf)); + + tk = strtok_r(buf, ":", &ctx); + do { + res = parse_option(args, tk); + if(res != RES_OK) goto error; + tk = strtok_r(NULL, ":", &ctx); + } while(tk); + +exit: + return res; +error: + goto exit; +} + +static res_T +parse_img_option(struct args* args, const char* str) +{ + char buf[128]; + char* key; + char* val; + char* tk_ctx; + res_T res = RES_OK; + + if(strlen(str) >= sizeof(buf) - 1/*NULL char*/) { + fprintf(stderr, + "Could not duplicate the image options string `%s'.\n", str); + res = RES_MEM_ERR; + goto error; + } + strncpy(buf, str, sizeof(buf)); + + key = strtok_r(buf, "=", &tk_ctx); + val = strtok_r(NULL, "", &tk_ctx); + + if(!strcmp(key, "default")) { + if(val) { + fprintf(stderr, "Unexpected value to the image option `%s'.\n", key); + res = RES_BAD_ARG; + goto error; + } + args->image = ARGS_DEFAULT.image; + + } else { + + if(!val) { + fprintf(stderr, "Missing value to the image option `%s'.\n", key); + res = RES_BAD_ARG; + goto error; + } + + if(!strcmp(key, "exposure")) { + res = cstr_to_double(val, &args->image.exposure); + if(res != RES_OK) goto error; + if(args->image.exposure < 0) { + fprintf(stderr, "Invalid image exposure %g.\n", args->image.exposure); + res = RES_BAD_ARG; + goto error; + } + } else if(!strcmp(key, "white")) { + res = cstr_to_double(val, &args->image.white); + if(res != RES_OK) goto error; + if(args->image.exposure < 0) { + fprintf(stderr, "Invalid image white scale %g.\n", args->image.white); + res = RES_BAD_ARG; + goto error; + } + } else { + fprintf(stderr, "Invalid image option `%s'.\n", key); + res = RES_BAD_ARG; + goto error; + } + } + +exit: + return res; +error: + goto exit; +} + +static res_T +parse_map_option(struct args* args, const char* str) +{ + char buf[128]; + char* key; + char* val; + char* tk_ctx; + res_T res = RES_OK; + + if(strlen(str) >= sizeof(buf) - 1/*NULL char*/) { + fprintf(stderr, + "Could not duplicate the map options string `%s'.\n", str); + res = RES_MEM_ERR; + goto error; + } + strncpy(buf, str, sizeof(buf)); + + key = strtok_r(buf, "=", &tk_ctx); + val = strtok_r(NULL, "", &tk_ctx); + + if(!strcmp(key, "default")) { + if(val) { + fprintf(stderr, "Unexpected value to the map option `%s'.\n", key); + res = RES_BAD_ARG; + goto error; + } + args->map = ARGS_DEFAULT.map; + + } else { + if(!val) { + fprintf(stderr, "Missing value to the map option `%s'.\n", key); + res = RES_BAD_ARG; + goto error; + } + + if(!strcmp(key, "pixcpnt")) { + res = cstr_to_uint(val, &args->map.pixcpnt); + if(res != RES_OK) goto error; + if(args->map.pixcpnt >= PIXCPNTS_COUNT__) { + fprintf(stderr, "Invalid pixel component `%u'.\n", args->map.pixcpnt); + res = RES_BAD_ARG; + goto error; + } + } else if(!strcmp(key, "palette")) { + args->map.palette = scmap_get_builtin_palette(val); + if(!args->map.palette) { + fprintf(stderr, "Invalid palette `%s'.\n", val); + res = RES_BAD_ARG; + goto error; + } + } else if(!strcmp(key, "range")) { + size_t len; + res = cstr_to_list_double(val, ',', args->map.range, &len, 2); + if(res != RES_OK) goto error; + } else { + fprintf(stderr, "Invalid map option `%s'.\n", key); + res = RES_BAD_ARG; + goto error; + } + } + +exit: + return res; +error: + goto exit; } static void @@ -130,30 +344,29 @@ args_init(struct args* args, const int argc, char** argv) } } - while((opt = getopt(argc, argv, "e:fho:Tt:uvw:")) != -1) { + /* Begin the optstring by ':' to make silent getopt */ + while((opt = getopt(argc, argv, "fhi:m:o:t:v")) != -1) { switch(opt) { - case 'e': - res = cstr_to_double(optarg, &args->exposure); - if(res == RES_OK && args->exposure < 0) res = RES_BAD_ARG; - break; case 'f': args->force_overwrite = 1; break; case 'h': print_help(argv[0]); args_release(args); args->quit = 1; goto exit; + case 'i': + args->pp_type = PP_IMAGE; + res = parse_multiple_options(args, optarg, parse_img_option); + break; + case 'm': + args->pp_type = PP_MAP; + res = parse_multiple_options(args, optarg, parse_map_option); + break; case 'o': args->output = optarg; break; - case 'T': args->pixdata = PIXEL_TIME; break; case 't': res = cstr_to_int(optarg, &args->nthreads); if(res == RES_OK && args->nthreads <= 0) res = RES_BAD_ARG; break; - case 'u': args->pixdata = PIXEL_UNCERTAINTY; break; case 'v': args->verbose = 1; break; - case 'w': - res = cstr_to_double(optarg, &args->white_scale); - if(res == RES_OK && args->white_scale <= 0) res = RES_BAD_ARG; - break; default: res = RES_BAD_ARG; break; } if(res != RES_OK) { @@ -264,7 +477,10 @@ error: /* http://filmicworlds.com/blog/filmic-tonemapping-operators/ */ static double* -filmic_tone_mapping(double pixel[3], const double exposure, const double Ymax) +filmic_tone_mapping + (double pixel[PIXCPNTS_COUNT__], + const double exposure, + const double Ymax) { const double A = 0.15; const double B = 0.50; @@ -277,15 +493,15 @@ filmic_tone_mapping(double pixel[3], const double exposure, const double Ymax) const double white_scale = TONE_MAP(W); ASSERT(pixel); - pixel[0] = TONE_MAP(pixel[0]*exposure) / white_scale; - pixel[1] = TONE_MAP(pixel[1]*exposure) / white_scale; - pixel[2] = TONE_MAP(pixel[2]*exposure) / white_scale; + pixel[PIXCPNT_X] = TONE_MAP(pixel[PIXCPNT_X]*exposure) / white_scale; + pixel[PIXCPNT_Y] = TONE_MAP(pixel[PIXCPNT_Y]*exposure) / white_scale; + pixel[PIXCPNT_Z] = TONE_MAP(pixel[PIXCPNT_Z]*exposure) / white_scale; #undef TONE_MAP return pixel; } static double* -XYZ_to_sRGB(double XYZ[3]) +XYZ_to_sRGB(double pixel[PIXCPNTS_COUNT__]) { #define D65_x 0.31271 #define D65_y 0.32902 @@ -295,51 +511,33 @@ XYZ_to_sRGB(double XYZ[3]) -1.5371385, 1.8760108, -0.2040259, -0.4985314, 0.0415560, 1.0572252 }; - double* sRGB = XYZ; - double* XYZ_D65 = XYZ; - ASSERT(XYZ); + double XYZ[3], sRGB[3], XYZ_D65[3]; + ASSERT(pixel); + + XYZ[0] = pixel[PIXCPNT_X]; + XYZ[1] = pixel[PIXCPNT_Y]; + XYZ[2] = pixel[PIXCPNT_Z]; + d3_mul(XYZ_D65, XYZ, D65); d33_muld3(sRGB, mat, XYZ_D65); sRGB[0] = MMAX(sRGB[0], 0); sRGB[1] = MMAX(sRGB[1], 0); sRGB[2] = MMAX(sRGB[2], 0); - return sRGB; -} -static double* -sRGB_gamma_correct(double sRGB[3]) -{ - int i; - FOR_EACH(i, 0, 3) { - if(sRGB[i] <= 0.0031308) { - sRGB[i] = sRGB[i] * 12.92; - } else { - sRGB[i] = 1.055 * pow(sRGB[i], 1.0/2.4) - 0.055; - } - } - return sRGB; + pixel[PIXCPNT_R] = sRGB[0]; + pixel[PIXCPNT_G] = sRGB[1]; + pixel[PIXCPNT_B] = sRGB[2]; + return pixel; } -static FINLINE char* -read_line(char** buf, FILE* stream) +static double +sRGB_gamma_correct(double value) { - char* b = *buf; - const int chunk = 128; - ASSERT(buf); - if(!b) b = sa_add(b, 128); - - do { - if(!fgets(b, (int)sa_size(b), stream)) return NULL; - - /* Ensure that he whole line is read */ - while(!strrchr(b, '\n') && !feof(stream)) { - CHK(fgets(sa_add(b, (size_t)chunk)-1/*'\0' char*/, chunk+1, stream)); - } - - b[strcspn(b, "#\n\r")] = '\0'; /* Rm new line & comments */ - } while(strspn(b, " \t") == strlen(b)); /* Empty line */ - *buf = b; - return b; + if(value <= 0.0031308) { + return value * 12.92; + } else { + return 1.055 * pow(value, 1.0/2.4) - 0.055; + } } static void @@ -352,89 +550,93 @@ img_release(struct img* img) static res_T img_load (struct img* img, - const enum pixel_data pixdata, FILE* stream, const char* stream_name) { - char* b = NULL; /* Temporary buffer used to read the stream */ + struct txtrdr* txtrdr = NULL; + size_t icpnt; size_t x, y; - unsigned definition[2]; + unsigned resolution[2]; res_T res = RES_OK; ASSERT(img && stream); - memset(img, 0, sizeof(*img)); + *img = IMG_NULL; - if(!read_line(&b, stream)) { - fprintf(stderr, "%s: could not read the image definition.\n", - stream_name); - res = RES_IO_ERR; + res = txtrdr_stream(NULL, stream, stream_name, '#', &txtrdr); + if(res != RES_OK) { + fprintf(stderr, "%s: could not setup the text reader -- %s.\n", + txtrdr_get_name(txtrdr), res_to_cstr(res)); + goto error; + } + + res = txtrdr_read_line(txtrdr); + if(res != RES_OK) { + fprintf(stderr, "%s: could not read the image resolution -- %s.\n", + txtrdr_get_name(txtrdr), res_to_cstr(res)); goto error; } - if(cstr_to_list_uint(b, ' ', definition, NULL, 2) != RES_OK) { - fprintf(stderr, "%s: invalid image definition '%s'\n", - stream_name, b) ; + if(!txtrdr_get_line(txtrdr)) { + fprintf(stderr, "%s: missing image definition.\n", txtrdr_get_name(txtrdr)); res = RES_BAD_ARG; goto error; } - img->width = (size_t)definition[0]; - img->height = (size_t)definition[1]; - img->pitch = ALIGN_SIZE(sizeof(double[3])*img->width, 16u); + res = cstr_to_list_uint(txtrdr_get_cline(txtrdr), ' ', resolution, NULL, 2); + if(res != RES_OK) { + fprintf(stderr, "%s: invalid image resolution `%s' -- %s\n", + txtrdr_get_name(txtrdr), txtrdr_get_cline(txtrdr), res_to_cstr(res)) ; + goto error; + } - img->pixels = mem_alloc(img->pitch * img->height); + img->width = (size_t)resolution[0]; + img->height = (size_t)resolution[1]; + img->pitch = ALIGN_SIZE(sizeof(double[PIXCPNTS_COUNT__])*img->width, 16u); + img->pixels = mem_calloc(img->height, img->pitch); if(!img->pixels) { fprintf(stderr, "Could not allocate the image pixels.\n"); res = RES_MEM_ERR; goto error; } - /* Read pixels and compute the luminance range of the image */ - img->Yrange[0] = DBL_MAX; - img->Yrange[1] =-DBL_MAX; + /* Reset the range of each pixel components */ + FOR_EACH(icpnt, 0, PIXCPNTS_COUNT__) { + img->ranges[icpnt][0] = DBL_MAX; + img->ranges[icpnt][1] = -DBL_MAX; + } + + /* Read pixel components */ FOR_EACH(y, 0, img->height) { double* row = (double*)(img->pixels + y*img->pitch); FOR_EACH(x, 0, img->width) { - double tmp[8] = {0}; - double* pixel = row + x*3; - if(!read_line(&b, stream)) { + double* pixel = row + x*PIXCPNTS_COUNT__; + size_t ncpnts; + + res = txtrdr_read_line(txtrdr); + if(res != RES_OK) { fprintf(stderr, - "%s: could not read the XYZ value of the (%lu, %lu) pixel.\n", - stream_name, (unsigned long)x, (unsigned long)y); - res = RES_IO_ERR; + "%s: could not read the components of the pixel (%lu, %lu) -- %s.\n", + txtrdr_get_name(txtrdr), (unsigned long)x, (unsigned long)y, + res_to_cstr(res)); goto error; } - if(cstr_to_list_double(b, ' ', tmp, 0, 8) != RES_OK /* X, Y, Z, Time */ - && cstr_to_list_double(b, ' ', tmp, 0, 6) != RES_OK /* X, Y, Z */) { - fprintf(stderr, "%s: invalid XYZ[Time] value for the (%lu, %lu) pixel.\n", - stream_name, (unsigned long)x, (unsigned long)y); + + res = cstr_to_list_double(txtrdr_get_cline(txtrdr), ' ', pixel, &ncpnts, 8); + if(res != RES_OK || ncpnts < 6) { + fprintf(stderr, "%s: invalid components for the (%lu, %lu) pixel.\n", + txtrdr_get_name(txtrdr), (unsigned long)x, (unsigned long)y); res = RES_BAD_ARG; goto error; } - switch(pixdata) { - case PIXEL_RADIANCE: - pixel[0] = tmp[0]; - pixel[1] = tmp[2]; - pixel[2] = tmp[4]; - break; - case PIXEL_UNCERTAINTY: - pixel[0] = tmp[1]; - pixel[1] = tmp[3]; - pixel[2] = tmp[5]; - break; - case PIXEL_TIME: - pixel[0] = tmp[6]; - pixel[1] = tmp[6]; - pixel[2] = tmp[6]; - break; - default: FATAL("Unreachable code.\n"); break; + + FOR_EACH(icpnt, 0, ncpnts) { + img->ranges[icpnt][0] = MMIN(img->ranges[icpnt][0], pixel[icpnt]); + img->ranges[icpnt][1] = MMAX(img->ranges[icpnt][1], pixel[icpnt]); } - img->Yrange[0] = MMIN(img->Yrange[0], pixel[1]); - img->Yrange[1] = MMAX(img->Yrange[1], pixel[1]); } } exit: - if(b) sa_release(b); + if(txtrdr) txtrdr_ref_put(txtrdr); return res; error: img_release(img); @@ -460,11 +662,11 @@ img_write_ppm(const struct img* img, FILE* stream, const char* stream_name) FOR_EACH(y, 0, img->height) { const double* row = (double*)(img->pixels + y*img->pitch); FOR_EACH(x, 0, img->width) { - const double* pixel = row + x*3; + const double* pixel = row + x*PIXCPNTS_COUNT__; i = fprintf(stream, "%i %i %i\n", - (uint8_t)(CLAMP(pixel[0], 0.0, 1.0) * 255.0 + 0.5/*Round*/), - (uint8_t)(CLAMP(pixel[1], 0.0, 1.0) * 255.0 + 0.5/*Round*/), - (uint8_t)(CLAMP(pixel[2], 0.0, 1.0) * 255.0 + 0.5/*Round*/)); + (uint8_t)(CLAMP(pixel[PIXCPNT_X], 0.0, 1.0) * 255.0 + 0.5/*Round*/), + (uint8_t)(CLAMP(pixel[PIXCPNT_Y], 0.0, 1.0) * 255.0 + 0.5/*Round*/), + (uint8_t)(CLAMP(pixel[PIXCPNT_Z], 0.0, 1.0) * 255.0 + 0.5/*Round*/)); if(i < 0) { fprintf(stderr, "%s: could not write the (%lu, %lu) pixel.\n", stream_name, (unsigned long)x, (unsigned long)y); @@ -481,7 +683,7 @@ error: } static double -compute_img_normalization_factor(const struct img* img) +compute_XYZ_normalization_factor(const struct img* img) { double* Y = NULL; double Ymax; @@ -495,8 +697,8 @@ compute_img_normalization_factor(const struct img* img) FOR_EACH(y, 0, img->height) { const double* row = (double*)(img->pixels + y*img->pitch); FOR_EACH(x, 0, img->width) { - const double* pixel = row + x*3; - Y[i] = pixel[1]; + const double* pixel = row + x*PIXCPNTS_COUNT__; + Y[i] = pixel[PIXCPNT_Y]; i++; } } @@ -513,6 +715,167 @@ compute_img_normalization_factor(const struct img* img) return Ymax; } +static uint8_t +rgb_to_c256(const uint8_t rgb[3]) +{ + uint8_t c256 = 0; + ASSERT(rgb); + + if(rgb[0] == rgb[1] && rgb[1] == rgb[2]) { + int c = rgb[0]; + if(c < 4) { + c256 = 16; /* Grey 0 */ + } else if(c >= 243) { + c256 = 231; /* Grey 100 */ + } else { + c256 = (uint8_t)(232 + (c-8+5) / 10); + } + } else { + int r, g, b; + r = rgb[0] < 48 ? 0 : (rgb[0] < 95 ? 1 : 1 + (rgb[0] - 95 + 20) / 40); + g = rgb[1] < 48 ? 0 : (rgb[1] < 95 ? 1 : 1 + (rgb[1] - 95 + 20) / 40); + b = rgb[2] < 48 ? 0 : (rgb[2] < 95 ? 1 : 1 + (rgb[2] - 95 + 20) / 40); + c256 = (uint8_t)(36*r + 6*g + b + 16); + } + return c256; +} + +static void +print_color_map(const struct scmap* scmap, const double range[2]) +{ + const double ransz = range[1] - range[0]; + const int map_length = 65; + const int map_quarter = map_length / 4; + const int label_length = map_length / 4; + int i; + ASSERT(range && range[0] < range[1]); + + FOR_EACH(i, 0, map_length) { + const double u = (double)i / (double)(map_length-1); + double color[3] = {0,0,0}; + uint8_t rgb[3]; + uint8_t c256; + SCMAP(fetch_color(scmap, u, SCMAP_FILTER_LINEAR, color)); + + rgb[0] = (uint8_t)(CLAMP(color[0], 0, 1) * 255. + 0.5/*round*/); + rgb[1] = (uint8_t)(CLAMP(color[1], 0, 1) * 255. + 0.5/*round*/); + rgb[2] = (uint8_t)(CLAMP(color[2], 0, 1) * 255. + 0.5/*round*/); + c256 = rgb_to_c256(rgb); + if(i == 0 * map_quarter + || i == 1 * map_quarter + || i == 2 * map_quarter + || i == 3 * map_quarter + || i == 4 * map_quarter) { + fprintf(stderr, "\x1b[0m|"); + } else { + fprintf(stderr, "\x1b[48;5;%dm ", c256); + } + } + fprintf(stderr, "\n"); + fprintf(stderr, "%-*.5g", label_length, range[0]); + fprintf(stderr, "%-*.5g", label_length, 0.25 * ransz + range[0]); + fprintf(stderr, "%-*.5g", label_length, 0.50 * ransz + range[0]); + fprintf(stderr, "%-*.5g", label_length, 0.75 * ransz + range[0]); + fprintf(stderr, "%-*.5g", label_length, range[1]); + fprintf(stderr, "\n"); +} + +static res_T +pp_map(struct img* img, const struct args* args) +{ + struct scmap* scmap = NULL; + int64_t i; + double range[2]; + double ransz; + res_T res = RES_OK; + ASSERT(img && args && args->pp_type == PP_MAP); + + res = scmap_create(NULL, NULL, 1, args->map.palette, &scmap); + if(res != RES_OK) { + fprintf(stderr, "Could not create the color map -- %s.\n", + res_to_cstr(res)); + goto error; + } + + if(args->map.range[0] < args->map.range[1]) { + /* The range is fixed */ + range[0] = args->map.range[0]; + range[1] = args->map.range[1]; + } else { + /* The range is defined from the loaded data */ + range[0] = img->ranges[args->map.pixcpnt][0]; + range[1] = img->ranges[args->map.pixcpnt][1]; + } + ransz = range[1] - range[0]; + + omp_set_num_threads(args->nthreads); + #pragma omp parallel for + for(i=0; i < (int64_t)(img->width*img->height); ++i) { + const size_t y = (size_t)i / img->width; + const size_t x = (size_t)i % img->width; + double* row = (double*)(img->pixels + img->pitch*y); + double* pixel = row + x*PIXCPNTS_COUNT__; + if(ransz == 0) { + pixel[PIXCPNT_R] = 0; + pixel[PIXCPNT_G] = 0; + pixel[PIXCPNT_B] = 0; + } else { + const double val = CLAMP((pixel[args->map.pixcpnt] - range[0])/ransz, 0, 1); + double color[3] = {0,0,0}; + + SCMAP(fetch_color(scmap, val, SCMAP_FILTER_LINEAR, color)); + pixel[PIXCPNT_R] = color[0]; + pixel[PIXCPNT_G] = color[1]; + pixel[PIXCPNT_B] = color[2]; + } + } + + if(args->verbose) { + print_color_map(scmap, range); + } + +exit: + if(scmap) SCMAP(ref_put(scmap)); + return res; +error: + goto exit; +} + +static res_T +pp_image(struct img* img, const struct args* args) +{ + int64_t i; + double Ymax; + res_T res = RES_OK; + ASSERT(img && args && args->pp_type == PP_IMAGE); + + if(args->image.white> 0) { + Ymax = args->image.white; + } else { + Ymax = compute_XYZ_normalization_factor(img); + if(args->verbose) { + fprintf(stderr, "White scale = %g\n", Ymax); + } + } + + omp_set_num_threads(args->nthreads); + #pragma omp parallel for + for(i=0; i < (int64_t)(img->width*img->height); ++i) { + const size_t y = (size_t)i / img->width; + const size_t x = (size_t)i % img->width; + double* row = (double*)(img->pixels + img->pitch*y); + double* pixel = row + x*PIXCPNTS_COUNT__; + + filmic_tone_mapping(pixel, args->image.exposure, Ymax); + XYZ_to_sRGB(pixel); + pixel[PIXCPNT_R] = sRGB_gamma_correct(pixel[PIXCPNT_R]); + pixel[PIXCPNT_G] = sRGB_gamma_correct(pixel[PIXCPNT_G]); + pixel[PIXCPNT_B] = sRGB_gamma_correct(pixel[PIXCPNT_B]); + } + + return res; +} + /******************************************************************************* * Program ******************************************************************************/ @@ -525,10 +888,8 @@ main(int argc, char** argv) const char* stream_in_name = "stdin"; struct img img = IMG_NULL; struct args args = ARGS_DEFAULT; - double Ymax; int img_is_loaded = 0; int err = 0; - int64_t i; res_T res = RES_OK; res = args_init(&args, argc, argv); @@ -548,32 +909,16 @@ main(int argc, char** argv) fprintf(stderr, "Read image from standard input.\n"); } - res = img_load(&img, args.pixdata, stream_in, stream_in_name); + res = img_load(&img, stream_in, stream_in_name); if(res != RES_OK) goto error; img_is_loaded = 1; - if(args.white_scale > 0) { - Ymax = args.white_scale; - } else { - Ymax = compute_img_normalization_factor(&img); - if(args.verbose) fprintf(stderr, "White scale = %g\n", Ymax); - } - - omp_set_num_threads(args.nthreads); - /* Convert input HDR XYZ image in LDR image */ - #pragma omp parallel for - for(i=0; i < (int64_t)(img.width*img.height); ++i) { - const size_t y = (size_t)i / img.width; - const size_t x = (size_t)i % img.width; - double* row = (double*)(img.pixels + img.pitch*y); - double* pixel = row + x*3; - - filmic_tone_mapping(pixel, args.exposure, Ymax); /* Tone map the XYZ pixel */ - if(args.pixdata == PIXEL_RADIANCE) { - XYZ_to_sRGB(pixel); /* Convert in RGB color space */ - sRGB_gamma_correct(pixel); /* Gamma correction */ - } + switch(args.pp_type) { + case PP_IMAGE: res = pp_image(&img, &args); break; + case PP_MAP: res = pp_map(&img, &args); break; + default: FATAL("Unreachable code.\n"); break; } + if(res != RES_OK) goto error; res = img_write_ppm(&img, stream_out, stream_out_name); if(res != RES_OK) goto error; @@ -583,6 +928,10 @@ exit: if(stream_in && stream_in != stdin) fclose(stream_in); if(img_is_loaded) img_release(&img); args_release(&args); + if(mem_allocated_size() != 0) { + fprintf(stderr, "Memory leaks: %lu Bytes.\n", + (unsigned long)mem_allocated_size()); + } return err; error: err = -1;