/* 
 * Argyll Color Correction System
 * Verify two sets of PCS values.
 *
 * Author: Graeme W. Gill
 * Date:   7/6/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 program takes in two CGATS files (probably but not necesserily .ti3 files) of PCS
 * values (either XYZ, L*a*b* or spectral), matches the values, and computes
 * overall errors. This is useful for verifying proofing systems.
 */

/*
 * TTBD:
 *
 *	We're no warning about setting illuminant or FWA for emissive spectral data,
 *  they are just ignored.
 */

#define DEBUG

#define verbo stdout

#include <stdio.h>
#include <string.h>
#if defined(__IBMC__)
#include <float.h>
#endif
#include "copyright.h"
#include "aconfig.h"
#include "numlib.h"
#include "vrml.h"
#include "cgats.h"
#include "xicc.h"
#include "insttypes.h"
#include "disptechs.h"
#include "ccmx.h"
#include "sort.h"
#include "plot.h"
#include "ui.h"

#ifdef DEBUG
#undef DBG
#define DBG(xxx) printf xxx ;
#else
#undef DBG
#define DBG(xxx) 
#endif

void
usage(void) {
	fprintf(stderr,"Verify CIE values, Version %s\n",ARGYLL_VERSION_STR);
	fprintf(stderr,"Author: Graeme W. Gill, licensed under the AGPL Version 3\n");
	fprintf(stderr,"usage: colverify [-options] target.ti3 measured.ti3\n");
	fprintf(stderr," -v [n]          Verbose mode, n >= 2 print each value\n");
	fprintf(stderr," -n              Normalise each files reading to its white Y\n");
	fprintf(stderr," -N              Normalise each files reading to its white XYZ\n");
	fprintf(stderr," -m              Normalise each files reading to its white X+Y+Z\n");
	fprintf(stderr," -M              Normalise both files reading to mean white XYZ\n");
	fprintf(stderr," -D              Use D50 100.0 as L*a*b* white reference\n");
	fprintf(stderr," -c              Show CIE94 delta E values\n");
	fprintf(stderr," -k              Show CIEDE2000 delta E values\n");
	fprintf(stderr," -h [hist.txt]   Plot a histogram of delta E's [Optionaly save points to .txt]\n");
	fprintf(stderr," -s              Sort patch values by error\n");
	fprintf(stderr," -w              create PCS %s vector visualisation (measured%s)\n",vrml_format(),vrml_ext());
	fprintf(stderr," -W              create PCS %s marker visualisation (measured%s)\n",vrml_format(),vrml_ext());
	fprintf(stderr," -d              create Device RGB %s marker visualisation (measured%s)\n",vrml_format(),vrml_ext());
//	fprintf(stderr," -d y            create Device YCbCr %s marker visualisation (measured%s)\n",vrml_format(),vrml_ext());
	fprintf(stderr," -x              Use %s axes\n",vrml_format());
	fprintf(stderr," -f [illum]      Use Fluorescent Whitening Agent compensation [opt. simulated inst. illum.:\n");
	fprintf(stderr,"                  M0, M1, M2, A, C, D50 (def.), D50M2, D65, F5, F8, F10 or file.sp]\n");
	fprintf(stderr," -i illum        Choose illuminant for computation of CIE XYZ from spectral data & FWA:\n");
	fprintf(stderr,"                  A, C, D50 (def.), D50M2, D65, F5, F8, F10 or file.sp\n");
	fprintf(stderr," -o observ       Choose CIE Observer for spectral data:\n");
	fprintf(stderr,"                 1931_2 (def), 1964_10, S&B 1955_2, shaw, J&V 1978_2\n");
	fprintf(stderr," -L profile.%s  Skip any first file out of profile gamut patches\n",ICC_FILE_EXT_ND);
	fprintf(stderr," -X file.ccmx    Apply Colorimeter Correction Matrix to second file\n");
	fprintf(stderr," target.ti3      Target (reference) PCS or spectral values.\n");
	fprintf(stderr," measured.ti3    Measured (actual) PCS or spectral values\n");
	exit(1);
	}

/* Patch value type */
typedef struct {
	char sid[50];		/* sample id */
	char loc[100];		/* sample location (empty if none) */
	double rgb[3];		/* RGB value if RGB device space present, or YCbCr if dovrml==4 */
	double ycc[3];		/* YCbCr if RGB and dovrml==4 */
	int og;				/* Out of gamut flag */
	double xyz[3];		/* XYZ value */
	double v[3];		/* Lab value */
	double de;			/* Delta E */
	double ixde[3];		/* XYZ Component DE */
	double ide[3];		/* Lab Component DE */
} pval;

/* Histogram bin type */
typedef struct {
	int count;			/* Raw count */
	double val;			/* Normalized value */
	double min, max;	/* Bin range */
} hbin;

