/* ---------------------------------------------------------------------- * * Replace every pixel in an image with one of equal luminance * * By Scott Pakin * * ---------------------------------------------------------------------- * * Copyright (C) 2010 Scott Pakin * * 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 * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * ---------------------------------------------------------------------- */ #include #include #include #include #include #include "mallocvar.h" #include "nstring.h" #include "shhopt.h" #include "pam.h" /* Two numbers less than REAL_EPSILON apart are considered equal. */ #define REAL_EPSILON 0.00001 /* Ensure a number N is no less than A and no greater than B. */ #define CLAMPxy(N, A, B) MAX(MIN((float)(N), (float)(B)), (float)(A)) struct rgbfrac { /* This structure represents red, green, and blue, each expressed as a fraction from 0.0 to 1.0. */ float rfrac; float gfrac; float bfrac; }; struct cmdlineInfo { /* This structure represents all of the information the user supplied in the command line but in a form that's easy for the program to use. */ const char * inputFileName; /* '-' if stdin */ const char * colorfile; /* NULL if unspecified */ struct rgbfrac color2gray; /* colorspace/rmult/gmult/bmult options. Negative numbers if unspecified. */ unsigned int targetcolorSpec; struct rgbfrac targetcolor; }; static float rgb2gray(struct rgbfrac * const color2grayP, float const red, float const grn, float const blu) { return color2grayP->rfrac * red + color2grayP->gfrac * grn + color2grayP->bfrac * blu; } static tuplen * getColorRow(struct pam * const pamP, tuplen ** const imageData, unsigned int const row, unsigned int const desiredWidth) { /*---------------------------------------------------------------------- Return a row of color data. If the number of columns is too small, repeat the existing columns in tiled fashion. ------------------------------------------------------------------------*/ unsigned int const imageRow = row % pamP->height; static tuplen * oneRow = NULL; tuplen * retval; if (pamP->width >= desiredWidth) retval = imageData[imageRow]; else { unsigned int col; if (!oneRow) { struct pam widePam; widePam = *pamP; widePam.width = desiredWidth; oneRow = pnm_allocpamrown(&widePam); } for (col = 0; col < desiredWidth; ++col) oneRow[col] = imageData[imageRow][col % pamP->width]; retval = oneRow; } return retval; } static void convertRowToGray(struct pam * const pamP, struct rgbfrac * const color2gray, tuplen * const tupleRow, samplen * const grayRow) { /*---------------------------------------------------------------------- Convert a row of RGB, grayscale, or black-and-white pixels to a row of grayscale values in the range [0, 1]. ------------------------------------------------------------------------*/ switch (pamP->depth) { case 1: case 2: { /* Black-and-white or grayscale */ unsigned int col; for (col = 0; col < pamP->width; ++col) grayRow[col] = tupleRow[col][0]; } break; case 3: case 4: { /* RGB color */ unsigned int col; for (col = 0; col < pamP->width; ++col) grayRow[col] = rgb2gray(color2gray, tupleRow[col][PAM_RED_PLANE], tupleRow[col][PAM_GRN_PLANE], tupleRow[col][PAM_BLU_PLANE]); } break; default: pm_error("internal error: unexpected image depth %u", pamP->depth); break; } } static void explicitlyColorRow(struct pam * const pamP, tuplen * const rowData, struct rgbfrac const tint) { unsigned int col; for (col = 0; col < pamP->width; ++col) { rowData[col][PAM_RED_PLANE] = tint.rfrac; rowData[col][PAM_GRN_PLANE] = tint.gfrac; rowData[col][PAM_BLU_PLANE] = tint.bfrac; } } static void randomlyColorRow(struct pam * const pamP, tuplen * const rowData) { /*---------------------------------------------------------------------- Assign each tuple in a row a random color. ------------------------------------------------------------------------*/ unsigned int col; for (col = 0; col < pamP->width; ++col) { rowData[col][PAM_RED_PLANE] = rand() / (float)RAND_MAX; rowData[col][PAM_GRN_PLANE] = rand() / (float)RAND_MAX; rowData[col][PAM_BLU_PLANE] = rand() / (float)RAND_MAX; } } static void recolorRow(struct pam * const inPamP, tuplen * const inRow, struct rgbfrac * const color2grayP, tuplen * const colorRow, struct pam * const outPamP, tuplen * const outRow) { /*---------------------------------------------------------------------- Map each tuple in a given row to a random color with the same luminance. ------------------------------------------------------------------------*/ static samplen * grayRow = NULL; unsigned int col; if (!grayRow) MALLOCARRAY_NOFAIL(grayRow, inPamP->width); convertRowToGray(inPamP, color2grayP, inRow, grayRow); for (col = 0; col < inPamP->width; ++col) { float targetgray; float givengray; float red, grn, blu; red = colorRow[col][PAM_RED_PLANE]; /* initial value */ grn = colorRow[col][PAM_GRN_PLANE]; /* initial value */ blu = colorRow[col][PAM_BLU_PLANE]; /* initial value */ targetgray = grayRow[col]; givengray = rgb2gray(color2grayP, red, grn, blu); if (givengray == 0.0) { /* Special case for black so we don't divide by zero */ red = targetgray; grn = targetgray; blu = targetgray; } else { /* Try simply scaling each channel equally. */ red *= targetgray / givengray; grn *= targetgray / givengray; blu *= targetgray / givengray; if (red > 1.0 || grn > 1.0 || blu > 1.0) { /* Repeatedly raise the level of all non-1.0 channels * until all channels are at 1.0 or we reach our * target gray. */ red = MIN(red, 1.0); grn = MIN(grn, 1.0); blu = MIN(blu, 1.0); givengray = rgb2gray(color2grayP, red, grn, blu); while (fabsf(givengray - targetgray) > REAL_EPSILON) { float increment; /* How much to increase each channel (unscaled amount) */ int subOne = 0; /* Number of channels with sub-1.0 values */ /* Tally the number of channels that aren't yet maxed out. */ if (red < 1.0) subOne++; if (grn < 1.0) subOne++; if (blu < 1.0) subOne++; /* Stop if we've reached our target or can't increment * any channel any further. */ if (subOne == 0) break; /* Brighten each non-maxed channel equally. */ increment = (targetgray - givengray) / subOne; if (red < 1.0) red = MIN(red + increment / color2grayP->rfrac, 1.0); if (grn < 1.0) grn = MIN(grn + increment / color2grayP->gfrac, 1.0); if (blu < 1.0) blu = MIN(blu + increment / color2grayP->bfrac, 1.0); /* Prepare to try again. */ givengray = rgb2gray(color2grayP, red, grn, blu); } } else givengray = rgb2gray(color2grayP, red, grn, blu); } outRow[col][PAM_RED_PLANE] = red; outRow[col][PAM_GRN_PLANE] = grn; outRow[col][PAM_BLU_PLANE] = blu; if (outPamP->depth == 4) outRow[col][PAM_TRN_PLANE] = inRow[col][PAM_TRN_PLANE]; } } static struct rgbfrac color2GrayFromCsName(const char * const csName) { struct rgbfrac retval; /* Thanks to http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html for these values. */ if (streq(csName, "ntsc")) { /* NTSC RGB with an Illuminant C reference white */ retval.rfrac = 0.2989164; retval.gfrac = 0.5865990; retval.bfrac = 0.1144845; } else if (streq(csName, "srgb")) { /* sRGB with a D65 reference white */ retval.rfrac = 0.2126729; retval.gfrac = 0.7151522; retval.bfrac = 0.0721750; } else if (streq(csName, "adobe")) { /* Adobe RGB (1998) with a D65 reference white */ retval.rfrac = 0.2973769; retval.gfrac = 0.6273491; retval.bfrac = 0.0752741; } else if (streq(csName, "apple")) { /* Apple RGB with a D65 reference white */ retval.rfrac = 0.2446525; retval.gfrac = 0.6720283; retval.bfrac = 0.0833192; } else if (streq(csName, "cie")) { /* CIE with an Illuminant E reference white */ retval.rfrac = 0.1762044; retval.gfrac = 0.8129847; retval.bfrac = 0.0108109; } else if (streq(csName, "pal")) { /* PAL/SECAM with a D65 reference white */ retval.rfrac = 0.2220379; retval.gfrac = 0.7066384; retval.bfrac = 0.0713236; } else if (streq(csName, "smpte-c")) { /* SMPTE-C with a D65 reference white */ retval.rfrac = 0.2124132; retval.gfrac = 0.7010437; retval.bfrac = 0.0865432; } else if (streq(csName, "wide")) { /* Wide gamut with a D50 reference white */ retval.rfrac = 0.2581874; retval.gfrac = 0.7249378; retval.bfrac = 0.0168748; } else pm_error("Unknown color space name \"%s\"", csName); return retval; } static void parseCommandLine(int argc, const char ** const argv, struct cmdlineInfo * const cmdlineP ) { optEntry * option_def; /* Instructions to OptParseOptions3 on how to parse our options */ optStruct3 opt; unsigned int option_def_index; const char * colorspaceOpt; const char * targetcolorOpt; unsigned int csSpec, rmultSpec, gmultSpec, bmultSpec, colorfileSpec; MALLOCARRAY_NOFAIL(option_def, 100); option_def_index = 0; /* Incremented by OPTENTRY */ OPTENT3(0, "colorspace", OPT_STRING, &colorspaceOpt, &csSpec, 0); OPTENT3(0, "rmult", OPT_FLOAT, &cmdlineP->color2gray.rfrac, &rmultSpec, 0); OPTENT3(0, "gmult", OPT_FLOAT, &cmdlineP->color2gray.gfrac, &gmultSpec, 0); OPTENT3(0, "bmult", OPT_FLOAT, &cmdlineP->color2gray.bfrac, &bmultSpec, 0); OPTENT3(0, "colorfile", OPT_STRING, &cmdlineP->colorfile, &colorfileSpec, 0); OPTENT3(0, "targetcolor", OPT_STRING, &targetcolorOpt, &cmdlineP->targetcolorSpec, 0); opt.opt_table = option_def; opt.short_allowed = 0; opt.allowNegNum = 0; pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0); if (rmultSpec || gmultSpec || bmultSpec) { /* If the user explicitly specified RGB multipliers, ensure that * (a) he didn't specify --colorspace, * (b) he specified all three channels, and * (c) the values add up to 1. */ float maxLuminance; if (csSpec) pm_error("The --colorspace option is mutually exclusive with " "the --rmult, --gmult, and --bmult options"); if (!(rmultSpec && gmultSpec && bmultSpec)) pm_error("If you specify any of --rmult, --gmult, or --bmult, " "you must specify all of them"); maxLuminance = cmdlineP->color2gray.rfrac + cmdlineP->color2gray.gfrac + cmdlineP->color2gray.bfrac; if (fabsf(1.0 - maxLuminance) > REAL_EPSILON) pm_error("The values given for --rmult, --gmult, and --bmult must " "sum to 1.0, not %.10g", maxLuminance); } else if (csSpec) cmdlineP->color2gray = color2GrayFromCsName(colorspaceOpt); else cmdlineP->color2gray = color2GrayFromCsName("ntsc"); if (colorfileSpec && cmdlineP->targetcolorSpec) pm_error("The --colorfile option and the --targetcolor option are " "mutually exclusive"); if (!colorfileSpec) cmdlineP->colorfile = NULL; if (cmdlineP->targetcolorSpec) { sample const colorMaxVal = (1<<16) - 1; /* Maximum PAM maxval for precise sample-to-float conversion */ tuple const targetTuple = pnm_parsecolor(targetcolorOpt, colorMaxVal); cmdlineP->targetcolor.rfrac = targetTuple[PAM_RED_PLANE] / (float)colorMaxVal; cmdlineP->targetcolor.gfrac = targetTuple[PAM_GRN_PLANE] / (float)colorMaxVal; cmdlineP->targetcolor.bfrac = targetTuple[PAM_BLU_PLANE] / (float)colorMaxVal; } if (argc-1 < 1) cmdlineP->inputFileName = "-"; else { cmdlineP->inputFileName = argv[1]; if (argc-1 > 1) pm_error("Too many arguments: %u. The only argument is the " "optional input file name", argc-1); } } int main(int argc, const char *argv[]) { struct cmdlineInfo cmdline; /* Command-line parameters */ struct pam inPam; struct pam outPam; struct pam colorPam; FILE * ifP; FILE * colorfP; const char * comments; tuplen * inRow; tuplen * outRow; tuplen ** colorData; tuplen * colorRowBuffer; unsigned int row; pm_proginit(&argc, argv); srand(pm_randseed()); parseCommandLine(argc, argv, &cmdline); ifP = pm_openr(cmdline.inputFileName); inPam.comment_p = &comments; pnm_readpaminit(ifP, &inPam, PAM_STRUCT_SIZE(comment_p)); outPam = inPam; outPam.file = stdout; outPam.format = PAM_FORMAT; outPam.depth = 4 - (inPam.depth % 2); outPam.allocation_depth = outPam.depth; strcpy(outPam.tuple_type, PAM_PPM_TUPLETYPE); pnm_writepaminit(&outPam); if (cmdline.colorfile) { colorfP = pm_openr(cmdline.colorfile); colorPam.comment_p = NULL; colorData = pnm_readpamn(colorfP, &colorPam, PAM_STRUCT_SIZE(comment_p)); } else { colorfP = NULL; colorPam = outPam; colorData = NULL; } inRow = pnm_allocpamrown(&inPam); outRow = pnm_allocpamrown(&outPam); colorRowBuffer = pnm_allocpamrown(&outPam); for (row = 0; row < inPam.height; ++row) { tuplen * colorRow; pnm_readpamrown(&inPam, inRow); if (cmdline.colorfile) colorRow = getColorRow(&colorPam, colorData, row, outPam.width); else { colorRow = colorRowBuffer; if (cmdline.targetcolorSpec) explicitlyColorRow(&colorPam, colorRow, cmdline.targetcolor); else randomlyColorRow(&colorPam, colorRow); } recolorRow(&inPam, inRow, &cmdline.color2gray, colorRow, &outPam, outRow); pnm_writepamrown(&outPam, outRow); } pnm_freepamrown(outRow); pnm_freepamrown(inRow); pnm_freepamrown(colorRowBuffer); if (colorData) pnm_freepamarrayn(colorData, &colorPam); if (colorfP) pm_close(colorfP); pm_close(ifP); return 0; }