modules/lcms/src/cmscam97.c
author dbaron@dbaron.org
Wed, 15 Aug 2007 17:03:29 -0700
changeset 4701 5c27a0fafb0f6720de04309a79b80a0b0d81796c
parent 4506 0f1b7da7647bda9d9249298ab0c44d4f4cd22f02
permissions -rw-r--r--
Enable Linux stack walking code on Mac OS X. b=336517 r+a=bsmedberg

//
//  Little cms
//  Copyright (C) 1998-2007 Marti Maria
//
// Permission is hereby granted, free of charge, to any person obtaining 
// a copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software 
// is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


#include "lcms.h"


/*
typedef struct {
               double J;
               double C;
               double h;

               } cmsJCh, FAR* LPcmsJCh;


#define AVG_SURROUND_4     0
#define AVG_SURROUND       1
#define DIM_SURROUND       2
#define DARK_SURROUND      3
#define CUTSHEET_SURROUND  4


typedef struct {

              cmsCIEXYZ whitePoint;
              double    Yb;
              double    La;
              int       surround;
              double    D_value;

              } cmsViewingConditions, FAR* LPcmsViewingConditions;



LCMSAPI LCMSHANDLE LCMSEXPORT cmsCIECAM97sInit(LPcmsViewingConditions pVC);
LCMSAPI void   LCMSEXPORT cmsCIECAM97sDone(LCMSHANDLE hModel);
LCMSAPI void   LCMSEXPORT cmsCIECAM97sForward(LCMSHANDLE hModel, LPcmsCIEXYZ pIn, LPcmsJCh pOut);
LCMSAPI void   LCMSEXPORT cmsCIECAM97sReverse(LCMSHANDLE hModel, LPcmsJCh pIn,    LPcmsCIEXYZ pOut);

*/

// ---------- Implementation --------------------------------------------

// #define USE_CIECAM97s2  1

#ifdef USE_CIECAM97s2

#       define NOISE_CONSTANT   3.05              
#else
#       define NOISE_CONSTANT   2.05
#endif


/*
  The model input data are the adapting field luminance in cd/m2
  (normally taken to be 20% of the luminance of white in the adapting field),
  LA , the relative tristimulus values of the stimulus, XYZ, the relative
  tristimulus values of white in the same viewing conditions, Xw Yw Zw ,
  and the relative luminance of the background, Yb . Relative tristimulus
  values should be expressed on a scale from Y = 0 for a perfect black
  to Y = 100 for a perfect reflecting diffuser. Additionally, the
  parameters c, for the impact of surround, Nc , a chromatic induction factor,
  and F, a factor for degree of adaptation, must be selected according to the
  guidelines in table

  All CIE tristimulus values are obtained using the CIE 1931
  Standard Colorimetric Observer (2�).

*/

typedef struct {

    cmsCIEXYZ WP;
    int surround;
    int calculate_D;

    double  Yb;         // rel. luminance of background

    cmsCIEXYZ RefWhite;

    double La;    // The adapting field luminance in cd/m2

    double c;     // Impact of surround
    double Nc;    // Chromatic induction factor
    double Fll;   // Lightness contrast factor (Removed on rev 2)
    double F;     // Degree of adaptation


    double k;
    double Fl;

    double Nbb;  // The background and chromatic brightness induction factors.
    double Ncb;
    double z;    // base exponential nonlinearity
    double n;    // background induction factor
    double D;

    MAT3 MlamRigg;
    MAT3 MlamRigg_1;

    MAT3 Mhunt;
    MAT3 Mhunt_1;

    MAT3 Mhunt_x_MlamRigg_1;
    MAT3 MlamRigg_x_Mhunt_1;


    VEC3 RGB_subw;
    VEC3 RGB_subw_prime;

    double p;

    VEC3 RGB_subwc;

    VEC3 RGB_subaw_prime;
    double A_subw;
    double Q_subw;

    } cmsCIECAM97s,FAR *LPcmsCIECAM97s;



// Free model structure

