Monday, August 13, 2007

CVSpyder.dll API description

For quite a while I own a Spyder2 colorimeter. I mainly use it for color-calibration with the provided software but was always keen on using it together with open-source software.

There is an open-source project called ColorimetreHCFR (a french home-cinema-enthusiasts TV calibration software). They are (ab)using CVSpyder.dll, a Win32-dynamic-link-library that comes with some Colorvision products. With this dll an external program can access the colorimeter and make measurements.

I digged a little deeper and found the first project doing so: S2Fly . This program just makes reads and displays the measured values on the screen. However, the author seems to be no more active.

But then few weeks ago I found ColorAxis - a small program which makes reads using this dll and is open-source. Unfortunately it's written in VisualBasic - a programming language I'm not at all familiar with. You can find it at http://coloraxis.sf.net .

Okay, I thought let's do it and re-engineer the API for using it in C/C++ programs. And here it is:

/* re-engineered header file for CVSypder.dll
* (c) 2007 Stefan Döhla, published under GPLv2
*
* API blatanly stolen from ColorAxis VB code
* see http://coloraxis.sf.net for ColorAxis details
*/

#ifndef __CV_SYPDER_H__
#define __CV_SPYDER_H__ __CV_SPYDER_H__

#define CV_STATUS_SUCCESS 0x01
#define CV_STATUS_FAILURE 0x00

#define CV_ERROR_API 0x00
#define CV_ERROR_SYSTEM 0x01

#define CV_ERROR_NOT_INITIALIZED 0x00010001
#define CV_ERROR_INVALID_PARAMETER 0x00010002
#define CV_ERROR_REFRESH_UNDETERMINED 0x00010003
#define CV_ERROR_TRANSMISSION_ERROR 0x00010004
#define CV_ERROR_XLNX_NOT_CONFIGURED 0x00010005
#define CV_ERROR_TIMEOUT 0x00010006
#define CV_LAST_FATAL_ERROR 0x0001ffff

#define CV_WARNING_ALREADY_INITIALIZED 0x00020001
#define CV_WARNING_TRIGGER_TIMEOUT 0x00020002
#define CV_WARNING_OVERALL_TIMEOUT 0x00020003
#define CV_WARNING_APPARENTLY_NOT_CRT 0x00020004
#define CV_WARNING_NO_CRT_CALIBRATION 0x00020005
#define CV_WARNING_NO_LCD_CALIBRATION 0x00020006
#define CV_WARNING_NO_TOK_CALIBRATION 0x00020007

#define CV_XILINX_CONFIGURED 0x00
#define CV_XILINX_READY_FOR_DOWNLOAD 0x01
#define CV_XILINX_UNCONFIGURED 0x02
#define CV_XILINX_QUERY_ERROR 0x03

#define CV_SCREENTYPE_CRT 0x01
#define CV_SCREENTYPE_LCD 0x02

#define CV_SERIAL_NUMBER_LEN 0x08

typedef unsigned char CV_STATUS;
typedef unsigned long CV_MESSAGE;
typedef unsigned short CV_SCREENTYPE;
typedef struct
{
unsigned short DriverVersion;
unsigned short HardwareVersion;
unsigned char SerialNumber[CV_SERIAL_NUMBER_LEN];
} CV_VendorData;

#define DLL_IMPORT __declspec(dllimport)
#define CALLING_CONVENTION _cdecl

#ifdef __cplusplus
extern "C" {
#endif

DLL_IMPORT CV_STATUS CALLING_CONVENTION CV_Startup(CV_VendorData * vendorData, unsigned int tmp, unsigned int tmp2);

DLL_IMPORT CV_STATUS CALLING_CONVENTION CV_GetRefreshRate(unsigned long * rate);

DLL_IMPORT CV_STATUS CALLING_CONVENTION CV_GetXYZ(unsigned short nFrames,
long * X,
long * Y,
long * Z);

DLL_IMPORT CV_STATUS CALLING_CONVENTION CV_GetXYZEx(unsigned short nFrames,
long * X,
long * Y,
long * Z,
unsigned char dolevel2);

DLL_IMPORT CV_MESSAGE CALLING_CONVENTION CV_GetDetailedError(unsigned char * systemError);

DLL_IMPORT CV_STATUS CALLING_CONVENTION CV_UseCalibration(CV_SCREENTYPE screenType);

DLL_IMPORT void CALLING_CONVENTION CV_Shutdown(void);

#ifdef __cplusplus
}
#endif

#endif /* __CV_SPYDER_H__ */

This header file can be used to create either a lib (you need to create stub functions and so on - all the nasty tricks) or use it as a template to DLLImport the functions from CVSpyder.dll .

