/****************************************************************************** pnmcolormap.c ******************************************************************************* Create a colormap file (a PPM image containing one pixel of each of a set of colors). Base the set of colors on an input image. For PGM input, do the equivalent for grayscale and produce a PGM graymap. By Bryan Henderson, San Jose, CA 2001.12.17 Derived from ppmquant, originally by Jef Poskanzer. Copyright (C) 1989, 1991 by Jef Poskanzer. Copyright (C) 2001 by Bryan Henderson. Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. This software is provided "as is" without express or implied warranty. ******************************************************************************/ #include #include "pam.h" #include "pammap.h" #include "shhopt.h" #include "mallocvar.h" #define MAXCOLORS 32767u #define MAXDEPTH 3 enum methodForLargest {LARGE_NORM, LARGE_LUM}; enum methodForRep {REP_CENTER_BOX, REP_AVERAGE_COLORS, REP_AVERAGE_PIXELS}; typedef struct box* boxVector; struct box { int ind; int colors; int sum; }; struct cmdlineInfo { /* All the information the user supplied in the command line, in a form easy for the program to use. */ const char *inputFilespec; /* Filespec of input file */ unsigned int allcolors; /* boolean: select all colors from the input */ unsigned int newcolors; /* Number of colors argument; meaningless if allcolors true */ enum methodForLargest methodForLargest; /* -spreadintensity/-spreadluminosity options */ enum methodForRep methodForRep; /* -center/-meancolor/-meanpixel options */ unsigned int sort; unsigned int square; unsigned int verbose; }; static void parseCommandLine (int argc, char ** argv, struct cmdlineInfo *cmdlineP) { /*---------------------------------------------------------------------------- parse program command line described in Unix standard form by argc and argv. Return the information in the options as *cmdlineP. If command line is internally inconsistent (invalid options, etc.), issue error message to stderr and abort program. Note that the strings we return are stored in the storage that was passed to us as the argv array. We also trash *argv. -----------------------------------------------------------------------------*/ optEntry *option_def = malloc( 100*sizeof( optEntry ) ); /* Instructions to optParseOptions3 on how to parse our options. */ optStruct3 opt; unsigned int option_def_index; unsigned int spreadbrightness, spreadluminosity; unsigned int center, meancolor, meanpixel; option_def_index = 0; /* incremented by OPTENT3 */ OPTENT3(0, "spreadbrightness", OPT_FLAG, NULL, &spreadbrightness, 0); OPTENT3(0, "spreadluminosity", OPT_FLAG, NULL, &spreadluminosity, 0); OPTENT3(0, "center", OPT_FLAG, NULL, ¢er, 0); OPTENT3(0, "meancolor", OPT_FLAG, NULL, &meancolor, 0); OPTENT3(0, "meanpixel", OPT_FLAG, NULL, &meanpixel, 0); OPTENT3(0, "sort", OPT_FLAG, NULL, &cmdlineP->sort, 0 ); OPTENT3(0, "square", OPT_FLAG, NULL, &cmdlineP->square, 0 ); OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0 ); opt.opt_table = option_def; opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ opt.allowNegNum = FALSE; /* We have no parms that are negative numbers */ optParseOptions3( &argc, argv, opt, sizeof(opt), 0 ); /* Uses and sets argc, argv, and some of *cmdline_p and others. */ if (spreadbrightness && spreadluminosity) pm_error("You cannot specify both -spreadbrightness and " "spreadluminosity."); if (spreadluminosity) cmdlineP->methodForLargest = LARGE_LUM; else cmdlineP->methodForLargest = LARGE_NORM; if (center + meancolor + meanpixel > 1) pm_error("You can specify only one of -center, -meancolor, and " "-meanpixel."); if (meancolor) cmdlineP->methodForRep = REP_AVERAGE_COLORS; else if (meanpixel) cmdlineP->methodForRep = REP_AVERAGE_PIXELS; else cmdlineP->methodForRep = REP_CENTER_BOX; if (argc-1 > 2) pm_error("Program takes at most two arguments: number of colors " "and input file specification. " "You specified %d arguments.", argc-1); else { if (argc-1 < 2) cmdlineP->inputFilespec = "-"; else cmdlineP->inputFilespec = argv[2]; if (argc-1 < 1) pm_error("You must specify the number of colors in the " "output as an argument."); else { if (strcmp(argv[1], "all") == 0) cmdlineP->allcolors = TRUE; else { char * tail; long int const newcolors = strtol(argv[1], &tail, 10); if (*tail != '\0') pm_error("The number of colors argument '%s' is not " "a number or 'all'", argv[1]); else if (newcolors < 1) pm_error("The number of colors must be positive"); else if (newcolors == 1) pm_error("The number of colors must be greater than 1."); else { cmdlineP->newcolors = newcolors; cmdlineP->allcolors = FALSE; } } } } } static int __inline__ compareplane(struct tupleint ** const comparandP, struct tupleint ** const comparatorP, unsigned int const plane) { return (*comparandP)->tuple[plane] - (*comparatorP)->tuple[plane]; } static int compareplane0(const void * const comparandP, const void * const comparatorP) { return compareplane((struct tupleint **)comparandP, (struct tupleint **)comparatorP, 0); } static int compareplane1(const void * const comparandP, const void * const comparatorP) { return compareplane((struct tupleint **)comparandP, (struct tupleint **)comparatorP, 1); } static int compareplane2(const void * const comparandP, const void * const comparatorP) { return compareplane((struct tupleint **)comparandP, (struct tupleint **)comparatorP, 2); } typedef int qsort_comparison_fn(const void *, const void *); /* This ought to be an array of functions, but the compiler complains at that for some reason. */ static struct {qsort_comparison_fn * fn;} samplecompare[MAXDEPTH]; static void initSampleCompare(void) { samplecompare[0].fn = compareplane0; samplecompare[1].fn = compareplane1; samplecompare[2].fn = compareplane2; } static int sumcompare(const void * const b1, const void * const b2) { return(((boxVector)b2)->sum - ((boxVector)b1)->sum); } /* ** Here is the fun part, the median-cut colormap generator. This is based ** on Paul Heckbert's paper "Color Image Quantization for Frame Buffer ** Display", SIGGRAPH '82 Proceedings, page 297. */ static tupletable newColorMap(unsigned int const newcolors, unsigned int const depth) { tupletable colormap; unsigned int i; struct pam pam; pam.depth = depth; colormap = pnm_alloctupletable(&pam, newcolors); for (i = 0; i < newcolors; ++i) { unsigned int plane; for (plane = 0; plane < depth; ++plane) colormap[i]->tuple[plane] = 0; } return colormap; } static boxVector newBoxVector(int const colors, int const sum, int const newcolors) { boxVector bv; MALLOCARRAY(bv, newcolors); if (bv == NULL) pm_error("out of memory allocating box vector table"); /* Set up the initial box. */ bv[0].ind = 0; bv[0].colors = colors; bv[0].sum = sum; return bv; } static void findBoxBoundaries(tupletable const colorfreqtable, unsigned int const depth, unsigned int const boxStart, unsigned int const boxSize, sample minval[], sample maxval[]) { /*---------------------------------------------------------------------------- Go through the box finding the minimum and maximum of each component - the boundaries of the box. -----------------------------------------------------------------------------*/ unsigned int plane; unsigned int i; for (plane = 0; plane < depth; ++plane) minval[plane] = maxval[plane] = colorfreqtable[boxStart]->tuple[plane]; for (i = 1; i < boxSize; ++i) { unsigned int plane; for (plane = 0; plane < depth; ++plane) { sample const v = colorfreqtable[boxStart + i]->tuple[plane]; if (v < minval[plane]) minval[plane] = v; if (v > maxval[plane]) maxval[plane] = v; } } } static unsigned int largestByNorm(sample minval[], sample maxval[], unsigned int const depth) { unsigned int largestDimension; unsigned int plane; sample largestSpreadSoFar = 0; largestDimension = 0; for (plane = 0; plane < depth; ++plane) { sample const spread = maxval[plane]-minval[plane]; if (spread > largestSpreadSoFar) { largestDimension = plane; largestSpreadSoFar = spread; } } return largestDimension; } static unsigned int largestByLuminosity(sample minval[], sample maxval[], unsigned int const depth) { /*---------------------------------------------------------------------------- This subroutine presumes that the tuple type is either BLACKANDWHITE, GRAYSCALE, or RGB (which implies pamP->depth is 1 or 3). To save time, we don't actually check it. -----------------------------------------------------------------------------*/ unsigned int retval; if (depth == 1) retval = 0; else { /* An RGB tuple */ unsigned int largestDimension; unsigned int plane; double largestSpreadSoFar; largestSpreadSoFar = 0.0; for (plane = 0; plane < 3; ++plane) { double const spread = pnm_lumin_factor[plane] * (maxval[plane]-minval[plane]); if (spread > largestSpreadSoFar) { largestDimension = plane; largestSpreadSoFar = spread; } } retval = largestDimension; } return retval; } static void centerBox(int const boxStart, int const boxSize, tupletable const colorfreqtable, unsigned int const depth, tuple const newTuple) { unsigned int plane; for (plane = 0; plane < depth; ++plane) { int minval, maxval; unsigned int i; minval = maxval = colorfreqtable[boxStart]->tuple[plane]; for (i = 1; i < boxSize; ++i) { int const v = colorfreqtable[boxStart + i]->tuple[plane]; minval = MIN( minval, v); maxval = MAX( maxval, v); } newTuple[plane] = (minval + maxval) / 2; } } static void averageColors(int const boxStart, int const boxSize, tupletable const colorfreqtable, unsigned int const depth, tuple const newTuple) { unsigned int plane; for (plane = 0; plane < depth; ++plane) { sample sum; int i; sum = 0; for (i = 0; i < boxSize; ++i) sum += colorfreqtable[boxStart+i]->tuple[plane]; newTuple[plane] = sum / boxSize; } } static void averagePixels(int const boxStart, int const boxSize, tupletable const colorfreqtable, unsigned int const depth, tuple const newTuple) { unsigned int n; /* Number of tuples represented by the box */ unsigned int plane; unsigned int i; /* Count the tuples in question */ n = 0; /* initial value */ for (i = 0; i < boxSize; ++i) n += colorfreqtable[boxStart + i]->value; for (plane = 0; plane < depth; ++plane) { sample sum; int i; sum = 0; for (i = 0; i < boxSize; ++i) sum += colorfreqtable[boxStart+i]->tuple[plane] * colorfreqtable[boxStart+i]->value; newTuple[plane] = sum / n; } } static tupletable colormapFromBv(unsigned int const newcolors, boxVector const bv, unsigned int const boxes, tupletable const colorfreqtable, unsigned int const depth, enum methodForRep const methodForRep) { /* ** Ok, we've got enough boxes. Now choose a representative color for ** each box. There are a number of possible ways to make this choice. ** One would be to choose the center of the box; this ignores any structure ** within the boxes. Another method would be to average all the colors in ** the box - this is the method specified in Heckbert's paper. A third ** method is to average all the pixels in the box. */ tupletable colormap; unsigned int bi; colormap = newColorMap(newcolors, depth); for (bi = 0; bi < boxes; ++bi) { switch (methodForRep) { case REP_CENTER_BOX: centerBox(bv[bi].ind, bv[bi].colors, colorfreqtable, depth, colormap[bi]->tuple); break; case REP_AVERAGE_COLORS: averageColors(bv[bi].ind, bv[bi].colors, colorfreqtable, depth, colormap[bi]->tuple); break; case REP_AVERAGE_PIXELS: averagePixels(bv[bi].ind, bv[bi].colors, colorfreqtable, depth, colormap[bi]->tuple); break; default: pm_error("Internal error: invalid value of methodForRep: %d", methodForRep); } } return colormap; } static unsigned int freqTotal(tupletable const freqtable, unsigned int const entries) { unsigned int i; unsigned int sum; sum = 0; for (i = 0; i < entries; ++i) sum += freqtable[i]->value; return sum; } static void splitBox(boxVector const bv, unsigned int * const boxesP, unsigned int const bi, tupletable const colorfreqtable, unsigned int const depth, enum methodForLargest const methodForLargest) { /*---------------------------------------------------------------------------- Split Box 'bi' in the box vector bv (so that bv contains one more box than it did as input). Split it so that each new box represents about half of the pixels in the distribution given by 'colorfreqtable' for the colors in the original box, but with distinct colors in each of the two new boxes. Assume the box contains at least two colors. -----------------------------------------------------------------------------*/ unsigned int const boxStart = bv[bi].ind; unsigned int const boxSize = bv[bi].colors; unsigned int const sm = bv[bi].sum; sample minval[MAXDEPTH]; sample maxval[MAXDEPTH]; unsigned int largestDimension; /* number of the plane with the largest spread */ unsigned int medianIndex; int lowersum; /* Number of pixels whose value is "less than" the median */ findBoxBoundaries(colorfreqtable, depth, boxStart, boxSize, minval, maxval); /* ** Find the largest dimension, and sort by that component. I have ** included two methods for determining the "largest" dimension; ** first by simply comparing the range in RGB space, and second ** by transforming into luminosities before the comparison. */ switch (methodForLargest) { case LARGE_NORM: largestDimension = largestByNorm(minval, maxval, depth); break; case LARGE_LUM: largestDimension = largestByLuminosity(minval, maxval, depth); break; } /* TODO: I think this sort should go after creating a box, not before splitting. Because you need the sort to use the REP_CENTER_BOX method of choosing a color to represent the final boxes */ qsort((char*) &colorfreqtable[boxStart], boxSize, sizeof(colorfreqtable[boxStart]), samplecompare[largestDimension].fn); { /* Now find the median based on the counts, so that about half the pixels (not colors, pixels) are in each subdivision. */ unsigned int i; lowersum = colorfreqtable[boxStart]->value; /* initial value */ for (i = 1; i < boxSize - 1 && lowersum < sm/2; ++i) { lowersum += colorfreqtable[boxStart + i]->value; } medianIndex = i; } /* Split the box, and sort to bring the biggest boxes to the top. */ bv[bi].colors = medianIndex; bv[bi].sum = lowersum; bv[*boxesP].ind = boxStart + medianIndex; bv[*boxesP].colors = boxSize - medianIndex; bv[*boxesP].sum = sm - lowersum; ++(*boxesP); qsort((char*) bv, *boxesP, sizeof(struct box), sumcompare); } static tupletable mediancut(tupletable const colorfreqtable, int const colors, unsigned int const depth, int const newcolors, enum methodForLargest const methodForLargest, enum methodForRep const methodForRep) { /*---------------------------------------------------------------------------- Compute a set of only 'newcolors' colors that best represent an image whose pixels are summarized by the histogram 'colorfreqtable'. There are 'colors' colors in that table. Each tuple in it has depth 'depth'. colorfreqtable[i] tells the number of pixels in the subject image have a particular color. As a side effect, sort 'colorfreqtable'. -----------------------------------------------------------------------------*/ tupletable colormap; boxVector bv; unsigned int bi; unsigned int boxes; bool multicolorBoxesExist; /* There is at least one box that contains at least 2 colors; ergo, there is more splitting we can do. */ bv = newBoxVector(colors, freqTotal(colorfreqtable, colors), newcolors); boxes = 1; multicolorBoxesExist = (colors > 1); /* Main loop: split boxes until we have enough. */ while (boxes < newcolors && multicolorBoxesExist) { /* Find the first splittable box. */ for (bi = 0; bi < boxes && bv[bi].colors < 2; ++bi); if (bi >= boxes) multicolorBoxesExist = FALSE; else splitBox(bv, &boxes, bi, colorfreqtable, depth, methodForLargest); } colormap = colormapFromBv(newcolors, bv, boxes, colorfreqtable, depth, methodForRep); free(bv); return colormap; } static void computeHistogram(struct pam * const pamP, tupletable * const colorfreqtableP, unsigned int * const colorsP, sample * const freqtableMaxvalP) { /*---------------------------------------------------------------------------- Attempt to make a histogram of the colors in the image represented by *pamP, whose file is seekable and is now positioned to the raster. If at first we don't succeed, rescale the color resolution to half the present maxval and try again, repeatedly. This will eventually terminate, with maxval at worst 16, which is half of 32, and 32^3 is just slightly larger than MAXCOLORS. ----------------------------------------------------------------------------*/ tupletable colorfreqtable; sample maxval; pm_filepos rasterPos; pm_message("making histogram..."); pm_tell2(pamP->file, &rasterPos, sizeof(rasterPos)); colorfreqtable = NULL; /* initial value */ for (maxval = pamP->maxval; colorfreqtable == NULL && maxval > 0; maxval /= 2) { pm_seek2(pamP->file, &rasterPos, sizeof(rasterPos)); colorfreqtable = pnm_computetuplefreqtable2(pamP, NULL, MAXCOLORS, maxval, colorsP); if (colorfreqtable) *freqtableMaxvalP = maxval; } /* colorfreqtable is now non-null. The "maxval > 0" in the for loop limit above is just for robustness. */ if (*freqtableMaxvalP != pamP->maxval) pm_message("Too many colors (more than %u)! " "scaled color resolution from maxval %lu " "to maxval %lu before choosing", MAXCOLORS, pamP->maxval, *freqtableMaxvalP); pm_message("%u colors found", *colorsP); *colorfreqtableP = colorfreqtable; } static void computeColorMapFromInput(struct pam * const pamP, bool const allColors, int const reqColors, enum methodForLargest const methodForLargest, enum methodForRep const methodForRep, tupletable * const colormapP, int * const colorsP, sample * const colormapMaxvalP) { /*---------------------------------------------------------------------------- Produce a colormap containing the best colors to represent the image represented by *pamP, whose file is seekable and now positioned to the raster. Figure it out using the median cut technique. The colormap will have 'reqcolors' or fewer colors in it, unless 'allcolors' is true, in which case it will have all the colors that are in the input. The colormap has the same maxval as the input, unless the input contains more than MAXCOLORS colors. In that case, we compute the map from a maxval-reduced version of the input. The colormap has that reduced maxval. Note that in this case, 'allColors' does not cause the colormap to have all the colors from the input; rather, it contains all the colors from the input once the input's color resolution is reduced to the new maxval. Put the colormap in newly allocated storage as a tupletable and return its address as *colormapP. Return the number of colors in it as *colorsP and its maxval as *colormapMaxvalP. -----------------------------------------------------------------------------*/ tupletable colorfreqtable; unsigned int colors; sample colormapMaxval; computeHistogram(pamP, &colorfreqtable, &colors, &colormapMaxval); if (allColors) { *colormapP = colorfreqtable; *colorsP = colors; } else { if (colors <= reqColors) { pm_message("Image already has few enough colors (<=%d). " "Keeping same colors.", reqColors); *colormapP = colorfreqtable; *colorsP = colors; } else { pm_message("choosing %d colors...", reqColors); *colormapP = mediancut(colorfreqtable, colors, pamP->depth, reqColors, methodForLargest, methodForRep); *colorsP = reqColors; pnm_freetupletable(pamP, colorfreqtable); } } *colormapMaxvalP = colormapMaxval; } static void sortColormap(tupletable const colormap, sample const depth, unsigned int const colormapSize) { /*---------------------------------------------------------------------------- Sort the colormap in place, in order of ascending Plane 0 value, the Plane 1 value, etc. Use insertion sort. -----------------------------------------------------------------------------*/ int i; pm_message("Sorting %d colors...", colormapSize); for (i = 0; i < colormapSize; ++i) { int j; for (j = i+1; j < colormapSize; ++j) { unsigned int plane; bool iIsGreater, iIsLess; iIsGreater = FALSE; iIsLess = FALSE; for (plane = 0; plane < depth && !iIsGreater && !iIsLess; ++plane) { if (colormap[i]->tuple[plane] > colormap[j]->tuple[plane]) iIsGreater = TRUE; else if (colormap[i]->tuple[plane] < colormap[j]->tuple[plane]) iIsLess = TRUE; } if (iIsGreater) { for (plane = 0; plane < depth; ++plane) { sample const temp = colormap[i]->tuple[plane]; colormap[i]->tuple[plane] = colormap[j]->tuple[plane]; colormap[j]->tuple[plane] = temp; } } } } } static void colormapToSquare(struct pam * const pamP, tupletable const colormap, unsigned int const colormapSize, tuple *** const outputRasterP) { { unsigned int const intsqrt = (int)sqrt((float)colormapSize); if (intsqrt * intsqrt == colormapSize) pamP->width = intsqrt; else pamP->width = intsqrt + 1; } { unsigned int const intQuotient = colormapSize / pamP->width; if (pamP->width * intQuotient == colormapSize) pamP->height = intQuotient; else pamP->height = intQuotient + 1; } { tuple ** outputRaster; unsigned int row; unsigned int colormapIndex; outputRaster = pnm_allocpamarray(pamP); colormapIndex = 0; /* initial value */ for (row = 0; row < pamP->height; ++row) { unsigned int col; for (col = 0; col < pamP->width; ++col) { unsigned int plane; for (plane = 0; plane < pamP->depth; ++plane) { outputRaster[row][col][plane] = colormap[colormapIndex]->tuple[plane]; } if (colormapIndex < colormapSize-1) colormapIndex++; } } *outputRasterP = outputRaster; } } static void colormapToSingleRow(struct pam * const pamP, tupletable const colormap, unsigned int const colormapSize, tuple *** const outputRasterP) { tuple ** outputRaster; unsigned int col; pamP->width = colormapSize; pamP->height = 1; outputRaster = pnm_allocpamarray(pamP); for (col = 0; col < pamP->width; ++col) { int plane; for (plane = 0; plane < pamP->depth; ++plane) outputRaster[0][col][plane] = colormap[col]->tuple[plane]; } *outputRasterP = outputRaster; } static void colormapToImage(struct pam * const inpamP, struct pam * const outpamP, tupletable const colormap, unsigned int const colormapSize, sample const colormapMaxval, bool const sort, bool const square, tuple *** const outputRasterP) { /*---------------------------------------------------------------------------- Create a tuple array and pam structure for an image which includes one pixel of each of the colors in the colormap 'colormap'. May rearrange the contents of 'colormap'. -----------------------------------------------------------------------------*/ outpamP->size = sizeof(*outpamP); outpamP->len = sizeof(*outpamP); outpamP->format = inpamP->format; outpamP->plainformat = FALSE; outpamP->depth = inpamP->depth; outpamP->maxval = colormapMaxval; outpamP->bytes_per_sample = inpamP->bytes_per_sample; strcpy(outpamP->tuple_type, inpamP->tuple_type); if (sort) sortColormap(colormap, outpamP->depth, colormapSize); if (square) colormapToSquare(outpamP, colormap, colormapSize, outputRasterP); else colormapToSingleRow(outpamP, colormap, colormapSize, outputRasterP); } int main(int argc, char * argv[] ) { struct cmdlineInfo cmdline; FILE* ifP; struct pam inpam, outpam; tuple** colormapRaster; int newcolors; tupletable colormap; sample colormapMaxval; pnm_init(&argc, argv); initSampleCompare(); parseCommandLine(argc, argv, &cmdline); ifP = pm_openr_seekable(cmdline.inputFilespec); pnm_readpaminit(ifP, &inpam, sizeof(inpam)); computeColorMapFromInput(&inpam, cmdline.allcolors, cmdline.newcolors, cmdline.methodForLargest, cmdline.methodForRep, &colormap, &newcolors, &colormapMaxval); pm_close(ifP); colormapToImage(&inpam, &outpam, colormap, newcolors, colormapMaxval, cmdline.sort, cmdline.square, &colormapRaster); if (cmdline.verbose) pm_message("Generating %u x %u image", outpam.width, outpam.height); outpam.file = stdout; pnm_writepam(&outpam, colormapRaster); pnm_freepamarray(colormapRaster, &outpam); pm_close(stdout); exit(0); }