LCMSAPI void LCMSEXPORT cmsCIECAM97sDone(LCMSHANDLE hModel)
{
    LPcmsCIECAM97s lpMod = (LPcmsCIECAM97s) (LPSTR) hModel;
    if (lpMod) _cmsFree(lpMod);
}

// Partial discounting for adaptation degree computation

static
double discount(double d, double chan)
{
    return (d * chan + 1 - d);
}


// This routine does model exponential nonlinearity on the short wavelenght
// sensitive channel. On CIECAM97s rev 2 this has been reverted to linear.

static
void FwAdaptationDegree(LPcmsCIECAM97s lpMod, LPVEC3 RGBc, LPVEC3 RGB)
{


#ifdef USE_CIECAM97s2
    RGBc->n[0] = RGB->n[0]* discount(lpMod->D, 100.0/lpMod->RGB_subw.n[0]);
    RGBc->n[1] = RGB->n[1]* discount(lpMod->D, 100.0/lpMod->RGB_subw.n[1]);
    RGBc->n[2] = RGB->n[2]* discount(lpMod->D, 100.0/lpMod->RGB_subw.n[2]);
#else

    RGBc->n[0] = RGB->n[0]* discount(lpMod->D, 1.0/lpMod->RGB_subw.n[0]);
    RGBc->n[1] = RGB->n[1]* discount(lpMod->D, 1.0/lpMod->RGB_subw.n[1]);

    RGBc->n[2] = pow(fabs(RGB->n[2]), lpMod ->p) * discount(lpMod->D, (1.0/pow(lpMod->RGB_subw.n[2], lpMod->p)));

    // If B happens to be negative, Then Bc is also set to be negative

    if (RGB->n[2] < 0)
           RGBc->n[2] = -RGBc->n[2];
#endif
}


static
void RvAdaptationDegree(LPcmsCIECAM97s lpMod, LPVEC3 RGBc, LPVEC3 RGB)
{


#ifdef USE_CIECAM97s2
    RGBc->n[0] = RGB->n[0]/discount(lpMod->D, 100.0/lpMod->RGB_subw.n[0]);
    RGBc->n[1] = RGB->n[1]/discount(lpMod->D, 100.0/lpMod->RGB_subw.n[1]);
    RGBc->n[2] = RGB->n[2]/discount(lpMod->D, 100.0/lpMod->RGB_subw.n[2]);
#else

    RGBc->n[0] = RGB->n[0]/discount(lpMod->D, 1.0/lpMod->RGB_subw.n[0]);
    RGBc->n[1] = RGB->n[1]/discount(lpMod->D, 1.0/lpMod->RGB_subw.n[1]);
    RGBc->n[2] = pow(fabs(RGB->n[2]), 1.0/lpMod->p)/pow(discount(lpMod->D, 1.0/pow(lpMod->RGB_subw.n[2], lpMod->p)), 1.0/lpMod->p);
    if (RGB->n[2] < 0)
           RGBc->n[2] = -RGBc->n[2];
#endif
}



static
void PostAdaptationConeResponses(LPcmsCIECAM97s lpMod, LPVEC3 RGBa_prime, LPVEC3 RGBprime)
{
     if (RGBprime->n[0]>=0.0) {

            RGBa_prime->n[0]=((40.0*pow(lpMod -> Fl * RGBprime->n[0]/100.0, 0.73))/(pow(lpMod -> Fl * RGBprime->n[0]/100.0, 0.73)+2))+1;
     }
     else
     {
            RGBa_prime->n[0]=((-40.0*pow((-lpMod -> Fl * RGBprime->n[0])/100.0, 0.73))/(pow((-lpMod -> Fl * RGBprime->n[0])/100.0, 0.73)+2))+1;
     }

     if (RGBprime->n[1]>=0.0)
     {
            RGBa_prime->n[1]=((40.0*pow(lpMod -> Fl * RGBprime->n[1]/100.0, 0.73))/(pow(lpMod -> Fl * RGBprime->n[1]/100.0, 0.73)+2))+1;
     }
     else
     {
            RGBa_prime->n[1]=((-40.0*pow((-lpMod -> Fl * RGBprime->n[1])/100.0, 0.73))/(pow((-lpMod -> Fl * RGBprime->n[1])/100.0, 0.73)+2))+1;
     }

     if (RGBprime->n[2]>=0.0)
     {
            RGBa_prime->n[2]=((40.0*pow(lpMod -> Fl * RGBprime->n[2]/100.0, 0.73))/(pow(lpMod -> Fl * RGBprime->n[2]/100.0, 0.73)+2))+1;
     }
     else
     {
            RGBa_prime->n[2]=((-40.0*pow((-lpMod -> Fl * RGBprime->n[2])/100.0, 0.73))/(pow((-lpMod -> Fl * RGBprime->n[2])/100.0, 0.73)+2))+1;
     }
}


