UNPKG

empfindung

Version:

Library for common CIE color difference measurements

289 lines (250 loc) 10.2 kB
/** * @Author: Lim Mingjie, Kenneth <Astrianna> * @Date: 2016-06-10T17:40:54-04:00 * @Email: me@kenlimmj.com * @Last modified time: 2016-06-20T18:09:22-04:00 * @License: MIT * @flow */ import { labToLch, validateColorInputs } from './colorUtils'; import { sind, cosd, atan2d } from './trigDegreeLib'; /** * Maps an application type to the corresponding $k_l$ value. * * For graphic arts, $k_l = 1$. For textiles, $k_l = 2$. * Any other values default to 'graphicArts', i.e. $k_l = 1$. * * @method getklValueFromType * @param {string} applicationType Either 'graphicArts' or 'textiles' * @return {number} The corresponding $k_l$ value */ export function getklValueFromType(applicationType: string): number { switch (applicationType) { case 'textiles': return 2; case 'graphicArts': default: return 1; } } export default class DeltaE { /** * From Wikipedia: * The 1976 formula is the first color-difference formula that related a measured * to a known set of CIELAB coordinates. This formula has been succeeded by the * 1994 and 2000 formulas because the CIELAB space turned out to be not as perceptually * uniform as intended, especially in the saturated regions. This means that this formula * rates these colors too highly as opposed to other colors. * * @method cie1976 * @param {Color} x The first color in L*a*b* space * @param {Color} y The second color in L*a*b* space * @return {number} The ∆E between the two colors */ @validateColorInputs(false) static cie1976([l1, a1, b1]: Color, [l2, a2, b2]: Color): number { // Compute Euclidean distance const lDiff: number = l2 - l1; const aDiff: number = a2 - a1; const bDiff: number = b2 - b1; return Math.sqrt(lDiff * lDiff + aDiff * aDiff + bDiff * bDiff); } /** * If `applicationType` is not specified, it defaults to 'graphicArts'. * * From Wikipedia: * The 1976 definition was extended to address perceptual non-uniformities, * while retaining the L*a*b* color space, by the introduction of application- * specific weights derived from an automotive paint test's tolerance data. * * @method cie1994 * @param {Color} x The reference color in L*a*b* space * @param {Color} y The sample color in L*a*b* space * @param {string} applicationType Weighing factors. Possible values: *'graphicArts'*, 'textiles'. * @return {number} The ∆E between the two colors */ @validateColorInputs(false) static cie1994([l1, a1, b1]: Color, [l2, a2, b2]: Color, applicationType: string = 'graphicArts'): number { const kl: number = getklValueFromType(applicationType); const kc: number = 1; const kh: number = 1; let k1: number; let k2: number; switch (applicationType) { case 'textiles': k1 = 0.048; k2 = 0.014; break; case 'graphicArts': default: k1 = 0.045; k2 = 0.015; break; } const c1: number = Math.sqrt(a1 * a1 + b1 * b1); const c2: number = Math.sqrt(a2 * a2 + b2 * b2); const deltaA: number = a1 - a2; const deltaB: number = b1 - b2; const deltaC: number = c1 - c2; const deltaH: number = Math.sqrt(deltaA * deltaA + deltaB * deltaB - deltaC * deltaC); const deltaL: number = l1 - l2; const sl: number = 1; const sc: number = 1 + k1 * c1; const sh: number = 1 + k2 * c2; const firstTerm: number = deltaL / (kl * sl); const secondTerm: number = deltaC / (kc * sc); const thirdTerm: number = deltaH / (kh * sh); return Math.sqrt(firstTerm * firstTerm + secondTerm * secondTerm + thirdTerm * thirdTerm); } /** * If `applicationType` is not specified, it defaults to 'graphicArts'. * * From Wikipedia: * Since the 1994 definition did not adequately resolve the perceptual * uniformity issue, the CIE refined their definition, adding five corrections: * * - A hue rotation term (R_T), to deal with the problematic * blue region (hue angles in the neighborhood of 275°) * - Compensation for neutral colors (the primed values in the L*C*h differences) * - Compensation for lightness (S_L) * - Compensation for chroma (S_C) * - Compensation for hue (S_H) * * @method ciede2000 * @param {Color} x The reference color in L*a*b* space * @param {Color} y The sample color in L*a*b* space * @return {number} The ∆E between the two colors */ @validateColorInputs(false) static ciede2000([l1, a1, b1]: Color, [l2, a2, b2]: Color): number { const [_1, c1, __1]: Color = labToLch([l1, a1, b1]); const [_2, c2, __2]: Color = labToLch([l2, a2, b2]); const deltaLPrime: number = l2 - l1; const lBar: number = (l1 + l2) / 2; const cBar: number = (c1 + c2) / 2; const cBarPow7: number = Math.pow(cBar, 7); const cCoeff: number = Math.sqrt(cBarPow7 / (cBarPow7 + Math.pow(25, 7))); const a1Prime: number = a1 + (a1 / 2) * (1 - cCoeff); const a2Prime: number = a2 + (a2 / 2) * (1 - cCoeff); const c1Prime: number = Math.sqrt(a1Prime * a1Prime + b1 * b1); const c2Prime: number = Math.sqrt(a2Prime * a2Prime + b2 * b2); const cBarPrime: number = (c1Prime + c2Prime) / 2; const deltaCPrime: number = c2Prime - c1Prime; let h1Prime: number; if (a1Prime === 0 && b1 === 0) { h1Prime = 0; } else { h1Prime = atan2d(b1, a1Prime) % 360; } let h2Prime: number; if (a2Prime === 0 && b2 === 0) { h2Prime = 0; } else { h2Prime = atan2d(b2, a2Prime) % 360; } let deltaSmallHPrime: number; let deltaBigHPrime: number; let hBarPrime: number; if (c1Prime === 0 || c2Prime === 0) { deltaSmallHPrime = 0; deltaBigHPrime = 0; hBarPrime = h1Prime + h2Prime; } else { if (Math.abs(h1Prime - h2Prime) <= 180) { deltaSmallHPrime = h2Prime - h1Prime; } else { if (h2Prime <= h1Prime) { deltaSmallHPrime = h2Prime - h1Prime + 360; } else { deltaSmallHPrime = h2Prime - h1Prime - 360; } } deltaBigHPrime = 2 * Math.sqrt(c1Prime * c2Prime) * sind(deltaSmallHPrime / 2); if (Math.abs(h1Prime - h2Prime) <= 180) { hBarPrime = (h1Prime + h2Prime) / 2; } else { if ((h1Prime + h2Prime) < 360) { hBarPrime = (h1Prime + h2Prime + 360) / 2; } else { hBarPrime = (h1Prime + h2Prime - 360) / 2; } } } const T: number = 1 - 0.17 * cosd(hBarPrime - 30) + 0.24 * cosd(2 * hBarPrime) + 0.32 * cosd(3 * hBarPrime + 6) - 0.2 * cosd(4 * hBarPrime - 63); const slCoeff: number = (lBar - 50) * (lBar - 50); const sl: number = 1 + (0.015 * slCoeff) / Math.sqrt(20 + slCoeff); const sc: number = 1 + 0.045 * cBarPrime; const sh: number = 1 + 0.015 * cBarPrime * T; const RtCoeff: number = 60 * Math.exp(-((hBarPrime - 275) / 25) * ((hBarPrime - 275) / 25)); const Rt: number = -2 * cCoeff * sind(RtCoeff); const kl: number = 1; const kc: number = 1; const kh: number = 1; const firstTerm: number = deltaLPrime / (kl * sl); const secondTerm: number = deltaCPrime / (kc * sc); const thirdTerm: number = deltaBigHPrime / (kh * sh); const fourthTerm: number = Rt * secondTerm * thirdTerm; return Math.sqrt(firstTerm * firstTerm + secondTerm * secondTerm + thirdTerm * thirdTerm + fourthTerm); } /** * If `thresholdType` is not specified, it defaults to 'acceptability'. * * From Wikipedia: * In 1984, the Colour Measurement Committee (CMC) of the Society of Dyers and * Colourists (SDC) defined a difference measure, also based on the L*C*h color model. * Named after the developing committee, their metric is called CMC l:c. * * The quasimetric has two threshold parameters: lightness (l) and chroma (c), allowing the * users to weight the difference based on the ratio of l:c that is deemed appropriate * for the application. * * Commonly used values are 2:1 for acceptability and 1:1 for the threshold of imperceptibility. * * @method cmc1984 * @param {Color} x The first color in L*a*b* space * @param {Color} y The second color in L*a*b* space * @param {string} thresholdType Quasimetric parameter weight (l:c ratio). * Possible values: *'acceptability'*, 'imperceptibility'. * @return {number} The ∆E between the two colors */ @validateColorInputs(false) static cmc1984([l1, a1, b1]: Color, [l2, a2, b2]: Color, thresholdType: string = 'acceptability'): number { let l: number; let c: number; switch (thresholdType) { case 'acceptability': l = 2; c = 1; break; case 'imperceptibility': default: l = 1; c = 1; break; } const [_1, c1, h1]: Color = labToLch([l1, a1, b1]); const [_2, c2, __2]: Color = labToLch([l2, a2, b2]); const c1Pow4: number = c1 * c1 * c1 * c1; const F: number = Math.sqrt(c1Pow4 / (c1Pow4 + 1900)); let T: number; if (h1 >= 164 && h1 <= 345) { T = 0.56 + Math.abs(0.2 * cosd(h1 + 168)); } else { T = 0.36 + Math.abs(0.4 * cosd(h1 + 35)); } const sl: number = l1 < 16 ? 0.511 : (0.040975 * l1) / (1 + 0.01765 * l1); const sc: number = ((0.0638 * c1) / (1 + 0.0131 * c1)) + 0.638; const sh: number = sc * (F * T + 1 - F); const deltaA: number = a1 - a2; const deltaB: number = b1 - b2; const deltaC: number = c1 - c2; const deltaH: number = Math.sqrt(deltaA * deltaA + deltaB * deltaB - deltaC * deltaC); const firstTerm: number = (l1 - l2) / (l * sl); const secondTerm: number = (c1 - c2) / (c * sc); const thirdTerm: number = deltaH / sh; return Math.sqrt(firstTerm * firstTerm + secondTerm * secondTerm + thirdTerm * thirdTerm); } }