Files
2026-08-20 20:28:40 +01:00

735 lines
19 KiB
C

/*
* Calibration curve class.
*/
/*
*
* Argyll Color Management System
* Author: Graeme W. Gill
* Date: 30/10/2005
*
* Copyright 2005 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.
*
* This class allows reading and using a calibration file.
* Creation is currently left up to specialized programs (dispcal, printcal).
* This class doesn't handle the extra table that dispcal creates/uses.
*
*/
#undef DEBUG
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <math.h>
#include <sys/types.h>
#include <time.h>
#include <string.h>
#ifndef SALONEINSTLIB
#include "copyright.h"
#include "aconfig.h"
#include "numlib.h"
#include "plot.h"
#include "conv.h"
#include "xicc.h"
#else
#include "sa_config.h"
#include "numsup.h"
#include "rspl1.h"
#include "cgats.h"
#include "sa_conv.h"
#include "xcolorants.h"
#include "xcal.h"
#endif /* SALONEINSTLIB */
#ifdef NT /* You'd think there might be some standards.... */
# ifndef __BORLANDC__
# define stricmp _stricmp
# endif
#else
# define stricmp strcasecmp
#endif
/* rspl setting functions */
static void xcal_rsplset(void *cbntx, double *out, double *in) {
co *dpoints = (co *)cbntx;
int ix;
ix = *((int*)&in[-0-1]); /* Get grid index being looked up */
out[0] = dpoints[ix].v[0];
}
/* Read a calibration file from a cgats table */
/* "filename" is just for diagnostics */
/* Return nz if this fails */
static int xcal_read_cgats(xcal *p, cgats *tcg, int table, char *filename) {
int oi;
int i, j, ti;
char *ident;
char *bident;
int spi[1+MAX_CHAN]; /* CGATS indexes for each field */
char *ftname = "Calibration";
char buf[100];
oi = tcg->get_oi(tcg, "CAL");
if (oi < 0) {
sprintf(p->e.m, "Input file '%s' isn't a CAL format file", filename);
return p->e.c = 1;
}
if (tcg->t[table].tt != tt_other || tcg->t[table].oi != oi) {
sprintf(p->e.m, "Input file '%s' isn't a CAL format file", filename);
return p->e.c = 1;
}
/* Grab general information from it */
if ((ti = tcg->find_kword(tcg, table, "DESCRIPTOR")) >= 0) {
if ((p->descriptor = strdup(tcg->t[table].kdata[ti])) == NULL) {
sprintf(p->e.m, "%s file '%s' strdup of DESCRIPTOR failed",ftname,filename);
return p->e.c = 1;
}
}
if ((ti = tcg->find_kword(tcg, table, "ORIGINATOR")) >= 0) {
if ((p->originator = strdup(tcg->t[table].kdata[ti])) == NULL) {
sprintf(p->e.m, "%s file '%s' strdup of ORIGINATOR failed",ftname,filename);
return p->e.c = 1;
}
}
/* See what sort of device type this calibration is for */
if ((ti = tcg->find_kword(tcg, table, "DEVICE_CLASS")) < 0) {
sprintf(p->e.m, "%s file '%s' doesn't contain keyword DEVICE_CLASS",ftname,filename);
return p->e.c = 1;
}
if (strcmp(tcg->t[table].kdata[ti],"INPUT") == 0) {
p->devclass = icSigInputClass;
} else if (strcmp(tcg->t[table].kdata[ti],"OUTPUT") == 0) {
p->devclass = icSigOutputClass;
} else if (strcmp(tcg->t[table].kdata[ti],"DISPLAY") == 0) {
p->devclass = icSigDisplayClass;
} else {
sprintf(p->e.m,"%s file '%s' contain unknown DEVICE_CLASS '%s'",
ftname,filename,tcg->t[table].kdata[ti]);
return p->e.c = 1;
}
if ((ti = tcg->find_kword(tcg, table, "COLOR_REP")) < 0) {
/* Be backwards compatible with V1.0.4 display calibration files */
if (p->devclass != icSigDisplayClass) {
sprintf(p->e.m, "%s file '%s' doesn't contain keyword COLOR_REP",ftname,filename);
return p->e.c = 1;
}
warning("\n *** %s file '%s' doesn't contain keyword COLOR_REP, assuming RGB ***",ftname,filename);
if ((p->devmask = icx_char2inkmask("RGB") ) == 0) {
sprintf(p->e.m, "%s file '%s' has unrecognized COLOR_REP '%s'",
ftname,filename,tcg->t[table].kdata[ti]);
return p->e.c = 1;
}
} else {
if ((p->devmask = icx_char2inkmask(tcg->t[table].kdata[ti]) ) == 0) {
sprintf(p->e.m, "%s file '%s' has unrecognized COLOR_REP '%s'",
ftname,filename,tcg->t[table].kdata[ti]);
return p->e.c = 1;
}
}
if ((ti = tcg->find_kword(tcg, table, "VIDEO_LUT_CALIBRATION_POSSIBLE")) >= 0) {
if (stricmp(tcg->t[table].kdata[ti], "NO") == 0)
p->noramdac = 1;
}
if ((ti = tcg->find_kword(tcg, table, "TV_OUTPUT_ENCODING")) >= 0) {
if (strcmp(tcg->t[0].kdata[ti], "YES") == 0
|| strcmp(tcg->t[0].kdata[ti], "yes") == 0)
p->tvenc = 1;
}
p->colspace = icx_colorant_comb_to_icc(p->devmask); /* 0 if none */
p->devchan = icx_noofinks(p->devmask);
ident = icx_inkmask2char(p->devmask, 1);
bident = icx_inkmask2char(p->devmask, 0);
/* Grab any descriptive information */
if ((ti = tcg->find_kword(tcg, table, "MANUFACTURER")) >= 0)
p->xpi.deviceMfgDesc = strdup(tcg->t[table].kdata[ti]);
if ((ti = tcg->find_kword(tcg, table, "MODEL")) >= 0)
p->xpi.modelDesc = strdup(tcg->t[table].kdata[ti]);
if ((ti = tcg->find_kword(tcg, table, "DESCRIPTION")) >= 0)
p->xpi.profDesc = strdup(tcg->t[table].kdata[ti]);
if ((ti = tcg->find_kword(tcg, table, "COPYRIGHT")) >= 0)
p->xpi.copyright = strdup(tcg->t[table].kdata[ti]);
if (tcg->t[table].nsets <= 0) {
sprintf(p->e.m, "%s file '%s' has too few entries %d",
ftname,filename,tcg->t[table].nsets);
return p->e.c = 1;
}
/* Figure out the indexes of all the fields */
sprintf(buf, "%s_I",bident);
if ((spi[0] = tcg->find_field(tcg, table, buf)) < 0) {
sprintf(p->e.m,"%s file '%s' doesn't contain field '%s'",ftname,filename,buf);
return p->e.c = 1;
}
for (j = 0; j < p->devchan; j++) {
inkmask imask = icx_index2ink(p->devmask, j);
sprintf(buf, "%s_%s",bident,icx_ink2char(imask));
if ((spi[1+j] = tcg->find_field(tcg, table, buf)) < 0) {
sprintf(p->e.m,"%s file '%s' doesn't contain field '%s'",ftname,filename,buf);
return p->e.c = 1;
}
}
/* Read in each channels values and put them in a rspl */
for (j = 0; j < p->devchan; j++) {
datai low,high;
int gres[MXDI];
double smooth = 1.0;
co *dpoints;
low[0] = 0.0;
high[0] = 1.0;
gres[0] = tcg->t[table].nsets;
if ((p->cals[j] = new_rspl(RSPL_NOFLAGS,1, 1)) == NULL) {
sprintf(p->e.m,"new_rspl() failed");
return p->e.c = 2;
}
if ((dpoints = malloc(sizeof(co) * gres[0])) == NULL) {
sprintf(p->e.m,"malloc dpoints[%d] failed",gres[0]);
return p->e.c = 2;
}
/* Copy the points to our array */
for (i = 0; i < gres[0]; i++) {
dpoints[i].p[0] = i/(double)(gres[0]-1);
dpoints[i].v[0] = *((double *)tcg->t[table].fdata[i][spi[1+j]]);
}
/* Set the rspl */
p->cals[j]->set_rspl(p->cals[j],
0,
(void *)dpoints, /* Read points */
xcal_rsplset, /* Setting function */
low, high, gres, /* Low, high, resolution of grid */
NULL, NULL /* Default data scale */
);
free(dpoints);
}
free(ident);
free(bident);
return 0;
}
#ifndef SALONEINSTLIB
/* Read a calibration file from an ICC vcgt tag */
/* Return nz if this fails */
int xcal_read_icc(xcal *p, icc *c) {
icmVideoCardGamma *vg;
icmTextDescription *td;
icmText *tx;
int res, i, j;
/* See if there is a vcgt tag */
if ((vg = (icmVideoCardGamma *)c->read_tag(c, icSigVideoCardGammaTag)) == NULL) {
sprintf(p->e.m, "ICC profile has no vcgt");
return p->e.c = 1;
}
/* What sort of device the profile is for */
p->devclass = c->header->deviceClass;
p->colspace = c->header->colorSpace;
if ((p->devmask = icx_icc_to_colorant_comb(p->colspace, p->devclass)) == 0) {
sprintf(p->e.m, "Unable to determine inkmask from ICC profile");
return p->e.c = 1;
}
p->devchan = icx_noofinks(p->devmask);
/* Grab any descriptive information */
if ((td = (icmTextDescription *)c->read_tag(c, icSigDeviceMfgDescTag)) != NULL) {
p->xpi.deviceMfgDesc = strdup(td->desc);
}
if ((td = (icmTextDescription *)c->read_tag(c, icSigDeviceModelDescTag)) != NULL) {
p->xpi.modelDesc = strdup(td->desc);
}
if ((td = (icmTextDescription *)c->read_tag(c, icSigProfileDescriptionTag)) != NULL) {
p->xpi.profDesc = strdup(td->desc);
}
if ((tx = (icmText *)c->read_tag(c, icSigCopyrightTag)) != NULL) {
p->xpi.copyright = strdup(tx->desc);
}
/* Decide the lut resolution */
if (vg->tagType == icVideoCardGammaFormula)
res = 2048;
else
res = vg->u.table.entryCount;
/* Read in each channels values and put them in a rspl */
for (j = 0; j < p->devchan; j++) {
datai low,high;
int gres[MXDI];
double smooth = 1.0;
co *dpoints;
low[0] = 0.0;
high[0] = 1.0;
gres[0] = res;
if ((p->cals[j] = new_rspl(RSPL_NOFLAGS,1, 1)) == NULL) {
sprintf(p->e.m,"new_rspl() failed");
return p->e.c = 2;
}
if ((dpoints = malloc(sizeof(co) * gres[0])) == NULL) {
sprintf(p->e.m,"malloc dpoints[%d] failed",gres[0]);
return p->e.c = 2;
}
/* Copy the points to our array */
for (i = 0; i < gres[0]; i++) {
dpoints[i].p[0] = i/(double)(gres[0]-1);
dpoints[i].v[0] = vg->lookup(vg, j, dpoints[i].p[0]);
}
/* Set the rspl */
p->cals[j]->set_rspl(p->cals[j],
0,
(void *)dpoints, /* Read points */
xcal_rsplset, /* Setting function */
low, high, gres, /* Low, high, resolution of grid */
NULL, NULL /* Default data scale */
);
free(dpoints);
}
return 0;
}
#endif /* !SALONEINSTLIB */
/* Read a calibration file */
/* Return nz if this fails */
static int xcal_read(xcal *p, char *filename) {
cgats *tcg; /* .cal file */
int table = 0;
int rv;
if ((tcg = new_cgats()) == NULL) {
sprintf(p->e.m, "new_cgats() failed");
return p->e.c = 2;
}
tcg->add_other(tcg, "CAL"); /* our special input type is Calibration Table */
if (tcg->read_name(tcg, filename)) {
strcpy(p->e.m, tcg->e.m);
p->e.c = tcg->e.c;
tcg->del(tcg);
return p->e.c;
}
if (tcg->ntables < 1)
return 1;
rv = xcal_read_cgats(p, tcg, table, filename);
tcg->del(tcg);
return rv;
}
/* Write a calibration to a new cgats table */
/* Return nz if this fails */
static int xcal_write_cgats(xcal *p, cgats *tcg) {
int oi;
int table;
int i, j, ti;
char *ident, *bident;
time_t clk = time(0);
struct tm *tsp = localtime(&clk);
char *atm = asctime(tsp); /* Ascii time */
char buf[100];
cgats_set_elem *setel; /* Array of set value elements */
int nsetel = 0;
int calres;
oi = tcg->add_other(tcg, "CAL"); /* our special type is Calibration Target */
table = tcg->add_table(tcg, tt_other, oi); /* Add a table for calibration */
tcg->add_kword(tcg, table, "DESCRIPTOR", p->descriptor != NULL ? p->descriptor : "Argyll Device Calibration Curves",NULL);
tcg->add_kword(tcg, table, "ORIGINATOR", p->originator != NULL ? p->originator : "Argyll", NULL);
atm[strlen(atm)-1] = '\000'; /* Remove \n from end */
tcg->add_kword(tcg, table, "CREATED",atm, NULL);
if (p->devclass == icSigInputClass)
tcg->add_kword(tcg, table, "DEVICE_CLASS","INPUT", NULL);
else if (p->devclass == icSigOutputClass)
tcg->add_kword(tcg, table, "DEVICE_CLASS","OUTPUT", NULL);
else if (p->devclass == icSigDisplayClass)
tcg->add_kword(tcg, table, "DEVICE_CLASS","DISPLAY", NULL);
else {
#ifdef SALONEINSTLIB
sprintf(p->e.m,"Unknown device class 0x%x",p->devclass);
#else
sprintf(p->e.m,"Unknown device class '%s'",icm2str(icmProfileClassSig,p->devclass));
#endif
return p->e.c = 1;
}
/* Colorspace */
ident = icx_inkmask2char(p->devmask, 1);
bident = icx_inkmask2char(p->devmask, 0);
tcg->add_kword(tcg, table, "COLOR_REP", ident, NULL);
/* Other tags */
if (p->noramdac)
tcg->add_kword(tcg, table, "VIDEO_LUT_CALIBRATION_POSSIBLE", "NO", NULL);
if (p->tvenc)
tcg->add_kword(tcg, table, "TV_OUTPUT_ENCODING", "YES", NULL);
/* Grab any descriptive information */
if (p->xpi.deviceMfgDesc != NULL)
tcg->add_kword(tcg, table, "MANUFACTURER",p->xpi.deviceMfgDesc, NULL);
if (p->xpi.modelDesc != NULL)
tcg->add_kword(tcg, table, "MODEL",p->xpi.modelDesc, NULL);
if (p->xpi.profDesc != NULL)
tcg->add_kword(tcg, table, "DESCRIPTION",p->xpi.profDesc, NULL);
if (p->xpi.copyright != NULL)
tcg->add_kword(tcg, table, "COPYRIGHT",p->xpi.copyright, NULL);
sprintf(buf, "%s_I",bident);
tcg->add_field(tcg, table, buf, r_t);
nsetel++;
for (j = 0; j < p->devchan; j++) {
inkmask imask = icx_index2ink(p->devmask, j);
sprintf(buf, "%s_%s",bident,icx_ink2char(imask));
tcg->add_field(tcg, table, buf, r_t);
nsetel++;
}
if ((setel = (cgats_set_elem *)malloc(sizeof(cgats_set_elem) * nsetel)) == NULL) {
sprintf(p->e.m,"Malloc failed");
return p->e.c = 2;
}
calres = p->cals[0]->get_res(p->cals[0])[0];
for (i = 0; i < calres; i++) {
double vv = i/(calres-1.0);
co tp;
setel[0].d = vv;
for (j = 0; j < p->devchan; j++) {
tp.p[0] = vv;
p->cals[j]->interp(p->cals[j], &tp);
setel[j+1].d = tp.v[0];
}
tcg->add_setarr(tcg, table, setel);
}
free(setel);
free(ident);
free(bident);
return 0;
}
/* Write a calibration file */
/* Return nz if this fails */
static int xcal_write(xcal *p, char *filename) {
cgats *tcg; /* .cal file */
int table = 0;
int rv;
if ((tcg = new_cgats()) == NULL) {
sprintf(p->e.m, "new_cgats() failed");
return p->e.c = 2;
}
if ((rv = xcal_write_cgats(p, tcg)) != 0) {
strcpy(p->e.m, tcg->e.m);
p->e.c = tcg->e.c;
tcg->del(tcg);
return p->e.c;
}
if (tcg->write_name(tcg, filename)) {
strcpy(p->e.m, tcg->e.m);
p->e.c = tcg->e.c;
tcg->del(tcg);
return p->e.c;
}
tcg->del(tcg);
return rv;
}
/* Translate values through the curves. */
static void xcal_interp(xcal *p, double *out, double *in) {
int j;
co tp;
for (j = 0; j < p->devchan; j++) {
tp.p[0] = in[j];
p->cals[j]->interp(p->cals[j], &tp);
out[j] = tp.v[0];
}
}
#ifndef SALONEINSTLIB
#define MAX_INVSOLN 10 /* Rspl maximum reverse solutions */
/* Translate a value backwards through the curves. */
/* Return nz if the inversion fails */
static int xcal_inv_interp(xcal *p, double *out, double *in) {
int nsoln; /* Number of solutions found */
co pp[MAX_INVSOLN]; /* Room for all the solutions found */
int j, k; /* Chosen solution */
double dir = 0.5; /* target if multiple solutions */
int rv = 0;
for (j = 0; j < p->devchan; j++) {
pp[0].v[0] = in[j];
nsoln = p->cals[j]->rev_interp (
p->cals[j], /* this */
RSPL_NEARCLIP, /* Clip to nearest (faster than vector) */
MAX_INVSOLN, /* Maximum number of solutions allowed for */
NULL, /* No auxiliary input targets */
NULL, /* Clip vector direction and length */
pp); /* Input and output values */
nsoln &= RSPL_NOSOLNS; /* Get number of solutions */
if (nsoln == 1) { /* Exactly one solution */
k = 0;
} else if (nsoln == 0) { /* Zero solutions. This is unexpected. */
rv = 1;
return -1.0;
} else { /* Multiple solutions */
double bdist = 1e300;
int bsoln = 0;
for (k = 0; k < nsoln; k++) {
double tt;
tt = pp[k].p[0] - dir;
tt *= tt;
if (tt < bdist) { /* Better solution */
bdist = tt;
bsoln = k;
}
}
k = bsoln;
}
out[j] = pp[k].p[0];
}
return rv;
}
#endif /* !SALONEINSTLIB */
/* Translate a value through one of the curves */
static double xcal_interp_ch(xcal *p, int ch, double in) {
co tp;
if (ch < 0 || ch >= p->devchan)
return -1.0;
tp.p[0] = in;
p->cals[ch]->interp(p->cals[ch], &tp);
return tp.v[0];
}
#ifndef SALONEINSTLIB
/* Translate a value backwards through one of the curves */
/* Return -1.0 if the inversion fails */
static double xcal_inv_interp_ch(xcal *p, int ch, double in) {
int nsoln; /* Number of solutions found */
co pp[MAX_INVSOLN]; /* Room for all the solutions found */
int k; /* Chosen solution */
double dir = 0.5; /* target if multiple solutions */
if (ch < 0 || ch >= p->devchan)
return -1.0;
pp[0].v[0] = in;
nsoln = p->cals[ch]->rev_interp(
p->cals[ch], /* this */
RSPL_NEARCLIP, /* Clip to nearest (faster than vector) */
MAX_INVSOLN, /* Maximum number of solutions allowed for */
NULL, /* No auxiliary input targets */
NULL, /* Clip vector direction and length */
pp); /* Input and output values */
nsoln &= RSPL_NOSOLNS; /* Get number of solutions */
if (nsoln == 1) { /* Exactly one solution */
k = 0;
} else if (nsoln == 0) { /* Zero solutions. This is unexpected. */
return -1.0;
} else { /* Multiple solutions */
double bdist = 1e300;
int bsoln = 0;
for (k = 0; k < nsoln; k++) {
double tt;
tt = pp[k].p[0] - dir;
tt *= tt;
if (tt < bdist) { /* Better solution */
bdist = tt;
bsoln = k;
}
}
k = bsoln;
}
return pp[k].p[0];
}
#endif /* !SALONEINSTLIB */
/* Set the colorspace using an inkmask */
static void xcal_set_inkmask(xcal *p, inkmask devmask) {
p->devmask = devmask;
p->colspace = icx_colorant_comb_to_icc(p->devmask); /* 0 if none */
p->devchan = icx_noofinks(p->devmask);
}
/* Set the curve values (Need to have set_inkmask before calling!) */
/* Return nz on error */
static int xcal_set_curves(xcal *p, int res, double **curves) {
int j;
co *dpoints;
/* Free any existing rspl */
for (j = 0; j < p->devchan; j++) {
if (p->cals[j] != NULL)
p->cals[j]->del(p->cals[j]);
}
if ((dpoints = malloc(sizeof(co) * res)) == NULL) {
sprintf(p->e.m,"malloc dpoints[%d] failed",res);
return p->e.c = 2;
}
for (j = 0; j < p->devchan; j++) {
datai low,high;
int gres[MXDI];
double smooth = 1.0;
int i;
low[0] = 0.0;
high[0] = 1.0;
gres[0] = res;
if ((p->cals[j] = new_rspl(RSPL_NOFLAGS,1, 1)) == NULL) {
sprintf(p->e.m,"new_rspl() failed");
return p->e.c = 2;
}
/* Copy the points to our array */
for (i = 0; i < gres[0]; i++) {
dpoints[i].p[0] = i/(double)(gres[0]-1);
dpoints[i].v[0] = curves[j][i];
}
/* Set the rspl */
p->cals[j]->set_rspl(p->cals[j],
0,
(void *)dpoints, /* Read points */
xcal_rsplset, /* Setting function */
low, high, gres, /* Low, high, resolution of grid */
NULL, NULL /* Default data scale */
);
}
free(dpoints);
return 0;
}
/* Add a reference and return p */
static xcal *xcal_ref(xcal *p) {
p->refcount++;
return p;
}
/* Delete an xcal */
static void xcal_del(xcal *p) {
int j;
if (p != NULL && --p->refcount <= 0) {
if (p->descriptor != NULL)
free(p->descriptor);
if (p->originator != NULL)
free(p->originator);
if (p->xpi.deviceMfgDesc != NULL)
free(p->xpi.deviceMfgDesc);
if (p->xpi.modelDesc != NULL)
free(p->xpi.modelDesc);
if (p->xpi.profDesc != NULL)
free(p->xpi.profDesc);
if (p->xpi.copyright != NULL)
free(p->xpi.copyright);
for (j = 0; j < p->devchan; j++) {
if (p->cals[j] != NULL)
p->cals[j]->del(p->cals[j]);
}
free(p);
}
}
/* Create a new, uninitialised xcal */
xcal *new_xcal(void) {
xcal *p;
if ((p = (xcal *)calloc(1, sizeof(xcal))) == NULL)
return NULL;
/* Init method pointers */
p->ref = xcal_ref;
p->del = xcal_del;
p->read_cgats = xcal_read_cgats;
#ifndef SALONEINSTLIB
p->read_icc = xcal_read_icc;
#endif /* !SALONEINSTLIB */
p->read = xcal_read;
p->write_cgats = xcal_write_cgats;
p->write = xcal_write;
p->interp = xcal_interp;
#ifndef SALONEINSTLIB
p->inv_interp = xcal_inv_interp;
#endif /* !SALONEINSTLIB */
p->interp_ch = xcal_interp_ch;
#ifndef SALONEINSTLIB
p->inv_interp_ch = xcal_inv_interp_ch;
#endif /* !SALONEINSTLIB */
p->set_inkmask = xcal_set_inkmask;
p->set_curves = xcal_set_curves;
p->refcount++;
return p;
}