// Compute hue quadrature, eccentricity factor, e

static
void ComputeHueQuadrature(double h, double* H, double* e)
{


#define IRED    0
#define IYELLOW 1
#define IGREEN  2
#define IBLUE   3

      double e_tab[] = {0.8, 0.7, 1.0, 1.2};
      double H_tab[] = {  0, 100, 200, 300};
      int p1, p2;
      double e1, e2, h1, h2;


       if (h >= 20.14 && h < 90.0) { // Red

                        p1 = IRED;
                        p2 = IYELLOW;
       }
       else
       if (h >= 90.0 && h < 164.25) { // Yellow

                        p1 = IYELLOW;
                        p2 = IGREEN;
       }
       else
       if (h >= 164.25 && h < 237.53) { // Green

                        p1 = IGREEN;
                        p2 = IBLUE;       }
       else {                         // Blue

                        p1 = IBLUE;
                        p2 = IRED;
       }

       e1 = e_tab[p1]; e2 = e_tab[p2];
       h1 = H_tab[p1]; h2 = H_tab[p2];



       *e = e1 + ((e2-e1)*(h-h1)/(h2 - h1));
       *H = h1 + (100. * (h - h1) / e1) / ((h - h1)/e1 + (h2 - h) / e2);

#undef IRED
#undef IYELLOW
#undef IGREEN
#undef IBLUE

}






