From 07fcb08fe8fa831949cabf72c1f840fb8c880916 Mon Sep 17 00:00:00 2001 From: giraffedata Date: Sun, 8 Aug 2010 17:21:20 +0000 Subject: Add Pamrecolor git-svn-id: http://svn.code.sf.net/p/netpbm/code/trunk@1275 9d0c8265-081b-0410-96cb-a4ca84ce46f8 --- editor/Makefile | 2 +- editor/pamrecolor.c | 523 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 editor/pamrecolor.c (limited to 'editor') diff --git a/editor/Makefile b/editor/Makefile index 52b2b0bc..cbe44f79 100644 --- a/editor/Makefile +++ b/editor/Makefile @@ -20,7 +20,7 @@ PORTBINARIES = pamaddnoise pambackground pamcomp pamcut \ pamdice pamditherbw pamedge \ pamenlarge \ pamfunc pammasksharpen \ - pampaintspill pamperspective \ + pampaintspill pamperspective pamrecolor \ pamscale pamsistoaglyph pamstretch pamthreshold pamundice \ pbmclean pbmmask pbmpscale pbmreduce \ pgmdeshadow pgmenhance \ diff --git a/editor/pamrecolor.c b/editor/pamrecolor.c new file mode 100644 index 00000000..e6441ae4 --- /dev/null +++ b/editor/pamrecolor.c @@ -0,0 +1,523 @@ +/* ---------------------------------------------------------------------- + * + * 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/. + * + * ---------------------------------------------------------------------- + */ + +#define _XOPEN_SOURCE 600 /* Make sure random(), srandom() are in */ +#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] = random() / (float)RAND_MAX; + rowData[col][PAM_GRN_PLANE] = random() / (float)RAND_MAX; + rowData[col][PAM_BLU_PLANE] = random() / (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; + + 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 * colorRow; + tuplen ** colorData; + unsigned int row; + + pm_proginit(&argc, argv); + + srandom(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); + if (cmdline.colorfile) + colorRow = NULL; + else + colorRow = pnm_allocpamrown(&outPam); + + for (row = 0; row < inPam.height; ++row) { + pnm_readpamrown(&inPam, inRow); + + if (cmdline.colorfile) + colorRow = getColorRow(&colorPam, colorData, row, outPam.width); + else 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); + if (colorRow) + pnm_freepamrown(colorRow); + + if (colorData) + pnm_freepamarrayn(colorData, &colorPam); + + if (colorfP) + pm_close(colorfP); + pm_close(ifP); + + return 0; +} + -- cgit 1.4.1