/* * Argyll Color Correction System * Average one or more .ti3 (or other CGATS like) file values together. * If just one file is supplied, all patches within it with * the same device value are averaged together. * * Author: Graeme W. Gill * Date: 18/1/2011 * * Copyright 2005, 2010, 2011 Graeme W. Gill * All rights reserved. * * This material is licenced under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 :- * see the License.txt file for licencing details. * * (based on splitti3.c) */ /* * TTBD: Should probably re-index SAMPLE_ID field */ #undef DEBUG #define verbo stdout #include #include #include #include #include "copyright.h" #include "aconfig.h" #include "numlib.h" #include "sort.h" #include "cgats.h" #include "xicc.h" #include "insttypes.h" static double average(double *vals, int nvals); static double median(double *vals, int nvals); static void geommed(double res[3], double vals[][3], int nvals); void usage(char *diag, ...) { int i; fprintf(stderr,"Average or merge values in .ti3 like files, Version %s\n",ARGYLL_VERSION_STR); if (diag != NULL) { va_list args; fprintf(stderr," Diagnostic: "); va_start(args, diag); vfprintf(stderr, diag, args); va_end(args); fprintf(stderr,"\n"); } fprintf(stderr,"Author: Graeme W. Gill, licensed under the AGPL Version 3\n"); fprintf(stderr,"usage: average [-options] input1.ti3 input2.ti3 ... output.ti3\n"); fprintf(stderr," -v Verbose\n"); fprintf(stderr," -e Median rather than average\n"); fprintf(stderr," -g Geometric Median of PCS in encoded space\n"); fprintf(stderr," -L Geometric Median of PCS in L*a*b* space\n"); fprintf(stderr," -X Geometric Median of PCS in XYZ space\n"); fprintf(stderr," -m Merge rather than average\n"); fprintf(stderr," input1.ti3 First input file\n"); fprintf(stderr," input2.ti3 Second input file\n"); fprintf(stderr," ... etc.\n"); fprintf(stderr," output.ti3 Resulting averaged or merged output file\n"); exit(1); } /* Information about each file */ struct _inpinfo { char name[MAXNAMEL+1]; cgats *c; }; typedef struct _inpinfo inpinfo; int main(int argc, char *argv[]) { int fa,nfa; /* current argument we're looking at */ int verb = 0; int domedian = 0; /* Median rather than average */ int dogeom = 0; /* Do geometric median of PCS, 2 = Lab, 3 = PCS */ int domerge = 0; /* Merge rather than average */ int ninps = 0; /* Number of input files */ inpinfo *inps; /* Input file info. inp[ninp] == output file */ cgats *ocg; /* Copy of output cgats * */ cgats_set_elem *setel; /* Array of set value elements */ int *flags; /* Point to destination of set */ int nchan = 0; /* Number of device channels */ int chix[ICX_MXINKS]; /* Device chanel indexes */ int npcs = 0; int haspcs[2] = { 0 }; /* Has Lab, XYZ */ int pcsix[3][3]; /* Lab, XYZ chanel indexes */ int i, j, n; error_program = "average"; if (argc < 2) usage("Too few arguments (%d, minimum is 2)",argc-1); /* Process the arguments */ for(fa = 1;fa < argc;fa++) { nfa = fa; /* skip to nfa if next argument is used */ if (argv[fa][0] == '-') { /* Look for any flags */ char *na = NULL; /* next argument after flag, null if none */ if (argv[fa][2] != '\000') na = &argv[fa][2]; /* next is directly after flag */ else { if ((fa+1) < argc) { if (argv[fa+1][0] != '-') { nfa = fa + 1; na = argv[nfa]; /* next is seperate non-flag argument */ } } } if (argv[fa][1] == '?') usage("Usage requested"); /* Median */ else if (argv[fa][1] == 'e') { domedian = 1; } /* Geometric Median of PCS */ else if (argv[fa][1] == 'g') { dogeom = 1; } /* Geometric Median of PCS in L*a*b* */ else if (argv[fa][1] == 'L') { dogeom = 2; } /* Geometric Median of PCS in XYZ */ else if (argv[fa][1] == 'X') { dogeom = 3; } /* Merge */ else if (argv[fa][1] == 'm') { domerge = 1; } /* Verbosity */ else if (argv[fa][1] == 'v') { verb = 1; } else { usage("Unknown flag '%c'",argv[fa][1]); } } else if (argv[fa][0] != '\000') { /* Get the next filename */ if (ninps == 0) inps = (inpinfo *)malloc(sizeof(inpinfo)); else inps = (inpinfo *)realloc(inps, (ninps+1) * sizeof(inpinfo)); if (inps == NULL) error("Malloc failed in allocating space for file info."); memset((void *)&inps[ninps], 0, sizeof(inpinfo)); strncpy(inps[ninps].name,argv[fa],MAXNAMEL); inps[ninps].name[MAXNAMEL] = '\000'; ninps++; } else { break; } } if (ninps < 2) error("Must be at least one input and one output file specified"); ninps--; /* Number of inputs */ /* Open and read each input file, and create output file */ for (n = 0; n <= ninps; n++) { if ((inps[n].c = new_cgats()) == NULL) error("Failed to create cgats object for file '%s'",inps[n].name); if (n < ninps) { /* If input file, read it */ inps[n].c->add_other(inps[n].c, ""); /* Allow any signature file */ if (inps[n].c->read_name(inps[n].c, inps[n].name)) error("CGATS file '%s' read error : %s",inps[n].name,inps[n].c->err); if (inps[n].c->ntables < 1) error ("Input file '%s' doesn't contain at least one table",inps[n].name); } } ocg = inps[ninps].c; /* Alias for output file */ /* Duplicate everything from the first input file into the output file. */ for (n = 0; n < inps[0].c->ntables; n++) { if (inps[0].c->t[n].tt == cgats_X) { ocg->add_other(ocg, inps[0].c->cgats_type); ocg->add_table(ocg, tt_other, 0); } else if (inps[0].c->t[n].tt == tt_other) { int oi; oi = ocg->add_other(ocg, inps[0].c->others[inps[0].c->t[n].oi]); ocg->add_table(ocg, tt_other, oi); } else { ocg->add_table(ocg, inps[0].c->t[n].tt, 0); } /* Duplicate all the keywords */ for (i = 0; i < inps[0].c->t[n].nkwords; i++) { //printf("~1 table %d, adding keyword '%s'\n",n,inps[0].c->t[n].ksym[i]); ocg->add_kword(ocg, n, inps[0].c->t[n].ksym[i], inps[0].c->t[n].kdata[i], NULL); } /* Duplicate all of the fields */ for (i = 0; i < inps[0].c->t[n].nfields; i++) { ocg->add_field(ocg, n, inps[0].c->t[n].fsym[i], inps[0].c->t[n].ftype[i]); } /* If more than one file, must be merging or averaging between files */ if (ninps > 1) { /* Duplicate all of the data or first file to output file */ if ((setel = (cgats_set_elem *)malloc( sizeof(cgats_set_elem) * inps[0].c->t[n].nfields)) == NULL) error("Malloc failed!"); for (i = 0; i < inps[0].c->t[n].nsets; i++) { inps[0].c->get_setarr(inps[0].c, n, i, setel); ocg->add_setarr(ocg, n, setel); } free(setel); } } /* Figure out the indexes of the device channels */ if (inps[0].c->find_kword(inps[0].c, 0, "COLOR_REP") < 0) { warning("Input file '%s' doesn't contain keyword COLOR_REP", inps[0].name); } else { int ti; char *buf; char *outc; int nmask; char *bident; if ((ti = inps[0].c->find_kword(inps[0].c, 0, "COLOR_REP")) < 0) error("Input file '%s' doesn't contain keyword COLOR_REP", inps[0].name); if ((buf = strdup(inps[0].c->t[0].kdata[ti])) == NULL) error("Malloc failed"); /* Split COLOR_REP into device and PCS space */ if ((outc = strchr(buf, '_')) == NULL) error("COLOR_REP '%s' invalid", inps[0].c->t[0].kdata[ti]); *outc++ = '\000'; if ((nmask = icx_char2inkmask(buf)) == 0) { error ("File '%s' keyword COLOR_REP has unknown device value '%s'",inps[0].name,buf); } nchan = icx_noofinks(nmask); bident = icx_inkmask2char(nmask, 0); /* Find device fields */ for (j = 0; j < nchan; j++) { int ii, imask; char fname[100]; imask = icx_index2ink(nmask, j); sprintf(fname,"%s_%s",nmask == ICX_W || nmask == ICX_K ? "GRAY" : bident, icx_ink2char(imask)); if ((ii = inps[0].c->find_field(inps[0].c, 0, fname)) < 0) error ("Input file doesn't contain field %s",fname); if (inps[0].c->t[0].ftype[ii] != r_t) error ("Field %s is wrong type",fname); chix[j] = ii; } } /* Figure out the indexes of the PCS channels, if any */ { int npcs; char *fname[2][3] = { { "LAB_L", "LAB_A", "LAB_B" }, { "XYZ_X", "XYZ_Y", "XYZ_Z" } }; /* For Lab and XYZ */ for (j = 0; j < 2; j++) { for (npcs = 0; npcs < 3; npcs++) { int ii; if ((ii = inps[0].c->find_field(inps[0].c, 0, fname[j][npcs])) < 0) break; /* Try next or give up */ if (inps[0].c->t[0].ftype[ii] != r_t) error ("Field %s is wrong type",fname[j][npcs]); pcsix[j][npcs] = ii; } if (npcs == 3) haspcs[j] = 1; } } if (!haspcs[0] && !haspcs[1]) warning("No PCS fields found - hope that's OK!"); if (!domerge && verb) { printf("Averaging the following fields:"); for (j = 0; j < inps[0].c->t[0].nfields; j++) { int jj; /* Only real types */ if (inps[0].c->t[0].ftype[j] != r_t) { continue; } /* Not device channels */ for (jj = 0; jj < nchan; jj++) { if (chix[jj] == j) break; } if (jj < nchan) { continue; } printf(" %s",inps[0].c->t[0].fsym[j]); } printf("\n"); } /* Get ready to add more values to output, */ /* for merging or averaging within one file. */ if ((setel = (cgats_set_elem *)malloc( sizeof(cgats_set_elem) * inps[0].c->t[0].nfields)) == NULL) error("Malloc failed!"); /* If averaging values within the one file */ if (ninps == 1) { int *valdone; int npat; double *vlist; double (*v3list)[3] = NULL; int k, e; n = 0; /* Output set index */ if ((valdone = (int *)calloc(inps[0].c->t[0].nsets, sizeof(int))) == NULL) error("Malloc failed!"); if ((vlist = (double *)calloc(inps[0].c->t[0].nsets, sizeof(double))) == NULL) error("Malloc failed!"); if (dogeom && (haspcs[0] || haspcs[1])) { if ((v3list = (double (*)[3])calloc(inps[0].c->t[0].nsets, 3 * sizeof(double))) == NULL) error("Malloc failed!"); } /* For each patch */ for (i = 0; i < inps[0].c->t[0].nsets; i++) { if (valdone[i]) continue; inps[0].c->get_setarr(inps[0].c, 0, i, setel); ocg->add_setarr(ocg, 0, setel); /* For each non-device real field values */ for (j = 0; j < inps[0].c->t[0].nfields; j++) { int jj; /* Only real types */ if (inps[0].c->t[0].ftype[j] != r_t) continue; /* Not device channels */ for (jj = 0; jj < nchan; jj++) { if (chix[jj] == j) break; } if (jj < nchan) continue; /* Locate any patches (including starting patch) with matching device values */ npat = 0; for (k = i; k < inps[0].c->t[0].nsets; k++) { /* Check if the device values match */ for (e = 0; e < nchan; e++) { double diff; diff = *((double *)inps[0].c->t[0].fdata[i][chix[e]]) - *((double *)inps[0].c->t[0].fdata[k][chix[e]]); if (fabs(diff) > 0.001) { break; } } if (e < nchan) { continue; } vlist[npat++] = *((double *)inps[0].c->t[0].fdata[k][j]); valdone[k] = 1; } if (domedian) *((double *)ocg->t[0].fdata[n][j]) = median(vlist, npat); else *((double *)ocg->t[0].fdata[n][j]) = average(vlist, npat); } /* Override per-component average/median if PCS Geometric Median */ if (dogeom && (haspcs[0] || haspcs[1])) { double res[3]; /* For Lab and XYZ */ for (j = 0; j < 2; j++) { if (haspcs[j] == 0) continue; /* Locate any patches (including starting patch) with matching device values */ npat = 0; for (k = i; k < inps[0].c->t[0].nsets; k++) { /* Check if the device values match */ for (e = 0; e < nchan; e++) { double diff; diff = *((double *)inps[0].c->t[0].fdata[i][chix[e]]) - *((double *)inps[0].c->t[0].fdata[k][chix[e]]); if (fabs(diff) > 0.001) { break; } } if (e < nchan) { continue; } v3list[npat][0] = *((double *)inps[0].c->t[0].fdata[k][pcsix[j][0]]); v3list[npat][1] = *((double *)inps[0].c->t[0].fdata[k][pcsix[j][1]]); v3list[npat][2] = *((double *)inps[0].c->t[0].fdata[k][pcsix[j][2]]); if (j == 0 && dogeom == 3) /* Lab and want XYZ */ icmLab2XYZ(&icmD50_100, v3list[npat], v3list[npat]); else if (j == 1 && dogeom == 2) /* XYZ and want Lab */ icmXYZ2Lab(&icmD50_100, v3list[npat], v3list[npat]); npat++; } geommed(res, v3list, npat); if (j == 0 && dogeom == 3) icmXYZ2Lab(&icmD50_100, res, res); else if (j == 1 && dogeom == 2) icmLab2XYZ(&icmD50_100, res, res); *((double *)ocg->t[0].fdata[n][pcsix[j][0]]) = res[0]; *((double *)ocg->t[0].fdata[n][pcsix[j][1]]) = res[1]; *((double *)ocg->t[0].fdata[n][pcsix[j][2]]) = res[2]; } } n++; /* One more output set */ } if (v3list != NULL) free(v3list); free(vlist); free(valdone); /* Averaging patches between identical files, */ /* or concatenating (merging) several files */ } else { /* Check/process all the other input files */ for (n = 1; n < ninps; n++) { /* Check all the fields match the first file */ if (inps[0].c->t[0].nfields != inps[n].c->t[0].nfields) error ("File '%s' has %d fields, file '%s has %d", inps[n].name, inps[n].c->t[0].nfields, inps[0].name, inps[0].c->t[0].nfields); for (j = 0; j < inps[0].c->t[0].nfields; j++) { if (inps[0].c->t[0].ftype[j] != inps[n].c->t[0].ftype[j]) error ("File '%s' field no. %d named '%s' doesn't match file '%s' field '%s'", inps[n].name, j, inps[n].c->t[0].fsym[j], inps[0].name, inps[0].c->t[0].fsym[j]); } /* If merging, append all the values */ if (domerge) { for (i = 0; i < inps[n].c->t[0].nsets; i++) { inps[n].c->get_setarr(inps[n].c, 0, i, setel); ocg->add_setarr(ocg, 0, setel); } } else { /* Averaging */ /* Check the number of patches matches the first file */ if (inps[0].c->t[0].nsets != inps[n].c->t[0].nsets) error ("File '%s' has %d sets, file '%s has %d", inps[n].name, inps[n].c->t[0].nsets, inps[0].name, inps[0].c->t[0].nsets); /* For all the patches: */ for (i = 0; i < inps[n].c->t[0].nsets; i++) { /* Check that the device values match the first file */ for (j = 0; j < nchan; j++) { double diff; diff = *((double *)inps[0].c->t[0].fdata[i][chix[j]]) - *((double *)inps[n].c->t[0].fdata[i][chix[j]]); if (fabs(diff) > 0.001) error ("File '%s' set %d has field '%s' value that differs from '%s'", inps[n].name, i+1, inps[n].c->t[0].fsym[j], inps[0].name); } } } } /* If averaging */ if (!domerge) { int npat; double *vlist; double (*v3list)[3] = NULL; if ((vlist = (double *)calloc(ninps, sizeof(double))) == NULL) error("Malloc failed!"); if (dogeom && (haspcs[0] || haspcs[1])) { if ((v3list = (double (*)[3])calloc(inps[0].c->t[0].nsets, 3 * sizeof(double))) == NULL) error("Malloc failed!"); } /* For all the non-device real field values */ for (j = 0; j < inps[0].c->t[0].nfields; j++) { int jj; /* Only real types */ if (inps[0].c->t[0].ftype[j] != r_t) continue; /* Not device channels */ for (jj = 0; jj < nchan; jj++) { if (chix[jj] == j) break; } if (jj < nchan) continue; /* For each patch */ for (i = 0; i < inps[n].c->t[0].nsets; i++) { /* For all input files */ npat = 0; for (n = 0; n < ninps; n++) { vlist[npat++] = *((double *)inps[n].c->t[0].fdata[i][j]); } if (domedian) *((double *)ocg->t[0].fdata[i][j]) = median(vlist, npat); else *((double *)ocg->t[0].fdata[i][j]) = average(vlist, npat); } } /* Override per-component average/median if PCS Geometric Median */ if (dogeom && (haspcs[0] || haspcs[1])) { double res[3]; /* For each patch */ for (i = 0; i < inps[n].c->t[0].nsets; i++) { /* For Lab and XYZ */ for (j = 0; j < 2; j++) { if (haspcs[j] == 0) continue; /* For all input files */ npat = 0; for (n = 0; n < ninps; n++) { v3list[npat][0] = *((double *)inps[n].c->t[0].fdata[i][pcsix[j][0]]); v3list[npat][1] = *((double *)inps[n].c->t[0].fdata[i][pcsix[j][1]]); v3list[npat][2] = *((double *)inps[n].c->t[0].fdata[i][pcsix[j][2]]); if (j == 0 && dogeom == 3) /* Lab and want XYZ */ icmLab2XYZ(&icmD50_100, v3list[npat], v3list[npat]); else if (j == 1 && dogeom == 2) /* XYZ and want Lab */ icmXYZ2Lab(&icmD50_100, v3list[npat], v3list[npat]); npat++; } geommed(res, v3list, npat); if (j == 0 && dogeom == 3) icmXYZ2Lab(&icmD50_100, res, res); else if (j == 1 && dogeom == 2) icmLab2XYZ(&icmD50_100, res, res); *((double *)ocg->t[0].fdata[i][pcsix[j][0]]) = res[0]; *((double *)ocg->t[0].fdata[i][pcsix[j][1]]) = res[1]; *((double *)ocg->t[0].fdata[i][pcsix[j][2]]) = res[2]; } } } if (v3list != NULL) free(v3list); free(vlist); } } /* Write out the output and free the cgats * */ for (n = 0; n <= ninps; n++) { if (n >= ninps) { /* If ouput file, write it */ if (inps[n].c->write_name(inps[n].c, inps[ninps].name)) error("CGATS file '%s' write error : %s",inps[n].name,inps[n].c->err); } inps[n].c->del(inps[n].c); } free(setel); free(inps); return 0; } static double average(double *vals, int nvals) { double rv; int i; for (rv = 0.0, i = 0; i < nvals; i++) rv += vals[i]; if (nvals > 0) rv /= (double)nvals; return rv; } static double median(double *vals, int nvals) { if (nvals < 3) return average(vals, nvals); #define HEAP_COMPARE(A,B) (A < B) HEAPSORT(double,vals,nvals); if ((nvals & 1) != 0) return vals[nvals/2]; else return 0.5 * (vals[nvals/2] + vals[nvals/2-1]); } /* Compute Geometric Median of PCS values */ /* using Weiszfeld's algorithm. */ static void geommed(double res[3], double vals[][3], int nvals) { int i, j; /* Start with mean value */ icmSet3(res, 0.0); for (i = 0; i < nvals; i++) icmAdd3(res, res, vals[i]); icmScale3(res, res, 1.0/(double)nvals); //printf("\n~1 average = %f %f %f\n", res[0], res[1], res[2]); /* Itterate to approach Geometric Mean */ for (j = 0; j < 20; j++) { double tv[3], tt; int k; icmSet3(tv, 0.0); tt = 0.0; for (k = i = 0; i < nvals; i++) { double norm = icmNorm33(vals[i], res); if (norm < 1e-6) continue; tv[0] += vals[i][0]/norm; tv[1] += vals[i][1]/norm; tv[2] += vals[i][2]/norm; tt += 1.0/norm; k++; //printf("Norm = %f, tv = %f %f %f, tt = %f\n",norm, tv[0], tv[1], tv[2], tt); } if (k > 0) icmScale3(res, tv, 1.0/tt); //printf("~1 res = %f %f %f\n", res[0], res[1], res[2]); } //printf("~1 geomm = %f %f %f\n", res[0], res[1], res[2]); }