I tested it with a small program (that includes Bruce Lindbloom's color conversion code at the beginning):

/* test for for CVSypder.dll-usage
* (c) 2007 Stefan Döhla, published under GPLv2
*
* XYZ->RGB conversion blatanly stolen from Bruce Lindbloom
* see http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_T.html
*/

#include "cvspyder.h"

#include
#include
#include

/* LERP(a,b,c) = linear interpolation macro, is 'a' when c == 0.0 and 'b' when c == 1.0 */
#define LERP(a,b,c) (((b) - (a)) * (c) + (a))

typedef struct UVT {
double u;
double v;
double t;
} UVT;

double rt[31] = { /* reciprocal temperature (K) */
DBL_MIN, 10.0e-6, 20.0e-6, 30.0e-6, 40.0e-6, 50.0e-6,
60.0e-6, 70.0e-6, 80.0e-6, 90.0e-6, 100.0e-6, 125.0e-6,
150.0e-6, 175.0e-6, 200.0e-6, 225.0e-6, 250.0e-6, 275.0e-6,
300.0e-6, 325.0e-6, 350.0e-6, 375.0e-6, 400.0e-6, 425.0e-6,
450.0e-6, 475.0e-6, 500.0e-6, 525.0e-6, 550.0e-6, 575.0e-6,
600.0e-6
};

UVT uvt[31] = {
{0.18006, 0.26352, -0.24341},
{0.18066, 0.26589, -0.25479},
{0.18133, 0.26846, -0.26876},
{0.18208, 0.27119, -0.28539},
{0.18293, 0.27407, -0.30470},
{0.18388, 0.27709, -0.32675},
{0.18494, 0.28021, -0.35156},
{0.18611, 0.28342, -0.37915},
{0.18740, 0.28668, -0.40955},
{0.18880, 0.28997, -0.44278},
{0.19032, 0.29326, -0.47888},
{0.19462, 0.30141, -0.58204},
{0.19962, 0.30921, -0.70471},
{0.20525, 0.31647, -0.84901},
{0.21142, 0.32312, -1.0182},
{0.21807, 0.32909, -1.2168},
{0.22511, 0.33439, -1.4512},
{0.23247, 0.33904, -1.7298},
{0.24010, 0.34308, -2.0637},
{0.24792, 0.34655, -2.4681}, /* Note: 0.24792 is a corrected value for the error found in W&S as 0.24702 */
{0.25591, 0.34951, -2.9641},
{0.26400, 0.35200, -3.5814},
{0.27218, 0.35407, -4.3633},
{0.28039, 0.35577, -5.3762},
{0.28863, 0.35714, -6.7262},
{0.29685, 0.35823, -8.5955},
{0.30505, 0.35907, -11.324},
{0.31320, 0.35968, -15.628},
{0.32129, 0.36011, -23.325},
{0.32931, 0.36038, -40.770},
{0.33724, 0.36051, -116.45}
};


int XYZtoCorColorTemp(double X, double Y, double Z, double *temp)
{
double us, vs, p, di, dm;
int i;


if ((X < 1.0e-20) && (Y < 1.0e-20) && (Z < 1.0e-20))
return(-1); /* protect against possible divide-by-zero failure */
us = (4.0 * X) / (X + 15.0 * Y + 3.0 * Z);
vs = (6.0 * Y) / (X + 15.0 * Y + 3.0 * Z);
dm = 0.0;
for (i = 0; i < 31; i++) {
di = (vs - uvt[i].v) - uvt[i].t * (us - uvt[i].u);
if ((i > 0) && (((di <>= 0.0)) || ((di >= 0.0) && (dm < 0.0))))
break; /* found lines bounding (us, vs) : i-1 and i */
dm = di;
}
if (i == 31)
return(-1); /* bad XYZ input, color temp would be less than minimum of 1666.7 degrees, or too far towards blue */
di = di / sqrt(1.0 + uvt[i ].t * uvt[i ].t);
dm = dm / sqrt(1.0 + uvt[i - 1].t * uvt[i - 1].t);
p = dm / (dm - di); /* p = interpolation parameter, 0.0 : i-1, 1.0 : i */
p = 1.0 / (LERP(rt[i - 1], rt[i], p));
*temp = p;
return(0); /* success */
}

int __cdecl main(int argc, char ** argv)
{
CV_VendorData data = {0};
CV_STATUS status = CV_STATUS_SUCCESS;
CV_MESSAGE message;
unsigned long rate = 0;
long Xlong=0, Ylong=0, Zlong=0;
double X=0.0, Y=0.0, Z=0.0;
double x=0.0, y=0.0;
double colorTemperature = 0.0;

status = CV_Startup(&data, 1, 1);
if(status != CV_STATUS_SUCCESS)
message = CV_GetDetailedError(&status);


status = CV_GetRefreshRate(&rate);
if(status != CV_STATUS_SUCCESS)
message = CV_GetDetailedError(&status);

if(message == CV_WARNING_APPARENTLY_NOT_CRT)
status = CV_UseCalibration(CV_SCREENTYPE_LCD);
else
status = CV_UseCalibration(CV_SCREENTYPE_CRT);
if(status != CV_STATUS_SUCCESS)
message = CV_GetDetailedError(&status);

status = CV_GetXYZ(300, &Xlong, &Ylong, &Zlong);
if(status != CV_STATUS_SUCCESS)
message = CV_GetDetailedError(&status);

X=(double)Xlong/1000.0;
Y=(double)Ylong/1000.0;
Z=(double)Zlong/1000.0;

x = X / (X+Y+Z);
y = Y / (X+Y+Z);

XYZtoCorColorTemp(X, Y, Z, &colorTemperature);

CV_Shutdown();
return 0;
}


As you see it's not that complicated making measurements with the CVSpyder API. It's important that one calls the functions in this order
  1. CV_Startup
  2. CV_GetRefreshRate
  3. CV_UseCalibration
  4. CV_GetXYZ
  5. CV_Shutdown
Initially I got bad reads because I haven't called CV_UseCalibration - there are weird results then!

Maybe I can manage to integrate this API into some open-source monitor calibration projects (argyll/lprof) but honestly, I'd be happier if the USB protocol can be hacked (maybe with the help of this API).

1 comment:

Jordi said...

Thanks a lot for thast code!!!!!

I also want to get my spyder 2 working for linux but unfortunatelly I did not have the api for that dll and the author of s2fly was unavailable.

My idea for a first approach of solving this problem is encapsulate the API whih a c++ windows program exposing SOAP methods. So an spyder II meter can be uset remotelly from the linux box. (spyder II connected to a windows laptop runnning the server program)

Unfortunatelly I wont be available since mid october. Put with this info I can make advancements in this line.

Once again thanks a lot!!