LCMSAPI LCMSHANDLE LCMSEXPORT cmsCIECAM97sInit(LPcmsViewingConditions pVC)
{
    LPcmsCIECAM97s lpMod;
    VEC3 tmp;

    if((lpMod = (LPcmsCIECAM97s) _cmsMalloc(sizeof(cmsCIECAM97s))) == NULL) {
        return (LCMSHANDLE) NULL;
    }


    lpMod->WP.X = pVC->whitePoint.X;
    lpMod->WP.Y = pVC->whitePoint.Y;
    lpMod->WP.Z = pVC->whitePoint.Z;

    lpMod->Yb   = pVC->Yb;
    lpMod->La   = pVC->La;

    lpMod->surround = pVC->surround;

    lpMod->RefWhite.X = 100.0;
    lpMod->RefWhite.Y = 100.0;
    lpMod->RefWhite.Z = 100.0;

#ifdef USE_CIECAM97s2

    VEC3init(&lpMod->MlamRigg.v[0],  0.8562, 0.3372, -0.1934);
    VEC3init(&lpMod->MlamRigg.v[1], -0.8360, 1.8327,  0.0033);
    VEC3init(&lpMod->MlamRigg.v[2],  0.0357,-0.0469,  1.0112);

    VEC3init(&lpMod->MlamRigg_1.v[0], 0.9874, -0.1768, 0.1894);
    VEC3init(&lpMod->MlamRigg_1.v[1], 0.4504,  0.4649, 0.0846);
    VEC3init(&lpMod->MlamRigg_1.v[2],-0.0139,  0.0278, 0.9861);

#else
    // Bradford transform: Lam-Rigg cone responses
    VEC3init(&lpMod->MlamRigg.v[0],  0.8951,  0.2664, -0.1614);
    VEC3init(&lpMod->MlamRigg.v[1], -0.7502,  1.7135,  0.0367);
    VEC3init(&lpMod->MlamRigg.v[2],  0.0389, -0.0685,  1.0296);


    // Inverse of Lam-Rigg
    VEC3init(&lpMod->MlamRigg_1.v[0],  0.98699, -0.14705,  0.15996);
    VEC3init(&lpMod->MlamRigg_1.v[1],  0.43231,  0.51836,  0.04929);
    VEC3init(&lpMod->MlamRigg_1.v[2], -0.00853,  0.04004,  0.96849);

#endif

    // Hunt-Pointer-Estevez cone responses
    VEC3init(&lpMod->Mhunt.v[0],   0.38971,  0.68898, -0.07868);
    VEC3init(&lpMod->Mhunt.v[1],  -0.22981,  1.18340,  0.04641);
    VEC3init(&lpMod->Mhunt.v[2],   0.0,      0.0,      1.0);

    // Inverse of Hunt-Pointer-Estevez
    VEC3init(&lpMod->Mhunt_1.v[0],     1.91019, -1.11214, 0.20195);
    VEC3init(&lpMod->Mhunt_1.v[1],     0.37095,  0.62905, 0.0);
    VEC3init(&lpMod->Mhunt_1.v[2],     0.0,      0.0,     1.0);


    if (pVC->D_value == -1.0)
          lpMod->calculate_D = 1;
    else
    if (pVC->D_value == -2.0)
           lpMod->calculate_D = 2;
    else {
        lpMod->calculate_D = 0;
        lpMod->D = pVC->D_value;
    }

   // Table I (revised)

   switch (lpMod->surround) {

    case AVG_SURROUND_4:
       lpMod->F = 1.0;
       lpMod->c = 0.69;
       lpMod->Fll = 0.0;    // Not included on Rev 2
       lpMod->Nc = 1.0;
       break;
    case AVG_SURROUND:
       lpMod->F = 1.0;
       lpMod->c = 0.69;
       lpMod->Fll = 1.0;
       lpMod->Nc = 1.0;
       break;
    case DIM_SURROUND:
       lpMod->F = 0.99;
       lpMod->c = 0.59;
       lpMod->Fll = 1.0;
       lpMod->Nc = 0.95;
       break;
    case DARK_SURROUND:
       lpMod->F = 0.9;
       lpMod->c = 0.525;
       lpMod->Fll = 1.0;
       lpMod->Nc = 0.8;
       break;
    case CUTSHEET_SURROUND:
       lpMod->F = 0.9;
       lpMod->c = 0.41;
       lpMod->Fll = 1.0;
       lpMod->Nc = 0.8;
       break;
    default:
       lpMod->F = 1.0;
       lpMod->c = 0.69;
       lpMod->Fll = 1.0;
       lpMod->Nc = 1.0;
       break;
    }

    lpMod->k = 1 / (5 * lpMod->La  + 1);
    lpMod->Fl = lpMod->La * pow(lpMod->k, 4) + 0.1*pow(1 - pow(lpMod->k, 4), 2.0) * pow(5*lpMod->La, 1.0/3.0);

    if (lpMod->calculate_D > 0) {

       lpMod->D = lpMod->F * (1 - 1 / (1 + 2*pow(lpMod->La, 0.25) + pow(lpMod->La, 2)/300.0));
       if (lpMod->calculate_D > 1)
           lpMod->D = (lpMod->D + 1.0) / 2;
    }


    // RGB_subw = [MlamRigg][WP/YWp]
#ifdef USE_CIECAM97s2
    MAT3eval(&lpMod -> RGB_subw, &lpMod -> MlamRigg, (LPVEC3) &lpMod -> WP);
#else
    VEC3divK(&tmp, (LPVEC3) &lpMod -> WP, lpMod->WP.Y);
    MAT3eval(&lpMod -> RGB_subw, &lpMod -> MlamRigg, &tmp);
#endif



    MAT3per(&lpMod -> Mhunt_x_MlamRigg_1,   &lpMod -> Mhunt,   &lpMod->MlamRigg_1  );
    MAT3per(&lpMod -> MlamRigg_x_Mhunt_1,   &lpMod -> MlamRigg, &lpMod -> Mhunt_1  );

    // p is used on forward model
    lpMod->p = pow(lpMod->RGB_subw.n[2], 0.0834);

    FwAdaptationDegree(lpMod, &lpMod->RGB_subwc, &lpMod->RGB_subw);

#if USE_CIECAM97s2
    MAT3eval(&lpMod->RGB_subw_prime, &lpMod->Mhunt_x_MlamRigg_1, &lpMod -> RGB_subwc);
#else
    VEC3perK(&tmp, &lpMod -> RGB_subwc, lpMod->WP.Y);
    MAT3eval(&lpMod->RGB_subw_prime, &lpMod->Mhunt_x_MlamRigg_1, &tmp);
#endif

    lpMod->n = lpMod-> Yb / lpMod-> WP.Y;

    lpMod->z = 1 + lpMod->Fll * sqrt(lpMod->n);
    lpMod->Nbb = lpMod->Ncb = 0.725 / pow(lpMod->n, 0.2);

    PostAdaptationConeResponses(lpMod, &lpMod->RGB_subaw_prime, &lpMod->RGB_subw_prime);

    lpMod->A_subw=lpMod->Nbb*(2.0*lpMod->RGB_subaw_prime.n[0]+lpMod->RGB_subaw_prime.n[1]+lpMod->RGB_subaw_prime.n[2]/20.0-NOISE_CONSTANT);

    return (LCMSHANDLE) lpMod;
}