int main(int argc, char *argv[])
{
	int fa,nfa,mfa;			/* current argument we're looking at */
	int verb = 0;       	/* Verbose level */
	int norm = 0;			/* 1 = norm to White Y, 2 = norm to White XYZ */
							/* 3 = norm to White X+Y+Z, 4 = norm to average XYZ */
	int usestdd50 = 0;		/* Use standard D50 instead of avg white as reference */
	int cie94 = 0;
	int cie2k = 0;
	int dovrml = 0;			/* 1 = PCS vector, 2 = PCS marker, 3 = RGB, 4 - YCbCr */
	int doaxes = 0;
	int dohisto = 0;			/* Plot histogram of delta E's */
	char histoname[MAXNAMEL+1] = "\000";  /* Optional file to save histogram points to */
	int dosort = 0;
	char ccmxname[MAXNAMEL+1] = "\000";  /* Colorimeter Correction Matrix name */
	ccmx *cmx = NULL;					/* Colorimeter Correction Matrix */
	char gprofname[MAXNAMEL+1] = "\000";  /* Gamut limit profile name */
	icmFile *fp = NULL;
	icc *icco = NULL;
	xicc *xicco = NULL;
	icxLuBase *luo = NULL;

	struct {
		char name[MAXNAMEL+1];	/* Patch filename  */
		int isemis;				/* nz if emsissive spectral reference data */
		int isdisp;				/* nz if display */
		int isdnormed;      	/* Has display data been normalised to 100 ? */
		int isrgb;				/* Is RGB device space ? */
		int npat;				/* Number of patches */
		int nig;				/* Number of patches in gamut */
		double w[3];			/* XYZ of "white" */
		double nw[3];			/* Normalised XYZ of "white" */
		pval *pat;				/* patch values */
	} cg[2];					/* Target and current patch file information */

	int *match;					/* Array mapping first list indexes to corresponding second */
	int *sort;					/* Array of first list indexes in sorted order */
	int fwacomp = 0;			/* FWA compensation */
	int spec = 0;				/* Use spectral data flag */
	icxIllumeType tillum = icxIT_none;	/* Target/simulated instrument illuminant */ 
	xspect cust_tillum, *tillump = NULL; /* Custom target/simulated illumination spectrum */
	icxIllumeType illum = icxIT_none;	/* Spectral defaults to D50 */
	xspect cust_illum;			/* Custom illumination spectrum */
	icxObserverType observ = icxOT_none;	/* Defaults to 1931 2 degree */

	icmXYZNumber labw = icmD50;	/* The Lab white reference */

	char out_name[MAXNAMEL+4+1]; /* VRML/X3D name */
	vrml *wrl = NULL;

	int i, j, n;

	if (argc <= 1)
		usage();

	/* Process the arguments */
	mfa = 2;        				/* Minimum final 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+mfa) < argc) {
					if (argv[fa+1][0] != '-') {
						nfa = fa + 1;
						na = argv[nfa];		/* next is seperate non-flag argument */
					}
				}
			}

			if (argv[fa][1] == '?')
				usage();

			/* Verbose */
			else if (argv[fa][1] == 'v') {
				verb = 1;

				if (na != NULL && na[0] >= '0' && na[0] <= '9') {
					fa = nfa;
					verb = atoi(na);
				}
			}

			/* normalize */
			else if (argv[fa][1] == 'n'
			      || argv[fa][1] == 'N') {
				norm = 1;
				if (argv[fa][1] == 'N')
					norm = 2;
			}

			else if (argv[fa][1] == 'm') {
				norm = 3;
			}

			else if (argv[fa][1] == 'M') {
				norm = 4;
			}

			else if (argv[fa][1] == 'D')
				usestdd50 = 1;

			/* VRML/X3D */
			else if (argv[fa][1] == 'w')
				dovrml = 1;

			else if (argv[fa][1] == 'W')
				dovrml = 2;

			else if (argv[fa][1] == 'd') {
				dovrml = 3;
				if (na != NULL) {	/* Argument is present - RGB or YCbCr. */
					fa = nfa;
					if (strcmp(na, "y") == 0)
						dovrml = 4;
					else
						usage();
				}					
			}

			/* Axes */
			else if (argv[fa][1] == 'x')
				doaxes = 1;

			/* CIE94 delta E */
			else if (argv[fa][1] == 'c') {
				cie94 = 1;
				cie2k = 0;
			}

			else if (argv[fa][1] == 'k') {
				cie94 = 0;
				cie2k = 1;
			}

			/* Plot histogram */
			else if (argv[fa][1] == 'h') {
				dohisto = 1;

				if (na != NULL) {	/* Argument is present - file to save points to */
					fa = nfa;
					strncpy(histoname,na,MAXNAMEL); histoname[MAXNAMEL] = '\000';
				}
			}

			/* Sort */
			else if (argv[fa][1] == 's')
				dosort = 1;

			/* FWA compensation */
			else if (argv[fa][1] == 'f') {
				fwacomp = 1;

				if (na != NULL) {	/* Argument is present - target/simulated instr. illum. */
					fa = nfa;
					if (strcmp(na, "A") == 0
					 || strcmp(na, "M0") == 0) {
						spec = 1;
						tillum = icxIT_A;
					} else if (strcmp(na, "C") == 0) {
						spec = 1;
						tillum = icxIT_C;
					} else if (strcmp(na, "D50") == 0
					        || strcmp(na, "M1") == 0) {
						spec = 1;
						tillum = icxIT_D50;
					} else if (strcmp(na, "D50M2") == 0
					        || strcmp(na, "M2") == 0) {
						spec = 1;
						tillum = icxIT_D50M2;
					} else if (strcmp(na, "D65") == 0) {
						spec = 1;
						tillum = icxIT_D65;
					} else if (strcmp(na, "F5") == 0) {
						spec = 1;
						tillum = icxIT_F5;
					} else if (strcmp(na, "F8") == 0) {
						spec = 1;
						tillum = icxIT_F8;
					} else if (strcmp(na, "F10") == 0) {
						spec = 1;
						tillum = icxIT_F10;
					} else {	/* Assume it's a filename */
						spec = 1;
						tillum = icxIT_custom;
						if (read_xspect(&cust_tillum, na) != 0)
							usage();
					}
				}
			}

			/* Spectral to CIE Illuminant type */
			else if (argv[fa][1] == 'i') {
				if (na == NULL) usage();
				fa = nfa;
				if (strcmp(na, "A") == 0) {
					spec = 1;
					illum = icxIT_A;
				} else if (strcmp(na, "C") == 0) {
					spec = 1;
					illum = icxIT_C;
				} else if (strcmp(na, "D50") == 0) {
					spec = 1;
					illum = icxIT_D50;
				} else if (strcmp(na, "D50M2") == 0) {
					spec = 1;
					illum = icxIT_D50M2;
				} else if (strcmp(na, "D65") == 0) {
					spec = 1;
					illum = icxIT_D65;
				} else if (strcmp(na, "F5") == 0) {
					spec = 1;
					illum = icxIT_F5;
				} else if (strcmp(na, "F8") == 0) {
					spec = 1;
					illum = icxIT_F8;
				} else if (strcmp(na, "F10") == 0) {
					spec = 1;
					illum = icxIT_F10;
				} else {	/* Assume it's a filename */
					spec = 1;
					illum = icxIT_custom;
					if (read_xspect(&cust_illum, na) != 0)
						usage();
				}
			}

			/* Spectral Observer type */
			else if (argv[fa][1] == 'o') {
				if (na == NULL) usage();
				fa = nfa;
				if (strcmp(na, "1931_2") == 0) {			/* Classic 2 degree */
					spec = 1;
					observ = icxOT_CIE_1931_2;
				} else if (strcmp(na, "1964_10") == 0) {	/* Classic 10 degree */
					spec = 1;
					observ = icxOT_CIE_1964_10;
				} else if (strcmp(na, "1955_2") == 0) {		/* Stiles and Burch 1955 2 degree */
					spec = 1;
					observ = icxOT_Stiles_Burch_2;
				} else if (strcmp(na, "1978_2") == 0) {		/* Judd and Voss 1978 2 degree */
					spec = 1;
					observ = icxOT_Judd_Voss_2;
				} else if (strcmp(na, "shaw") == 0) {		/* Shaw and Fairchilds 1997 2 degree */
					spec = 1;
					observ = icxOT_Shaw_Fairchild_2;
				} else
					usage();
			}

			/* Gamut limit profile for first file */
			else if (argv[fa][1] == 'L') {
				if (na == NULL) usage();
				fa = nfa;
				strncpy(gprofname,na,MAXNAMEL-1); gprofname[MAXNAMEL-1] = '\000';
			}

			/* Colorimeter Correction Matrix for second file */
			else if (argv[fa][1] == 'X') {
				if (na == NULL) usage();
				fa = nfa;
				strncpy(ccmxname,na,MAXNAMEL-1); ccmxname[MAXNAMEL-1] = '\000';

			} else 
				usage();
		} else
			break;
	}

	/* Get the file name arguments */
	if (fa >= argc || argv[fa][0] == '-') usage();
	strncpy(cg[0].name,argv[fa++],MAXNAMEL); cg[0].name[MAXNAMEL] = '\000';

	if (fa >= argc || argv[fa][0] == '-') usage();
	strncpy(cg[1].name,argv[fa],MAXNAMEL); cg[1].name[MAXNAMEL] = '\000';

	/* Create VRML/X3D base name */
	{
		char *xl;
		strcpy(out_name, cg[1].name);
		if ((xl = strrchr(out_name, '.')) == NULL)	/* Figure where extention is */
			xl = out_name + strlen(out_name);
		xl[0] = '\000';		/* Remove extension */
	}

	if (fwacomp && spec == 0)
		error ("FWA compensation only works when viewer and/or illuminant selected");

	/* Gamut limit profile */
	if (gprofname[0] != '\000') {
		int rv;

		if ((fp = new_icmFileStd_name(gprofname,"r")) == NULL)
			error ("Can't open file '%s'",gprofname);
	
		if ((icco = new_icc()) == NULL)
			error ("Creation of ICC object failed");
	
		if ((rv = icco->read(icco,fp,0)) != 0)
			error("Reading profile '%s' failed failed with error %d:'%s'\n",
		     	       gprofname, icco->errc,  icco->err);

		if (icco->header->deviceClass != icSigInputClass
		 && icco->header->deviceClass != icSigDisplayClass
		 && icco->header->deviceClass != icSigOutputClass)
			error("Profile '%s' must be a device profile to filter by gamut",gprofname);

		/* Wrap with an expanded icc */
		if ((xicco = new_xicc(icco)) == NULL)
			error ("Creation of xicc failed");

		/* Get a expanded color conversion object */
		if ((luo = xicco->get_luobj(xicco, ICX_CLIP_NEAREST | ICX_FAST_SETUP,
		    icmFwd, icRelativeColorimetric, icSigXYZData, icmLuOrdNorm, NULL, NULL)) == NULL)
			error ("%d, %s",xicco->errc, xicco->err);
	}

	/* Colorimeter Correction Matrix */
	if (ccmxname[0] != '\000') {
		if ((cmx = new_ccmx()) == NULL)
			error("new_ccmx failed\n");
		if (cmx->read_ccmx(cmx,ccmxname))
			error("Reading Colorimeter Correction Matrix file '%s' failed with error %d:'%s'\n",
		     	       ccmxname, cmx->errc,  cmx->err);
	}


	/* Open up each file in turn, target then measured, */
	/* and read in the CIE values. */
	for (n = 0; n < 2; n++) {
		cgats *cgf = NULL;			/* cgats file data */
		int isLab = 0;				/* 0 if file CIE is XYZ, 1 if is Lab */
		int sidx;					/* Sample ID index */
		int sldx = -1;				/* Sample location index, < 0 if invalid */
		int xix, yix, zix;
		int rgbix[3];				/* RGB field indexes (if rgb ) */
		int dti;					/* Device type index */

		/* Open CIE target values */
		cgf = new_cgats();			/* Create a CGATS structure */
		cgf->add_other(cgf, ""); 	/* Allow any signature file */
	
		DBG(("Opening file '%s'\n",cg[n].name))

		if (cgf->read_name(cgf, cg[n].name))
			error("CGATS file '%s' read error : %s",cg[n].name,cgf->err);
	
		if (cgf->ntables < 1)
			error ("Input file '%s' doesn't contain at least one table",cg[n].name);
	
		if ((dti = cgf->find_kword(cgf, 0, "DEVICE_CLASS")) < 0)
			warning("Input file '%s' doesn't contain keyword DEVICE_CLASS",cg[n].name);

		/* Figure out what sort of device it is */
		{
			int ti;
	
			cg[n].isemis = 0;
			cg[n].isdisp = 0;
			cg[n].isdnormed = 0;
			cg[n].w[0] = cg[n].w[1] = cg[n].w[2] = 0.0;

			if (dti >= 0) {
				if (strcmp(cgf->t[0].kdata[dti],"DISPLAY") == 0) {
					cg[n].isemis = 1;
					cg[n].isdisp = 1;
					cg[n].isdnormed = 1;	/* Assume display type is normalised to 100 */

				} else if (strcmp(cgf->t[0].kdata[dti],"EMISINPUT") == 0) {
					cg[n].isemis = 1;
				}
	
				if (cg[n].isdisp) {
	
					if ((ti = cgf->find_kword(cgf, 0, "LUMINANCE_XYZ_CDM2")) >= 0) {
						if (sscanf(cgf->t[0].kdata[ti], " %lf %lf %lf ",&cg[n].w[0], &cg[n].w[1], &cg[n].w[2]) != 3)
							cg[n].w[0] = cg[n].w[1] = cg[n].w[2] = 0.0;
					}
	
					/* See if there is an explicit tag indicating data has been normalised to Y = 100 */
					if ((ti = cgf->find_kword(cgf, 0, "NORMALIZED_TO_Y_100")) >= 0) {
						if (strcmp(cgf->t[0].kdata[ti],"NO") == 0) {
							cg[n].isdnormed = 0;
						} else {
							cg[n].isdnormed = 1;
						}
					}
				}
			}
		}

		/* Check if the file is suitable */
		if (!spec
		 && cgf->find_field(cgf, 0, "LAB_L") < 0
		 && cgf->find_field(cgf, 0, "XYZ_X") < 0) {
	
			if (cgf->find_kword(cgf, 0, "SPECTRAL_BANDS") < 0)
				error ("Neither CIE nor spectral data found in file '%s'",cg[n].name);
	
			/* Switch to using spectral information */
			if (verb)
				printf("No CIE data found, switching to spectral with standard observer & D50 for file '%s'\n",cg[n].name);
			spec = 1;
		}
		if (spec && cgf->find_kword(cgf, 0, "SPECTRAL_BANDS") < 0)
			error ("No spectral data data found in file '%s' when spectral expected",cg[n].name);
	
		if (!spec && cgf->find_field(cgf, 0, "LAB_L") >= 0)
			isLab = 1;
		
		cg[n].nig = cg[n].npat = cgf->t[0].nsets;		/* Number of patches */

		/* See if it has RGB device space (for -d option) */
		if ((rgbix[0] = cgf->find_field(cgf, 0, "RGB_R")) >= 0
		 && cgf->t[0].ftype[rgbix[0]] == r_t

		 &&	(rgbix[1] = cgf->find_field(cgf, 0, "RGB_G")) >= 0
		 && cgf->t[0].ftype[rgbix[1]] == r_t

		 &&	(rgbix[2] = cgf->find_field(cgf, 0, "RGB_B")) >= 0
		 && cgf->t[0].ftype[rgbix[2]] == r_t) {
			cg[n].isrgb = 1;
		} else {
			cg[n].isrgb = 0;
		}

		/* Read all the target patches */
		if (cg[n].npat <= 0)
			error("No sets of data in file '%s'",cg[n].name);
	
		if (verb && n == 0) {
			fprintf(verbo,"No of test patches = %d\n",cg[n].npat);
		}
	
		/* Allocate arrays to hold test patch input and output values */
		if ((cg[n].pat = (pval *)malloc(sizeof(pval) * cg[n].npat)) == NULL)
			error("Malloc failed - pat[]");
	
		/* Read in the CGATs fields */
		if ((sidx = cgf->find_field(cgf, 0, "SAMPLE_ID")) < 0
		 && (sidx = cgf->find_field(cgf, 0, "SampleName")) < 0
		 && (sidx = cgf->find_field(cgf, 0, "Sample_Name")) < 0
		 && (sidx = cgf->find_field(cgf, 0, "SAMPLE_NAME")) < 0
		 && (sidx = cgf->find_field(cgf, 0, "SAMPLE_LOC")) < 0)
			error("Input file '%s' doesn't contain field SAMPLE_ID, SampleName, Sample_Name, SAMPLE_NAME or SAMPLE_LOC",cg[n].name);
		if (cgf->t[0].ftype[sidx] != nqcs_t
		 && cgf->t[0].ftype[sidx] != cs_t)
			error("Sample ID/Name field isn't a quoted or non quoted character string");

		if ((sldx = cgf->find_field(cgf, 0, "SAMPLE_LOC")) < 0
		 || cgf->t[0].ftype[sldx] != cs_t)
			sldx = -1;

		if (spec == 0) { 		/* Using instrument tristimulous value */

			if (isLab) {		/* Expect Lab */
				if ((xix = cgf->find_field(cgf, 0, "LAB_L")) < 0)
					error("Input file '%s' doesn't contain field LAB_L",cg[n].name);
				if (cgf->t[0].ftype[xix] != r_t)
					error("Field LAB_L is wrong type - expect float");
				if ((yix = cgf->find_field(cgf, 0, "LAB_A")) < 0)
					error("Input file '%s' doesn't contain field LAB_A",cg[n].name);
				if (cgf->t[0].ftype[yix] != r_t)
					error("Field LAB_A is wrong type - expect float");
				if ((zix = cgf->find_field(cgf, 0, "LAB_B")) < 0)
					error("Input file '%s' doesn't contain field LAB_B",cg[n].name);
				if (cgf->t[0].ftype[zix] != r_t)
					error("Field LAB_B is wrong type - expect float");

			} else { 		/* Expect XYZ */
				if ((xix = cgf->find_field(cgf, 0, "XYZ_X")) < 0)
					error("Input file '%s' doesn't contain field XYZ_X",cg[n].name);
				if (cgf->t[0].ftype[xix] != r_t)
					error("Field XYZ_X is wrong type - expect float");
				if ((yix = cgf->find_field(cgf, 0, "XYZ_Y")) < 0)
					error("Input file '%s' doesn't contain field XYZ_Y",cg[n].name);
				if (cgf->t[0].ftype[yix] != r_t)
					error("Field XYZ_Y is wrong type - expect float");
				if ((zix = cgf->find_field(cgf, 0, "XYZ_Z")) < 0)
					error("Input file '%s' doesn't contain field XYZ_Z",cg[n].name);
				if (cgf->t[0].ftype[zix] != r_t)
					error("Field XYZ_Z is wrong type - expect float");
			}

			for (i = 0; i < cg[n].npat; i++) {
				strcpy(cg[n].pat[i].sid, (char *)cgf->t[0].fdata[i][sidx]);
				if (sldx >= 0)
					strcpy(cg[n].pat[i].loc, (char *)cgf->t[0].fdata[i][sldx]);
				else
					cg[n].pat[i].loc[0] = '\000';
				cg[n].pat[i].og = 0;
				cg[n].pat[i].xyz[0] = *((double *)cgf->t[0].fdata[i][xix]);
				cg[n].pat[i].xyz[1] = *((double *)cgf->t[0].fdata[i][yix]);
				cg[n].pat[i].xyz[2] = *((double *)cgf->t[0].fdata[i][zix]);

				if (isLab) {	/* Convert Lab to XYZ */
					icmLab2XYZ(&icmD50, cg[n].pat[i].xyz, cg[n].pat[i].xyz);
				}
//printf("~1 file %d patch %d = XYZ %f %f %f\n", n,i,cg[n].pat[i].xyz[0],cg[n].pat[i].xyz[1],cg[n].pat[i].xyz[2]);

				/* restore normalised display values to absolute */
				if (cg[n].isdnormed) {
					if (cg[n].w[1] > 0.0) {		// Found absoluute display white tag
						cg[n].pat[i].xyz[0] *= cg[n].w[1]/100.0;
						cg[n].pat[i].xyz[1] *= cg[n].w[1]/100.0;
						cg[n].pat[i].xyz[2] *= cg[n].w[1]/100.0;
					}

				} else if (!cg[n].isdisp) {
					/* If reflective or transmissive that are 0..100%, */
					/* scale back to 0.. 1 */
					cg[n].pat[i].xyz[0] /= 100.0;		/* scale back to XYZ 1.0 */
					cg[n].pat[i].xyz[1] /= 100.0;
					cg[n].pat[i].xyz[2] /= 100.0;
				}

				/* Apply ccmx */
				if (n == 1 && cmx != NULL) {
					cmx->xform(cmx, cg[n].pat[i].xyz, cg[n].pat[i].xyz);
				}

				if ((dovrml == 3 || dovrml == 4) && cg[n].isrgb) {
					cg[n].pat[i].rgb[0] = 0.01 * *((double *)cgf->t[0].fdata[i][rgbix[0]]);
					cg[n].pat[i].rgb[1] = 0.01 * *((double *)cgf->t[0].fdata[i][rgbix[1]]);
					cg[n].pat[i].rgb[2] = 0.01 * *((double *)cgf->t[0].fdata[i][rgbix[2]]);

					if (dovrml == 4) {
						icmRec709_RGBd_2_YPbPr(cg[n].pat[i].ycc, cg[n].pat[i].rgb);
					}
				}
			}

		} else { 		/* Using spectral data */
			int ii;
			xspect sp;
			char buf[100];
			int  spi[XSPECT_MAX_BANDS];	/* CGATS indexes for each wavelength */
			xsp2cie *sp2cie;	/* Spectral conversion object */

			/* Copies of global values: */
			int l_fwacomp = fwacomp;
			int l_spec = spec;
			icxIllumeType l_tillum = tillum;
			xspect *l_tillump = tillump;
			icxIllumeType l_illum = illum;
			icxObserverType l_observ = observ;

			if ((ii = cgf->find_kword(cgf, 0, "SPECTRAL_BANDS")) < 0)
				error ("Input file doesn't contain keyword SPECTRAL_BANDS");
			sp.spec_n = atoi(cgf->t[0].kdata[ii]);
			if ((ii = cgf->find_kword(cgf, 0, "SPECTRAL_START_NM")) < 0)
				error ("Input file doesn't contain keyword SPECTRAL_START_NM");
			sp.spec_wl_short = atof(cgf->t[0].kdata[ii]);
			if ((ii = cgf->find_kword(cgf, 0, "SPECTRAL_END_NM")) < 0)
				error ("Input file doesn't contain keyword SPECTRAL_END_NM");
			sp.spec_wl_long = atof(cgf->t[0].kdata[ii]);
			if (!cg[n].isdisp || cg[n].isdnormed != 0)
				sp.norm = 100.0;
			else
				sp.norm = 1.0;

			/* Find the fields for spectral values */
			for (j = 0; j < sp.spec_n; j++) {
				int nm;
		
				/* Compute nearest integer wavelength */
				nm = (int)(sp.spec_wl_short + ((double)j/(sp.spec_n-1.0))
				            * (sp.spec_wl_long - sp.spec_wl_short) + 0.5);
				
				sprintf(buf,"SPEC_%03d",nm);

				if ((spi[j] = cgf->find_field(cgf, 0, buf)) < 0)
					error("Input file doesn't contain field %s",buf);

				if (cgf->t[0].ftype[spi[j]] != r_t)
					error("Field %s is wrong type - expect float",buf);
			}

			/* Create a spectral conversion object */
			if (cg[n].isemis) {
				if (l_illum != icxIT_none)
					warning("-i illuminant ignored for emissive reference type");
				if (l_fwacomp)
					warning("-f FWA ignored for emissive reference type");
				l_illum = icxIT_none;		/* Make emissive conversion */
				l_tillum = icxIT_none;
				l_fwacomp = 0;
			} else {
				/* Set default */
				if (l_illum == icxIT_none)
					l_illum = icxIT_D50;
			}
	
			/* Set default */
			if (l_observ == icxOT_none)
				l_observ = icxOT_CIE_1931_2;

			if ((sp2cie = new_xsp2cie(l_illum, l_illum == icxIT_none ? NULL : &cust_illum,
			                          l_observ, NULL, icSigXYZData, icxClamp)) == NULL)
				error("Creation of spectral conversion object failed");

			if (l_fwacomp) {
				int ti;
				xspect mwsp;			/* Medium spectrum */
				instType itype;			/* Spectral instrument type */
				xspect insp;			/* Instrument illuminant */

				mwsp = sp;		/* Struct copy */

	 			if ((ti = cgf->find_kword(cgf, 0, "TARGET_INSTRUMENT")) < 0)
					error ("Can't find target instrument in '%s' needed for FWA compensation",cg[n].name);

				if ((itype = inst_enum(cgf->t[0].kdata[ti])) == instUnknown)
					error ("Unrecognised target instrument '%s'", cgf->t[0].kdata[ti]);

				if (inst_illuminant(&insp, itype) != 0)
					error ("Instrument doesn't have an FWA illuminent");

				/* Determine a media white spectral reflectance */
				for (j = 0; j < mwsp.spec_n; j++)
					mwsp.spec[j] = 0.0;

				/* Since we don't want to assume that there are any associated device */
				/* values present in each file, we can't use this as means of */
				/* determining the media color. Use an alternative approach here, */
				/* which may give slightly different results to profile. */

				/* Track the maximum reflectance for any band to determine white. */
				/* This might silently fail, if there isn't white in the sample set. */
				for (i = 0; i < cg[0].npat; i++) {
					for (j = 0; j < mwsp.spec_n; j++) {
						double rv = *((double *)cgf->t[0].fdata[i][spi[j]]);
						if (rv > mwsp.spec[j])
							mwsp.spec[j] = rv;
					}
				}

				/* If we are setting a specific simulated instrument illuminant */
				if (l_tillum != icxIT_none) {
					l_tillump = &cust_tillum;
					if (l_tillum != icxIT_custom) {
						if (standardIlluminant(l_tillump, l_tillum, 0.0)) {
							error("simulated inst. illum. not recognised");
						}
					}
				}

				if (sp2cie->set_fwa(sp2cie, &insp, l_tillump, &mwsp)) 
					error ("Set FWA on sp2cie failed");

				if (verb) {
					double FWAc;
					sp2cie->get_fwa_info(sp2cie, &FWAc);
					fprintf(verbo,"FWA content = %f\n",FWAc);
				}
			}

			for (i = 0; i < cg[0].npat; i++) {

				strcpy(cg[n].pat[i].sid, (char *)cgf->t[0].fdata[i][sidx]);
				if (sldx >= 0)
					strcpy(cg[n].pat[i].loc, (char *)cgf->t[0].fdata[i][sldx]);
				else
					cg[n].pat[i].loc[0] = '\000';
				cg[n].pat[i].og = 0;

				/* Read the spectral values for this patch */
				for (j = 0; j < sp.spec_n; j++) {
					sp.spec[j] = *((double *)cgf->t[0].fdata[i][spi[j]]);
				}

				/* Convert it to XYZ space */
				sp2cie->convert(sp2cie, cg[n].pat[i].xyz, &sp);

				/* restore normalised display values to absolute */
				if (cg[n].isdnormed) {
					if (cg[n].w[1] > 0.0) {		// Found absoluute display white tag
						cg[n].pat[i].xyz[0] *= cg[n].w[1];
						cg[n].pat[i].xyz[1] *= cg[n].w[1];
						cg[n].pat[i].xyz[2] *= cg[n].w[1];
					}

				}

				/* Apply ccmx */
				if (n == 1 && cmx != NULL) {
					cmx->xform(cmx, cg[n].pat[i].xyz, cg[n].pat[i].xyz);
				}
			}

			sp2cie->del(sp2cie);		/* Done with this */
		}	/* End of reading in CGATs file spectral data */


		/* Locate the patch with maximum Y, a possible white patch */
		/* in case we need it latter. */
		{
			int ii;

			if (cg[n].w[1] == 0.0) {	/* No display white patch tag */

				/* Locate patch with biggest Y, assume it is white... */
				for (i = 0; i < cg[n].npat; i++) {
					if (cg[n].pat[i].xyz[1] > cg[n].w[1]) {
						icmCpy3(cg[n].w, cg[n].pat[i].xyz);
						ii = i;
					}
				}
				if (verb) printf("File %d Chose patch %d as white, XYZ %f %f %f\n",
				                       n, ii+1,cg[n].w[0],cg[n].w[1],cg[n].w[2]);
			} else {
				if (verb) printf("File %d White is from display luminance ref. XYZ %f %f %f\n",
				                       n, cg[n].w[0],cg[n].w[1],cg[n].w[2]);
			}
			icmCpy3(cg[n].nw, cg[n].w);
		}
		cgf->del(cgf);		/* Clean up */
	}	/* Next file */

	if (norm == 4) {		/* Normalise to average of white XYZ of the two files */
		icmBlend3(cg[0].w, cg[0].w, cg[1].w, 0.5);
		icmCpy3(cg[1].w, cg[0].w);
//		if (verb) printf("Average White XYZ %f %f %f\n",cg[0].w[0],cg[0].w[1],cg[0].w[2]);
	}

	/* For both files */
	for (n = 0; n < 2; n++) {

		/* Normalise this file to white = 1.0 or D50 */
		if (norm) {
			int ii;

			double chmat[3][3];				/* Chromatic adapation matrix */

			DBG(("Normalizng '%s' to white\n",cg[n].name))

			if (norm == 2 || norm == 4) {		/* Norm to white XYZ */ 
				icmXYZNumber s_wp;
				icmAry2XYZ(s_wp, cg[n].w);
				icmChromAdaptMatrix(ICM_CAM_BRADFORD, icmD50, s_wp, chmat);
			}

			for (i = 0; i < cg[n].npat; i++) {
				if (norm == 1) {
					cg[n].pat[i].xyz[0] *= 100.0 / cg[n].w[1];
					cg[n].pat[i].xyz[1] *= 100.0 / cg[n].w[1];
					cg[n].pat[i].xyz[2] *= 100.0 / cg[n].w[1];
				} else if (norm == 2 || norm == 4) { 
					icmMulBy3x3(cg[n].pat[i].xyz, chmat, cg[n].pat[i].xyz);
				} else {
					cg[n].pat[i].xyz[0] *= 100.0 / (cg[n].w[0] + cg[n].w[1] + cg[n].w[2]);
					cg[n].pat[i].xyz[1] *= 100.0 / (cg[n].w[0] + cg[n].w[1] + cg[n].w[2]);
					cg[n].pat[i].xyz[2] *= 100.0 / (cg[n].w[0] + cg[n].w[1] + cg[n].w[2]);
				}
//printf("~1 file %d patch %d = norm XYZ %f %f %f\n", n,i,cg[n].pat[i].xyz[0],cg[n].pat[i].xyz[1],cg[n].pat[i].xyz[2]);
			}
			/* Compute normalised white too */
			if (norm == 1) {
				cg[n].nw[0] *= 100.0 / cg[n].w[1];
				cg[n].nw[1] *= 100.0 / cg[n].w[1];
				cg[n].nw[2] *= 100.0 / cg[n].w[1];
			} else if (norm == 2 || norm == 4) { 
				icmMulBy3x3(cg[n].nw, chmat, cg[n].w);
			} else {
				cg[n].nw[0] *= 100.0 / (cg[n].w[0] + cg[n].w[1] + cg[n].w[2]);
				cg[n].nw[1] *= 100.0 / (cg[n].w[0] + cg[n].w[1] + cg[n].w[2]);
				cg[n].nw[2] *= 100.0 / (cg[n].w[0] + cg[n].w[1] + cg[n].w[2]);
			}
//printf("~1 file %d norm white XYZ %f %f %f\n", n,cg[n].nw[0], cg[n].nw[1], cg[n].nw[2]);
		}
	}	/* Next file */

	if (cmx != NULL) 
		cmx->del(cmx);
	cmx = NULL;

	/* Check that the number of test patches matches */
	if (cg[0].npat != cg[1].npat)
		error("Number of patches between '%s' and '%s' doesn't match",cg[0].name,cg[1].name);
	
	/* Create a list to map the second list of patches to the first */
	if ((match = (int *)malloc(sizeof(int) * cg[0].npat)) == NULL)
		error("Malloc failed - match[]");
	for (i = 0; i < cg[0].npat; i++) {
		for (j = 0; j < cg[1].npat; j++) {
			if (strcmp(cg[0].pat[i].sid, cg[1].pat[j].sid) == 0)
				break;			/* Found it */
		}
		if (j < cg[1].npat) {
			match[i] = j;
		} else {
			error("Failed to find matching patch to '%s'",cg[0].pat[i].sid);
		}
	}

	/* Figure out which patches to skip because they are out of gamut */
	if (luo != NULL) {
		double chmat[3][3];				/* Chromatic adapation matrix */
		double out[MAX_CHAN], in[3], check[3];
		icmXYZNumber s_wp;
		int rv;

		DBG(("Figuring out of gamut patches\n"))

		/* Convert sample PCS to relative */
//printf("   cg[0].w %f %f %f\n", cg[0].w[0], cg[0].w[1], cg[0].w[2]);
		icmAry2XYZ(s_wp, cg[0].w);
		s_wp.X /= s_wp.Y;		// Normalise the white to 1.0
		s_wp.Y /= s_wp.Y;		// so that matrix doesn't change magnitude
		s_wp.Z /= s_wp.Y;
//printf("   s_wp %f %f %f\n", s_wp.X, s_wp.Y, s_wp.Z);
		icmChromAdaptMatrix(ICM_CAM_BRADFORD, icmD50, s_wp, chmat);
//printf("~1 matrix = \n");
//printf("   %f %f %f\n", chmat[0][0], chmat[0][1], chmat[0][2]);
//printf("   %f %f %f\n", chmat[1][0], chmat[1][1], chmat[1][2]);
//printf("   %f %f %f\n", chmat[2][0], chmat[2][1], chmat[2][2]);

		for (i = 0; i < cg[0].npat; i++) {
			icmMulBy3x3(in, chmat, cg[0].pat[i].xyz);

//printf("~1 %d: xyz %f %f %f, rel %f %f %f\n", i+1, cg[0].pat[i].xyz[0], cg[0].pat[i].xyz[1], cg[0].pat[i].xyz[2], in[0], in[1], in[2]);

			if ((rv = luo->inv_lookup(luo, out, in)) > 0 || 1) {
				double de;

				luo->lookup(luo, check, out);
				de = icmXYZLabDE(&icmD50,check, in);
//printf("~1 %d: rv %d, de %f, check XYZ %f %f %f\n",i+1,rv, de, check[0],check[1],check[2]);

				if (de >= 0.01) {
					cg[0].pat[i].og = 1;
//printf("~1 Patch %d is out of gamut by DE %f RGB %f %f %f\n",i+1,de,out[0],out[1],out[2]);
					if (verb >= 3)
						printf("Patch %d is out of gamut by DE %f\n",i+1,de);
					cg[0].nig--;
				}
			}
		}
		if (verb)
			fprintf(verbo,"No of test patches in gamut = %d/%d\n",cg[0].nig,cg[0].npat);
	}

	if (cg[0].nig <= 0) {
		if (verb)
			fprintf(verbo,"No test patches in gamut - givig up\n");
		return 0;
	}

	/* Adjust the Lab reference white to be the mean of the white of the two files */
	if (norm != 0 && !usestdd50) {
		labw.X = 0.5 * (cg[0].nw[0] + cg[1].nw[0]);
		labw.Y = 0.5 * (cg[0].nw[1] + cg[1].nw[1]);
		labw.Z = 0.5 * (cg[0].nw[2] + cg[1].nw[2]);

		if (verb)
			printf("L*a*b* white reference = XYZ %f %f %f\n",labw.X,labw.Y,labw.Z);
	}

	/* labw defaults to D50 */

	/* Convert XYZ to Lab */
	for (n = 0; n < 2; n++) {
		for (i = 0; i < cg[n].npat; i++) {
			icmXYZ2Lab(&labw, cg[n].pat[i].v, cg[n].pat[i].xyz);
		}
	}

	/* Compute the delta E's */
	DBG(("Computing the delta E's\n"))
	for (i = 0; i < cg[0].npat; i++) {

		cg[0].pat[i].ixde[0] = fabs(cg[0].pat[i].xyz[0] - cg[1].pat[match[i]].xyz[0]);
		cg[0].pat[i].ixde[1] = fabs(cg[0].pat[i].xyz[1] - cg[1].pat[match[i]].xyz[1]);
		cg[0].pat[i].ixde[2] = fabs(cg[0].pat[i].xyz[2] - cg[1].pat[match[i]].xyz[2]);

		if (cie2k)
			cg[0].pat[i].de = icmCIE2K(cg[0].pat[i].v, cg[1].pat[match[i]].v);
		else if (cie94)
			cg[0].pat[i].de = icmCIE94(cg[0].pat[i].v, cg[1].pat[match[i]].v);
		else
			cg[0].pat[i].de = icmLabDE(cg[0].pat[i].v, cg[1].pat[match[i]].v);

		cg[0].pat[i].ide[0] = fabs(cg[0].pat[i].v[0] - cg[1].pat[match[i]].v[0]);
		cg[0].pat[i].ide[1] = fabs(cg[0].pat[i].v[1] - cg[1].pat[match[i]].v[1]);
		cg[0].pat[i].ide[2] = fabs(cg[0].pat[i].v[2] - cg[1].pat[match[i]].v[2]);
	}

	/* Create sorted list, from worst to best. */
	if ((sort = (int *)malloc(sizeof(int) * cg[0].npat)) == NULL)
		error("Malloc failed - sort[]");
	for (i = 0; i < cg[0].npat; i++) 
		sort[i] = i;

