/* * Argyll Color Correction System * Colorimeter Correction Matrix * * Author: Graeme W. Gill * Date: 19/8/2010 * * Copyright 2010 Graeme W. Gill * All rights reserved. * * This material is licenced under the GNU GENERAL PUBLIC LICENSE Version 2 or later :- * see the License2.txt file for licencing details. * * NOTE though that if SALONEINSTLIB is not defined, that this file depends * on other libraries that are licenced under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3. */ /* * TTBD: */ #undef DEBUG #define verbo stdout #include #include #include #include #include #ifndef SALONEINSTLIB #include "numlib.h" #include "icc.h" #else #include "numsup.h" #include "conv.h" #endif #include "cgats.h" #include "disptechs.h" #include "ccmx.h" #ifdef NT /* You'd think there might be some standards.... */ # ifndef __BORLANDC__ # define stricmp _stricmp # endif #else # define stricmp strcasecmp #endif /* Forward declarations */ /* Utilities */ /* Method implimentations */ /* Write out the ccmx to a CGATS format .ccmx file */ /* Return nz on error */ static int create_ccmx_cgats( ccmx *p, /* This */ cgats **pocg /* return CGATS structure */ ) { int i, j, n; time_t clk = time(0); struct tm *tsp = localtime(&clk); char *atm = asctime(tsp); /* Ascii time */ cgats *ocg; /* CGATS structure */ char buf[100]; atm[strlen(atm)-1] = '\000'; /* Remove \n from end */ /* Setup output cgats file */ ocg = new_cgats(); /* Create a CGATS structure */ ocg->add_other(ocg, "CCMX"); /* our special type is Colorimeter Correction Matrix */ ocg->add_table(ocg, tt_other, 0); /* Start the first table */ if (p->desc != NULL) ocg->add_kword(ocg, 0, "DESCRIPTOR", p->desc,NULL); ocg->add_kword(ocg, 0, "INSTRUMENT",p->inst, NULL); if (p->disp != NULL) ocg->add_kword(ocg, 0, "DISPLAY",p->disp, NULL); ocg->add_kword(ocg, 0, "TECHNOLOGY", disptech_get_id(p->dtech)->strid,NULL); if (p->cc_cbid != 0) { sprintf(buf, "%d", p->cc_cbid); ocg->add_kword(ocg, 0, "DISPLAY_TYPE_BASE_ID", buf, NULL); } if (p->refrmode >= 0) ocg->add_kword(ocg, 0, "DISPLAY_TYPE_REFRESH", p->refrmode ? "YES" : "NO", NULL); if (p->sel != NULL) ocg->add_kword(ocg, 0, "UI_SELECTORS", p->sel, NULL); if (p->ref != NULL) ocg->add_kword(ocg, 0, "REFERENCE",p->ref, NULL); if (p->oem != 0) ocg->add_kword(ocg, 0, "OEM","YES", NULL); ocg->add_kword(ocg, 0, "ORIGINATOR", "Argyll ccmx", NULL); ocg->add_kword(ocg, 0, "CREATED",atm, NULL); ocg->add_kword(ocg, 0, "COLOR_REP", "XYZ", NULL); /* Add fields for the matrix */ ocg->add_field(ocg, 0, "XYZ_X", r_t); ocg->add_field(ocg, 0, "XYZ_Y", r_t); ocg->add_field(ocg, 0, "XYZ_Z", r_t); /* Write out the matrix values */ for (i = 0; i < 3; i++) { ocg->add_set(ocg, 0, p->matrix[i][0], p->matrix[i][1], p->matrix[i][2]); } if (pocg != NULL) *pocg = ocg; return 0; } /* Write out the ccmx to a CGATS format .ccmx file */ /* Return nz on error */ static int write_ccmx( ccmx *p, /* This */ char *outname /* Filename to write to */ ) { int rv; cgats *ocg; /* CGATS structure */ /* Create CGATS elements */ if ((rv = create_ccmx_cgats(p, &ocg)) != 0) { return rv; } /* Write it to file */ if (ocg->write_name(ocg, outname)) { strcpy(p->err, ocg->err); ocg->del(ocg); /* Clean up */ return 1; } ocg->del(ocg); /* Clean up */ return 0; } /* write to a CGATS .ccmx file to a memory buffer. */ /* return nz on error, with message in err[] */ static int buf_write_ccmx( ccmx *p, unsigned char **buf, /* Return allocated buffer */ int *len /* Return length */ ) { int rv; cgats *ocg; /* CGATS structure */ cgatsFile *fp; /* Create CGATS elements */ if ((rv = create_ccmx_cgats(p, &ocg)) != 0) { return rv; } if ((fp = new_cgatsFileMem(NULL, 0)) == NULL) { strcpy(p->err, "new_cgatsFileMem failed"); return 2; } /* Write it to file */ if (ocg->write(ocg, fp)) { strcpy(p->err, ocg->err); ocg->del(ocg); /* Clean up */ fp->del(fp); return 1; } /* Get the buffer the ccmx has been written to */ if (fp->get_buf(fp, buf, (size_t *)len)) { strcpy(p->err, "cgatsFileMem get_buf failed"); return 2; } ocg->del(ocg); /* Clean up */ fp->del(fp); return 0; } /* Read in the ccmx CGATS .ccmx file */ /* Return nz on error */ static int read_ccmx_cgats( ccmx *p, /* This */ cgats *icg /* input cgats structure */ ) { int i, j, n, ix; int ti; /* Temporary CGATs index */ int spi[3]; /* CGATS indexes for each band */ char *xyzfname[3] = { "XYZ_X", "XYZ_Y", "XYZ_Z" }; if (icg->ntables == 0 || icg->t[0].tt != tt_other || icg->t[0].oi != 0) { sprintf(p->err, "read_ccmx: Input file isn't a CCMX format file"); return 1; } if (icg->ntables != 1) { sprintf(p->err, "Input file doesn't contain exactly one table"); return 1; } if ((ti = icg->find_kword(icg, 0, "COLOR_REP")) < 0) { sprintf(p->err, "read_ccmx: Input file doesn't contain keyword COLOR_REP"); return 1; } if (strcmp(icg->t[0].kdata[ti],"XYZ") != 0) { sprintf(p->err, "read_ccmx: Input file doesn't have COLOR_REP of XYZ"); return 1; } if ((ti = icg->find_kword(icg, 0, "DESCRIPTOR")) >= 0) { if ((p->desc = strdup(icg->t[0].kdata[ti])) == NULL) { sprintf(p->err, "read_ccmx: malloc failed"); return 2; } } if ((ti = icg->find_kword(icg, 0, "INSTRUMENT")) < 0) { sprintf(p->err, "read_ccmx: Input file doesn't contain keyword INSTRUMENT"); return 1; } if ((p->inst = strdup(icg->t[0].kdata[ti])) == NULL) { sprintf(p->err, "read_ccmx: malloc failed"); return 2; } if ((ti = icg->find_kword(icg, 0, "DISPLAY")) >= 0) { if ((p->disp = strdup(icg->t[0].kdata[ti])) == NULL) { sprintf(p->err, "read_ccmx: malloc failed"); return 2; } } if ((ti = icg->find_kword(icg, 0, "TECHNOLOGY")) >= 0) { if ((p->tech = strdup(icg->t[0].kdata[ti])) == NULL) { sprintf(p->err, "read_ccmx: malloc failed"); return 2; } /* Get disptech enum from standard TECHNOLOGY string */ p->dtech = disptech_get_strid(p->tech)->dtech; } if (p->disp == NULL && p->tech == NULL) { sprintf(p->err, "read_ccmx: Input file doesn't contain keyword DISPLAY or TECHNOLOGY"); return 1; } if ((ti = icg->find_kword(icg, 0, "DISPLAY_TYPE_REFRESH")) >= 0) { if (stricmp(icg->t[0].kdata[ti], "YES") == 0) p->refrmode = 1; else if (stricmp(icg->t[0].kdata[ti], "NO") == 0) p->refrmode = 0; } else { p->refrmode = -1; } if ((ti = icg->find_kword(icg, 0, "DISPLAY_TYPE_BASE_ID")) >= 0) { p->cc_cbid = atoi(icg->t[0].kdata[ti]); } else { p->cc_cbid = 0; } if ((ti = icg->find_kword(icg, 0, "UI_SELECTORS")) >= 0) { if ((p->sel = strdup(icg->t[0].kdata[ti])) == NULL) { sprintf(p->err, "read_ccmx: malloc failed"); return 2; } } if ((ti = icg->find_kword(icg, 0, "REFERENCE")) >= 0) { if ((p->ref = strdup(icg->t[0].kdata[ti])) == NULL) { sprintf(p->err, "read_ccmx: malloc failed"); return 2; } } if ((ti = icg->find_kword(icg, 0, "OEM")) >= 0) { if ((p->ref = strdup(icg->t[0].kdata[ti])) == NULL) { sprintf(p->err, "read_ccmx: malloc failed"); return 2; } } if ((ti = icg->find_kword(icg, 0, "OEM")) >= 0) { if (stricmp(icg->t[0].kdata[ti], "YES") == 0) p->oem = 1; else if (stricmp(icg->t[0].kdata[ti], "NO") == 0) p->oem = 0; } else { p->oem = 0; } /* Locate the fields */ for (i = 0; i < 3; i++) { /* XYZ fields */ if ((spi[i] = icg->find_field(icg, 0, xyzfname[i])) < 0) { sprintf(p->err, "read_ccmx: Input file doesn't contain field %s", xyzfname[i]); return 1; } if (icg->t[0].ftype[spi[i]] != r_t) { sprintf(p->err, "read_ccmx: Input file field %s is wrong type", xyzfname[i]); return 1; } } if (icg->t[0].nsets != 3) { sprintf(p->err, "read_ccmx: Input file doesn't have exactly 3 sets"); return 1; } /* Read the matrix values */ for (ix = 0; ix < icg->t[0].nsets; ix++) { p->matrix[ix][0] = *((double *)icg->t[0].fdata[ix][spi[0]]); p->matrix[ix][1] = *((double *)icg->t[0].fdata[ix][spi[1]]); p->matrix[ix][2] = *((double *)icg->t[0].fdata[ix][spi[2]]); } return 0; } /* Read in the ccmx CGATS .ccmx file */ /* Return nz on error */ static int read_ccmx( ccmx *p, /* This */ char *inname /* Filename to read from */ ) { int rv; cgats *icg; /* input cgats structure */ /* Open and look at the .ccmx */ if ((icg = new_cgats()) == NULL) { /* Create a CGATS structure */ sprintf(p->err, "read_ccmx: new_cgats() failed"); return 2; } icg->add_other(icg, "CCMX"); /* our special type is Model Printer Profile */ if (icg->read_name(icg, inname)) { strcpy(p->err, icg->err); icg->del(icg); return 1; } if ((rv = read_ccmx_cgats(p, icg)) != 0) { icg->del(icg); return rv; } icg->del(icg); /* Clean up */ return 0; } /* Read in the ccmx CGATS .ccmx file from a memory buffer */ /* Return nz on error */ static int buf_read_ccmx( ccmx *p, /* This */ unsigned char *buf, int len ) { int rv; cgatsFile *fp; cgats *icg; /* input cgats structure */ if ((fp = new_cgatsFileMem(buf, len)) == NULL) { strcpy(p->err, "new_cgatsFileMem failed"); return 2; } /* Open and look at the .ccmx file */ if ((icg = new_cgats()) == NULL) { /* Create a CGATS structure */ sprintf(p->err, "read_ccmx: new_cgats() failed"); fp->del(fp); return 2; } icg->add_other(icg, "CCMX"); /* our special type is Model Printer Profile */ if (icg->read(icg, fp)) { strcpy(p->err, icg->err); icg->del(icg); fp->del(fp); return 1; } fp->del(fp); if ((rv = read_ccmx_cgats(p, icg)) != 0) { icg->del(icg); /* Clean up */ return rv; } icg->del(icg); /* Clean up */ return 0; } /* Lookup an XYZ or Lab color */ static void xform( ccmx *p, /* This */ double *out, /* Output XYZ */ double *in /* Input XYZ */ ) { icmMulBy3x3(out, p->matrix, in); } /* Set the contents of the ccmx. return nz on error. */ static int set_ccmx(ccmx *p, char *desc, /* General description (optional) */ char *inst, /* Instrument description to copy from */ char *disp, /* Display make and model (optional) */ disptech dtech, /* Display technology enum */ int refrmode, /* Display refresh mode, -1 = unknown, 0 = n, 1 = yes */ int cbid, /* Display type calibration base ID, 0 = unknown */ char *sel, /* UI selector characters - NULL for none */ char *refd, /* Reference spectrometer description (optional) */ int oem, /* NZ if OEM source */ double mtx[3][3] /* Transform matrix to copy from */ ) { if ((p->desc = desc) != NULL && (p->desc = strdup(desc)) == NULL) { sprintf(p->err, "set_ccmx: malloc failed"); return 2; } if ((p->inst = inst) != NULL && (p->inst = strdup(inst)) == NULL) { sprintf(p->err, "set_ccmx: malloc failed"); return 2; } if ((p->disp = disp) != NULL && (p->disp = strdup(disp)) == NULL) { sprintf(p->err, "set_ccmx: malloc failed"); return 2; } p->dtech = dtech; p->refrmode = refrmode; p->cc_cbid = cbid; if (sel != NULL) { if ((p->sel = strdup(sel)) == NULL) { sprintf(p->err, "set_ccmx: malloc sel failed"); return 2; } } if ((p->ref = refd) != NULL && (p->ref = strdup(refd)) == NULL) { sprintf(p->err, "set_ccmx: malloc failed"); return 2; } p->oem = oem; icmCpy3x3(p->matrix, mtx); return 0; } #ifndef SALONEINSTLIB /* ------------------------------------------- */ /* Modified version that de-weights Luminance errors by 5: */ /* Return the CIE94 Delta E color difference measure, squared */ static double wCIE94sq(double Lab0[3], double Lab1[3]) { double desq, dhsq; double dlsq, dcsq; double c12; { double dl, da, db; dl = Lab0[0] - Lab1[0]; dlsq = dl * dl; /* dl squared */ da = Lab0[1] - Lab1[1]; db = Lab0[2] - Lab1[2]; /* Compute normal Lab delta E squared */ desq = dlsq + da * da + db * db; } { double c1, c2, dc; /* Compute chromanance for the two colors */ c1 = sqrt(Lab0[1] * Lab0[1] + Lab0[2] * Lab0[2]); c2 = sqrt(Lab1[1] * Lab1[1] + Lab1[2] * Lab1[2]); c12 = sqrt(c1 * c2); /* Symetric chromanance */ /* delta chromanance squared */ dc = c2 - c1; dcsq = dc * dc; } /* Compute delta hue squared */ if ((dhsq = desq - dlsq - dcsq) < 0.0) dhsq = 0.0; { double sc, sh; /* Weighting factors for delta chromanance & delta hue */ sc = 1.0 + 0.048 * c12; sh = 1.0 + 0.014 * c12; return 0.2 * 0.2 * dlsq + dcsq/(sc * sc) + dhsq/(sh * sh); } } /* ------------------------------------------- */ /* Context for optimisation callback */ typedef struct { int npat; double (*refs)[3]; /* Array of XYZ values from spectrometer */ double (*cols)[3]; /* Array of XYZ values from colorimeter */ int wix; /* White index */ icmXYZNumber wh; /* White reference */ } cntx; /* Optimisation function */ /* Compute the sum of delta E's squared for the */ /* tp will be the 9 matrix values */ /* It's not clear if the white weighting is advantagous or not. */ double optf(void *fdata, double *tp) { cntx *cx = (cntx *)fdata; int i; double de; double m[3][3]; m[0][0] = tp[0]; m[0][1] = tp[1]; m[0][2] = tp[2]; m[1][0] = tp[3]; m[1][1] = tp[4]; m[1][2] = tp[5]; m[2][0] = tp[6]; m[2][1] = tp[7]; m[2][2] = tp[8]; for (de = 0.0, i = 0; i < cx->npat; i++) { double tlab[3], xyz[3], lab[3]; icmXYZ2Lab(&cx->wh, tlab, cx->refs[i]); icmMulBy3x3(xyz, m, cx->cols[i]); icmXYZ2Lab(&cx->wh, lab, xyz); if (i == cx->wix) de += cx->npat/4.0 * wCIE94sq(tlab, lab); /* Make white weight = 1/4 all others */ else de += wCIE94sq(tlab, lab); //printf("~1 %d: txyz %f %f %f, tlab %f %f %f\n", i,cx->refs[i][0], cx->refs[i][1], cx->refs[i][2], tlab[0], tlab[1], tlab[2]); //printf("~1 %d: xyz %f %f %f\n", i,cx->cols[i][0], cx->cols[i][1], cx->cols[i][2]); //printf("~1 %d: mxyz %f %f %f, lab %f %f %f\n", i,xyz[0], xyz[1], xyz[2], lab[0], lab[1], lab[2]); //printf("~1 %d: de %f\n", i,wCIE94(tlab, lab)); } de /= cx->npat; #ifdef DEBUG // printf("~1 return values = %f\n",de); #endif return de; } /* Create a ccmx from measurements. return nz on error. */ static int create_ccmx(ccmx *p, char *desc, /* General description (optional) */ char *inst, /* Instrument description to copy from */ char *disp, /* Display make and model (optional) */ disptech dtech, /* Display technology enum */ int refrmode, /* Display refresh mode, -1 = unknown, 0 = n, 1 = yes */ int cbid, /* Display type calibration base index, 0 = unknown */ char *sel, /* UI selector characters - NULL for none */ char *refd, /* Reference spectrometer description (optional) */ int oem, /* NZ if OEM source */ int npat, /* Number of samples in following arrays */ double (*refs)[3], /* Array of XYZ values from spectrometer */ double (*cols)[3] /* Array of XYZ values from colorimeter */ ) { int i, mxix; double maxy = -1e6; cntx cx; double cp[9], sa[9]; if ((p->desc = desc) != NULL && (p->desc = strdup(desc)) == NULL) { sprintf(p->err, "create_ccmx: malloc failed"); return 2; } if ((p->inst = inst) != NULL && (p->inst = strdup(inst)) == NULL) { sprintf(p->err, "create_ccmx: malloc failed"); return 2; } if ((p->disp = disp) != NULL && (p->disp = strdup(disp)) == NULL) { sprintf(p->err, "create_ccmx: malloc failed"); return 2; } p->dtech = dtech; p->refrmode = refrmode; p->cc_cbid = cbid; if (sel != NULL) { if ((p->sel = strdup(sel)) == NULL) { sprintf(p->err, "create_ccmx: malloc sel failed"); return 2; } } if ((p->ref = refd) != NULL && (p->ref = strdup(refd)) == NULL) { sprintf(p->err, "create_ccmx: malloc failed"); return 2; } p->oem = oem; /* Find the white patch */ cx.npat = npat; cx.refs = refs; cx.cols = cols; for (i = 0; i < npat; i++) { if (refs[i][1] > maxy) { maxy = refs[i][1]; cx.wix = i; } } #ifdef DEBUG printf("white = %f %f %f\n",refs[cx.wix][0],refs[cx.wix][1],refs[cx.wix][1]); #endif cx.wh.X = refs[cx.wix][0]; cx.wh.Y = refs[cx.wix][1]; cx.wh.Z = refs[cx.wix][2]; /* Starting matrix */ cp[0] = 1.0; cp[1] = 0.0; cp[2] = 0.0; cp[3] = 0.0; cp[4] = 1.0; cp[5] = 0.0; cp[6] = 0.0; cp[7] = 0.0; cp[8] = 1.0; for (i = 0; i < 9; i++) sa[i] = 0.1; if (powell(NULL, 9, cp, sa, 1e-6, 2000, optf, &cx, NULL, NULL) < 0.0) { sprintf(p->err, "create_ccmx: powell() failed"); return 1; } p->matrix[0][0] = cp[0]; p->matrix[0][1] = cp[1]; p->matrix[0][2] = cp[2]; p->matrix[1][0] = cp[3]; p->matrix[1][1] = cp[4]; p->matrix[1][2] = cp[5]; p->matrix[2][0] = cp[6]; p->matrix[2][1] = cp[7]; p->matrix[2][2] = cp[8]; /* Compute the average and max errors */ p->av_err = p->mx_err = 0.0; for (i = 0; i < npat; i++) { double tlab[3], xyz[3], lab[3], de; icmXYZ2Lab(&cx.wh, tlab, cx.refs[i]); icmMulBy3x3(xyz, p->matrix, cx.cols[i]); icmXYZ2Lab(&cx.wh, lab, xyz); de = icmCIE94(tlab, lab); p->av_err += de; if (de > p->mx_err) { p->mx_err = de; mxix = i; } //printf("~1 %d: de %f, tlab %f %f %f, lab %f %f %f\n",i,de,tlab[0], tlab[1], tlab[2], lab[0], lab[1], lab[2]); } p->av_err /= (double)npat; //printf("~1 max error is index %d\n",mxix); #ifdef DEBUG printf("Average error %f, max %f\n",p->av_err,p->mx_err); printf("Correction matrix is:\n"); printf(" %f %f %f\n", cp[0], cp[1], cp[2]); printf(" %f %f %f\n", cp[3], cp[4], cp[5]); printf(" %f %f %f\n", cp[6], cp[7], cp[8]); #endif return 0; } #else /* SALONEINSTLIB */ /* Create a ccmx from measurements. return nz on error. */ static int create_ccmx(ccmx *p, char *desc, /* General description (optional) */ char *inst, /* Instrument description to copy from */ char *disp, /* Display make and model (optional) */ disptech dtech, /* Display technology enum */ int refrmode, /* Display refresh mode, -1 = unknown, 0 = n, 1 = yes */ int cbid, /* Display type calibration base index, 0 = unknown */ char *sel, /* UI selector characters - NULL for none */ char *refd, /* Reference spectrometer description (optional) */ int npat, /* Number of samples in following arrays */ double (*refs)[3], /* Array of XYZ values from spectrometer */ double (*cols)[3] /* Array of XYZ values from colorimeter */ ) { sprintf(p->err, "create_ccmx: not implemented in ccmx.c"); return 1; } #endif /* SALONEINSTLIB */ /* Delete it */ static void del_ccmx(ccmx *p) { if (p != NULL) { if (p->desc != NULL) free(p->desc); if (p->inst != NULL) free(p->inst); if (p->disp != NULL) free(p->disp); if (p->tech != NULL) free(p->tech); if (p->sel != NULL) free(p->sel); if (p->ref != NULL) free(p->ref); free(p); } } /* Allocate a new, uninitialised ccmx */ /* Note thate black and white points aren't allocated */ ccmx *new_ccmx(void) { ccmx *p; if ((p = (ccmx *)calloc(1, sizeof(ccmx))) == NULL) return NULL; p->refrmode = -1; p->cc_cbid = 0; /* Init method pointers */ p->del = del_ccmx; p->set_ccmx = set_ccmx; p->create_ccmx = create_ccmx; p->write_ccmx = write_ccmx; p->buf_write_ccmx = buf_write_ccmx; p->read_ccmx = read_ccmx; p->buf_read_ccmx = buf_read_ccmx; p->xform = xform; return p; }