//
// The forward model: XYZ -> JCh
//

LCMSAPI void LCMSEXPORT cmsCIECAM97sForward(LCMSHANDLE hModel, LPcmsCIEXYZ inPtr, LPcmsJCh outPtr)
{

        LPcmsCIECAM97s lpMod = (LPcmsCIECAM97s) (LPSTR) hModel;
        double a, b, h, s, H1val, es, A;
        VEC3 In, RGB, RGBc, RGBprime, RGBa_prime;

        if (inPtr -> Y <= 0.0) {

      outPtr -> J = outPtr -> C = outPtr -> h = 0.0;
          return;
        }

       // An initial chromatic adaptation transform is used to go from the source
       // viewing conditions to corresponding colours under the equal-energy-illuminant
       // reference viewing conditions. This is handled differently on rev 2

       VEC3init(&In, inPtr -> X, inPtr -> Y, inPtr -> Z);    // 2.1

#ifdef USE_CIECAM97s2
       // Since the chromatic adaptation transform has been linearized, it
       // is no longer required to divide the stimulus tristimulus values
       // by their own Y tristimulus value prior to the chromatic adaptation.
#else
       VEC3divK(&In, &In, inPtr -> Y);
#endif

       MAT3eval(&RGB, &lpMod -> MlamRigg, &In);              // 2.2

       FwAdaptationDegree(lpMod, &RGBc, &RGB);

       // The post-adaptation signals for both the sample and the white are then
       // transformed from the sharpened cone responses to the Hunt-Pointer-Estevez
       // cone responses.
#ifdef USE_CIECAM97s2
#else
       VEC3perK(&RGBc, &RGBc, inPtr->Y);
#endif

       MAT3eval(&RGBprime, &lpMod->Mhunt_x_MlamRigg_1, &RGBc);

       // The post-adaptation cone responses (for both the stimulus and the white)
       // are then calculated.

       PostAdaptationConeResponses(lpMod, &RGBa_prime, &RGBprime);

       // Preliminary red-green and yellow-blue opponent dimensions are calculated

       a = RGBa_prime.n[0] - (12.0 * RGBa_prime.n[1] / 11.0) + RGBa_prime.n[2]/11.0;
       b = (RGBa_prime.n[0] + RGBa_prime.n[1] - 2.0 * RGBa_prime.n[2]) / 9.0;


       // The CIECAM97s hue angle, h, is then calculated
       h = (180.0/M_PI)*(atan2(b, a));


       while (h < 0)
              h += 360.0;

       outPtr->h = h;

       // hue quadrature and eccentricity factors, e, are calculated

       ComputeHueQuadrature(h, &H1val, &es);

       // ComputeHueQuadrature(h, &H1val, &h1, &e1, &h2, &e2, &es);


      // The achromatic response A
      A = lpMod->Nbb * (2.0 * RGBa_prime.n[0] + RGBa_prime.n[1] + RGBa_prime.n[2]/20.0 - NOISE_CONSTANT);

      // CIECAM97s Lightness J
      outPtr -> J = 100.0 * pow(A / lpMod->A_subw, lpMod->c * lpMod->z);

      // CIECAM97s saturation s
      s =  (50 * hypot (a, b) * 100 * es * (10.0/13.0) * lpMod-> Nc * lpMod->Ncb) / (RGBa_prime.n[0] + RGBa_prime.n[1] + 1.05 * RGBa_prime.n[2]);

      // CIECAM97s Chroma C

#ifdef USE_CIECAM97s2
      // Eq. 26 has been modified to allow accurate prediction of the Munsell chroma scales.
      outPtr->C = 0.7487 * pow(s, 0.973) * pow(outPtr->J/100.0, 0.945 * lpMod->n) * (1.64 - pow(0.29, lpMod->n));

#else
      outPtr->C = 2.44 * pow(s, 0.69) * pow(outPtr->J/100.0, 0.67 * lpMod->n) * (1.64 - pow(0.29, lpMod->n));
#endif
}


