From 7279710137e1fc57e48d3d819f987de6099b1818 Mon Sep 17 00:00:00 2001 From: Jed Smith Date: Sun, 11 Apr 2021 16:32:10 -0400 Subject: [PATCH] change to parabolic compression function. simplify. add presets for different camera source gamuts to nuke nodes. --- GamutCompress.blink | 107 +++++----------- GamutCompress.dctl | 134 +++++++------------- GamutCompress.fuse | 210 +++++++++++-------------------- GamutCompress.glsl | 100 +++++---------- GamutCompress.nk | 276 +++++++++++++---------------------------- GamutCompress.xml | 42 +------ GamutCompress_blink.nk | 52 ++++---- 7 files changed, 286 insertions(+), 635 deletions(-) diff --git a/GamutCompress.blink b/GamutCompress.blink index 8e6703b..b16c6cc 100644 --- a/GamutCompress.blink +++ b/GamutCompress.blink @@ -1,95 +1,46 @@ +/* GamutCompress v0.7 + Written by Jed Smith with lots of help from the ACES Gamut Mapping VWG + https://github.com/jedypod + https://community.acescentral.com/t/rgb-saturation-gamut-mapping-approach-and-a-comp-vfx-perspective + https://community.acescentral.com/c/aces-development-acesnext/vwg-aces-gamut-mapping-working-group +*/ kernel GamutCompression : public ImageComputationKernel { Image src; Image dst; param: - float3 threshold; - float p; - float shd_rolloff; - float cyan; - float magenta; - float yellow; + float3 th; + float3 dl; bool invert; local: - float3 thr; - float3 lim; + float3 s; void init() { - // thr is the percentage of the core gamut to protect: the complement of threshold. - thr = float3(1.0f-threshold.x, 1.0f-threshold.y, 1.0f-threshold.z); - - // lim is the distance beyond the gamut boundary that will be compressed to the gamut boundary. - // lim = 0.2 will compress from a distance of 1.2 from achromatic to 1.0 (the gamut boundary). - lim = float3(cyan+1.0f, magenta+1.0f, yellow+1.0f); - } - - // calculate hyperbolic tangent - float tanh( float in) { - float f = exp(2.0f*in); - return (f-1.0f)/(f+1.0f); - } - - // calculate compressed distance - float compress(float x, float l, float t) { - float cdist; - // power(p) compression function plot https://www.desmos.com/calculator/54aytu7hek - // suggested by James Eggleton https://community.acescentral.com/t/gamut-mapping-compression-curves/3073/10 - float s = (l-t)/pow(pow((1.0f-t)/(l-t),-p)-1.0f,1.0f/p); // calc y=1 intersect - if (l < 1.0001) { - return x; // disable compression, avoid nan - } - if (x < t) { - cdist = x; - } else { - if (invert == 0) { - cdist = t+s*((x-t)/s)/(pow(1.0f+pow((x-t)/s,p),1.0f/p)); // compress - } else { - if (x > (t + s)) { - cdist = x; // avoid singularity - } - cdist = t+s*pow(-(pow((x-t)/s,p)/(pow((x-t)/s,p)-1.0f)),1.0f/p); // uncompress - } - } - return cdist; + // Pre-calculate scale so compression function passes through distance limit: (x=dl, y=1) + s = ((float3)(1.0f)-th)/sqrt(max((float3)(1.001f),dl)-(float3)(1.0f)); } void process() { - // source pixels - SampleType(src) rgba = src(); - float3 rgb = float3(rgba.x, rgba.y, rgba.z); - - // achromatic axis - float ach = max(rgb.x, max(rgb.y, rgb.z)); - - // achromatic shadow rolloff - float ach_shd; - if (shd_rolloff < 0.004f) { - // disable shadow rolloff functionality. - // values below 0.004 cause strange behavior, actually increasing distance in some cases. - // if ach < 0.0 and shd_rolloff is disabled, take absolute value. This preserves negative components after compression. - ach_shd = fabs(ach); + float3 rgb = float3(src().x, src().y, src().z); + + float ac = max(rgb.x, max(rgb.y, rgb.z)); // Achromatic axis + // Inverse RGB Ratios: distance from achromatic axis + float3 d = ac == 0.0f ? (float3)(0.0f) : (ac-rgb)/fabs(ac); + + float3 cd; // Compressed distance + // Parabolic compression function: https://www.desmos.com/calculator/nvhp63hmtj + if (!invert) { + cd.x = d.x (t + s)) { - cdist = x; // avoid singularity - } - cdist = t+s*_powf(-(_powf((x-t)/s,p)/(_powf((x-t)/s,p)-1.0f)),1.0f/p); // uncompress - } - } - return cdist; -} - __DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B) { - // source pixels float3 rgb = make_float3(p_R, p_G, p_B); - // working colorspace to linear + // Linearize working colorspace if (working_colorspace == acescct) { rgb.x = acescct_to_lin(rgb.x); rgb.y = acescct_to_lin(rgb.y); @@ -91,69 +64,50 @@ __DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p rgb.z = acescc_to_lin(rgb.z); } - // thr is the percentage of the core gamut to protect: the complement of threshold. - float3 thr = make_float3( - 1.0f-_fmaxf(0.00001, threshold_c), - 1.0f-_fmaxf(0.00001, threshold_m), - 1.0f-_fmaxf(0.00001, threshold_y)); - - // lim is the distance beyond the gamut boundary that will be compressed to the gamut boundary. - // lim = 0.2 will compress from a distance of 1.2 from achromatic to 1.0 (the gamut boundary). - float3 lim; - lim = make_float3(cyan+1.0f, magenta+1.0f, yellow+1.0f); - - // achromatic axis - float ach = _fmaxf(rgb.x, _fmaxf(rgb.y, rgb.z)); - - // achromatic shadow rolloff - float ach_shd; - if (shd_rolloff < 0.004f) { - // disable shadow rolloff functionality. - // values below 0.004 cause strange behavior, actually increasing distance in some cases. - // if ach < 0.0 and shd_rolloff is disabled, take absolute value. This preserves negative components after compression. - ach_shd = _fabs(ach); - } else { - // lift ach below threshold using a tanh compression function. - // this reduces large distance values in shadow grain, which can cause differences when inverting. - ach_shd = 1.0f-((1.0f-ach)<(1.0f-shd_rolloff)?(1.0f-ach):(1.0f-shd_rolloff)+shd_rolloff*_tanhf((((1.0f-ach)-(1.0f-shd_rolloff))/shd_rolloff))); - } + // Amount of outer gamut to affect + float3 th = 1.0f-make_float3(th_c, th_m, th_y); + + // Distance limit: How far beyond the gamut boundary to compress + float3 dl = 1.0f+make_float3(d_c, d_m, d_y); - // distance from the achromatic axis for each color component aka inverse rgb ratios - // distance is normalized by achromatic, so that 1.0f is at gamut boundary. avoid 0 div - float3 dist; - dist.x = ach_shd == 0.0f ? 0.0f : (ach-rgb.x)/ach_shd; - dist.y = ach_shd == 0.0f ? 0.0f : (ach-rgb.y)/ach_shd; - dist.z = ach_shd == 0.0f ? 0.0f : (ach-rgb.z)/ach_shd; + // Calculate scale so compression function passes through distance limit: (x=dl, y=1) + float3 s; + s.x = (1.0f-th.x)/_sqrtf(_fmaxf(1.001f, dl.x)-1.0f); + s.y = (1.0f-th.y)/_sqrtf(_fmaxf(1.001f, dl.y)-1.0f); + s.z = (1.0f-th.z)/_sqrtf(_fmaxf(1.001f, dl.z)-1.0f); + + // Achromatic axis + float ac = _fmaxf(rgb.x, _fmaxf(rgb.y, rgb.z)); - // compress distance with user controlled parameterized shaper function - float3 cdist = make_float3( - compress(dist.x, lim.x, thr.x, power, invert), - compress(dist.y, lim.y, thr.y, power, invert), - compress(dist.z, lim.z, thr.z, power, invert)); + // Inverse RGB Ratios: distance from achromatic axis + float3 d = ac == 0.0f ? make_float3(0.0f) : (ac-rgb)/_fabs(ac); - // recalculate rgb from compressed distance and achromatic - // effectively this scales each color component relative to achromatic axis by the compressed distance - float3 crgb = make_float3( - ach-cdist.x*ach_shd, - ach-cdist.y*ach_shd, - ach-cdist.z*ach_shd); + float3 cd; // Compressed distance + // Parabolic compression function: https://www.desmos.com/calculator/nvhp63hmtj + if (invert == 0) { + cd.x = d.x (t + s)) { - cdist = x; // avoid singularity - } - cdist = t+s*_powf(-(_powf((x-t)/s,p)/(_powf((x-t)/s,p)-1.0f)),1.0f/p); // uncompress - } - } - return cdist; - } - __KERNEL__ void SolidKernel(__CONSTANTREF__ SolidParams *params, __TEXTURE2D__ src, __TEXTURE2D_WRITE__ dst) - { - DEFINE_KERNEL_ITERATORS_XY(x, y); - float4 rgb = _tex2DVecN(src, x, y, params->srcCompOrder); +__KERNEL__ void SolidKernel(__CONSTANTREF__ SolidParams *params, __TEXTURE2D__ src, __TEXTURE2D_WRITE__ dst) +{ + DEFINE_KERNEL_ITERATORS_XY(x, y); + float4 rgba = _tex2DVecN(src, x, y, params->srcCompOrder); + float3 rgb = make_float3(rgba.x, rgba.y, rgba.z); - // thr is the percentage of the core gamut to protect: the complement of threshold. - float3 thr = make_float3( - 1.0f-_fmaxf(0.0001f, params->threshold_c), - 1.0f-_fmaxf(0.0001f, params->threshold_m), - 1.0f-_fmaxf(0.0001f, params->threshold_y)); - - // lim is the max distance from the gamut boundary that will be compressed - // 0 is a no-op, 1 will compress colors from a distance of 2.0 from achromatic to the gamut boundary - // if method is Reinhard, use the limit as-is - float3 lim; - lim = make_float3(params->cyan+1.0f, params->magenta+1.0f, params->yellow+1.0f); - - // achromatic axis - float ach = _fmaxf(rgb.x, _fmaxf(rgb.y, rgb.z)); - - // achromatic shadow rolloff - float ach_shd; - if (params->shd_rolloff < 0.004f) { - // disable shadow rolloff functionality. - // values below 0.004 cause strange behavior, actually increasing distance in some cases. - // if ach < 0.0 and shd_rolloff is disabled, take absolute value. This preserves negative components after compression. - ach_shd = _fabs(ach); - } else { - // lift ach below threshold using a tanh compression function. - // this reduces large distance values in shadow grain, which can cause differences when inverting. - ach_shd = 1.0f-((1.0f-ach)<(1.0f-params->shd_rolloff)?(1.0f-ach):(1.0f-params->shd_rolloff)+params->shd_rolloff*_tanhf((((1.0f-ach)-(1.0f-params->shd_rolloff))/params->shd_rolloff))); - } - - // distance from the achromatic axis for each color component aka inverse rgb ratios - float3 dist; - dist.x = ach_shd == 0.0f ? 0.0f : (ach-rgb.x)/ach_shd; - dist.y = ach_shd == 0.0f ? 0.0f : (ach-rgb.y)/ach_shd; - dist.z = ach_shd == 0.0f ? 0.0f : (ach-rgb.z)/ach_shd; - - // compress distance with user controlled parameterized shaper function - float3 cdist = make_float3( - compress(dist.x, lim.x, thr.x, params->power, params->invert), - compress(dist.y, lim.y, thr.y, params->power, params->invert), - compress(dist.z, lim.z, thr.z, params->power, params->invert)); - - // recalculate rgb from compressed distance and achromatic - // effectively this scales each color component relative to achromatic axis by the compressed distance - float4 crgb = make_float4( - ach-cdist.x*ach_shd, - ach-cdist.y*ach_shd, - ach-cdist.z*ach_shd, - rgb.w); - - _tex2DVec4Write(dst, x, y, crgb); + // Amount of outer gamut to affect + float3 th = 1.0f-make_float3(params->th_c, params->th_m, params->th_y); + + // Distance limit: How far beyond the gamut boundary to compress + float3 dl = 1.0f+make_float3(params->d_c, params->d_m, params->d_y); + + // Calculate scale so compression function passes through distance limit: (x=dl, y=1) + float3 s; + s.x = (1.0f-th.x)/_sqrtf(_fmaxf(1.001f, dl.x)-1.0f); + s.y = (1.0f-th.y)/_sqrtf(_fmaxf(1.001f, dl.y)-1.0f); + s.z = (1.0f-th.z)/_sqrtf(_fmaxf(1.001f, dl.z)-1.0f); + + // Achromatic axis + float ac = _fmaxf(rgb.x, _fmaxf(rgb.y, rgb.z)); + + // Inverse RGB Ratios: distance from achromatic axis + float3 d = ac == 0.0f ? make_float3(0.0f) : (ac-rgb)/_fabs(ac); + + float3 cd; // Compressed distance + // Parabolic compression function: https://www.desmos.com/calculator/nvhp63hmtj + if (params->invert == 0) { + cd.x = d.x (t + s)) { - cdist = x; // avoid singularity - } - cdist = t+s*pow(-(pow((x-t)/s,p)/(pow((x-t)/s,p)-1.0)),1.0/p); // uncompress - } - } - return cdist; -} - void main() { vec2 coords = gl_FragCoord.xy / vec2( adsk_result_w, adsk_result_h ); // source pixels @@ -81,7 +57,7 @@ void main() { float alpha = texture2D(matteTex, coords).g; float select = texture2D(selectiveTex, coords).g; - // Working colorspace to linear + // Linearize working colorspace if (working_colorspace == 1) { rgb.x = acescct_to_lin(rgb.x); rgb.y = acescct_to_lin(rgb.y); @@ -92,52 +68,38 @@ void main() { rgb.z = acescc_to_lin(rgb.z); } - // thr is the percentage of the core gamut to protect: the complement of threshold. - vec3 thr = vec3( - 1.0-max(0.00001, threshold.x), - 1.0-max(0.00001, threshold.y), - 1.0-max(0.00001, threshold.z)); + // Amount of outer gamut to affect + vec3 th = 1.0-threshold; - // lim is the distance beyond the gamut boundary that will be compressed to the gamut boundary. - // lim = 0.2 will compress from a distance of 1.2 from achromatic to 1.0 (the gamut boundary). - vec3 lim; - lim = vec3(cyan+1.0, magenta+1.0, yellow+1.0); - - // achromatic axis - float ach = max(rgb.x, max(rgb.y, rgb.z)); - - // achromatic shadow rolloff - float ach_shd; - if (shd_rolloff < 0.004) { - // disable shadow rolloff functionality. - // values below 0.004 cause strange behavior, actually increasing distance in some cases. - // if ach < 0.0 and shd_rolloff is disabled, take absolute value. This preserves negative components after compression. - ach_shd = abs(ach); + // Distance limit: How far beyond the gamut boundary to compress + vec3 dl = 1.0+vec3(cyan, magenta, yellow); + + // Calculate scale so compression function passes through distance limit: (x=dl, y=1) + vec3 s; + s.x = (1.0-th.x)/sqrt(max(1.001, dl.x)-1.0); + s.y = (1.0-th.y)/sqrt(max(1.001, dl.y)-1.0); + s.z = (1.0-th.z)/sqrt(max(1.001, dl.z)-1.0); + + // Achromatic axis + float ac = max(rgb.x, max(rgb.y, rgb.z)); + + // Inverse RGB Ratios: distance from achromatic axis + vec3 d = ac==0.0?vec3(0.0):(ac-rgb)/abs(ac); + + vec3 cd; // Compressed distance + // Parabolic compression function: https://www.desmos.com/calculator/nvhp63hmtj + if (!invert) { + cd.x = d.xmax distance limit"} - addUserKnob {41 cyan t "Maximum distance beyond the green-blue gamut boundary to compress to the gamut boundary." T compress.cyan} - addUserKnob {41 magenta t "Maximum distance beyond the blue-red gamut boundary to compress to the gamut boundary." T compress.magenta} - addUserKnob {41 yellow t "Maximum distance beyond the red-green gamut boundary to compress to the gamut boundary." T compress.yellow} - addUserKnob {22 reset t "Reset knobs to default values. Distance limits are calculated based on an the average of a selection of digital cinema cameras." T "n = nuke.thisNode()\nnuke.root().begin()\ndefaults = \{\n 'threshold': 0.2,\n 'power': 1.2,\n 'shd_rolloff': 0.0,\n 'cyan': 0.09,\n 'magenta':0.24,\n 'yellow': 0.12,\n\}\nfor k, v in defaults.items():\n n\[k].setValue(v)" +STARTLINE} - addUserKnob {26 ""} + addUserKnob {18 threshold t "Percentage of the outer gamut boundary to affect. A value of 0.2 means 20% of the outer gamut will be utilized for gamut compression." R 0 0.2} + threshold 0.15 + addUserKnob {6 threshold_panelDropped l "panel dropped state" -STARTLINE +HIDDEN} + addUserKnob {35 distance_presets l distance t "choose distance presets for common digital cinema camera source gamuts" M {"presets/ACEScg from Filmlight E-Gamut" "knobs this \{cyan 0.122 magenta 0.201 yellow 0.139\}" "presets/ACEScg from Alexa Wide Gamut" "knobs this \{cyan 0.078 magenta 0.22 yellow 0.056\}" "presets/ACEScg from RedWideGamutRGB" "knobs this \{cyan 0.143 magenta 0.239 yellow 0.316\}" "presets/ACEScg from Sony S-Gamut3.Cine" "knobs this \{cyan 0.074 magenta 0.264 yellow 0.049\}" "presets/ACEScg from Sony S-Gamut3" "knobs this \{cyan 0.089 magenta 0.182 yellow 0.004\}" "presets/ACEScg from Panasonic V-Gamut" "knobs this \{cyan 0.06 magenta 0.148 yellow 0.012\}"}} + addUserKnob {7 cyan t "Maximum distance beyond the green-blue gamut boundary to compress to the gamut boundary." R 0 0.4} + cyan 0.1 + addUserKnob {7 magenta t "Maximum distance beyond the blue-red gamut boundary to compress to the gamut boundary." R 0 0.4} + magenta 0.2 + addUserKnob {7 yellow t "Maximum distance beyond the red-green gamut boundary to compress to the gamut boundary." R 0 0.4} + yellow 0.1 addUserKnob {4 direction M {forward inverse}} addUserKnob {20 info_tab l Info} - addUserKnob {26 info_label l " " T "\n\nGamutCompress
\nmaps out of gamut colors back into gamut.\n

Documentation"} + addUserKnob {26 info_label l " " T "\n\nGamutCompress v0.7
\nmaps out of gamut colors back into gamut.\n

Documentation"} addUserKnob {26 about_label l " " T "\n
\nAbout
\nWritten by Jed Smith
with help from the ACES Gamut Mapping VWG"} } Input { @@ -25,212 +25,102 @@ Group { ypos -298 } Dot { - name Dot4 + name Dot5 xpos -6 - ypos -198 - } -set N358f7310 [stack 0] - Dot { - name Dot2 - xpos -226 - ypos -198 + ypos -186 } +set N50366260 [stack 0] Expression { - channel0 {rgba.red rgba.green rgba.blue none} + channel0 rgba expr0 max(r,g,b) - name achromatic + channel1 none + channel2 none + channel3 none + name Norm_MaxRGB xpos -260 - ypos -154 + ypos -189 } -set N420bd290 [stack 0] Dot { - name Dot1 + name Dot6 xpos -226 - ypos -102 + ypos -126 } -set N354f7b20 [stack 0] +set N4b7ba180 [stack 0] Dot { - name Dot3 + name Dot8 xpos -226 - ypos 66 + ypos 54 } -push $N354f7b20 +push $N4b7ba180 +push $N50366260 + MergeExpression { + inputs 2 + expr0 Ar==0?0:(Ar-Br)/fabs(Ar) + expr1 Ag==0?0:(Ag-Bg)/fabs(Ag) + expr2 Ab==0?0:(Ab-Bb)/fabs(Ab) + name RGB_to_InverseRGBRatios + xpos -40 + ypos -129 + } + Dot { + name Dot9 + xpos -6 + ypos -96 + } +set N4b7ae1c0 [stack 0] Expression { - temp_name0 c_r - temp_expr0 1-r - temp_name1 c_g - temp_expr1 1-g - temp_name2 c_b - temp_expr2 1-b - expr0 1-(c_r<(1-thr)?c_r:(1-thr)+thr*tanh(((c_r-(1-thr))/thr))) - expr1 1-(c_g<(1-thr)?c_g:(1-thr)+thr*tanh(((c_g-(1-thr))/thr))) - expr2 1-(c_b<(1-thr)?c_b:(1-thr)+thr*tanh(((c_b-(1-thr))/thr))) - name toe + expr0 r(thr_x+s_x)?r:thr_x+s_x*pow(-(pow((r-thr_x)/s_x,p)/(pow((r-thr_x)/s_x,p)-1)),1/p) - expr1 g(thr_y+s_y)?g:thr_y+s_y*pow(-(pow((g-thr_y)/s_y,p)/(pow((g-thr_y)/s_y,p)-1)),1/p) - expr2 b(thr_z+s_z)?b:thr_z+s_z*pow(-(pow((b-thr_z)/s_z,p)/(pow((b-thr_z)/s_z,p)-1)),1/p) - name uncompress - xpos -150 - ypos -202 - addUserKnob {20 Params_tab l Params} - addUserKnob {7 thr_x} - thr_x {{1-parent.threshold.r}} - addUserKnob {7 thr_y} - thr_y {{1-parent.threshold.g}} - addUserKnob {7 thr_z} - thr_z {{1-parent.threshold.b}} - addUserKnob {7 lim_x} - lim_x {{parent.cyan+1}} - addUserKnob {7 lim_y} - lim_y {{parent.magenta+1}} - addUserKnob {7 lim_z} - lim_z {{parent.yellow+1}} - addUserKnob {7 p R 1 5} - p {{parent.power}} - } -push $N326b77d0 - Expression { - temp_name0 s_x - temp_expr0 (lim_x-thr_x)/pow(pow((1-thr_x)/(lim_x-thr_x),-p)-1,1/p) - temp_name1 s_y - temp_expr1 (lim_y-thr_y)/pow(pow((1-thr_y)/(lim_y-thr_y),-p)-1,1/p) - temp_name2 s_z - temp_expr2 (lim_z-thr_z)/pow(pow((1-thr_z)/(lim_z-thr_z),-p)-1,1/p) - expr0 r - + - + - + - - - - - - - diff --git a/GamutCompress_blink.nk b/GamutCompress_blink.nk index 018b572..9efa856 100644 --- a/GamutCompress_blink.nk +++ b/GamutCompress_blink.nk @@ -5,62 +5,52 @@ Group { addUserKnob {20 GamutCompress} addUserKnob {6 use_gpu l "use gpu" t "use gpu for blinkscript node" -STARTLINE} use_gpu true - addUserKnob {26 "" +STARTLINE} - addUserKnob {18 threshold t "Percentage of the outer gamut boundary to affect. A value of 0.2 means 20% of the outer gamut will be utilized for gamut compression." R 0 0.6} - threshold 0.2 - addUserKnob {6 threshold_panelDropped l "panel dropped state" -STARTLINE +HIDDEN} - addUserKnob {7 power t "Adjust the exponent of the compression function. 1 = Reinhard. Higher values have C2 continuity and result in a more \"aggressive\" curve which preserves more color purity or saturation.\n\nBe wary of high values if accurate inversion is important to you." R 1 4} - power 1.2 - addUserKnob {7 shd_rolloff l "shd rolloff" t "Shadow rolloff reduces the gamut compression effect below the specified pixel value. This reduces invertability issues with negative pixels in shadow grain. Make sure there is only shadow grain below the threshold you specify." R 0 0.03} - shd_rolloff 0 - addUserKnob {26 distance_limit_label l " " t "Specifies the maximum distance beyond the gamut boundary to map to the gamut boundary for each color component." T "max distance limit"} - addUserKnob {7 cyan t "Maximum distance beyond the green-blue gamut boundary to compress to the gamut boundary." R 0.001 1} - cyan 0.09 - addUserKnob {7 magenta t "Maximum distance beyond the blue-red gamut boundary to compress to the gamut boundary." R 0.001 1} - magenta 0.24 - addUserKnob {7 yellow t "Maximum distance beyond the red-green gamut boundary to compress to the gamut boundary." R 0.001 1} - yellow 0.12 - addUserKnob {22 reset t "Reset knobs to default values. Distance limits are calculated based on an the average of a selection of digital cinema cameras." T "n = nuke.thisNode()\nnuke.root().begin()\ndefaults = \{\n 'threshold': 0.2,\n 'power': 1.2,\n 'shd_rolloff': 0.0,\n 'cyan': 0.09,\n 'magenta':0.24,\n 'yellow': 0.12,\n\}\nfor k, v in defaults.items():\n n\[k].setValue(v)" +STARTLINE} addUserKnob {26 ""} + addUserKnob {18 threshold t "Percentage of the outer gamut boundary to affect. A value of 0.2 means 20% of the outer gamut will be utilized for gamut compression." R 0 0.2} + threshold 0.15 + addUserKnob {6 threshold_panelDropped l "panel dropped state" -STARTLINE +HIDDEN} + addUserKnob {35 distance_presets l distance t "choose distance presets for common digital cinema camera source gamuts" M {"presets/ACEScg from Filmlight E-Gamut" "knobs this \{cyan 0.122 magenta 0.201 yellow 0.139\}" "presets/ACEScg from Alexa Wide Gamut" "knobs this \{cyan 0.078 magenta 0.22 yellow 0.056\}" "presets/ACEScg from RedWideGamutRGB" "knobs this \{cyan 0.143 magenta 0.239 yellow 0.316\}" "presets/ACEScg from Sony S-Gamut3.Cine" "knobs this \{cyan 0.074 magenta 0.264 yellow 0.049\}" "presets/ACEScg from Sony S-Gamut3" "knobs this \{cyan 0.089 magenta 0.182 yellow 0.004\}" "presets/ACEScg from Panasonic V-Gamut" "knobs this \{cyan 0.06 magenta 0.148 yellow 0.012\}"}} + addUserKnob {7 cyan t "Maximum distance beyond the green-blue gamut boundary to compress to the gamut boundary." R 0 0.4} + cyan 0.1 + addUserKnob {7 magenta t "Maximum distance beyond the blue-red gamut boundary to compress to the gamut boundary." R 0 0.4} + magenta 0.2 + addUserKnob {7 yellow t "Maximum distance beyond the red-green gamut boundary to compress to the gamut boundary." R 0 0.4} + yellow 0.1 addUserKnob {4 direction M {forward inverse}} addUserKnob {20 info_tab l Info} - addUserKnob {26 info_label l " " T "\n\nGamutCompress
\nmaps out of gamut colors back into gamut.\n

Documentation"} + addUserKnob {26 info_label l " " T "\n\nGamutCompress v0.7
\nmaps out of gamut colors back into gamut.\n

Documentation"} addUserKnob {26 about_label l " " T "\n
\nAbout
\nWritten by Jed Smith
with help from the ACES Gamut Mapping VWG"} } Input { inputs 0 name Input xpos -40 - ypos -34 + ypos -10 } AddChannels { name AddChannels xpos -40 - ypos 32 + ypos 44 } BlinkScript { - recompileCount 0 + recompileCount 1 ProgramGroup 1 - KernelDescription "2 \"GamutCompression\" iterate pixelWise 1fb8436bea38392fe899f6abe1c61df92c39765596a6c0f62126f80a936bab70 2 \"src\" Read Point \"dst\" Write Point 7 \"threshold\" Float 3 AAAAAAAAAAAAAAAAAAAAAA== \"p\" Float 1 AAAAAA== \"shd_rolloff\" Float 1 AAAAAA== \"cyan\" Float 1 AAAAAA== \"magenta\" Float 1 AAAAAA== \"yellow\" Float 1 AAAAAA== \"invert\" Bool 1 AA== 7 \"threshold\" 3 1 \"p\" 1 1 \"shd_rolloff\" 1 1 \"cyan\" 1 1 \"magenta\" 1 1 \"yellow\" 1 1 \"invert\" 1 1 2 \"thr\" Float 3 1 AAAAAAAAAAAAAAAAAAAAAA== \"lim\" Float 3 1 AAAAAAAAAAAAAAAAAAAAAA==" - kernelSource "kernel GamutCompression : public ImageComputationKernel \{\n Image src;\n Image dst;\n\n param:\n float3 threshold;\n float p;\n float shd_rolloff;\n float cyan;\n float magenta;\n float yellow;\n bool invert;\n\n local:\n float3 thr;\n float3 lim;\n\n void init() \{\n // thr is the percentage of the core gamut to protect: the complement of threshold.\n thr = float3(1.0f-threshold.x, 1.0f-threshold.y, 1.0f-threshold.z);\n\n // lim is the distance beyond the gamut boundary that will be compressed to the gamut boundary.\n // lim = 0.2 will compress from a distance of 1.2 from achromatic to 1.0 (the gamut boundary).\n lim = float3(cyan+1.0f, magenta+1.0f, yellow+1.0f);\n \}\n\n // calculate hyperbolic tangent\n float tanh( float in) \{\n float f = exp(2.0f*in);\n return (f-1.0f)/(f+1.0f);\n \}\n\n // calculate compressed distance\n float compress(float x, float l, float t) \{\n float cdist;\n // power(p) compression function plot https://www.desmos.com/calculator/54aytu7hek\n // suggested by James Eggleton https://community.acescentral.com/t/gamut-mapping-compression-curves/3073/10\n float s = (l-t)/pow(pow((1.0f-t)/(l-t),-p)-1.0f,1.0f/p); // calc y=1 intersect\n if (l < 1.0001) \{\n return x; // disable compression, avoid nan\n \}\n if (x < t) \{\n cdist = x;\n \} else \{\n if (invert == 0) \{\n cdist = t+s*((x-t)/s)/(pow(1.0f+pow((x-t)/s,p),1.0f/p)); // compress\n \} else \{\n if (x > (t + s)) \{\n cdist = x; // avoid singularity\n \}\n cdist = t+s*pow(-(pow((x-t)/s,p)/(pow((x-t)/s,p)-1.0f)),1.0f/p); // uncompress\n \}\n \}\n return cdist;\n \}\n\n void process() \{\n // source pixels\n SampleType(src) rgba = src();\n float3 rgb = float3(rgba.x, rgba.y, rgba.z);\n\n // achromatic axis \n float ach = max(rgb.x, max(rgb.y, rgb.z));\n\n // achromatic shadow rolloff\n float ach_shd;\n if (shd_rolloff < 0.004f) \{\n // disable shadow rolloff functionality. \n // values below 0.004 cause strange behavior, actually increasing distance in some cases.\n // if ach < 0.0 and shd_rolloff is disabled, take absolute value. This preserves negative components after compression.\n ach_shd = fabs(ach);\n \} else \{\n // lift ach below threshold using a tanh compression function. \n // this reduces large distance values in shadow grain, which can cause differences when inverting.\n ach_shd = 1.0f-((1.0f-ach)<(1.0f-shd_rolloff)?(1.0f-ach):(1.0f-shd_rolloff)+shd_rolloff*tanh((((1.0f-ach)-(1.0f-shd_rolloff))/shd_rolloff)));\n \} \n \n // distance from the achromatic axis for each color component aka inverse rgb ratios.\n // we normalize the distance by dividing by achromatic, so that 1.0 is at gamut boundary, avoid 0 division errors.\n float3 dist = ach_shd == 0.0f ? float3(0.0f, 0.0f, 0.0f) : (ach-rgb)/ach_shd;\n\n // compress distance with parameterized compression function\n float3 cdist = float3(\n compress(dist.x, lim.x, thr.x),\n compress(dist.y, lim.y, thr.y),\n compress(dist.z, lim.z, thr.z));\n \n // recalculate rgb from compressed distance and achromatic\n // effectively this scales each color component relative to achromatic axis by the compressed distance\n float3 crgb = ach-cdist*ach_shd;\n\n // write to output\n dst() = float4(crgb.x, crgb.y, crgb.z, rgba.w);\n \}\n\};" + KernelDescription "2 \"GamutCompression\" iterate pixelWise c0d43fd0eba9efe2ea0c3fb1b173b0116570ea6ec879ba3765d9333e4c27883a 2 \"src\" Read Point \"dst\" Write Point 3 \"th\" Float 3 AAAAAAAAAAAAAAAAAAAAAA== \"dl\" Float 3 AAAAAAAAAAAAAAAAAAAAAA== \"invert\" Bool 1 AA== 3 \"th\" 3 1 \"dl\" 3 1 \"invert\" 1 1 1 \"s\" Float 3 1 AAAAAAAAAAAAAAAAAAAAAA==" + kernelSource "kernel GamutCompression : public ImageComputationKernel \{\n Image src;\n Image dst;\n\n param:\n float3 th;\n float3 dl;\n bool invert;\n\n local:\n float3 s;\n\n void init() \{\n // Pre-calculate scale so compression function passes through distance limit: (x=dl, y=1)\n s = ((float3)(1.0f)-th)/sqrt(max((float3)(1.001f),dl)-(float3)(1.0f));\n \}\n\n void process() \{\n float3 rgb = float3(src().x, src().y, src().z);\n\n float ac = max(rgb.x, max(rgb.y, rgb.z)); // Achromatic axis\n // Inverse RGB Ratios: distance from achromatic axis\n float3 d = ac == 0.0f ? (float3)(0.0f) : (ac-rgb)/fabs(ac);\n\n float3 cd; // Compressed distance\n // Parabolic compression function: https://www.desmos.com/calculator/nvhp63hmtj\n if (!invert) \{\n cd.x = d.x