1 /* ***** BEGIN LICENSE BLOCK *****
2 * This file is part of openfx-misc <https://github.com/devernay/openfx-misc>,
3 * Copyright (C) 2013-2018 INRIA
4 *
5 * openfx-misc is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * openfx-misc is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with openfx-misc. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>
17 * ***** END LICENSE BLOCK ***** */
18
19 /*
20 * OFX ColorCorrect plugin.
21 */
22
23 #include <cmath>
24 #include <algorithm>
25 #include <cfloat> // DBL_MAX
26
27 #include "ofxsProcessing.H"
28 #include "ofxsMaskMix.h"
29 #include "ofxsCoords.h"
30 #include "ofxsLut.h"
31 #include "ofxsMacros.h"
32 #include "ofxsThreadSuite.h"
33
34 using namespace OFX;
35
36 OFXS_NAMESPACE_ANONYMOUS_ENTER
37
38 #define kPluginName "ColorCorrectOFX"
39 #define kPluginGrouping "Color"
40 #define kPluginDescription "Adjusts the saturation, constrast, gamma, gain and offset of an image.\n" \
41 "The ranges of the shadows, midtones and highlights are controlled by the curves " \
42 "in the \"Ranges\" tab.\n" \
43 "The Contrast adjustment works using the formula: Output = (Input/0.18)^Contrast*0.18.\n" \
44 "\n" \
45 "See also:\n" \
46 "- http://opticalenquiry.com/nuke/index.php?title=ColorCorrect\n" \
47 "- https://compositormathematic.wordpress.com/2013/07/06/gamma-contrast/"
48
49 #define kPluginIdentifier "net.sf.openfx.ColorCorrectPlugin"
50 // History:
51 // version 1.0: initial version
52 // version 2.0: use kNatronOfxParamProcess* parameters
53 // version 2.1: add range params
54 #define kPluginVersionMajor 2 // Incrementing this number means that you have broken backwards compatibility of the plug-in.
55 #define kPluginVersionMinor 1 // Increment this when you have fixed a bug or made it faster.
56
57 #define kSupportsTiles 1
58 #define kSupportsMultiResolution 1
59 #define kSupportsRenderScale 1
60 #define kSupportsMultipleClipPARs false
61 #define kSupportsMultipleClipDepths false
62 #define kRenderThreadSafety eRenderFullySafe
63
64 ////std strings because we need them in changedParam
65 static const std::string kGroupMaster = std::string("Master");
66 static const std::string kGroupShadows = std::string("Shadows");
67 static const std::string kGroupMidtones = std::string("Midtones");
68 static const std::string kGroupHighlights = std::string("Highlights");
69 static const std::string kParamEnable = std::string("Enable");
70 static const std::string kParamSaturation = std::string("Saturation");
71 static const std::string kParamContrast = std::string("Contrast");
72 static const std::string kParamGamma = std::string("Gamma");
73 static const std::string kParamGain = std::string("Gain");
74 static const std::string kParamOffset = std::string("Offset");
75
76 #ifdef OFX_EXTENSIONS_NATRON
77 #define kParamProcessR kNatronOfxParamProcessR
78 #define kParamProcessRLabel kNatronOfxParamProcessRLabel
79 #define kParamProcessRHint kNatronOfxParamProcessRHint
80 #define kParamProcessG kNatronOfxParamProcessG
81 #define kParamProcessGLabel kNatronOfxParamProcessGLabel
82 #define kParamProcessGHint kNatronOfxParamProcessGHint
83 #define kParamProcessB kNatronOfxParamProcessB
84 #define kParamProcessBLabel kNatronOfxParamProcessBLabel
85 #define kParamProcessBHint kNatronOfxParamProcessBHint
86 #define kParamProcessA kNatronOfxParamProcessA
87 #define kParamProcessALabel kNatronOfxParamProcessALabel
88 #define kParamProcessAHint kNatronOfxParamProcessAHint
89 #else
90 #define kParamProcessR "processR"
91 #define kParamProcessRLabel "R"
92 #define kParamProcessRHint "Process red component."
93 #define kParamProcessG "processG"
94 #define kParamProcessGLabel "G"
95 #define kParamProcessGHint "Process green component."
96 #define kParamProcessB "processB"
97 #define kParamProcessBLabel "B"
98 #define kParamProcessBHint "Process blue component."
99 #define kParamProcessA "processA"
100 #define kParamProcessALabel "A"
101 #define kParamProcessAHint "Process alpha component."
102 #endif
103
104 #define kParamRange "range"
105 #define kParamRangeLabel "Range"
106 #define kParamRangeHint "Expected range for input values. Within this range, a lookup table is used for faster computation."
107
108 #define kParamColorCorrectToneRanges "toneRanges"
109 #define kParamColorCorrectToneRangesLabel "Tone Ranges"
110 #define kParamColorCorrectToneRangesHint "Tone ranges lookup table"
111 #define kParamColorCorrectToneRangesDim0 "Shadow"
112 #define kParamColorCorrectToneRangesDim1 "Highlight"
113
114 #define kParamLuminanceMath "luminanceMath"
115 #define kParamLuminanceMathLabel "Luminance Math"
116 #define kParamLuminanceMathHint "Formula used to compute luminance from RGB values (used for saturation adjustments)."
117 #define kParamLuminanceMathOptionRec709 "Rec. 709", "Use Rec. 709 (0.2126r + 0.7152g + 0.0722b).", "rec709"
118 #define kParamLuminanceMathOptionRec2020 "Rec. 2020", "Use Rec. 2020 (0.2627r + 0.6780g + 0.0593b).", "rec2020"
119 #define kParamLuminanceMathOptionACESAP0 "ACES AP0", "Use ACES AP0 (0.3439664498r + 0.7281660966g + -0.0721325464b).", "acesap0"
120 #define kParamLuminanceMathOptionACESAP1 "ACES AP1", "Use ACES AP1 (0.2722287168r + 0.6740817658g + 0.0536895174b).", "acesap1"
121 #define kParamLuminanceMathOptionCcir601 "CCIR 601", "Use CCIR 601 (0.2989r + 0.5866g + 0.1145b).", "ccir601"
122 #define kParamLuminanceMathOptionAverage "Average", "Use average of r, g, b.", "average"
123 #define kParamLuminanceMathOptionMaximum "Max", "Use max or r, g, b.", "max"
124
125 enum LuminanceMathEnum
126 {
127 eLuminanceMathRec709,
128 eLuminanceMathRec2020,
129 eLuminanceMathACESAP0,
130 eLuminanceMathACESAP1,
131 eLuminanceMathCcir601,
132 eLuminanceMathAverage,
133 eLuminanceMathMaximum,
134 };
135
136 #define kParamClampBlack "clampBlack"
137 #define kParamClampBlackLabel "Clamp Black"
138 #define kParamClampBlackHint "All colors below 0 on output are set to 0."
139
140 #define kParamClampWhite "clampWhite"
141 #define kParamClampWhiteLabel "Clamp White"
142 #define kParamClampWhiteHint "All colors above 1 on output are set to 1."
143
144 #define kParamPremultChanged "premultChanged"
145
146 struct ColorControlValues
147 {
148 double r;
149 double g;
150 double b;
151 double a;
152
ColorControlValuesColorControlValues153 ColorControlValues() : r(0.), g(0.), b(0.), a(0.) {}
154
getValueFromColorControlValues155 void getValueFrom(double time,
156 RGBAParam* p)
157 {
158 p->getValueAtTime(time, r, g, b, a);
159 }
160
setColorControlValues161 void set(double r_,
162 double g_,
163 double b_,
164 double a_)
165 {
166 r = r_;
167 g = g_;
168 b = b_;
169 a = a_;
170 }
171
setColorControlValues172 void set(double v)
173 {
174 r = g = b = a = v;
175 }
176 };
177
178 struct ColorControlGroup
179 {
180 ColorControlValues saturation;
181 ColorControlValues contrast;
182 ColorControlValues gamma;
183 ColorControlValues gain;
184 ColorControlValues offset;
185 };
186
187 template<typename T>
188 T
luminance(T r,T g,T b,LuminanceMathEnum luminanceMath)189 luminance(T r,
190 T g,
191 T b,
192 LuminanceMathEnum luminanceMath)
193 {
194 switch (luminanceMath) {
195 case eLuminanceMathRec709:
196 default:
197
198 return Color::rgb709_to_y(r, g, b);
199
200 case eLuminanceMathRec2020: // https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2087-0-201510-I!!PDF-E.pdf
201
202 return Color::rgb2020_to_y(r, g, b);
203 case eLuminanceMathACESAP0: // https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#Converting_ACES_RGB_values_to_CIE_XYZ_values
204
205 return Color::rgbACESAP0_to_y(r, g, b);
206 case eLuminanceMathACESAP1: // https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#Converting_ACES_RGB_values_to_CIE_XYZ_values
207
208 return Color::rgbACESAP1_to_y(r, g, b);
209 case eLuminanceMathCcir601:
210
211 return (T)(0.2989f * r + 0.5866f * g + 0.1145f * b);
212 case eLuminanceMathAverage:
213
214 return (r + g + b) / 3;
215 case eLuminanceMathMaximum:
216
217 return std::max(std::max(r, g), b);
218 }
219 }
220
221 template<bool processR, bool processG, bool processB, bool processA>
222 struct RGBAPixel
223 {
224 double r, g, b, a;
225 LuminanceMathEnum luminanceMath;
226
RGBAPixelRGBAPixel227 RGBAPixel(double r_,
228 double g_,
229 double b_,
230 double a_,
231 LuminanceMathEnum luminanceMath_)
232 : r(r_)
233 , g(g_)
234 , b(b_)
235 , a(a_)
236 , luminanceMath(luminanceMath_)
237 {
238 }
239
applySMHRGBAPixel240 void applySMH(const ColorControlGroup& sValues,
241 double s_scale,
242 const ColorControlGroup& mValues,
243 double m_scale,
244 const ColorControlGroup& hValues,
245 double h_scale,
246 const ColorControlGroup& masterValues)
247 {
248 RGBAPixel s = *this;
249 RGBAPixel m = *this;
250 RGBAPixel h = *this;
251
252 s.applyGroup(sValues);
253 m.applyGroup(mValues);
254 h.applyGroup(hValues);
255
256 if (processR) {
257 r = s.r * s_scale + m.r * m_scale + h.r * h_scale;
258 }
259 if (processG) {
260 g = s.g * s_scale + m.g * m_scale + h.g * h_scale;
261 }
262 if (processB) {
263 b = s.b * s_scale + m.b * m_scale + h.b * h_scale;
264 }
265 if (processA) {
266 a = s.a * s_scale + m.a * m_scale + h.a * h_scale;
267 }
268 applyGroup(masterValues);
269 }
270
271 private:
applySaturationRGBAPixel272 void applySaturation(const ColorControlValues &c)
273 {
274 if (!(processR && c.r != 1.) &&
275 !(processG && c.g != 1.) &&
276 !(processB && c.b != 1.)) {
277
278 return;
279 }
280 double l = luminance(r, g, b, luminanceMath);
281 if (processR && c.r != 1.) {
282 r = (1.f - c.r) * l + c.r * r;
283 }
284 if (processG && c.g != 1.) {
285 g = (1.f - c.g) * l + c.g * g;
286 }
287 if (processB && c.b != 1.) {
288 b = (1.f - c.b) * l + c.b * b;
289 }
290 }
291
applyContrastRGBAPixel292 void applyContrast(const ColorControlValues &c)
293 {
294 // See https://compositormathematic.wordpress.com/2013/07/06/gamma-contrast/
295 // 0.18 is the value that a (maybe) correctly exposed grey card is in sRGB
296 // colour space. A grey card is a piece of card who’s surface is specially
297 // designed to reflect 18% of the light that hits it. It’s used in
298 // photography alongside a light meter to judge the correct exposure of a
299 // scene. The argument is that for some reason 18% is the value of middle
300 // grey, and all fingers seem to point to a photographer named Ansel
301 // Adams who somehow convinced the people at Kodak of this. You can read
302 // about it here: http://bythom.com/graycards.htm. People in the know say
303 // that this value of 18% is about 1/2 a stop wrong, and it should be more
304 // like 12%. It would also seem that the people making grey cards aren’t
305 // talking to the people making light meters.
306
307 if (processR && (r > 0) && c.r != 1.) {
308 r = std::pow(r / 0.18, c.r) * 0.18;
309 }
310 if (processG && (g > 0) && c.g != 1.) {
311 g = std::pow(g / 0.18, c.g) * 0.18;
312 }
313 if (processB && (b > 0) && c.b != 1.) {
314 b = std::pow(b / 0.18, c.b) * 0.18;
315 }
316 if (processA && (a > 0) && c.a != 1.) {
317 a = std::pow(a / 0.18, c.a) * 0.18;
318 }
319 }
320
applyGainRGBAPixel321 void applyGain(const ColorControlValues &c)
322 {
323 if (processR && c.r != 1.) {
324 r = r * c.r;
325 }
326 if (processG && c.g != 1.) {
327 g = g * c.g;
328 }
329 if (processB && c.b != 1.) {
330 b = b * c.b;
331 }
332 if (processA && c.a != 1.) {
333 a = a * c.a;
334 }
335 }
336
applyGammaRGBAPixel337 void applyGamma(const ColorControlValues &c)
338 {
339 if ( processR && (r > 0) && c.r != 1. ) {
340 r = std::pow(r, 1. / c.r);
341 }
342 if ( processG && (g > 0) && c.g != 1. ) {
343 g = std::pow(g, 1. / c.g);
344 }
345 if ( processB && (b > 0) && c.b != 1. ) {
346 b = std::pow(b, 1. / c.b);
347 }
348 if ( processA && (a > 0) && c.a != 1. ) {
349 a = std::pow(a, 1. / c.a);
350 }
351 }
352
applyOffsetRGBAPixel353 void applyOffset(const ColorControlValues &c)
354 {
355 if (processR && c.r != 0.) {
356 r = r + c.r;
357 }
358 if (processG && c.g != 0.) {
359 g = g + c.g;
360 }
361 if (processB && c.b != 0.) {
362 b = b + c.b;
363 }
364 if (processA && c.a != 0.) {
365 a = a + c.a;
366 }
367 }
368
applyGroupRGBAPixel369 void applyGroup(const ColorControlGroup& group)
370 {
371 applySaturation(group.saturation);
372 applyContrast(group.contrast);
373 applyGamma(group.gamma);
374 applyGain(group.gain);
375 applyOffset(group.offset);
376 }
377 };
378
379 class ColorCorrecterBase
380 : public ImageProcessor
381 {
382 protected:
383 const Image *_srcImg;
384 const Image *_maskImg;
385 bool _premult;
386 int _premultChannel;
387 bool _doMasking;
388 const bool _clampBlack;
389 const bool _clampWhite;
390 double _mix;
391 bool _maskInvert;
392 bool _processR, _processG, _processB, _processA;
393
394 public:
ColorCorrecterBase(ImageEffect & instance,bool clampBlack,bool clampWhite,const RenderArguments &)395 ColorCorrecterBase(ImageEffect &instance,
396 bool clampBlack,
397 bool clampWhite,
398 const RenderArguments & /*args*/)
399 : ImageProcessor(instance)
400 , _srcImg(NULL)
401 , _maskImg(NULL)
402 , _premult(false)
403 , _premultChannel(3)
404 , _doMasking(false)
405 , _clampBlack(clampBlack)
406 , _clampWhite(clampWhite)
407 , _mix(1.)
408 , _maskInvert(false)
409 , _processR(false)
410 , _processG(false)
411 , _processB(false)
412 , _processA(false)
413 , _luminanceMath(eLuminanceMathRec709)
414 {
415 }
416
setSrcImg(const Image * v)417 void setSrcImg(const Image *v) {_srcImg = v; }
418
setMaskImg(const Image * v,bool maskInvert)419 void setMaskImg(const Image *v,
420 bool maskInvert) {_maskImg = v; _maskInvert = maskInvert; }
421
doMasking(bool v)422 void doMasking(bool v) {_doMasking = v; }
423
setColorControlValues(const ColorControlGroup & master,const ColorControlGroup & shadow,const ColorControlGroup & midtone,const ColorControlGroup & hightlights,LuminanceMathEnum luminanceMath,bool premult,int premultChannel,double mix,bool processR,bool processG,bool processB,bool processA)424 void setColorControlValues(const ColorControlGroup& master,
425 const ColorControlGroup& shadow,
426 const ColorControlGroup& midtone,
427 const ColorControlGroup& hightlights,
428 LuminanceMathEnum luminanceMath,
429 bool premult,
430 int premultChannel,
431 double mix,
432 bool processR,
433 bool processG,
434 bool processB,
435 bool processA)
436 {
437 _masterValues = master;
438 _shadowValues = shadow;
439 _midtoneValues = midtone;
440 _highlightsValues = hightlights;
441 _luminanceMath = luminanceMath;
442 _premult = premult;
443 _premultChannel = premultChannel;
444 _mix = mix;
445 _processR = processR;
446 _processG = processG;
447 _processB = processB;
448 _processA = processA;
449 }
450
451
452 protected:
453 // clamp for integer PIX types
454 template<class PIX>
clamp(float value,int maxValue) const455 float clamp(float value,
456 int maxValue) const
457 {
458 return std::max( 0.f, std::min( value, float(maxValue) ) );
459 }
460
461 // clamp for integer PIX types
462 template<class PIX>
clamp(double value,int maxValue) const463 double clamp(double value,
464 int maxValue) const
465 {
466 return std::max( 0., std::min( value, double(maxValue) ) );
467 }
468
469 protected:
470 ColorControlGroup _masterValues;
471 ColorControlGroup _shadowValues;
472 ColorControlGroup _midtoneValues;
473 ColorControlGroup _highlightsValues;
474 LuminanceMathEnum _luminanceMath;
475 };
476
477 // floats don't clamp except if _clampBlack or _clampWhite
478 template<>
479 float
clamp(float value,int maxValue) const480 ColorCorrecterBase::clamp<float>(float value,
481 int maxValue) const
482 {
483 assert(maxValue == 1.);
484 if ( _clampBlack && (value < 0.) ) {
485 value = 0.f;
486 } else if ( _clampWhite && (value > 1.0) ) {
487 value = 1.0f;
488 }
489
490 return value;
491 }
492
493 template<>
494 double
clamp(double value,int maxValue) const495 ColorCorrecterBase::clamp<float>(double value,
496 int maxValue) const
497 {
498 assert(maxValue == 1.);
499 if ( _clampBlack && (value < 0.) ) {
500 value = 0.f;
501 } else if ( _clampWhite && (value > 1.0) ) {
502 value = 1.0f;
503 }
504
505 return value;
506 }
507
508 // template to do the processing.
509 // nbValues is the number of values in the LUT minus 1. For integer types, it should be the same as
510 // maxValue
511 template <class PIX, int nComponents, int maxValue, int nbValues>
512 class ColorCorrecter
513 : public ColorCorrecterBase
514 {
515 public:
ColorCorrecter(ImageEffect & instance,const RenderArguments & args,ParametricParam * lookupTableParam,double rangeMin,double rangeMax,bool clampBlack,bool clampWhite)516 ColorCorrecter(ImageEffect &instance,
517 const RenderArguments &args,
518 ParametricParam *lookupTableParam,
519 double rangeMin,
520 double rangeMax,
521 bool clampBlack,
522 bool clampWhite)
523 : ColorCorrecterBase(instance, clampBlack, clampWhite, args)
524 , _lookupTableParam(lookupTableParam)
525 , _rangeMin( std::min(rangeMin, rangeMax) )
526 , _rangeMax( std::max(rangeMin, rangeMax) )
527 {
528 _time = args.time;
529 // build the LUT
530 if (_rangeMin == _rangeMax) {
531 // avoid divisions by zero
532 _rangeMax = _rangeMin + 1.;
533 }
534 assert( (PIX)maxValue == maxValue );
535 // except for float, maxValue is the same as nbValues
536 assert( maxValue == 1 || (maxValue == nbValues) );
537 for (int curve = 0; curve < 2; ++curve) {
538 _lookupTable[curve].resize(nbValues + 1);
539 for (int position = 0; position <= nbValues; ++position) {
540 // position to evaluate the param at
541 double parametricPos = _rangeMin + (_rangeMax - _rangeMin) * double(position) / nbValues;
542
543 // evaluate the parametric param
544 double value = lookupTableValue(curve, parametricPos);
545 // set that in the lut
546 _lookupTable[curve][position] = (float)clamp<PIX>(value, maxValue);
547 }
548 }
549 }
550
multiThreadProcessImages(OfxRectI procWindow)551 void multiThreadProcessImages(OfxRectI procWindow)
552 {
553 # ifndef __COVERITY__ // too many coverity[dead_error_line] errors
554 const bool r = _processR && (nComponents != 1);
555 const bool g = _processG && (nComponents >= 2);
556 const bool b = _processB && (nComponents >= 3);
557 const bool a = _processA && (nComponents == 1 || nComponents == 4);
558 if (r) {
559 if (g) {
560 if (b) {
561 if (a) {
562 return process<true, true, true, true >(procWindow); // RGBA
563 } else {
564 return process<true, true, true, false>(procWindow); // RGBa
565 }
566 } else {
567 if (a) {
568 return process<true, true, false, true >(procWindow); // RGbA
569 } else {
570 return process<true, true, false, false>(procWindow); // RGba
571 }
572 }
573 } else {
574 if (b) {
575 if (a) {
576 return process<true, false, true, true >(procWindow); // RgBA
577 } else {
578 return process<true, false, true, false>(procWindow); // RgBa
579 }
580 } else {
581 if (a) {
582 return process<true, false, false, true >(procWindow); // RgbA
583 } else {
584 return process<true, false, false, false>(procWindow); // Rgba
585 }
586 }
587 }
588 } else {
589 if (g) {
590 if (b) {
591 if (a) {
592 return process<false, true, true, true >(procWindow); // rGBA
593 } else {
594 return process<false, true, true, false>(procWindow); // rGBa
595 }
596 } else {
597 if (a) {
598 return process<false, true, false, true >(procWindow); // rGbA
599 } else {
600 return process<false, true, false, false>(procWindow); // rGba
601 }
602 }
603 } else {
604 if (b) {
605 if (a) {
606 return process<false, false, true, true >(procWindow); // rgBA
607 } else {
608 return process<false, false, true, false>(procWindow); // rgBa
609 }
610 } else {
611 if (a) {
612 return process<false, false, false, true >(procWindow); // rgbA
613 } else {
614 return process<false, false, false, false>(procWindow); // rgba
615 }
616 }
617 }
618 }
619 # endif // ifndef __COVERITY__
620 } // multiThreadProcessImages
621
622 private:
623
624
625 template<bool processR, bool processG, bool processB, bool processA>
process(OfxRectI procWindow)626 void process(OfxRectI procWindow)
627 {
628 assert( (!processR && !processG && !processB) || (nComponents == 3 || nComponents == 4) );
629 assert( !processA || (nComponents == 1 || nComponents == 4) );
630 assert(nComponents == 3 || nComponents == 4);
631 float unpPix[4];
632 float tmpPix[4];
633 for (int y = procWindow.y1; y < procWindow.y2; y++) {
634 if ( _effect.abort() ) {
635 break;
636 }
637
638 PIX *dstPix = (PIX *) _dstImg->getPixelAddress(procWindow.x1, y);
639 for (int x = procWindow.x1; x < procWindow.x2; x++) {
640 const PIX *srcPix = (const PIX *) (_srcImg ? _srcImg->getPixelAddress(x, y) : 0);
641 ofxsUnPremult<PIX, nComponents, maxValue>(srcPix, unpPix, _premult, _premultChannel);
642 double t_r = unpPix[0];
643 double t_g = unpPix[1];
644 double t_b = unpPix[2];
645 double t_a = unpPix[3];
646 colorTransform<processR, processG, processB, processA>(&t_r, &t_g, &t_b, &t_a);
647 tmpPix[0] = (float)t_r;
648 tmpPix[1] = (float)t_g;
649 tmpPix[2] = (float)t_b;
650 tmpPix[3] = (float)t_a;
651 ofxsPremultMaskMixPix<PIX, nComponents, maxValue, true>(tmpPix, _premult, _premultChannel, x, y, srcPix, _doMasking, _maskImg, (float)_mix, _maskInvert, dstPix);
652 // copy back original values from unprocessed channels
653 if (nComponents == 1) {
654 if (!processA) {
655 dstPix[0] = srcPix ? srcPix[0] : PIX();
656 }
657 } else if ( (nComponents == 3) || (nComponents == 4) ) {
658 if (!processR) {
659 dstPix[0] = srcPix ? srcPix[0] : PIX();
660 }
661 if (!processG) {
662 dstPix[1] = srcPix ? srcPix[1] : PIX();
663 }
664 if (!processB) {
665 dstPix[2] = srcPix ? srcPix[2] : PIX();
666 }
667 if ( !processA && (nComponents == 4) ) {
668 dstPix[3] = srcPix ? srcPix[3] : PIX();
669 }
670 }
671 // increment the dst pixel
672 dstPix += nComponents;
673 }
674 }
675 } // process
676
677 template<bool processR, bool processG, bool processB, bool processA>
colorTransform(double * r,double * g,double * b,double * a)678 void colorTransform(double *r,
679 double *g,
680 double *b,
681 double *a)
682 {
683 double l = luminance(*r, *g, *b, _luminanceMath);
684 double s_scale = interpolate(0, l);
685 double h_scale = interpolate(1, l);
686 double m_scale = 1.f - s_scale - h_scale;
687
688 RGBAPixel<processR, processG, processB, processA> p(*r, *g, *b, *a, _luminanceMath);
689 p.applySMH(_shadowValues, s_scale,
690 _midtoneValues, m_scale,
691 _highlightsValues, h_scale,
692 _masterValues);
693 if (processR) {
694 *r = clamp<float>(p.r, 1);
695 }
696 if (processG) {
697 *g = clamp<float>(p.g, 1);
698 }
699 if (processB) {
700 *b = clamp<float>(p.b, 1);
701 }
702 if (processA) {
703 *a = clamp<float>(p.a, 1);
704 }
705 }
706
lookupTableValue(int curve,double parametricPos) const707 double lookupTableValue(int curve, double parametricPos) const
708 {
709 double value;
710 if (_lookupTableParam) {
711 value = _lookupTableParam->getValue(curve, _time, parametricPos);
712 } else if (curve == 0) {
713 if (parametricPos <= 0.) {
714 value = 1.;
715 } else if (parametricPos < 0.09) {
716 double x = parametricPos / 0.09;
717 x = -2 * x * x * x + 3 * x * x; // cubic
718 value = 1. - x;
719 } else {
720 value = 0.;
721 }
722 } else {
723 assert(curve == 1);
724 if (parametricPos <= 0.5) {
725 value = 0.;
726 } else if (parametricPos >= 1.) {
727 value = 1.;
728 } else {
729 double x = (parametricPos - 0.5) / 0.5;
730 x = -2 * x * x * x + 3 * x * x; // cubic
731 value = x;
732 }
733 }
734 return value;
735 }
736 // on input to interpolate, value should be normalized to the [0-1] range
interpolate(int component,float value) const737 float interpolate(int component,
738 float value) const
739 {
740 if ( (value < _rangeMin) || (_rangeMax < value) ) {
741 // slow version
742 double ret = lookupTableValue(component, value);
743
744 return clamp<float>(ret, 1);;
745 } else {
746 double x = (value - _rangeMin) / (_rangeMax - _rangeMin);
747 if (x <= 0.) {
748 return _lookupTable[component][0];
749 } else if (x >= 1.) {
750 return _lookupTable[component][nbValues];
751 }
752 int i = (int)(x * nbValues);
753 assert(0 <= i && i < nbValues);
754 i = std::max( 0, std::min(i, nbValues - 1) );
755 double alpha = std::max( 0., std::min(x * nbValues - i, 1.) );
756 float a = _lookupTable[component][i];
757 float b = _lookupTable[component][i + 1];
758
759 return a * (1.f - alpha) + b * alpha;
760 }
761 }
762
763 std::vector<float> _lookupTable[2];
764 ParametricParam* _lookupTableParam;
765 double _time;
766 double _rangeMin;
767 double _rangeMax;
768 };
769
770 struct ColorControlParamGroup
771 {
ColorControlParamGroupColorControlParamGroup772 ColorControlParamGroup()
773 : enable(NULL)
774 , saturation(NULL)
775 , contrast(NULL)
776 , gamma(NULL)
777 , gain(NULL)
778 , offset(NULL) {}
779
780 BooleanParam* enable;
781 RGBAParam* saturation;
782 RGBAParam* contrast;
783 RGBAParam* gamma;
784 RGBAParam* gain;
785 RGBAParam* offset;
786 };
787
788 ////////////////////////////////////////////////////////////////////////////////
789 /** @brief The plugin that does our work */
790 class ColorCorrectPlugin
791 : public ImageEffect
792 {
793 public:
794
795 enum ColorCorrectGroupType
796 {
797 eGroupMaster = 0,
798 eGroupShadow,
799 eGroupMidtone,
800 eGroupHighlight
801 };
802
803 /** @brief ctor */
ColorCorrectPlugin(OfxImageEffectHandle handle,bool supportsParametricParameter)804 ColorCorrectPlugin(OfxImageEffectHandle handle,
805 bool supportsParametricParameter)
806 : ImageEffect(handle)
807 , _supportsParametricParameter(supportsParametricParameter)
808 , _dstClip(NULL)
809 , _srcClip(NULL)
810 , _maskClip(NULL)
811 , _processR(NULL)
812 , _processG(NULL)
813 , _processB(NULL)
814 , _processA(NULL)
815 , _range(NULL)
816 , _rangesParam(NULL)
817 , _luminanceMath(NULL)
818 , _clampBlack(NULL)
819 , _clampWhite(NULL)
820 , _premult(NULL)
821 , _premultChannel(NULL)
822 , _mix(NULL)
823 , _maskApply(NULL)
824 , _maskInvert(NULL)
825 , _premultChanged(NULL)
826 {
827 _dstClip = fetchClip(kOfxImageEffectOutputClipName);
828 assert( _dstClip && (!_dstClip->isConnected() || _dstClip->getPixelComponents() == ePixelComponentRGB ||
829 _dstClip->getPixelComponents() == ePixelComponentRGBA) );
830 _srcClip = getContext() == eContextGenerator ? NULL : fetchClip(kOfxImageEffectSimpleSourceClipName);
831 assert( (!_srcClip && getContext() == eContextGenerator) ||
832 ( _srcClip && (!_srcClip->isConnected() || _srcClip->getPixelComponents() == ePixelComponentRGB ||
833 _srcClip->getPixelComponents() == ePixelComponentRGBA) ) );
834 _maskClip = fetchClip(getContext() == eContextPaint ? "Brush" : "Mask");
835 assert(!_maskClip || !_maskClip->isConnected() || _maskClip->getPixelComponents() == ePixelComponentAlpha);
836 fetchColorControlGroup(kGroupMaster, &_masterParamsGroup);
837 fetchColorControlGroup(kGroupShadows, &_shadowsParamsGroup);
838 fetchColorControlGroup(kGroupMidtones, &_midtonesParamsGroup);
839 fetchColorControlGroup(kGroupHighlights, &_highlightsParamsGroup);
840 _range = fetchDouble2DParam(kParamRange);
841 assert(_range);
842 if (_supportsParametricParameter) {
843 _rangesParam = fetchParametricParam(kParamColorCorrectToneRanges);
844 assert(_rangesParam);
845 }
846 _luminanceMath = fetchChoiceParam(kParamLuminanceMath);
847 assert(_luminanceMath);
848 _clampBlack = fetchBooleanParam(kParamClampBlack);
849 _clampWhite = fetchBooleanParam(kParamClampWhite);
850 assert(_clampBlack && _clampWhite);
851 _premult = fetchBooleanParam(kParamPremult);
852 _premultChannel = fetchChoiceParam(kParamPremultChannel);
853 assert(_premult && _premultChannel);
854 _mix = fetchDoubleParam(kParamMix);
855 _maskApply = ( ofxsMaskIsAlwaysConnected( OFX::getImageEffectHostDescription() ) && paramExists(kParamMaskApply) ) ? fetchBooleanParam(kParamMaskApply) : 0;
856 _maskInvert = fetchBooleanParam(kParamMaskInvert);
857 assert(_mix && _maskInvert);
858 _premultChanged = fetchBooleanParam(kParamPremultChanged);
859 assert(_premultChanged);
860
861 _processR = fetchBooleanParam(kNatronOfxParamProcessR);
862 _processG = fetchBooleanParam(kNatronOfxParamProcessG);
863 _processB = fetchBooleanParam(kNatronOfxParamProcessB);
864 _processA = fetchBooleanParam(kNatronOfxParamProcessA);
865 assert(_processR && _processG && _processB && _processA);
866 }
867
868 private:
869 /* Override the render */
870 virtual void render(const RenderArguments &args) OVERRIDE FINAL;
871
872 /* set up and run a processor */
873 void setupAndProcess(ColorCorrecterBase &, const RenderArguments &args);
874
875 virtual bool isIdentity(const IsIdentityArguments &args, Clip * &identityClip, double &identityTime, int& view, std::string& plane) OVERRIDE FINAL;
876
877 /** @brief called when a clip has just been changed in some way (a rewire maybe) */
878 virtual void changedClip(const InstanceChangedArgs &args, const std::string &clipName) OVERRIDE FINAL;
879 virtual void changedParam(const InstanceChangedArgs &args, const std::string ¶mName) OVERRIDE FINAL;
fetchColorControlGroup(const std::string & groupName,ColorControlParamGroup * group)880 void fetchColorControlGroup(const std::string& groupName,
881 ColorControlParamGroup* group)
882 {
883 assert(group);
884 group->enable = (groupName == kGroupMaster) ? 0 : fetchBooleanParam(groupName + kParamEnable);
885 group->saturation = fetchRGBAParam(groupName + kParamSaturation);
886 group->contrast = fetchRGBAParam(groupName + kParamContrast);
887 group->gamma = fetchRGBAParam(groupName + kParamGamma);
888 group->gain = fetchRGBAParam(groupName + kParamGain);
889 group->offset = fetchRGBAParam(groupName + kParamOffset);
890 assert(group->saturation && group->contrast && group->gamma && group->gain && group->offset);
891 }
892
893 void getColorCorrectGroupValues(double time, ColorControlGroup* groupValues, ColorCorrectGroupType type);
894
getGroup(ColorCorrectGroupType type)895 ColorControlParamGroup& getGroup(ColorCorrectGroupType type)
896 {
897 switch (type) {
898 case eGroupMaster:
899
900 return _masterParamsGroup;
901 case eGroupShadow:
902
903 return _shadowsParamsGroup;
904 case eGroupMidtone:
905
906 return _midtonesParamsGroup;
907 case eGroupHighlight:
908
909 return _highlightsParamsGroup;
910 default:
911 assert(false);
912 break;
913 }
914 }
915
916 private:
917 bool _supportsParametricParameter;
918 // do not need to delete these, the ImageEffect is managing them for us
919 Clip *_dstClip;
920 Clip *_srcClip;
921 Clip *_maskClip;
922 ColorControlParamGroup _masterParamsGroup;
923 ColorControlParamGroup _shadowsParamsGroup;
924 ColorControlParamGroup _midtonesParamsGroup;
925 ColorControlParamGroup _highlightsParamsGroup;
926 BooleanParam* _processR;
927 BooleanParam* _processG;
928 BooleanParam* _processB;
929 BooleanParam* _processA;
930 Double2DParam* _range;
931 ParametricParam* _rangesParam;
932 ChoiceParam* _luminanceMath;
933 BooleanParam* _clampBlack;
934 BooleanParam* _clampWhite;
935 BooleanParam* _premult;
936 ChoiceParam* _premultChannel;
937 DoubleParam* _mix;
938 BooleanParam* _maskApply;
939 BooleanParam* _maskInvert;
940 BooleanParam* _premultChanged; // set to true the first time the user connects src
941 };
942
943
944 void
getColorCorrectGroupValues(double time,ColorControlGroup * groupValues,ColorCorrectGroupType type)945 ColorCorrectPlugin::getColorCorrectGroupValues(double time,
946 ColorControlGroup* groupValues,
947 ColorCorrectGroupType type)
948 {
949 ColorControlParamGroup& group = getGroup(type);
950 bool enable = true;
951
952 if (group.enable) {
953 group.enable->getValueAtTime(time, enable);
954 }
955 if (enable) {
956 groupValues->saturation.getValueFrom(time, group.saturation);
957 groupValues->contrast.getValueFrom(time, group.contrast);
958 groupValues->gamma.getValueFrom(time, group.gamma);
959 groupValues->gain.getValueFrom(time, group.gain);
960 groupValues->offset.getValueFrom(time, group.offset);
961 } else {
962 groupValues->saturation.set(1.);
963 groupValues->contrast.set(1.);
964 groupValues->gamma.set(1.);
965 groupValues->gain.set(1.);
966 groupValues->offset.set(0.);
967 }
968 }
969
970 ////////////////////////////////////////////////////////////////////////////////
971 /** @brief render for the filter */
972
973 ////////////////////////////////////////////////////////////////////////////////
974 // basic plugin render function, just a skelington to instantiate templates from
975
976 /* set up and run a processor */
977 void
setupAndProcess(ColorCorrecterBase & processor,const RenderArguments & args)978 ColorCorrectPlugin::setupAndProcess(ColorCorrecterBase &processor,
979 const RenderArguments &args)
980 {
981 const double time = args.time;
982
983 auto_ptr<Image> dst( _dstClip->fetchImage(time) );
984
985 if ( !dst.get() ) {
986 throwSuiteStatusException(kOfxStatFailed);
987 }
988 BitDepthEnum dstBitDepth = dst->getPixelDepth();
989 PixelComponentEnum dstComponents = dst->getPixelComponents();
990 if ( ( dstBitDepth != _dstClip->getPixelDepth() ) ||
991 ( dstComponents != _dstClip->getPixelComponents() ) ) {
992 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong depth or components");
993 throwSuiteStatusException(kOfxStatFailed);
994 }
995 if ( (dst->getRenderScale().x != args.renderScale.x) ||
996 ( dst->getRenderScale().y != args.renderScale.y) ||
997 ( ( dst->getField() != eFieldNone) /* for DaVinci Resolve */ && ( dst->getField() != args.fieldToRender) ) ) {
998 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
999 throwSuiteStatusException(kOfxStatFailed);
1000 }
1001 auto_ptr<const Image> src( ( _srcClip && _srcClip->isConnected() ) ?
1002 _srcClip->fetchImage(time) : 0 );
1003 if ( src.get() ) {
1004 if ( (src->getRenderScale().x != args.renderScale.x) ||
1005 ( src->getRenderScale().y != args.renderScale.y) ||
1006 ( ( src->getField() != eFieldNone) /* for DaVinci Resolve */ && ( src->getField() != args.fieldToRender) ) ) {
1007 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
1008 throwSuiteStatusException(kOfxStatFailed);
1009 }
1010 BitDepthEnum srcBitDepth = src->getPixelDepth();
1011 PixelComponentEnum srcComponents = src->getPixelComponents();
1012 if ( (srcBitDepth != dstBitDepth) || (srcComponents != dstComponents) ) {
1013 throwSuiteStatusException(kOfxStatErrImageFormat);
1014 }
1015 }
1016 bool doMasking = ( ( !_maskApply || _maskApply->getValueAtTime(time) ) && _maskClip && _maskClip->isConnected() );
1017 auto_ptr<const Image> mask(doMasking ? _maskClip->fetchImage(time) : 0);
1018 if ( mask.get() ) {
1019 if ( (mask->getRenderScale().x != args.renderScale.x) ||
1020 ( mask->getRenderScale().y != args.renderScale.y) ||
1021 ( ( mask->getField() != eFieldNone) /* for DaVinci Resolve */ && ( mask->getField() != args.fieldToRender) ) ) {
1022 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
1023 throwSuiteStatusException(kOfxStatFailed);
1024 }
1025 }
1026 if (doMasking) {
1027 bool maskInvert = _maskInvert->getValueAtTime(time);
1028 processor.doMasking(true);
1029 processor.setMaskImg(mask.get(), maskInvert);
1030 }
1031
1032 processor.setDstImg( dst.get() );
1033 processor.setSrcImg( src.get() );
1034 processor.setRenderWindow(args.renderWindow);
1035
1036 ColorControlGroup masterValues, shadowValues, midtoneValues, highlightValues;
1037 getColorCorrectGroupValues(time, &masterValues, eGroupMaster);
1038 getColorCorrectGroupValues(time, &shadowValues, eGroupShadow);
1039 getColorCorrectGroupValues(time, &midtoneValues, eGroupMidtone);
1040 getColorCorrectGroupValues(time, &highlightValues, eGroupHighlight);
1041 LuminanceMathEnum luminanceMath = (LuminanceMathEnum)_luminanceMath->getValueAtTime(time);
1042 bool premult = _premult->getValueAtTime(time);
1043 int premultChannel = _premultChannel->getValueAtTime(time);
1044 double mix = _mix->getValueAtTime(time);
1045 bool processR = _processR->getValueAtTime(time);
1046 bool processG = _processG->getValueAtTime(time);
1047 bool processB = _processB->getValueAtTime(time);
1048 bool processA = _processA->getValueAtTime(time);
1049
1050 processor.setColorControlValues(masterValues, shadowValues, midtoneValues, highlightValues, luminanceMath, premult, premultChannel, mix,
1051 processR, processG, processB, processA);
1052 processor.process();
1053 } // ColorCorrectPlugin::setupAndProcess
1054
1055 // the overridden render function
1056 void
render(const RenderArguments & args)1057 ColorCorrectPlugin::render(const RenderArguments &args)
1058 {
1059 //std::cout << "render!\n";
1060 // instantiate the render code based on the pixel depth of the dst clip
1061 BitDepthEnum dstBitDepth = _dstClip->getPixelDepth();
1062 PixelComponentEnum dstComponents = _dstClip->getPixelComponents();
1063
1064 assert( kSupportsMultipleClipPARs || !_srcClip || _srcClip->getPixelAspectRatio() == _dstClip->getPixelAspectRatio() );
1065 assert( kSupportsMultipleClipDepths || !_srcClip || _srcClip->getPixelDepth() == _dstClip->getPixelDepth() );
1066 assert(dstComponents == ePixelComponentRGB || dstComponents == ePixelComponentRGBA);
1067 double rangeMin, rangeMax;
1068 const double time = args.time;
1069
1070 _range->getValueAtTime(time, rangeMin, rangeMax);
1071 bool clampBlack = _clampBlack->getValueAtTime(time);
1072 bool clampWhite = _clampWhite->getValueAtTime(time);
1073
1074 if (dstComponents == ePixelComponentRGBA) {
1075 switch (dstBitDepth) {
1076 case eBitDepthUByte: {
1077 ColorCorrecter<unsigned char, 4, 255, 255> fred(*this, args, _rangesParam, rangeMin, rangeMax, clampBlack, clampWhite);
1078 setupAndProcess(fred, args);
1079 break;
1080 }
1081 case eBitDepthUShort: {
1082 ColorCorrecter<unsigned short, 4, 65535, 65535> fred(*this, args, _rangesParam, rangeMin, rangeMax, clampBlack, clampWhite);
1083 setupAndProcess(fred, args);
1084 break;
1085 }
1086 case eBitDepthFloat: {
1087 ColorCorrecter<float, 4, 1, 1023> fred(*this, args, _rangesParam, rangeMin, rangeMax, clampBlack, clampWhite);
1088 setupAndProcess(fred, args);
1089 break;
1090 }
1091 default:
1092 //std::cout << "depth usupported\n";
1093 throwSuiteStatusException(kOfxStatErrUnsupported);
1094 }
1095 } else {
1096 assert(dstComponents == ePixelComponentRGB);
1097 switch (dstBitDepth) {
1098 case eBitDepthUByte: {
1099 ColorCorrecter<unsigned char, 3, 255, 255> fred(*this, args, _rangesParam, rangeMin, rangeMax, clampBlack, clampWhite);
1100 setupAndProcess(fred, args);
1101 break;
1102 }
1103 case eBitDepthUShort: {
1104 ColorCorrecter<unsigned short, 3, 65535, 65535> fred(*this, args, _rangesParam, rangeMin, rangeMax, clampBlack, clampWhite);
1105 setupAndProcess(fred, args);
1106 break;
1107 }
1108 case eBitDepthFloat: {
1109 ColorCorrecter<float, 3, 1, 1023> fred(*this, args, _rangesParam, rangeMin, rangeMax, clampBlack, clampWhite);
1110 setupAndProcess(fred, args);
1111 break;
1112 }
1113 default:
1114 //std::cout << "components usupported\n";
1115 throwSuiteStatusException(kOfxStatErrUnsupported);
1116 }
1117 }
1118 //std::cout << "render! OK\n";
1119 } // ColorCorrectPlugin::render
1120
1121 static bool
groupIsIdentity(const ColorControlGroup & group)1122 groupIsIdentity(const ColorControlGroup& group)
1123 {
1124 return (group.saturation.r == 1. &&
1125 group.saturation.g == 1. &&
1126 group.saturation.b == 1. &&
1127 group.saturation.a == 1. &&
1128 group.contrast.r == 1. &&
1129 group.contrast.g == 1. &&
1130 group.contrast.b == 1. &&
1131 group.contrast.a == 1. &&
1132 group.gamma.r == 1. &&
1133 group.gamma.g == 1. &&
1134 group.gamma.b == 1. &&
1135 group.gamma.a == 1. &&
1136 group.gain.r == 1. &&
1137 group.gain.g == 1. &&
1138 group.gain.b == 1. &&
1139 group.gain.a == 1. &&
1140 group.offset.r == 0. &&
1141 group.offset.g == 0. &&
1142 group.offset.b == 0. &&
1143 group.offset.a == 0.);
1144 }
1145
1146 bool
isIdentity(const IsIdentityArguments & args,Clip * & identityClip,double &,int &,std::string &)1147 ColorCorrectPlugin::isIdentity(const IsIdentityArguments &args,
1148 Clip * &identityClip,
1149 double & /*identityTime*/
1150 , int& /*view*/, std::string& /*plane*/)
1151 {
1152 //std::cout << "isIdentity!\n";
1153 double mix;
1154
1155 _mix->getValueAtTime(args.time, mix);
1156
1157 if (mix == 0. /*|| (!processR && !processG && !processB && !processA)*/) {
1158 identityClip = _srcClip;
1159
1160 return true;
1161 }
1162
1163 {
1164 bool processR;
1165 bool processG;
1166 bool processB;
1167 bool processA;
1168 _processR->getValueAtTime(args.time, processR);
1169 _processG->getValueAtTime(args.time, processG);
1170 _processB->getValueAtTime(args.time, processB);
1171 _processA->getValueAtTime(args.time, processA);
1172 if (!processR && !processG && !processB && !processA) {
1173 identityClip = _srcClip;
1174
1175 return true;
1176 }
1177 }
1178
1179 bool clampBlack, clampWhite;
1180 _clampBlack->getValueAtTime(args.time, clampBlack);
1181 _clampWhite->getValueAtTime(args.time, clampWhite);
1182 if (clampBlack || clampWhite) {
1183 return false;
1184 }
1185
1186 ColorControlGroup masterValues, shadowValues, midtoneValues, highlightValues;
1187 getColorCorrectGroupValues(args.time, &masterValues, eGroupMaster);
1188 getColorCorrectGroupValues(args.time, &shadowValues, eGroupShadow);
1189 getColorCorrectGroupValues(args.time, &midtoneValues, eGroupMidtone);
1190 getColorCorrectGroupValues(args.time, &highlightValues, eGroupHighlight);
1191 if ( groupIsIdentity(masterValues) &&
1192 groupIsIdentity(shadowValues) &&
1193 groupIsIdentity(midtoneValues) &&
1194 groupIsIdentity(highlightValues) ) {
1195 identityClip = _srcClip;
1196
1197 //std::cout << "isIdentity! true\n";
1198 return true;
1199 }
1200
1201 bool doMasking = ( ( !_maskApply || _maskApply->getValueAtTime(args.time) ) && _maskClip && _maskClip->isConnected() );
1202 if (doMasking) {
1203 bool maskInvert;
1204 _maskInvert->getValueAtTime(args.time, maskInvert);
1205 if (!maskInvert) {
1206 OfxRectI maskRoD;
1207 if (getImageEffectHostDescription()->supportsMultiResolution) {
1208 // In Sony Catalyst Edit, clipGetRegionOfDefinition returns the RoD in pixels instead of canonical coordinates.
1209 // In hosts that do not support multiResolution (e.g. Sony Catalyst Edit), all inputs have the same RoD anyway.
1210 Coords::toPixelEnclosing(_maskClip->getRegionOfDefinition(args.time), args.renderScale, _maskClip->getPixelAspectRatio(), &maskRoD);
1211 // effect is identity if the renderWindow doesn't intersect the mask RoD
1212 if ( !Coords::rectIntersection<OfxRectI>(args.renderWindow, maskRoD, 0) ) {
1213 identityClip = _srcClip;
1214
1215 return true;
1216 }
1217 }
1218 }
1219 }
1220
1221 //std::cout << "isIdentity! false\n";
1222 return false;
1223 } // ColorCorrectPlugin::isIdentity
1224
1225 void
changedClip(const InstanceChangedArgs & args,const std::string & clipName)1226 ColorCorrectPlugin::changedClip(const InstanceChangedArgs &args,
1227 const std::string &clipName)
1228 {
1229 //std::cout << "changedClip!\n";
1230 if ( (clipName == kOfxImageEffectSimpleSourceClipName) &&
1231 _srcClip && _srcClip->isConnected() &&
1232 !_premultChanged->getValue() &&
1233 ( args.reason == eChangeUserEdit) ) {
1234 if (_srcClip->getPixelComponents() != ePixelComponentRGBA) {
1235 _premult->setValue(false);
1236 } else {
1237 switch ( _srcClip->getPreMultiplication() ) {
1238 case eImageOpaque:
1239 _premult->setValue(false);
1240 break;
1241 case eImagePreMultiplied:
1242 _premult->setValue(true);
1243 break;
1244 case eImageUnPreMultiplied:
1245 _premult->setValue(false);
1246 break;
1247 }
1248 }
1249 }
1250 //std::cout << "changedClip OK!\n";
1251 }
1252
1253 void
changedParam(const InstanceChangedArgs & args,const std::string & paramName)1254 ColorCorrectPlugin::changedParam(const InstanceChangedArgs &args,
1255 const std::string ¶mName)
1256 {
1257 const double time = args.time;
1258
1259 if ( (paramName == kParamRange) && (args.reason == eChangeUserEdit) ) {
1260 double rmin, rmax;
1261 _range->getValueAtTime(time, rmin, rmax);
1262 if (rmax < rmin) {
1263 _range->setValue(rmax, rmin);
1264 }
1265 } else if ( (paramName == kParamPremult) && (args.reason == eChangeUserEdit) ) {
1266 _premultChanged->setValue(true);
1267 }
1268 }
1269
1270 mDeclarePluginFactory(ColorCorrectPluginFactory, {ofxsThreadSuiteCheck();}, {});
1271 void
describe(ImageEffectDescriptor & desc)1272 ColorCorrectPluginFactory::describe(ImageEffectDescriptor &desc)
1273 {
1274 //std::cout << "describe!\n";
1275 // basic labels
1276 desc.setLabel(kPluginName);
1277 desc.setPluginGrouping(kPluginGrouping);
1278 desc.setPluginDescription(kPluginDescription);
1279
1280 desc.addSupportedContext(eContextFilter);
1281 desc.addSupportedContext(eContextGeneral);
1282 desc.addSupportedContext(eContextPaint);
1283 desc.addSupportedBitDepth(eBitDepthUByte);
1284 desc.addSupportedBitDepth(eBitDepthUShort);
1285 desc.addSupportedBitDepth(eBitDepthFloat);
1286
1287 // set a few flags
1288 desc.setSingleInstance(false);
1289 desc.setHostFrameThreading(false);
1290 desc.setSupportsMultiResolution(kSupportsMultiResolution);
1291 desc.setSupportsTiles(kSupportsTiles);
1292 desc.setTemporalClipAccess(false);
1293 desc.setRenderTwiceAlways(false);
1294 desc.setSupportsMultipleClipPARs(kSupportsMultipleClipPARs);
1295 desc.setSupportsMultipleClipDepths(kSupportsMultipleClipDepths);
1296 desc.setRenderThreadSafety(kRenderThreadSafety);
1297 //std::cout << "describe! OK\n";
1298
1299 #ifdef OFX_EXTENSIONS_NATRON
1300 desc.setChannelSelector(ePixelComponentNone); // we have our own channel selector
1301 #endif
1302 }
1303
1304 static void
defineRGBAScaleParam(ImageEffectDescriptor & desc,const std::string & name,const std::string & label,const std::string & hint,GroupParamDescriptor * parent,PageParamDescriptor * page,double def,double min,double max)1305 defineRGBAScaleParam(ImageEffectDescriptor &desc,
1306 const std::string &name,
1307 const std::string &label,
1308 const std::string &hint,
1309 GroupParamDescriptor *parent,
1310 PageParamDescriptor* page,
1311 double def,
1312 double min,
1313 double max)
1314 {
1315 RGBAParamDescriptor *param = desc.defineRGBAParam(name);
1316
1317 param->setLabel(label);
1318 param->setHint(hint);
1319 param->setDefault(def, def, def, def);
1320 param->setRange(-DBL_MAX, -DBL_MAX, -DBL_MAX, -DBL_MAX, DBL_MAX, DBL_MAX, DBL_MAX, DBL_MAX); // Resolve requires range and display range or values are clamped to (-1,1)
1321 param->setDisplayRange(min, min, min, min, max, max, max, max);
1322 if (parent) {
1323 param->setParent(*parent);
1324 }
1325 if (page) {
1326 page->addChild(*param);
1327 }
1328 }
1329
1330 static void
defineColorGroup(const std::string & groupName,const std::string & hint,PageParamDescriptor * page,ImageEffectDescriptor & desc,bool open)1331 defineColorGroup(const std::string& groupName,
1332 const std::string& hint,
1333 PageParamDescriptor* page,
1334 ImageEffectDescriptor &desc,
1335 bool open)
1336 {
1337 GroupParamDescriptor* group = desc.defineGroupParam(groupName);
1338
1339 if (group) {
1340 group->setLabel(groupName);
1341 group->setHint(hint);
1342 group->setOpen(open);
1343 if (page) {
1344 page->addChild(*group);
1345 }
1346 }
1347
1348 if (groupName != kGroupMaster) {
1349 BooleanParamDescriptor *param = desc.defineBooleanParam(groupName + kParamEnable);
1350 param->setLabel(kParamEnable);
1351 param->setHint("When checked, " + groupName + " correction is enabled.");
1352 param->setDefault(true);
1353 if (group) {
1354 param->setParent(*group);
1355 }
1356 if (page) {
1357 page->addChild(*param);
1358 }
1359 }
1360 defineRGBAScaleParam(desc, groupName + kParamSaturation, kParamSaturation, hint, group, page, 1, 0, 4);
1361 defineRGBAScaleParam(desc, groupName + kParamContrast, kParamContrast, hint, group, page, 1, 0, 4);
1362 defineRGBAScaleParam(desc, groupName + kParamGamma, kParamGamma, hint, group, page, 1, 0.2, 5);
1363 defineRGBAScaleParam(desc, groupName + kParamGain, kParamGain, hint, group, page, 1, 0, 4);
1364 defineRGBAScaleParam(desc, groupName + kParamOffset, kParamOffset, hint, group, page, 0, -1, 1);
1365 }
1366
1367 void
describeInContext(ImageEffectDescriptor & desc,ContextEnum context)1368 ColorCorrectPluginFactory::describeInContext(ImageEffectDescriptor &desc,
1369 ContextEnum context)
1370 {
1371 //std::cout << "describeInContext!\n";
1372 // Source clip only in the filter context
1373 // create the mandated source clip
1374 ClipDescriptor *srcClip = desc.defineClip(kOfxImageEffectSimpleSourceClipName);
1375
1376 srcClip->addSupportedComponent(ePixelComponentRGBA);
1377 srcClip->addSupportedComponent(ePixelComponentRGB);
1378 srcClip->setTemporalClipAccess(false);
1379 srcClip->setSupportsTiles(kSupportsTiles);
1380 srcClip->setIsMask(false);
1381
1382 // create the mandated output clip
1383 ClipDescriptor *dstClip = desc.defineClip(kOfxImageEffectOutputClipName);
1384 dstClip->addSupportedComponent(ePixelComponentRGBA);
1385 dstClip->addSupportedComponent(ePixelComponentRGB);
1386 dstClip->setSupportsTiles(kSupportsTiles);
1387
1388 ClipDescriptor *maskClip = (context == eContextPaint) ? desc.defineClip("Brush") : desc.defineClip("Mask");
1389 maskClip->addSupportedComponent(ePixelComponentAlpha);
1390 maskClip->setTemporalClipAccess(false);
1391 if (context != eContextPaint) {
1392 maskClip->setOptional(true);
1393 }
1394 maskClip->setSupportsTiles(kSupportsTiles);
1395 maskClip->setIsMask(true);
1396
1397 // make some pages and to things in
1398 PageParamDescriptor *page = desc.definePageParam("Controls");
1399
1400 {
1401 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamProcessR);
1402 param->setLabel(kParamProcessRLabel);
1403 param->setHint(kParamProcessRHint);
1404 param->setDefault(true);
1405 param->setLayoutHint(eLayoutHintNoNewLine, 1);
1406 if (page) {
1407 page->addChild(*param);
1408 }
1409 }
1410 {
1411 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamProcessG);
1412 param->setLabel(kParamProcessGLabel);
1413 param->setHint(kParamProcessGHint);
1414 param->setDefault(true);
1415 param->setLayoutHint(eLayoutHintNoNewLine, 1);
1416 if (page) {
1417 page->addChild(*param);
1418 }
1419 }
1420 {
1421 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamProcessB);
1422 param->setLabel(kParamProcessBLabel);
1423 param->setHint(kParamProcessBHint);
1424 param->setDefault(true);
1425 param->setLayoutHint(eLayoutHintNoNewLine, 1);
1426 if (page) {
1427 page->addChild(*param);
1428 }
1429 }
1430 {
1431 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamProcessA);
1432 param->setLabel(kParamProcessALabel);
1433 param->setHint(kParamProcessAHint);
1434 param->setDefault(false);
1435 if (page) {
1436 page->addChild(*param);
1437 }
1438 }
1439
1440 defineColorGroup(kGroupMaster, "", page, desc, true);
1441 defineColorGroup(kGroupShadows, "", page, desc, false);
1442 defineColorGroup(kGroupMidtones, "", page, desc, false);
1443 defineColorGroup(kGroupHighlights, "", page, desc, false);
1444
1445 {
1446 PageParamDescriptor* ranges = desc.definePageParam("Ranges");
1447 {
1448 Double2DParamDescriptor *param = desc.defineDouble2DParam(kParamRange);
1449 param->setLabel(kParamRangeLabel);
1450 param->setDimensionLabels("min", "max");
1451 param->setHint(kParamRangeHint);
1452 param->setDefault(0., 1.);
1453 param->setDoubleType(eDoubleTypePlain);
1454 param->setRange(-DBL_MAX, -DBL_MAX, DBL_MAX, DBL_MAX); // Resolve requires range and display range or values are clamped to (-1,1)
1455 param->setDisplayRange(0., 0., 1., 1.);
1456 param->setUseHostNativeOverlayHandle(false);
1457 param->setAnimates(true);
1458 if (ranges) {
1459 ranges->addChild(*param);
1460 }
1461 }
1462 const ImageEffectHostDescription &gHostDescription = *getImageEffectHostDescription();
1463 const bool supportsParametricParameter = ( gHostDescription.supportsParametricParameter &&
1464 !(gHostDescription.hostName == "uk.co.thefoundry.nuke" &&
1465 8 <= gHostDescription.versionMajor && gHostDescription.versionMajor <= 10) ); // Nuke 8-10 are known to *not* support Parametric
1466 if (supportsParametricParameter) {
1467 ParametricParamDescriptor* param = desc.defineParametricParam(kParamColorCorrectToneRanges);
1468 assert(param);
1469 param->setLabel(kParamColorCorrectToneRangesLabel);
1470 param->setHint(kParamColorCorrectToneRangesHint);
1471
1472 // define it as two dimensional
1473 param->setDimension(2);
1474
1475 param->setDimensionLabel(kParamColorCorrectToneRangesDim0, 0);
1476 param->setDimensionLabel(kParamColorCorrectToneRangesDim1, 1);
1477
1478 // set the UI colour for each dimension
1479 const OfxRGBColourD shadow = {0.6, 0.4, 0.6};
1480 const OfxRGBColourD highlight = {0.8, 0.7, 0.6};
1481 param->setUIColour( 0, shadow );
1482 param->setUIColour( 1, highlight );
1483
1484 // set the min/max parametric range to 0..1
1485 param->setRange(0.0, 1.0);
1486 // set the default Y range to 0..1 for all dimensions
1487 param->setDimensionDisplayRange(0., 1., 0);
1488 param->setDimensionDisplayRange(0., 1., 1);
1489
1490 param->addControlPoint(0, // curve to set
1491 0.0, // time, ignored in this case, as we are not adding a key
1492 0.0, // parametric position, zero
1493 1.0, // value to be, 0
1494 false); // don't add a key
1495 param->addControlPoint(0, 0.0, 0.09, 0.0, false);
1496
1497 param->addControlPoint(1, 0.0, 0.5, 0.0, false);
1498 param->addControlPoint(1, 0.0, 1.0, 1.0, false);
1499 if (ranges) {
1500 ranges->addChild(*param);
1501 }
1502 }
1503 }
1504 {
1505 ChoiceParamDescriptor* param = desc.defineChoiceParam(kParamLuminanceMath);
1506 param->setLabel(kParamLuminanceMathLabel);
1507 param->setHint(kParamLuminanceMathHint);
1508 assert(param->getNOptions() == eLuminanceMathRec709);
1509 param->appendOption(kParamLuminanceMathOptionRec709);
1510 assert(param->getNOptions() == eLuminanceMathRec2020);
1511 param->appendOption(kParamLuminanceMathOptionRec2020);
1512 assert(param->getNOptions() == eLuminanceMathACESAP0);
1513 param->appendOption(kParamLuminanceMathOptionACESAP0);
1514 assert(param->getNOptions() == eLuminanceMathACESAP1);
1515 param->appendOption(kParamLuminanceMathOptionACESAP1);
1516 assert(param->getNOptions() == eLuminanceMathCcir601);
1517 param->appendOption(kParamLuminanceMathOptionCcir601);
1518 assert(param->getNOptions() == eLuminanceMathAverage);
1519 param->appendOption(kParamLuminanceMathOptionAverage);
1520 assert(param->getNOptions() == eLuminanceMathMaximum);
1521 param->appendOption(kParamLuminanceMathOptionMaximum);
1522 if (page) {
1523 page->addChild(*param);
1524 }
1525 }
1526 {
1527 BooleanParamDescriptor *param = desc.defineBooleanParam(kParamClampBlack);
1528 param->setLabel(kParamClampBlackLabel);
1529 param->setHint(kParamClampBlackHint);
1530 param->setDefault(true);
1531 param->setAnimates(true);
1532 param->setLayoutHint(eLayoutHintNoNewLine, 0);
1533 if (page) {
1534 page->addChild(*param);
1535 }
1536 }
1537 {
1538 BooleanParamDescriptor *param = desc.defineBooleanParam(kParamClampWhite);
1539 param->setLabel(kParamClampWhiteLabel);
1540 param->setHint(kParamClampWhiteHint);
1541 param->setDefault(false);
1542 param->setAnimates(true);
1543 if (page) {
1544 page->addChild(*param);
1545 }
1546 }
1547
1548 ofxsPremultDescribeParams(desc, page);
1549 ofxsMaskMixDescribeParams(desc, page);
1550 //std::cout << "describeInCotext! OK\n";
1551
1552 {
1553 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamPremultChanged);
1554 param->setDefault(false);
1555 param->setIsSecretAndDisabled(true);
1556 param->setAnimates(false);
1557 param->setEvaluateOnChange(false);
1558 if (page) {
1559 page->addChild(*param);
1560 }
1561 }
1562 } // ColorCorrectPluginFactory::describeInContext
1563
1564 ImageEffect*
createInstance(OfxImageEffectHandle handle,ContextEnum)1565 ColorCorrectPluginFactory::createInstance(OfxImageEffectHandle handle,
1566 ContextEnum /*context*/)
1567 {
1568 const ImageEffectHostDescription &gHostDescription = *getImageEffectHostDescription();
1569 const bool supportsParametricParameter = ( gHostDescription.supportsParametricParameter &&
1570 !(gHostDescription.hostName == "uk.co.thefoundry.nuke" &&
1571 8 <= gHostDescription.versionMajor && gHostDescription.versionMajor <= 10) ); // Nuke 8-10 are known to *not* support Parametric
1572
1573 return new ColorCorrectPlugin(handle, supportsParametricParameter);
1574 }
1575
1576 static ColorCorrectPluginFactory p(kPluginIdentifier, kPluginVersionMajor, kPluginVersionMinor);
1577 mRegisterPluginFactoryInstance(p)
1578
1579 OFXS_NAMESPACE_ANONYMOUS_EXIT
1580