//
// The reverse model JCh -> XYZ
//


LCMSAPI void LCMSEXPORT cmsCIECAM97sReverse(LCMSHANDLE hModel, LPcmsJCh inPtr, LPcmsCIEXYZ outPtr)
{
    LPcmsCIECAM97s lpMod = (LPcmsCIECAM97s) (LPSTR) hModel;
    double J, C, h, A, H1val, es, s, a, b;
    double tan_h, sec_h;
    double R_suba_prime, G_suba_prime, B_suba_prime;
    double R_prime, G_prime, B_prime;
    double Y_subc, Y_prime, B_term;
    VEC3 tmp;    
    VEC3 RGB_prime, RGB_subc_Y;
    VEC3 Y_over_Y_subc_RGB;
    VEC3 XYZ_primeprime_over_Y_subc;
#ifdef USE_CIECAM92s2
    VEC3 RGBY;
    VEC3 Out;
#endif

    J = inPtr->J;
    h = inPtr->h;
    C = inPtr->C;

    if (J <= 0) {

        outPtr->X =  0.0;
        outPtr->Y =  0.0;
        outPtr->Z =  0.0;
        return;
    }



    // (2) From J Obtain A

    A =  pow(J/100.0, 1/(lpMod->c * lpMod->z)) * lpMod->A_subw;


    // (3), (4), (5) Using H Determine h1, h2, e1, e2
    // e1 and h1 are the values  of e and h for the unique hue having the
    // nearest lower valur of h and e2 and h2 are the values of e and h for
    // the unique hue having the nearest higher value of h.


    ComputeHueQuadrature(h, &H1val, &es);
    
    // (7) Calculate s

    s = pow(C / (2.44 * pow(J/100.0, 0.67*lpMod->n) * (1.64 - pow(0.29, lpMod->n))) , (1./0.69));


    // (8) Calculate a and b.
    // NOTE: sqrt(1 + tan^2) == sec(h)

    tan_h = tan ((M_PI/180.)*(h));
    sec_h = sqrt(1 + tan_h * tan_h);

    if ((h > 90) && (h < 270))
            sec_h = -sec_h;

    a = s * ( A/lpMod->Nbb + NOISE_CONSTANT) / ( sec_h * 50000.0 * es * lpMod->Nc * lpMod->Ncb/ 13.0 +
           s * (11.0 / 23.0 + (108.0/23.0) * tan_h));

    b = a * tan_h;

    //(9) Calculate R'a G'a and B'a

    R_suba_prime = (20.0/61.0) * (A/lpMod->Nbb + NOISE_CONSTANT) + (41.0/61.0) * (11.0/23.0) * a + (288.0/61.0) / 23.0 * b;
    G_suba_prime = (20.0/61.0) * (A/lpMod->Nbb + NOISE_CONSTANT) - (81.0/61.0) * (11.0/23.0) * a - (261.0/61.0) / 23.0 * b;
    B_suba_prime = (20.0/61.0) * (A/lpMod->Nbb + NOISE_CONSTANT) - (20.0/61.0) * (11.0/23.0) * a - (20.0/61.0) * (315.0/23.0) * b;

    // (10) Calculate R', G' and B'

    if ((R_suba_prime - 1) < 0) {

         R_prime = -100.0 * pow((2.0 - 2.0 * R_suba_prime) /
                            (39.0 + R_suba_prime), 1.0/0.73);
    }
    else
    {
         R_prime = 100.0 * pow((2.0 * R_suba_prime - 2.0) /
                            (41.0 - R_suba_prime), 1.0/0.73);
    }

    if ((G_suba_prime - 1) < 0)
    {
         G_prime = -100.0 * pow((2.0 - 2.0 * G_suba_prime) /
                            (39.0 + G_suba_prime), 1.0/0.73);
    }
    else
    {
         G_prime = 100.0 * pow((2.0 * G_suba_prime - 2.0) /
                            (41.0 - G_suba_prime), 1.0/0.73);
    }

    if ((B_suba_prime - 1) < 0)
    {
         B_prime = -100.0 * pow((2.0 - 2.0 * B_suba_prime) /
                            (39.0 + B_suba_prime), 1.0/0.73);
    }
    else
    {
         B_prime = 100.0 * pow((2.0 * B_suba_prime - 2.0) /
                            (41.0 - B_suba_prime), 1.0/0.73);
    }


    // (11) Calculate RcY, GcY and BcY

    VEC3init(&RGB_prime, R_prime, G_prime, B_prime);
    VEC3divK(&tmp, &RGB_prime, lpMod -> Fl);

    MAT3eval(&RGB_subc_Y, &lpMod->MlamRigg_x_Mhunt_1, &tmp);




#ifdef USE_CIECAM97s2

       // (12)


           RvAdaptationDegree(lpMod, &RGBY, &RGB_subc_Y);
           MAT3eval(&Out, &lpMod->MlamRigg_1, &RGBY);

           outPtr -> X = Out.n[0];
           outPtr -> Y = Out.n[1];
           outPtr -> Z = Out.n[2];

#else

           // (12) Calculate Yc

       Y_subc = 0.43231*RGB_subc_Y.n[0]+0.51836*RGB_subc_Y.n[1]+0.04929*RGB_subc_Y.n[2];

           // (13) Calculate (Y/Yc)R, (Y/Yc)G and (Y/Yc)B

           VEC3divK(&RGB_subc_Y, &RGB_subc_Y, Y_subc);
           RvAdaptationDegree(lpMod, &Y_over_Y_subc_RGB, &RGB_subc_Y);

           // (14) Calculate Y'
       Y_prime = 0.43231*(Y_over_Y_subc_RGB.n[0]*Y_subc) + 0.51836*(Y_over_Y_subc_RGB.n[1]*Y_subc) + 0.04929 * (Y_over_Y_subc_RGB.n[2]*Y_subc);

           if (Y_prime < 0 || Y_subc < 0)
           {
                // Discard to near black point
               
                outPtr -> X = 0;
                outPtr -> Y = 0;
                outPtr -> Z = 0;
                return;
           }

       B_term = pow(Y_prime / Y_subc, (1.0 / lpMod->p) - 1);

          // (15) Calculate X'', Y'' and Z''
           Y_over_Y_subc_RGB.n[2] /= B_term;
           MAT3eval(&XYZ_primeprime_over_Y_subc, &lpMod->MlamRigg_1, &Y_over_Y_subc_RGB);

           outPtr->X =  XYZ_primeprime_over_Y_subc.n[0] * Y_subc;
           outPtr->Y =  XYZ_primeprime_over_Y_subc.n[1] * Y_subc;
           outPtr->Z =  XYZ_primeprime_over_Y_subc.n[2] * Y_subc;
#endif

}