#define HEAP_COMPARE(A,B) (cg[0].pat[A].de > cg[0].pat[B].de)
	HEAPSORT(int, sort, cg[0].npat);
#undef HEAP_COMPARE

	/* - - - - - - - - - - */
	/* Plot a dE histogram */
	if (dohisto) {
		double demax = -1e6, demin = 1e6;
		int maxbins = 50;		/* Maximum bins */
//		int minbins = 20;		/* Target minimum bins (depends on distribution) */ 
//		int mincount = 10;		/* Minimum number of points in a bin */
		int minbins = 10;		/* Target minimum bins (depends on distribution) */ 
		int mincount = 5;		/* Minimum number of points in a bin */
		double mbwidth;
		int nbins = 0;
		hbin *bins = NULL;
		pval **stpat;          /* Pointers to sorted cg[0].pat[] */
		double tval;
		double *x, *y;
		
		DBG(("Plotting histogram\n"))

		/* Figure out the range of dE's */
		for (i = 0; i < cg[0].npat; i++) {
			double de = cg[0].pat[i].de;

			if (de > demax)
				demax = de;
			if (de < demin)
				demin = de;
		}

		if (demax < 1e-6)
			error("histogram: dE range is too small to plot");

		/* Bin width that gives maxbins */
		mbwidth = demax / maxbins;
		
#ifdef NEVER
		/* Reduce mincount if needed to get minbins */
		if (cg[0].npat/minbins < mincount)
			mincount = cg[0].npat/minbins;
#endif

		if ((bins = (hbin *)calloc(maxbins, sizeof(hbin))) == NULL)
			error("malloc of histogram bins failed");

		if ((stpat = (pval **)malloc(sizeof(pval *) * cg[0].npat)) == NULL)
			error("Malloc failed - stpat[]");

		for (i = 0; i < cg[0].npat; i++)
			stpat[i] = &cg[0].pat[i];

	  	/* Sort the dE's */
#define HEAP_COMPARE(A,B) (A->de < B->de)
		HEAPSORT(pval *, stpat, cg[0].npat);
#undef HEAP_COMPARE

		/* Create bins and add points */
		bins[0].min = 0.0;
		for (nbins = i = 0; i < cg[0].npat && nbins < maxbins; i++) {
			double de = stpat[i]->de;

			/* Move on to next bin ? */
			if (bins[nbins].count >= mincount
			 && (de - bins[nbins].min) >= mbwidth) {
				if (i > 0)
					bins[nbins].max = 0.5 * (de + stpat[i-1]->de);
				else
					bins[nbins].max = de;
				nbins++;
				bins[nbins].min = bins[nbins-1].max; 
			} 
			bins[nbins].count++;
			bins[nbins].max = de;
		}
		if (bins[nbins].count != 0)
			nbins++;

		/* Compute value */
		tval = 0.0;
		for (i = 0; i < nbins; i++) {
			bins[i].val = bins[i].count/(bins[i].max - bins[i].min);
			tval += bins[i].val;
		}

		tval /= 100.0;		/* Make it % */
		for (i = 0; i < nbins; i++) {
			bins[i].val /= tval;
			if (verb) fprintf(verbo,"Bin %d, %f - %f, % 2.4f%%, count %d\n",
			             i,bins[i].min,bins[i].max,bins[i].val,bins[i].count);
		}

		/* Plot it */
		if ((x = (double *)calloc(nbins+1, sizeof(double))) == NULL)
			error("malloc of histogram x array");
		if ((y = (double *)calloc(nbins+1, sizeof(double))) == NULL)
			error("malloc of histogram y array");

		for (i = 0; i < nbins; i++) {
			x[i] = 0.5 * (bins[i].min + bins[i].max);
			y[i] = bins[i].val;
		}
		x[i] = demax;
		y[i] = 0.0;
		do_plot(x, y, NULL, NULL, nbins+1);

		/* Save points to a file ? */
		if (histoname[0] != '\000') {
			FILE *fp;

			if ((fp = fopen(histoname, "w")) == NULL)
				error("Opening '%s' for writing failed",histoname);

			fprintf(fp, "%s\t%s\n\n",cg[0].name, cg[1].name);

 			for (i = 0; i < nbins; i++) {
				fprintf(fp, "%f\t%f\n",0.5 * (bins[i].min + bins[i].max), bins[i].val);
			}

			if (fclose(fp) != 0)
				error("Closing '%s' failed",histoname);
		}

		free(y);
		free(x);
		free(bins);
		free(stpat);
	}

	/* - - - - - - - - - - */
	/* Figure out the report */
	{
		double merr = 0.0, aerr = 0.0;
		int n90;
		double merr90 = 0.0, aerr90 = 0.0;
		int n10;
		double merr10 = 0.0, aerr10 = 0.0;
		double rad;
		double aierr[3] = { 0.0, 0.0, 0.0 };
		double aixerr[3] = { 0.0, 0.0, 0.0 };
		double red[3] = { 1.0, 0.2, 0.2 };
		double green[3] = { 0.2, 1.0, 0.2 };
		double min[3], max[3];
		double col[3];

		if (dovrml) {
			double vol;
			int k;

			wrl = new_vrml(out_name, doaxes, (dovrml == 3 || dovrml == 4) ? vrml_rgb : vrml_lab);
			wrl->start_line_set(wrl, 0);

			for (j = 0; j < 3; j++) {
				min[j] = 1e6;
				max[j] = -1e6;
			}

			/* Get bounding box */
			for (i = 0; i < cg[0].npat; i++) {
				for (k = 0; k < 2; k++) {
					for (j = 0; j < 3; j++) {
						if (dovrml == 3 || dovrml == 4) {		/* RGB or YCC device plot */
							if (cg[k].pat[i].rgb[j] > max[j])
								max[j] = cg[k].pat[i].rgb[j];
							if (cg[k].pat[i].rgb[j] < min[j])
								min[j] = cg[k].pat[i].rgb[j];
						} else {
							if (cg[k].pat[i].v[j] > max[j])
								max[j] = cg[k].pat[i].v[j];
							if (cg[k].pat[i].v[j] < min[j])
								min[j] = cg[k].pat[i].v[j];
						}
					}
				}
			}

			for (vol = 1.0, j = 0; j < 3; j++) {
//printf("~1 size[%d] = %f\n",j, max[j] - min[j]);
				vol *= (max[j] - min[j]);
			}
			vol = sqrt(vol);
//printf("~1 vol = %f\n",vol);
			rad = 0.02 * vol/pow(cg[0].npat, 1.0/3.0);
//printf("~1 rad = %f\n",rad);

			if (dovrml == 3)	// Hack
				rad = 0.02;
			else if (dovrml == 4)	// Hack
				rad = 0.015;
		}

		if (dovrml && (dovrml == 3 || dovrml == 4)) {		/* RGB/YCC device plot */
			if (!cg[0].isrgb || !cg[1].isrgb)
				error("Both files must have RGB devices space for -d option");
		}

		/* Do overall results */
		for (i = 0; i < cg[0].npat; i++) {
			double de;

			if (cg[0].pat[i].og)		/* Skip out of gamut patches */
				continue;

			if (dosort)
				j = sort[i];
			else
				j = i;

			de = cg[0].pat[j].de;
			aerr += de;

			aierr[0] += cg[0].pat[j].ide[0];
			aierr[1] += cg[0].pat[j].ide[1];
			aierr[2] += cg[0].pat[j].ide[2];

			aixerr[0] += cg[0].pat[j].ixde[0];
			aixerr[1] += cg[0].pat[j].ixde[1];
			aixerr[2] += cg[0].pat[j].ixde[2];

			if (verb >= 2) {

				printf("%s%s%s: %f %f %f <=> %f %f %f  de %f\n",
					cg[0].pat[j].sid,
					cg[0].pat[j].loc[0] != '\000' ? " " : "",
					cg[0].pat[j].loc,
					cg[0].pat[j].v[0], cg[0].pat[j].v[1], cg[0].pat[j].v[2],
					cg[1].pat[match[j]].v[0], cg[1].pat[match[j]].v[1], cg[1].pat[match[j]].v[2],
					de);

#ifdef NEVER	/* Print XYZ as well */
				printf("  %f %f %f <=> %f %f %f\n",
					cg[0].pat[j].xyz[0], cg[0].pat[j].xyz[1], cg[0].pat[j].xyz[2],
					cg[1].pat[match[j]].xyz[0], cg[1].pat[match[j]].xyz[1], cg[1].pat[match[j]].xyz[2]);
#endif
			}

			if (de > merr)
				merr = de;

			if (dovrml) {
				if ((dovrml == 3 || dovrml == 4)) {		/* RGB/YCC device plot */
					double *val1, *val2;
					int k;

					if (dovrml == 3) {
						val1 = cg[0].pat[i].rgb; 
						val2 = cg[1].pat[match[i]].rgb;
					} else {
						val1 = cg[0].pat[i].ycc; 
						val2 = cg[1].pat[match[i]].ycc;
					}

					de = icmNorm33(val1, val2);

					if (de > 1e-6) {
						wrl->add_vertex(wrl, 0, val1);
						wrl->add_vertex(wrl, 0, val2);
					}

#ifdef NEVER	// Green target
					wrl->add_marker(wrl, val1, green, rad);

#else		// Natural color
					for (k = 0; k < 3; k++)
						col[k] = 0.3 + 0.7 * (cg[0].pat[i].rgb[k] - min[k])/(max[k] - min[k]);
					wrl->add_marker(wrl, val1, col, rad);
#endif

					wrl->add_marker_trans(wrl, val2, red, 0.3, rad * 0.99);

				} else {		/* PCS */
					if (de > 1e-6) {
						wrl->add_vertex(wrl, 0, cg[0].pat[i].v);
						wrl->add_vertex(wrl, 0, cg[1].pat[match[i]].v);
					}
					if (dovrml == 2) {
						wrl->add_marker(wrl, cg[0].pat[i].v, green, rad);
						wrl->add_marker(wrl, cg[1].pat[match[i]].v, red, rad);
					}
				}
			}

		}
		if (cg[0].nig > 0) {
			aerr /= (double)cg[0].nig;
			aierr[0] /= (double)cg[0].nig;
			aierr[1] /= (double)cg[0].nig;
			aierr[2] /= (double)cg[0].nig;

			aixerr[0] /= (double)cg[0].nig;
			aixerr[1] /= (double)cg[0].nig;
			aixerr[2] /= (double)cg[0].nig;
		}

		if (dovrml) {
			wrl->make_lines(wrl, 0, 2);
			wrl->del(wrl);
			wrl = NULL;
		}

		/* Do best 90% */
		n90 = (int)(cg[0].nig * 9.0/10.0 + 0.5);
		for (i = j = 0; i < cg[0].npat; i++) {
			double de = cg[0].pat[sort[i]].de;
			if (cg[0].pat[i].og)		/* Skip out of gamut */
				continue;
			if (j >= (cg[0].nig-n90)) {	/* If in top 90% of in gamut patches */
				aerr90 += de;
				if (de > merr90)
					merr90 = de;
			}
			j++;						/* Index of within gamut patches */
		}
		if (n90 > 0)
			aerr90 /= (double)n90;

		/* Do worst 10% */
		n10 = (int)(cg[0].nig * 1.0/10.0 + 0.5);
		for (i = j = 0; i < cg[0].npat; i++) {
			double de = cg[0].pat[sort[i]].de;
			if (cg[0].pat[i].og)		/* Skip out of gamut */
				continue;
			if (j <= n10) {				/* If in worst 10% of in gamut patches */
				aerr10 += de;
				if (de > merr10)
					merr10 = de;
			}
			j++;
		}
		if (n10 > 0)
			aerr10 /= (double)n10;

		if (verb) {
			fprintf(verbo,"No of test patches in worst 10%% are = %d\n",n10);
			fprintf(verbo,"No of test patches in best 90%% are = %d\n",n90);
		}
		printf("Verify results:\n");
		if (norm == 4)
			printf("  L*a*b* ref. = average XYZ %f %f %f\n",cg[0].w[0],cg[0].w[1],cg[0].w[2]);
		else if (norm == 1) {
			printf("  File 1 L* ref. Y %f\n", cg[0].w[1]);
			printf("  File 2 L* ref. Y %f\n", cg[1].w[1]);
		} else if (norm == 2) {
			printf("  File 1 L*a*b* ref. XYZ %f %f %f\n", cg[0].w[0],cg[0].w[1],cg[0].w[2]);
			printf("  File 2 L*a*b* ref. XYZ %f %f %f\n", cg[1].w[0],cg[1].w[1],cg[1].w[2]);
		} else if (norm == 3) {
			printf("  File 1 L* ref. X+Y+Z %f %f %f\n", cg[0].w[0],cg[0].w[1],cg[0].w[2]);
			printf("  File 2 L* ref. X+Y+Z %f %f %f\n", cg[1].w[0],cg[1].w[1],cg[1].w[2]);
		}
		printf("  Total errors%s:     peak = %f, avg = %f\n", cie2k ? " (CIEDE2000)" : cie94 ? " (CIE94)" : "", merr, aerr);
		printf("  Worst 10%% errors%s: peak = %f, avg = %f\n", cie2k ? " (CIEDE2000)" : cie94 ? " (CIE94)" : "", merr10, aerr10);
		printf("  Best  90%% errors%s: peak = %f, avg = %f\n", cie2k ? " (CIEDE2000)" : cie94 ? " (CIE94)" : "", merr90, aerr90);
		printf("  avg err X  %f, Y  %f, Z  %f\n", aixerr[0], aixerr[1], aixerr[2]);
		printf("  avg err L* %f, a* %f, b* %f\n", aierr[0], aierr[1], aierr[2]);
	
		free(sort);
		free(match);
		free(cg[0].pat);
		free(cg[1].pat);
	}

	if (luo != NULL)
		luo->del(luo);
	if (xicco != NULL)
		xicco->del(xicco);		/* Expansion wrapper */
	if (icco != NULL)
		icco->del(icco);		/* Icc */
	if (fp != NULL)
		fp->del(fp);

	return 0;
}


