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 HueCorrect and HueKeyer plugins
21 */
22
23 #include <cmath>
24 #include <cfloat> // DBL_MAX
25 #include <algorithm>
26
27 #ifdef __APPLE__
28 #include <OpenGL/gl.h>
29 #else
30 #ifdef _WIN32
31 #define WIN32_LEAN_AND_MEAN
32 #ifndef NOMINMAX
33 #define NOMINMAX
34 #endif
35 #include <windows.h>
36 #endif
37
38 #include <GL/gl.h>
39 #endif
40
41 #include "ofxsProcessing.H"
42 #include "ofxsMaskMix.h"
43 #include "ofxsCoords.h"
44 #include "ofxsLut.h"
45 #include "ofxsMacros.h"
46
47 using namespace OFX;
48
49 OFXS_NAMESPACE_ANONYMOUS_ENTER
50
51 #define kPluginName "HueCorrectOFX"
52 #define kPluginGrouping "Color"
53 #define kPluginDescription \
54 "Apply hue-dependent color adjustments using lookup curves.\n" \
55 "Hue and saturation are computed from the the source RGB values. Depending on the hue value, the various adjustment values are computed, and then applied:\n" \
56 "sat: saturation gain. This modification is applied last.\n" \
57 "lum: luminance gain\n" \
58 "red: red gain\n" \
59 "green: green gain\n" \
60 "blue: blue gain\n" \
61 "r_sup: red suppression. If r > min(g,b), r = min(g,b) + r_sup * (r-min(g,b))\n" \
62 "g_sup: green suppression\n" \
63 "b_sup: blue suppression\n" \
64 "sat_thrsh: if source saturation is below this value, do not apply the lum, red, green, blue gains. Above this value, apply gain progressively.\n" \
65 "\n" \
66 "The 'Luminance Mix' parameter may be used to restore partially or fully the original luminance (luminance is computed using the 'Luminance Math' parameter).\n" \
67 "\n" \
68 "See also: http://opticalenquiry.com/nuke/index.php?title=HueCorrect"
69
70 #define kPluginIdentifier "net.sf.openfx.HueCorrect"
71 #define kPluginVersionMajor 1 // Incrementing this number means that you have broken backwards compatibility of the plug-in.
72 #define kPluginVersionMinor 0 // Increment this when you have fixed a bug or made it faster.
73
74 #define kSupportsTiles 1
75 #define kSupportsMultiResolution 1
76 #define kSupportsRenderScale 1
77 #define kSupportsMultipleClipPARs false
78 #define kSupportsMultipleClipDepths false
79 #define kRenderThreadSafety eRenderFullySafe
80
81 #define kParamHue "hue"
82 #define kParamHueLabel "Hue Curves"
83 #define kParamHueHint "Hue-dependent adjustment lookup curves:\n" \
84 "sat: saturation gain. This modification is applied last.\n" \
85 "lum: luminance gain\n" \
86 "red: red gain\n" \
87 "green: green gain\n" \
88 "blue: blue gain\n" \
89 "r_sup: red suppression. If r > min(g,b), r = min(g,b) + r_sup * (r-min(g,b))\n" \
90 "g_sup: green suppression\n" \
91 "b_sup: blue suppression\n" \
92 "sat_thrsh: if source saturation is below this value, do not apply the lum, red, green, blue gains. Above this value, apply gain progressively."
93
94
95 #define kParamLuminanceMath "luminanceMath"
96 #define kParamLuminanceMathLabel "Luminance Math"
97 #define kParamLuminanceMathHint "Formula used to compute luminance from RGB values (only used by 'Set Master')."
98 #define kParamLuminanceMathOptionRec709 "Rec. 709", "Use Rec. 709 (0.2126r + 0.7152g + 0.0722b).", "rec709"
99 #define kParamLuminanceMathOptionRec2020 "Rec. 2020", "Use Rec. 2020 (0.2627r + 0.6780g + 0.0593b).", "rec2020"
100 #define kParamLuminanceMathOptionACESAP0 "ACES AP0", "Use ACES AP0 (0.3439664498r + 0.7281660966g + -0.0721325464b).", "acesap0"
101 #define kParamLuminanceMathOptionACESAP1 "ACES AP1", "Use ACES AP1 (0.2722287168r + 0.6740817658g + 0.0536895174b).", "acesap1"
102 #define kParamLuminanceMathOptionCcir601 "CCIR 601", "Use CCIR 601 (0.2989r + 0.5866g + 0.1145b).", "ccir601"
103 #define kParamLuminanceMathOptionAverage "Average", "Use average of r, g, b.", "average"
104 #define kParamLuminanceMathOptionMaximum "Max", "Use max or r, g, b.", "max"
105
106 enum LuminanceMathEnum
107 {
108 eLuminanceMathRec709,
109 eLuminanceMathRec2020,
110 eLuminanceMathACESAP0,
111 eLuminanceMathACESAP1,
112 eLuminanceMathCcir601,
113 eLuminanceMathAverage,
114 eLuminanceMathMaximum,
115 };
116
117 #define kParamClampBlack "clampBlack"
118 #define kParamClampBlackLabel "Clamp Black"
119 #define kParamClampBlackHint "All colors below 0 on output are set to 0."
120
121 #define kParamClampWhite "clampWhite"
122 #define kParamClampWhiteLabel "Clamp White"
123 #define kParamClampWhiteHint "All colors above 1 on output are set to 1."
124
125 #define kParamMixLuminanceEnable "mixLuminanceEnable"
126 #define kParamMixLuminanceEnableLabel "Mix Luminance"
127 #define kParamMixLuminanceEnableHint "Mix luminance"
128
129 #define kParamMixLuminance "mixLuminance"
130 #define kParamMixLuminanceLabel ""
131 #define kParamMixLuminanceHint "Mix luminance"
132
133 #define kParamPremultChanged "premultChanged"
134
135 #define kCurveSat 0
136 #define kCurveLum 1
137 #define kCurveRed 2
138 #define kCurveGreen 3
139 #define kCurveBlue 4
140 #define kCurveRSup 5
141 #define kCurveGSup 6
142 #define kCurveBSup 7
143 #define kCurveSatThrsh 8
144 #define kCurveNb 9
145
146
147 class HueCorrectProcessorBase
148 : public ImageProcessor
149 {
150 protected:
151 const Image *_srcImg;
152 const Image *_maskImg;
153 bool _doMasking;
154 LuminanceMathEnum _luminanceMath;
155 double _luminanceMix;
156 const bool _clampBlack;
157 const bool _clampWhite;
158 bool _premult;
159 int _premultChannel;
160 double _mix;
161 bool _maskInvert;
162
163 public:
HueCorrectProcessorBase(ImageEffect & instance,bool clampBlack,bool clampWhite)164 HueCorrectProcessorBase(ImageEffect &instance,
165 bool clampBlack,
166 bool clampWhite)
167 : ImageProcessor(instance)
168 , _srcImg(NULL)
169 , _maskImg(NULL)
170 , _doMasking(false)
171 , _luminanceMath(eLuminanceMathRec709)
172 , _luminanceMix(0.)
173 , _clampBlack(clampBlack)
174 , _clampWhite(clampWhite)
175 , _premult(false)
176 , _premultChannel(3)
177 , _mix(1.)
178 , _maskInvert(false)
179 {
180 }
181
setSrcImg(const Image * v)182 void setSrcImg(const Image *v) {_srcImg = v; }
183
setMaskImg(const Image * v,bool maskInvert)184 void setMaskImg(const Image *v,
185 bool maskInvert) { _maskImg = v; _maskInvert = maskInvert; }
186
doMasking(bool v)187 void doMasking(bool v) {_doMasking = v; }
188
setValues(LuminanceMathEnum luminanceMath,double luminanceMix,bool premult,int premultChannel,double mix)189 void setValues(LuminanceMathEnum luminanceMath,
190 double luminanceMix,
191 bool premult,
192 int premultChannel,
193 double mix)
194 {
195 _luminanceMath = luminanceMath;
196 _luminanceMix = luminanceMix;
197 _premult = premult;
198 _premultChannel = premultChannel;
199 _mix = mix;
200 }
201
202 protected:
203 // clamp for integer types
204 template<class PIX>
clamp(float value,int maxValue)205 float clamp(float value,
206 int maxValue)
207 {
208 return std::max( 0.f, std::min( value, float(maxValue) ) );
209 }
210
211 // clamp for integer types
212 template<class PIX>
clamp(double value,int maxValue)213 double clamp(double value,
214 int maxValue)
215 {
216 return std::max( 0., std::min( value, double(maxValue) ) );
217 }
218 };
219
220
221 // floats don't clamp except if _clampBlack or _clampWhite
222 template<>
223 float
clamp(float value,int maxValue)224 HueCorrectProcessorBase::clamp<float>(float value,
225 int maxValue)
226 {
227 assert(maxValue == 1.);
228 if ( _clampBlack && (value < 0.) ) {
229 value = 0.f;
230 } else if ( _clampWhite && (value > 1.0) ) {
231 value = 1.0f;
232 }
233
234 return value;
235 }
236
237 template<typename T>
238 T
luminance(T r,T g,T b,LuminanceMathEnum luminanceMath)239 luminance(T r,
240 T g,
241 T b,
242 LuminanceMathEnum luminanceMath)
243 {
244 switch (luminanceMath) {
245 case eLuminanceMathRec709:
246 default:
247
248 return Color::rgb709_to_y(r, g, b);
249
250 case eLuminanceMathRec2020: // https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2087-0-201510-I!!PDF-E.pdf
251
252 return Color::rgb2020_to_y(r, g, b);
253 case eLuminanceMathACESAP0: // https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#Converting_ACES_RGB_values_to_CIE_XYZ_values
254
255 return Color::rgbACESAP0_to_y(r, g, b);
256 case eLuminanceMathACESAP1: // https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#Converting_ACES_RGB_values_to_CIE_XYZ_values
257
258 return Color::rgbACESAP1_to_y(r, g, b);
259 case eLuminanceMathCcir601:
260
261 return (T)(0.2989f * r + 0.5866f * g + 0.1145f * b);
262 case eLuminanceMathAverage:
263
264 return (r + g + b) / 3;
265 case eLuminanceMathMaximum:
266
267 return std::max(std::max(r, g), b);
268 }
269 }
270
271 // template to do the processing.
272 // nbValues is the number of values in the LUT minus 1. For integer types, it should be the same as
273 // maxValue
274 template <class PIX, int nComponents, int maxValue, int nbValues>
275 class HueCorrectProcessor
276 : public HueCorrectProcessorBase
277 {
278 public:
279 // ctor
HueCorrectProcessor(ImageEffect & instance,const RenderArguments & args,ParametricParam * hueParam,bool clampBlack,bool clampWhite)280 HueCorrectProcessor(ImageEffect &instance,
281 const RenderArguments &args,
282 ParametricParam *hueParam,
283 bool clampBlack,
284 bool clampWhite)
285 : HueCorrectProcessorBase(instance, clampBlack, clampWhite)
286 , _hueParam(hueParam)
287 {
288 // build the LUT
289 assert(_hueParam);
290 _time = args.time;
291 for (int c = 0; c < kCurveNb; ++c) {
292 _hue[c].resize(nbValues + 1);
293 for (int position = 0; position <= nbValues; ++position) {
294 // position to evaluate the param at
295 double parametricPos = 6 * double(position) / nbValues;
296
297 // evaluate the parametric param
298 double value = _hueParam->getValue(c, _time, parametricPos);
299
300 // all the values (in HueCorrect) must be positive. We don't care if sat_thrsh goes above 1.
301 value = std::max(0., value);
302 // set that in the lut
303 _hue[c][position] = value;
304 }
305 }
306 }
307
308 private:
309 // and do some processing
multiThreadProcessImages(OfxRectI procWindow)310 void multiThreadProcessImages(OfxRectI procWindow)
311 {
312 assert(nComponents == 3 || nComponents == 4);
313 assert(_dstImg);
314 float tmpPix[4];
315 for (int y = procWindow.y1; y < procWindow.y2; y++) {
316 if ( _effect.abort() ) {
317 break;
318 }
319
320 PIX *dstPix = (PIX *) _dstImg->getPixelAddress(procWindow.x1, y);
321
322 for (int x = procWindow.x1; x < procWindow.x2; x++) {
323 const PIX *srcPix = (const PIX *) (_srcImg ? _srcImg->getPixelAddress(x, y) : 0);
324 float unpPix[4];
325 ofxsUnPremult<PIX, nComponents, maxValue>(srcPix, unpPix, _premult, _premultChannel);
326 // ofxsUnPremult outputs normalized data
327
328 float r = unpPix[0];
329 float g = unpPix[1];
330 float b = unpPix[2];
331 float l_in = 0.;
332 if (_luminanceMix > 0.) {
333 l_in = luminance(r, g, b, _luminanceMath);
334 }
335 float h, s, v;
336 Color::rgb_to_hsv( r, g, b, &h, &s, &v );
337 h = h * 6 + 1;
338 if (h > 6) {
339 h -= 6;
340 }
341 double sat = interpolate(kCurveSat, h);
342 double lum = interpolate(kCurveLum, h);
343 double red = interpolate(kCurveRed, h);
344 double green = interpolate(kCurveGreen, h);
345 double blue = interpolate(kCurveBlue, h);
346 double r_sup = interpolate(kCurveRSup, h);
347 double g_sup = interpolate(kCurveGSup, h);
348 double b_sup = interpolate(kCurveBSup, h);
349 float sat_thrsh = interpolate(kCurveSatThrsh, h);
350
351 if (r_sup != 1.) {
352 // If r > min(g,b), r = min(g,b) + r_sup * (r-min(g,b))
353 float m = std::min(g, b);
354 if (r > m) {
355 r = m + r_sup * (r - m);
356 }
357 }
358 if (g_sup != 1.) {
359 float m = std::min(r, b);
360 if (g > m) {
361 g = m + g_sup * (g - m);
362 }
363 }
364 if (b_sup != 1.) {
365 float m = std::min(r, g);
366 if (b > m) {
367 b = m + b_sup * (b - m);
368 }
369 }
370 if (s > sat_thrsh) {
371 // Get a smooth effect: identity at s=sat_thrsh, full if sat_thrsh = 0
372 r *= (float)((sat_thrsh * 1. + (s - sat_thrsh) * red * lum) / s); // red * lum
373 g *= (float)((sat_thrsh * 1. + (s - sat_thrsh) * green * lum) / s); // green * lum;
374 b *= (float)((sat_thrsh * 1. + (s - sat_thrsh) * blue * lum) / s); // blue * lum;
375 } else if (sat_thrsh == 0.) {
376 assert(s == 0.);
377 r *= (float)(red * lum); // red * lum
378 g *= (float)(green * lum); // green * lum;
379 b *= (float)(blue * lum); // blue * lum;
380 }
381 if (sat != 1.) {
382 float l_sat = luminance(r, g, b, _luminanceMath);
383 r = (float)((1. - sat) * l_sat + sat * r);
384 g = (float)((1. - sat) * l_sat + sat * g);
385 b = (float)((1. - sat) * l_sat + sat * b);
386 }
387 if (_luminanceMix > 0.) {
388 float l_out = luminance(r, g, b, _luminanceMath);
389 if (l_out <= 0.) {
390 r = g = b = l_in;
391 } else {
392 float f = (float)(1 + _luminanceMix * (l_in / l_out - 1.));
393 r *= f;
394 g *= f;
395 b *= f;
396 }
397 }
398
399 tmpPix[0] = clamp<float>(r, 1.);
400 tmpPix[1] = clamp<float>(g, 1.);
401 tmpPix[2] = clamp<float>(b, 1.);
402 tmpPix[3] = unpPix[3]; // alpha is left unchanged
403 for (int c = 0; c < nComponents; ++c) {
404 assert( !OFX::IsNaN(unpPix[c]) && !OFX::IsNaN(unpPix[c]) &&
405 !OFX::IsNaN(tmpPix[c]) && !OFX::IsNaN(tmpPix[c]) );
406 }
407
408 // ofxsPremultMaskMixPix expects normalized input
409 ofxsPremultMaskMixPix<PIX, nComponents, maxValue, true>(tmpPix, _premult, _premultChannel, x, y, srcPix, _doMasking, _maskImg, (float)_mix, _maskInvert, dstPix);
410 // increment the dst pixel
411 dstPix += nComponents;
412 }
413 }
414 } // multiThreadProcessImages
415
interpolate(int c,double value)416 double interpolate(int c, // the curve number
417 double value)
418 {
419 if ( (value < 0.) || (6. < value) ) {
420 // slow version
421 double ret = _hueParam->getValue(c, _time, value);
422
423 return ret;
424 } else {
425 double x = value / 6.;
426 int i = (int)(x * nbValues);
427 assert(0 <= i && i <= nbValues);
428 double alpha = std::max( 0., std::min(x * nbValues - i, 1.) );
429 double a = _hue[c][i];
430 double b = (i < nbValues) ? _hue[c][i + 1] : 0.f;
431
432 return a * (1.f - alpha) + b * alpha;
433 }
434 }
435
436 private:
437 std::vector<double> _hue[kCurveNb];
438 ParametricParam* _hueParam;
439 double _time;
440 };
441
442
443 ////////////////////////////////////////////////////////////////////////////////
444 /** @brief The plugin that does our work */
445 class HueCorrectPlugin
446 : public ImageEffect
447 {
448 public:
HueCorrectPlugin(OfxImageEffectHandle handle)449 HueCorrectPlugin(OfxImageEffectHandle handle)
450 : ImageEffect(handle)
451 , _dstClip(NULL)
452 , _srcClip(NULL)
453 , _maskClip(NULL)
454 , _luminanceMath(NULL)
455 , _premultChanged(NULL)
456 {
457 _dstClip = fetchClip(kOfxImageEffectOutputClipName);
458 assert( _dstClip && (!_dstClip->isConnected() || _dstClip->getPixelComponents() == ePixelComponentAlpha ||
459 _dstClip->getPixelComponents() == ePixelComponentRGB ||
460 _dstClip->getPixelComponents() == ePixelComponentRGBA) );
461 _srcClip = getContext() == eContextGenerator ? NULL : fetchClip(kOfxImageEffectSimpleSourceClipName);
462 assert( (!_srcClip && getContext() == eContextGenerator) ||
463 ( _srcClip && (!_srcClip->isConnected() || _srcClip->getPixelComponents() == ePixelComponentAlpha ||
464 _srcClip->getPixelComponents() == ePixelComponentRGB ||
465 _srcClip->getPixelComponents() == ePixelComponentRGBA) ) );
466
467 _maskClip = fetchClip(getContext() == eContextPaint ? "Brush" : "Mask");
468 assert(!_maskClip || !_maskClip->isConnected() || _maskClip->getPixelComponents() == ePixelComponentAlpha);
469 _hue = fetchParametricParam(kParamHue);
470 _luminanceMath = fetchChoiceParam(kParamLuminanceMath);
471 _luminanceMixEnable = fetchBooleanParam(kParamMixLuminanceEnable);
472 _luminanceMix = fetchDoubleParam(kParamMixLuminance);
473 assert(_luminanceMath);
474 _clampBlack = fetchBooleanParam(kParamClampBlack);
475 _clampWhite = fetchBooleanParam(kParamClampWhite);
476 assert(_clampBlack && _clampWhite);
477 _premult = fetchBooleanParam(kParamPremult);
478 _premultChannel = fetchChoiceParam(kParamPremultChannel);
479 assert(_premult && _premultChannel);
480 _mix = fetchDoubleParam(kParamMix);
481 _maskApply = ( ofxsMaskIsAlwaysConnected( OFX::getImageEffectHostDescription() ) && paramExists(kParamMaskApply) ) ? fetchBooleanParam(kParamMaskApply) : 0;
482 _maskInvert = fetchBooleanParam(kParamMaskInvert);
483 assert(_mix && _maskInvert);
484 _premultChanged = fetchBooleanParam(kParamPremultChanged);
485 assert(_premultChanged);
486 _luminanceMix->setEnabled( _luminanceMixEnable->getValue() );
487 }
488
489 private:
490 virtual void render(const RenderArguments &args) OVERRIDE FINAL;
491 virtual bool isIdentity(const IsIdentityArguments &args, Clip * &identityClip, double &identityTime, int& view, std::string& plane) OVERRIDE FINAL;
492
493 /** @brief called when a clip has just been changed in some way (a rewire maybe) */
494 virtual void changedClip(const InstanceChangedArgs &args, const std::string &clipName) OVERRIDE FINAL;
495
496 template <int nComponents>
497 void renderForComponents(const RenderArguments &args, BitDepthEnum dstBitDepth);
498
499 void setupAndProcess(HueCorrectProcessorBase &, const RenderArguments &args);
500
changedParam(const InstanceChangedArgs & args,const std::string & paramName)501 virtual void changedParam(const InstanceChangedArgs &args,
502 const std::string ¶mName) OVERRIDE FINAL
503 {
504 const double time = args.time;
505
506 if ( (paramName == kParamPremult) && (args.reason == eChangeUserEdit) ) {
507 _premultChanged->setValue(true);
508 }
509 if ( (paramName == kParamMixLuminanceEnable) && (args.reason == eChangeUserEdit) ) {
510 _luminanceMix->setEnabled( _luminanceMixEnable->getValueAtTime(time) );
511 }
512 } // changedParam
513
514 private:
515 Clip *_dstClip;
516 Clip *_srcClip;
517 Clip *_maskClip;
518 ParametricParam *_hue;
519 ChoiceParam* _luminanceMath;
520 BooleanParam* _luminanceMixEnable;
521 DoubleParam* _luminanceMix;
522 BooleanParam* _clampBlack;
523 BooleanParam* _clampWhite;
524 BooleanParam* _premult;
525 ChoiceParam* _premultChannel;
526 DoubleParam* _mix;
527 BooleanParam* _maskApply;
528 BooleanParam* _maskInvert;
529 BooleanParam* _premultChanged; // set to true the first time the user connects src
530 };
531
532
533 void
setupAndProcess(HueCorrectProcessorBase & processor,const RenderArguments & args)534 HueCorrectPlugin::setupAndProcess(HueCorrectProcessorBase &processor,
535 const RenderArguments &args)
536 {
537 const double time = args.time;
538
539 assert(_dstClip);
540 auto_ptr<Image> dst( _dstClip->fetchImage(time) );
541 if ( !dst.get() ) {
542 throwSuiteStatusException(kOfxStatFailed);
543 }
544 BitDepthEnum dstBitDepth = dst->getPixelDepth();
545 PixelComponentEnum dstComponents = dst->getPixelComponents();
546 if ( ( dstBitDepth != _dstClip->getPixelDepth() ) ||
547 ( dstComponents != _dstClip->getPixelComponents() ) ) {
548 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong depth or components");
549 throwSuiteStatusException(kOfxStatFailed);
550 }
551 if ( (dst->getRenderScale().x != args.renderScale.x) ||
552 ( dst->getRenderScale().y != args.renderScale.y) ||
553 ( ( dst->getField() != eFieldNone) /* for DaVinci Resolve */ && ( dst->getField() != args.fieldToRender) ) ) {
554 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
555 throwSuiteStatusException(kOfxStatFailed);
556 }
557 auto_ptr<const Image> src( ( _srcClip && _srcClip->isConnected() ) ?
558 _srcClip->fetchImage(time) : 0 );
559 if ( src.get() ) {
560 if ( (src->getRenderScale().x != args.renderScale.x) ||
561 ( src->getRenderScale().y != args.renderScale.y) ||
562 ( ( src->getField() != eFieldNone) /* for DaVinci Resolve */ && ( src->getField() != args.fieldToRender) ) ) {
563 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
564 throwSuiteStatusException(kOfxStatFailed);
565 }
566 BitDepthEnum srcBitDepth = src->getPixelDepth();
567 PixelComponentEnum srcComponents = src->getPixelComponents();
568 if ( (srcBitDepth != dstBitDepth) || (srcComponents != dstComponents) ) {
569 throwSuiteStatusException(kOfxStatErrImageFormat);
570 }
571 }
572 bool doMasking = ( ( !_maskApply || _maskApply->getValueAtTime(time) ) && _maskClip && _maskClip->isConnected() );
573 auto_ptr<const Image> mask(doMasking ? _maskClip->fetchImage(time) : 0);
574 if (doMasking) {
575 if ( mask.get() ) {
576 if ( (mask->getRenderScale().x != args.renderScale.x) ||
577 ( mask->getRenderScale().y != args.renderScale.y) ||
578 ( ( mask->getField() != eFieldNone) /* for DaVinci Resolve */ && ( mask->getField() != args.fieldToRender) ) ) {
579 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
580 throwSuiteStatusException(kOfxStatFailed);
581 }
582 }
583 bool maskInvert;
584 _maskInvert->getValueAtTime(time, maskInvert);
585 processor.doMasking(true);
586 processor.setMaskImg(mask.get(), maskInvert);
587 }
588
589 if ( src.get() && dst.get() ) {
590 BitDepthEnum srcBitDepth = src->getPixelDepth();
591 PixelComponentEnum srcComponents = src->getPixelComponents();
592 BitDepthEnum dstBitDepth = dst->getPixelDepth();
593 PixelComponentEnum dstComponents = dst->getPixelComponents();
594
595 // see if they have the same depths and bytes and all
596 if ( (srcBitDepth != dstBitDepth) || (srcComponents != dstComponents) ) {
597 throwSuiteStatusException(kOfxStatErrImageFormat);
598 }
599 }
600
601 processor.setDstImg( dst.get() );
602 processor.setSrcImg( src.get() );
603 processor.setRenderWindow(args.renderWindow);
604 LuminanceMathEnum luminanceMath = (LuminanceMathEnum)_luminanceMath->getValueAtTime(time);
605 bool luminanceMixEnable = _luminanceMixEnable->getValueAtTime(time);
606 double luminanceMix = luminanceMixEnable ? _luminanceMix->getValueAtTime(time) : 0;
607 bool premult = _premult->getValueAtTime(time);
608 int premultChannel = _premultChannel->getValueAtTime(time);
609 double mix = _mix->getValueAtTime(time);
610 processor.setValues(luminanceMath, luminanceMix, premult, premultChannel, mix);
611 processor.process();
612 } // HueCorrectPlugin::setupAndProcess
613
614 // the internal render function
615 template <int nComponents>
616 void
renderForComponents(const RenderArguments & args,BitDepthEnum dstBitDepth)617 HueCorrectPlugin::renderForComponents(const RenderArguments &args,
618 BitDepthEnum dstBitDepth)
619 {
620 const double time = args.time;
621 bool clampBlack = _clampBlack->getValueAtTime(time);
622 bool clampWhite = _clampWhite->getValueAtTime(time);
623
624 switch (dstBitDepth) {
625 case eBitDepthUByte: {
626 HueCorrectProcessor<unsigned char, nComponents, 255, 255> fred(*this, args, _hue, clampBlack, clampWhite);
627 setupAndProcess(fred, args);
628 break;
629 }
630 case eBitDepthUShort: {
631 HueCorrectProcessor<unsigned short, nComponents, 65535, 65535> fred(*this, args, _hue, clampBlack, clampWhite);
632 setupAndProcess(fred, args);
633 break;
634 }
635 case eBitDepthFloat: {
636 HueCorrectProcessor<float, nComponents, 1, 1023> fred(*this, args, _hue, clampBlack, clampWhite);
637 setupAndProcess(fred, args);
638 break;
639 }
640 default:
641 throwSuiteStatusException(kOfxStatErrUnsupported);
642 }
643 }
644
645 void
render(const RenderArguments & args)646 HueCorrectPlugin::render(const RenderArguments &args)
647 {
648 BitDepthEnum dstBitDepth = _dstClip->getPixelDepth();
649 PixelComponentEnum dstComponents = _dstClip->getPixelComponents();
650
651 assert( kSupportsMultipleClipPARs || !_srcClip || _srcClip->getPixelAspectRatio() == _dstClip->getPixelAspectRatio() );
652 assert( kSupportsMultipleClipDepths || !_srcClip || _srcClip->getPixelDepth() == _dstClip->getPixelDepth() );
653 if (dstComponents == ePixelComponentRGBA) {
654 renderForComponents<4>(args, dstBitDepth);
655 } else if (dstComponents == ePixelComponentRGB) {
656 renderForComponents<3>(args, dstBitDepth);
657 #ifdef OFX_EXTENSIONS_NATRON
658 //} else if (dstComponents == ePixelComponentXY) {
659 // renderForComponents<2>(args, dstBitDepth);
660 #endif
661 //} else {
662 // assert(dstComponents == ePixelComponentAlpha);
663 // renderForComponents<1>(args, dstBitDepth);
664 } else {
665 throwSuiteStatusException(kOfxStatErrFormat);
666 }
667 }
668
669 bool
isIdentity(const IsIdentityArguments & args,Clip * & identityClip,double &,int &,std::string &)670 HueCorrectPlugin::isIdentity(const IsIdentityArguments &args,
671 Clip * &identityClip,
672 double & /*identityTime*/
673 , int& /*view*/, std::string& /*plane*/)
674 {
675 const double time = args.time;
676 bool doMasking = ( ( !_maskApply || _maskApply->getValueAtTime(time) ) && _maskClip && _maskClip->isConnected() );
677
678 if (doMasking) {
679 bool maskInvert;
680 _maskInvert->getValueAtTime(time, maskInvert);
681 if (!maskInvert) {
682 OfxRectI maskRoD;
683 if (getImageEffectHostDescription()->supportsMultiResolution) {
684 // In Sony Catalyst Edit, clipGetRegionOfDefinition returns the RoD in pixels instead of canonical coordinates.
685 // In hosts that do not support multiResolution (e.g. Sony Catalyst Edit), all inputs have the same RoD anyway.
686 Coords::toPixelEnclosing(_maskClip->getRegionOfDefinition(time), args.renderScale, _maskClip->getPixelAspectRatio(), &maskRoD);
687 // effect is identity if the renderWindow doesn't intersect the mask RoD
688 if ( !Coords::rectIntersection<OfxRectI>(args.renderWindow, maskRoD, 0) ) {
689 identityClip = _srcClip;
690
691 return true;
692 }
693 }
694 }
695 }
696
697 return false;
698 }
699
700 void
changedClip(const InstanceChangedArgs & args,const std::string & clipName)701 HueCorrectPlugin::changedClip(const InstanceChangedArgs &args,
702 const std::string &clipName)
703 {
704 if ( (clipName == kOfxImageEffectSimpleSourceClipName) &&
705 _srcClip && _srcClip->isConnected() &&
706 !_premultChanged->getValue() &&
707 ( args.reason == eChangeUserEdit) ) {
708 if (_srcClip->getPixelComponents() != ePixelComponentRGBA) {
709 _premult->setValue(false);
710 } else {
711 switch ( _srcClip->getPreMultiplication() ) {
712 case eImageOpaque:
713 _premult->setValue(false);
714 break;
715 case eImagePreMultiplied:
716 _premult->setValue(true);
717 break;
718 case eImageUnPreMultiplied:
719 _premult->setValue(false);
720 break;
721 }
722 }
723 }
724 }
725
726 class HueCorrectInteract
727 : public ParamInteract
728 {
729 public:
HueCorrectInteract(OfxInteractHandle handle,ImageEffect * effect,const std::string & paramName)730 HueCorrectInteract(OfxInteractHandle handle,
731 ImageEffect* effect,
732 const std::string& paramName)
733 : ParamInteract(handle, effect)
734 , _hueParam(NULL)
735 , _xMin(0.)
736 , _xMax(0.)
737 , _yMin(0.)
738 , _yMax(0.)
739 {
740 _hueParam = effect->fetchParametricParam(paramName);
741 setColourPicking(true);
742 _hueParam->getRange(_xMin, _xMax);
743 _hueParam->getDimensionDisplayRange(0, _yMin, _yMax);
744 if ( (_yMin == 0.) && (_yMax == 0.) ) {
745 _yMax = 2.; // default for hosts that don't support displayrange
746 }
747 }
748
draw(const DrawArgs & args)749 virtual bool draw(const DrawArgs &args) OVERRIDE FINAL
750 {
751 //const double time = args.time;
752
753 // let us draw one slice every 8 pixels
754 const int sliceWidth = 8;
755 const float s = 1.;
756 const float v = 0.3;
757 int nbValues = args.pixelScale.x > 0 ? std::ceil( (_xMax - _xMin) / (sliceWidth * args.pixelScale.x) ) : 1;
758
759 glBegin (GL_TRIANGLE_STRIP);
760
761 for (int position = 0; position <= nbValues; ++position) {
762 // position to evaluate the param at
763 double parametricPos = _xMin + (_xMax - _xMin) * double(position) / nbValues;
764
765 // red is at parametricPos = 1
766 float h = (parametricPos - 1) / 6;
767 float r, g, b;
768 Color::hsv_to_rgb( h, s, v, &r, &g, &b );
769 glColor3f(r, g, b);
770 glVertex2f(parametricPos, _yMin);
771 glVertex2f(parametricPos, _yMax);
772 }
773
774 glEnd();
775
776 if (args.hasPickerColour) {
777 glLineWidth(1.5);
778 glBegin(GL_LINES);
779 {
780 float h, s, v;
781 Color::rgb_to_hsv( args.pickerColour.r, args.pickerColour.g, args.pickerColour.b, &h, &s, &v );
782 const OfxRGBColourD yellow = {1, 1, 0};
783 const OfxRGBColourD grey = {2. / 3., 2. / 3., 2. / 3.};
784 glColor3f(yellow.r, yellow.g, yellow.b);
785 // map [0,1] to [0,6]
786 h = _xMin + h * (_xMax - _xMin) + 1;
787 if (h > 6) {
788 h -= 6;
789 }
790 glVertex2f(h, _yMin);
791 glVertex2f(h, _yMax);
792 glColor3f(grey.r, grey.g, grey.b);
793 glVertex2f(_xMin, s);
794 glVertex2f(_xMax, s);
795 }
796 glEnd();
797 }
798
799 return true;
800 } // draw
801
~HueCorrectInteract()802 virtual ~HueCorrectInteract() {}
803
804 protected:
805 ParametricParam* _hueParam;
806 double _xMin, _xMax;
807 double _yMin, _yMax;
808 };
809
810 // We are lucky, there's only one hue param, so we need only one interact
811 // descriptor. If there were several, be would have to use a template parameter,
812 // as in propTester.cpp
813 class HueCorrectInteractDescriptor
814 : public DefaultParamInteractDescriptor<HueCorrectInteractDescriptor, HueCorrectInteract>
815 {
816 public:
describe()817 virtual void describe() OVERRIDE FINAL
818 {
819 setColourPicking(true);
820 }
821 };
822
823 mDeclarePluginFactory(HueCorrectPluginFactory, {ofxsThreadSuiteCheck();}, {});
824
825 void
describe(ImageEffectDescriptor & desc)826 HueCorrectPluginFactory::describe(ImageEffectDescriptor &desc)
827 {
828 desc.setLabel(kPluginName);
829 desc.setPluginGrouping(kPluginGrouping);
830 desc.setPluginDescription(kPluginDescription);
831
832 desc.addSupportedContext(eContextFilter);
833 desc.addSupportedContext(eContextPaint);
834 desc.addSupportedContext(eContextGeneral);
835 desc.addSupportedBitDepth(eBitDepthUByte);
836 desc.addSupportedBitDepth(eBitDepthUShort);
837 desc.addSupportedBitDepth(eBitDepthFloat);
838
839 desc.setSingleInstance(false);
840 desc.setHostFrameThreading(false);
841 desc.setSupportsMultiResolution(kSupportsMultiResolution);
842 desc.setSupportsTiles(kSupportsTiles);
843 desc.setTemporalClipAccess(false);
844 desc.setRenderTwiceAlways(false);
845 desc.setSupportsMultipleClipPARs(kSupportsMultipleClipPARs);
846 desc.setSupportsMultipleClipDepths(kSupportsMultipleClipDepths);
847 desc.setRenderThreadSafety(kRenderThreadSafety);
848 // returning an error here crashes Nuke
849 //if (!getImageEffectHostDescription()->supportsParametricParameter) {
850 // throwHostMissingSuiteException(kOfxParametricParameterSuite);
851 //}
852 #ifdef OFX_EXTENSIONS_NATRON
853 desc.setChannelSelector(ePixelComponentRGB);
854 #endif
855 }
856
857 void
describeInContext(ImageEffectDescriptor & desc,ContextEnum context)858 HueCorrectPluginFactory::describeInContext(ImageEffectDescriptor &desc,
859 ContextEnum context)
860 {
861 const ImageEffectHostDescription &gHostDescription = *getImageEffectHostDescription();
862 const bool supportsParametricParameter = ( gHostDescription.supportsParametricParameter &&
863 !(gHostDescription.hostName == "uk.co.thefoundry.nuke" &&
864 8 <= gHostDescription.versionMajor && gHostDescription.versionMajor <= 10) ); // Nuke 8-10 are known to *not* support Parametric
865
866 if (!supportsParametricParameter) {
867 throwHostMissingSuiteException(kOfxParametricParameterSuite);
868 }
869
870 ClipDescriptor *srcClip = desc.defineClip(kOfxImageEffectSimpleSourceClipName);
871 assert(srcClip);
872 srcClip->addSupportedComponent(ePixelComponentRGBA);
873 srcClip->addSupportedComponent(ePixelComponentRGB);
874 #ifdef OFX_EXTENSIONS_NATRON
875 //srcClip->addSupportedComponent(ePixelComponentXY);
876 #endif
877 //srcClip->addSupportedComponent(ePixelComponentAlpha);
878 srcClip->setTemporalClipAccess(false);
879 srcClip->setSupportsTiles(kSupportsTiles);
880 srcClip->setIsMask(false);
881
882 ClipDescriptor *dstClip = desc.defineClip(kOfxImageEffectOutputClipName);
883 assert(dstClip);
884 dstClip->addSupportedComponent(ePixelComponentRGBA);
885 dstClip->addSupportedComponent(ePixelComponentRGB);
886 #ifdef OFX_EXTENSIONS_NATRON
887 //dstClip->addSupportedComponent(ePixelComponentXY);
888 #endif
889 //dstClip->addSupportedComponent(ePixelComponentAlpha);
890 dstClip->setSupportsTiles(kSupportsTiles);
891
892 ClipDescriptor *maskClip = (context == eContextPaint) ? desc.defineClip("Brush") : desc.defineClip("Mask");
893 maskClip->addSupportedComponent(ePixelComponentAlpha);
894 maskClip->setTemporalClipAccess(false);
895 if (context != eContextPaint) {
896 maskClip->setOptional(true);
897 }
898 maskClip->setSupportsTiles(kSupportsTiles);
899 maskClip->setIsMask(true);
900
901 // make some pages and to things in
902 PageParamDescriptor *page = desc.definePageParam("Controls");
903
904 // define it
905 {
906 ParametricParamDescriptor* param = desc.defineParametricParam(kParamHue);
907 assert(param);
908 param->setPeriodic(true);
909 param->setLabel(kParamHueLabel);
910 param->setHint(kParamHueHint);
911 {
912 HueCorrectInteractDescriptor* interact = new HueCorrectInteractDescriptor;
913 param->setInteractDescriptor(interact);
914 }
915
916 // define it as three dimensional
917 param->setDimension(kCurveNb);
918
919 // label our dimensions are r/g/b
920 param->setDimensionLabel("sat", kCurveSat);
921 param->setDimensionLabel("lum", kCurveLum);
922 param->setDimensionLabel("red", kCurveRed);
923 param->setDimensionLabel("green", kCurveGreen);
924 param->setDimensionLabel("blue", kCurveBlue);
925 param->setDimensionLabel("r_sup", kCurveRSup);
926 param->setDimensionLabel("g_sup", kCurveGSup);
927 param->setDimensionLabel("b_sup", kCurveBSup);
928 param->setDimensionLabel("sat_thrsh", kCurveSatThrsh);
929
930 // set the UI colour for each dimension
931 //const OfxRGBColourD master = {0.9, 0.9, 0.9};
932 // the following are magic colors, they all have the same Rec709 luminance
933 const OfxRGBColourD red = {0.711519527404004, 0.164533420851110, 0.164533420851110}; //set red color to red curve
934 const OfxRGBColourD green = {0., 0.546986106552894, 0.}; //set green color to green curve
935 const OfxRGBColourD blue = {0.288480472595996, 0.288480472595996, 0.835466579148890}; //set blue color to blue curve
936 const OfxRGBColourD alpha = {0.398979, 0.398979, 0.398979};
937 param->setUIColour( kCurveSat, alpha );
938 param->setUIColour( kCurveLum, alpha );
939 param->setUIColour( kCurveRed, red );
940 param->setUIColour( kCurveGreen, green );
941 param->setUIColour( kCurveBlue, blue );
942 param->setUIColour( kCurveRSup, red );
943 param->setUIColour( kCurveGSup, green );
944 param->setUIColour( kCurveBSup, blue );
945 param->setUIColour( kCurveSatThrsh, alpha );
946
947 // set the min/max parametric range to 0..6
948 param->setRange(0.0, 6.0);
949 // set the default Y range to 0..1 for all dimensions
950 param->setDimensionDisplayRange(0., 1., kCurveSat);
951 param->setDimensionDisplayRange(0., 1., kCurveLum);
952 param->setDimensionDisplayRange(0., 1., kCurveRed);
953 param->setDimensionDisplayRange(0., 1., kCurveGreen);
954 param->setDimensionDisplayRange(0., 1., kCurveBlue);
955 param->setDimensionDisplayRange(0., 1., kCurveRSup);
956 param->setDimensionDisplayRange(0., 1., kCurveGSup);
957 param->setDimensionDisplayRange(0., 1., kCurveBSup);
958 param->setDimensionDisplayRange(0., 1., kCurveSatThrsh);
959
960
961 int plast = param->supportsPeriodic() ? 5 : 6;
962 // set a default curve
963 for (int c = 0; c < kCurveNb; ++c) {
964 // minimum/maximum: are these supported by OpenFX?
965 param->setDimensionRange(0., c == kCurveSatThrsh ? 1. : DBL_MAX, c);
966 param->setDimensionDisplayRange(0., 2., c);
967 for (int p = 0; p <= plast; ++p) {
968 // add a control point at p
969 param->addControlPoint(c, // curve to set
970 0.0, // time, ignored in this case, as we are not adding a key
971 p, // parametric position, zero
972 (c == kCurveSatThrsh) ? 0. : 1., // value to be
973 false); // don't add a key
974 }
975 }
976
977 if (page) {
978 page->addChild(*param);
979 }
980 }
981 {
982 ChoiceParamDescriptor* param = desc.defineChoiceParam(kParamLuminanceMath);
983 param->setLabel(kParamLuminanceMathLabel);
984 param->setHint(kParamLuminanceMathHint);
985 param->setEvaluateOnChange(false); // WARNING: RENDER IS NOT AFFECTED BY THIS OPTION IN THIS PLUGIN
986 assert(param->getNOptions() == eLuminanceMathRec709);
987 param->appendOption(kParamLuminanceMathOptionRec709);
988 assert(param->getNOptions() == eLuminanceMathRec2020);
989 param->appendOption(kParamLuminanceMathOptionRec2020);
990 assert(param->getNOptions() == eLuminanceMathACESAP0);
991 param->appendOption(kParamLuminanceMathOptionACESAP0);
992 assert(param->getNOptions() == eLuminanceMathACESAP1);
993 param->appendOption(kParamLuminanceMathOptionACESAP1);
994 assert(param->getNOptions() == eLuminanceMathCcir601);
995 param->appendOption(kParamLuminanceMathOptionCcir601);
996 assert(param->getNOptions() == eLuminanceMathAverage);
997 param->appendOption(kParamLuminanceMathOptionAverage);
998 assert(param->getNOptions() == eLuminanceMathMaximum);
999 param->appendOption(kParamLuminanceMathOptionMaximum);
1000 if (page) {
1001 page->addChild(*param);
1002 }
1003 }
1004 {
1005 BooleanParamDescriptor *param = desc.defineBooleanParam(kParamClampBlack);
1006 param->setLabel(kParamClampBlackLabel);
1007 param->setHint(kParamClampBlackHint);
1008 param->setDefault(false);
1009 param->setAnimates(true);
1010 param->setLayoutHint(eLayoutHintNoNewLine, 0);
1011 if (page) {
1012 page->addChild(*param);
1013 }
1014 }
1015 {
1016 BooleanParamDescriptor *param = desc.defineBooleanParam(kParamClampWhite);
1017 param->setLabel(kParamClampWhiteLabel);
1018 param->setHint(kParamClampWhiteHint);
1019 param->setDefault(false);
1020 param->setAnimates(true);
1021 if (page) {
1022 page->addChild(*param);
1023 }
1024 }
1025
1026 ofxsPremultDescribeParams(desc, page);
1027 {
1028 BooleanParamDescriptor *param = desc.defineBooleanParam(kParamMixLuminanceEnable);
1029 param->setLabel(kParamMixLuminanceEnableLabel);
1030 param->setHint(kParamMixLuminanceEnableHint);
1031 param->setDefault(true);
1032 param->setAnimates(false);
1033 param->setLayoutHint(eLayoutHintNoNewLine, 1);
1034 if (page) {
1035 page->addChild(*param);
1036 }
1037 }
1038 {
1039 DoubleParamDescriptor *param = desc.defineDoubleParam(kParamMixLuminance);
1040 param->setLabel(kParamMixLuminanceLabel);
1041 param->setHint(kParamMixLuminanceHint);
1042 param->setDefault(0.);
1043 param->setRange(0., 1.);
1044 param->setDisplayRange(0., 1.);
1045 param->setAnimates(true);
1046 if (page) {
1047 page->addChild(*param);
1048 }
1049 }
1050
1051 ofxsMaskMixDescribeParams(desc, page);
1052
1053 {
1054 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamPremultChanged);
1055 param->setDefault(false);
1056 param->setIsSecretAndDisabled(true);
1057 param->setAnimates(false);
1058 param->setEvaluateOnChange(false);
1059 if (page) {
1060 page->addChild(*param);
1061 }
1062 }
1063 } // HueCorrectPluginFactory::describeInContext
1064
1065 ImageEffect*
createInstance(OfxImageEffectHandle handle,ContextEnum)1066 HueCorrectPluginFactory::createInstance(OfxImageEffectHandle handle,
1067 ContextEnum /*context*/)
1068 {
1069 return new HueCorrectPlugin(handle);
1070 }
1071
1072 static HueCorrectPluginFactory p(kPluginIdentifier, kPluginVersionMajor, kPluginVersionMinor);
1073 mRegisterPluginFactoryInstance(p)
1074
1075
1076 //////////////////////////////////////////////////////////////////////////////////
1077 //////////////////////////////////////////////////////////////////////////////////
1078 //////////////////////////////////////////////////////////////////////////////////
1079 //////////////////////////////////////////////////////////////////////////////////
1080 //
1081 // HueKeyer
1082 //
1083 //////////////////////////////////////////////////////////////////////////////////
1084
1085 #define kPluginKeyerName "HueKeyerOFX"
1086 #define kPluginKeyerGrouping "Keyer"
1087 #define kPluginKeyerDescription \
1088 "Compute a key depending on hue value.\n" \
1089 "Hue and saturation are computed from the the source RGB values. Depending on the hue value, the various adjustment values are computed, and then applied:\n" \
1090 "amount: output transparency for the given hue (amount=1 means alpha=0).\n" \
1091 "sat_thrsh: if source saturation is below this value, the output transparency is gradually decreased."
1092
1093 #define kPluginKeyerIdentifier "net.sf.openfx.HueKeyer"
1094
1095 #define kParamKeyerHue "hue"
1096 #define kParamKeyerHueLabel "Hue Curves"
1097 #define kParamKeyerHueHint "Hue-dependent alpha lookup curves:\n" \
1098 "amount: transparency (1-alpha) amount for the given hue\n" \
1099 "sat_thrsh: if source saturation is below this value, transparency is decreased progressively."
1100
1101 #define kCurveKeyerAmount 0
1102 #define kCurveKeyerSatThrsh 1
1103 #define kCurveKeyerNb 2
1104
1105
1106 class HueKeyerProcessorBase
1107 : public ImageProcessor
1108 {
1109 protected:
1110 const Image *_srcImg;
1111
1112 public:
HueKeyerProcessorBase(ImageEffect & instance)1113 HueKeyerProcessorBase(ImageEffect &instance)
1114 : ImageProcessor(instance)
1115 , _srcImg(NULL)
1116 {
1117 }
1118
setSrcImg(const Image * v)1119 void setSrcImg(const Image *v) {_srcImg = v; }
1120 };
1121
1122 template<class PIX, int maxValue>
1123 static float
sampleToFloat(PIX value)1124 sampleToFloat(PIX value)
1125 {
1126 return (maxValue == 1) ? value : (value / (float)maxValue);
1127 }
1128
1129 template<class PIX, int maxValue>
1130 static PIX
floatToSample(float value)1131 floatToSample(float value)
1132 {
1133 if (maxValue == 1) {
1134 return PIX(value);
1135 }
1136 if (value <= 0) {
1137 return 0;
1138 } else if (value >= 1.) {
1139 return maxValue;
1140 }
1141
1142 return PIX(value * maxValue + 0.5f);
1143 }
1144
1145 template<class PIX, int maxValue>
1146 static PIX
floatToSample(double value)1147 floatToSample(double value)
1148 {
1149 if (maxValue == 1) {
1150 return PIX(value);
1151 }
1152 if (value <= 0) {
1153 return 0;
1154 } else if (value >= 1.) {
1155 return maxValue;
1156 }
1157
1158 return PIX(value * maxValue + 0.5);
1159 }
1160
1161 template <class PIX, int nComponents, int maxValue, int nbValues>
1162 class HueKeyerProcessor
1163 : public HueKeyerProcessorBase
1164 {
1165 private:
1166 std::vector<double> _hue[kCurveKeyerNb];
1167 ParametricParam* _hueParam;
1168 double _time;
1169
1170 public:
1171 // ctor
HueKeyerProcessor(ImageEffect & instance,const RenderArguments & args,ParametricParam * hueParam)1172 HueKeyerProcessor(ImageEffect &instance,
1173 const RenderArguments &args,
1174 ParametricParam *hueParam)
1175 : HueKeyerProcessorBase(instance)
1176 , _hueParam(hueParam)
1177 {
1178 assert(nComponents == 4);
1179 // build the LUT
1180 assert(_hueParam);
1181 _time = args.time;
1182 for (int c = 0; c < kCurveKeyerNb; ++c) {
1183 _hue[c].resize(nbValues + 1);
1184 for (int position = 0; position <= nbValues; ++position) {
1185 // position to evaluate the param at
1186 double parametricPos = 6 * double(position) / nbValues;
1187
1188 // evaluate the parametric param
1189 double value = _hueParam->getValue(c, _time, parametricPos);
1190
1191 // all the values (in HueKeyer) must be positive. We don't care if sat_thrsh goes above 1.
1192 value = std::max(0., value);
1193 // set that in the lut
1194 _hue[c][position] = value;
1195 }
1196 }
1197 }
1198
1199 private:
1200 // and do some processing
multiThreadProcessImages(OfxRectI procWindow)1201 void multiThreadProcessImages(OfxRectI procWindow)
1202 {
1203 assert(nComponents == 4);
1204 assert(_dstImg);
1205 for (int y = procWindow.y1; y < procWindow.y2; y++) {
1206 if ( _effect.abort() ) {
1207 break;
1208 }
1209
1210 PIX *dstPix = (PIX *) _dstImg->getPixelAddress(procWindow.x1, y);
1211
1212 for (int x = procWindow.x1; x < procWindow.x2; x++) {
1213 const PIX *srcPix = (const PIX *) (_srcImg ? _srcImg->getPixelAddress(x, y) : 0);
1214 if (!srcPix) {
1215 std::fill( dstPix, dstPix + 3, PIX() );
1216 dstPix[3] = maxValue;
1217 } else {
1218 std::copy(srcPix, srcPix + 3, dstPix);
1219 float r = sampleToFloat<PIX, maxValue>(srcPix[0]);
1220 float g = sampleToFloat<PIX, maxValue>(srcPix[1]);
1221 float b = sampleToFloat<PIX, maxValue>(srcPix[2]);
1222 float h, s, v;
1223 Color::rgb_to_hsv( r, g, b, &h, &s, &v );
1224 h = h * 6 + 1;
1225 if (h > 6) {
1226 h -= 6;
1227 }
1228 double amount = interpolate(kCurveKeyerAmount, h);
1229 double sat_thrsh = interpolate(kCurveKeyerSatThrsh, h);
1230 float a = 0.;
1231 if (s == 0) {
1232 // saturation is 0, hue is undetermined
1233 a = 0.;
1234 } else if (s >= sat_thrsh) {
1235 a = amount;
1236 } else {
1237 a = amount * s / sat_thrsh;
1238 }
1239 std::copy(srcPix, srcPix + 3, dstPix);
1240 dstPix[3] = floatToSample<PIX, maxValue>(1. - a);
1241 }
1242 // increment the dst pixel
1243 dstPix += nComponents;
1244 }
1245 }
1246 }
1247
interpolate(int c,double value)1248 double interpolate(int c, // the curve number
1249 double value)
1250 {
1251 if ( (value < 0.) || (6. < value) ) {
1252 // slow version
1253 double ret = _hueParam->getValue(c, _time, value);
1254
1255 return ret;
1256 } else {
1257 double x = value / 6.;
1258 int i = (int)(x * nbValues);
1259 assert(0 <= i && i <= nbValues);
1260 double alpha = std::max( 0., std::min(x * nbValues - i, 1.) );
1261 double a = _hue[c][i];
1262 double b = (i < nbValues) ? _hue[c][i + 1] : 0.f;
1263
1264 return a * (1.f - alpha) + b * alpha;
1265 }
1266 }
1267 };
1268
1269 ////////////////////////////////////////////////////////////////////////////////
1270 /** @brief The plugin that does our work */
1271 class HueKeyerPlugin
1272 : public ImageEffect
1273 {
1274 public:
HueKeyerPlugin(OfxImageEffectHandle handle)1275 HueKeyerPlugin(OfxImageEffectHandle handle)
1276 : ImageEffect(handle)
1277 , _dstClip(NULL)
1278 , _srcClip(NULL)
1279 {
1280 _dstClip = fetchClip(kOfxImageEffectOutputClipName);
1281 assert( _dstClip && (!_dstClip->isConnected() || _dstClip->getPixelComponents() == ePixelComponentAlpha ||
1282 _dstClip->getPixelComponents() == ePixelComponentRGB ||
1283 _dstClip->getPixelComponents() == ePixelComponentRGBA) );
1284 _srcClip = getContext() == eContextGenerator ? NULL : fetchClip(kOfxImageEffectSimpleSourceClipName);
1285 assert( (!_srcClip && getContext() == eContextGenerator) ||
1286 ( _srcClip && (!_srcClip->isConnected() || _srcClip->getPixelComponents() == ePixelComponentAlpha ||
1287 _srcClip->getPixelComponents() == ePixelComponentRGB ||
1288 _srcClip->getPixelComponents() == ePixelComponentRGBA) ) );
1289
1290 _hue = fetchParametricParam(kParamKeyerHue);
1291 }
1292
1293 private:
1294 virtual void render(const RenderArguments &args) OVERRIDE FINAL;
1295
1296 template <int nComponents>
1297 void renderForComponents(const RenderArguments &args, BitDepthEnum dstBitDepth);
1298
1299 void setupAndProcess(HueKeyerProcessorBase &, const RenderArguments &args);
1300
1301 private:
1302 Clip *_dstClip;
1303 Clip *_srcClip;
1304 ParametricParam *_hue;
1305 };
1306
1307
1308 void
setupAndProcess(HueKeyerProcessorBase & processor,const RenderArguments & args)1309 HueKeyerPlugin::setupAndProcess(HueKeyerProcessorBase &processor,
1310 const RenderArguments &args)
1311 {
1312 const double time = args.time;
1313
1314 assert(_dstClip);
1315 auto_ptr<Image> dst( _dstClip->fetchImage(time) );
1316 if ( !dst.get() ) {
1317 throwSuiteStatusException(kOfxStatFailed);
1318 }
1319 BitDepthEnum dstBitDepth = dst->getPixelDepth();
1320 PixelComponentEnum dstComponents = dst->getPixelComponents();
1321 if ( ( dstBitDepth != _dstClip->getPixelDepth() ) ||
1322 ( dstComponents != _dstClip->getPixelComponents() ) ) {
1323 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong depth or components");
1324 throwSuiteStatusException(kOfxStatFailed);
1325 }
1326 if ( (dst->getRenderScale().x != args.renderScale.x) ||
1327 ( dst->getRenderScale().y != args.renderScale.y) ||
1328 ( ( dst->getField() != eFieldNone) /* for DaVinci Resolve */ && ( dst->getField() != args.fieldToRender) ) ) {
1329 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
1330 throwSuiteStatusException(kOfxStatFailed);
1331 }
1332 auto_ptr<const Image> src( ( _srcClip && _srcClip->isConnected() ) ?
1333 _srcClip->fetchImage(time) : 0 );
1334 if ( src.get() ) {
1335 if ( (src->getRenderScale().x != args.renderScale.x) ||
1336 ( src->getRenderScale().y != args.renderScale.y) ||
1337 ( ( src->getField() != eFieldNone) /* for DaVinci Resolve */ && ( src->getField() != args.fieldToRender) ) ) {
1338 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
1339 throwSuiteStatusException(kOfxStatFailed);
1340 }
1341 BitDepthEnum srcBitDepth = src->getPixelDepth();
1342 PixelComponentEnum srcComponents = src->getPixelComponents();
1343 if ( (srcBitDepth != dstBitDepth) || (srcComponents != dstComponents) ) {
1344 throwSuiteStatusException(kOfxStatErrImageFormat);
1345 }
1346 }
1347
1348 if ( src.get() && dst.get() ) {
1349 BitDepthEnum srcBitDepth = src->getPixelDepth();
1350 PixelComponentEnum srcComponents = src->getPixelComponents();
1351 BitDepthEnum dstBitDepth = dst->getPixelDepth();
1352 PixelComponentEnum dstComponents = dst->getPixelComponents();
1353
1354 // see if they have the same depths and bytes and all
1355 if ( (srcBitDepth != dstBitDepth) || (srcComponents != dstComponents) ) {
1356 throwSuiteStatusException(kOfxStatErrImageFormat);
1357 }
1358 }
1359
1360 processor.setDstImg( dst.get() );
1361 processor.setSrcImg( src.get() );
1362 processor.setRenderWindow(args.renderWindow);
1363 processor.process();
1364 } // HueKeyerPlugin::setupAndProcess
1365
1366 // the internal render function
1367 template <int nComponents>
1368 void
renderForComponents(const RenderArguments & args,BitDepthEnum dstBitDepth)1369 HueKeyerPlugin::renderForComponents(const RenderArguments &args,
1370 BitDepthEnum dstBitDepth)
1371 {
1372 switch (dstBitDepth) {
1373 case eBitDepthUByte: {
1374 HueKeyerProcessor<unsigned char, nComponents, 255, 255> fred(*this, args, _hue);
1375 setupAndProcess(fred, args);
1376 break;
1377 }
1378 case eBitDepthUShort: {
1379 HueKeyerProcessor<unsigned short, nComponents, 65535, 65535> fred(*this, args, _hue);
1380 setupAndProcess(fred, args);
1381 break;
1382 }
1383 case eBitDepthFloat: {
1384 HueKeyerProcessor<float, nComponents, 1, 1023> fred(*this, args, _hue);
1385 setupAndProcess(fred, args);
1386 break;
1387 }
1388 default:
1389 throwSuiteStatusException(kOfxStatErrUnsupported);
1390 }
1391 }
1392
1393 void
render(const RenderArguments & args)1394 HueKeyerPlugin::render(const RenderArguments &args)
1395 {
1396 BitDepthEnum dstBitDepth = _dstClip->getPixelDepth();
1397 PixelComponentEnum dstComponents = _dstClip->getPixelComponents();
1398
1399 assert( kSupportsMultipleClipPARs || !_srcClip || _srcClip->getPixelAspectRatio() == _dstClip->getPixelAspectRatio() );
1400 assert( kSupportsMultipleClipDepths || !_srcClip || _srcClip->getPixelDepth() == _dstClip->getPixelDepth() );
1401 if (dstComponents == ePixelComponentRGBA) {
1402 renderForComponents<4>(args, dstBitDepth);
1403 } else {
1404 throwSuiteStatusException(kOfxStatErrFormat);
1405 }
1406 }
1407
1408 mDeclarePluginFactory(HueKeyerPluginFactory, {ofxsThreadSuiteCheck();}, {});
1409 void
describe(ImageEffectDescriptor & desc)1410 HueKeyerPluginFactory::describe(ImageEffectDescriptor &desc)
1411 {
1412 desc.setLabel(kPluginKeyerName);
1413 desc.setPluginGrouping(kPluginKeyerGrouping);
1414 desc.setPluginDescription(kPluginKeyerDescription);
1415
1416 desc.addSupportedContext(eContextFilter);
1417 //desc.addSupportedContext(eContextPaint);
1418 desc.addSupportedContext(eContextGeneral);
1419 desc.addSupportedBitDepth(eBitDepthUByte);
1420 desc.addSupportedBitDepth(eBitDepthUShort);
1421 desc.addSupportedBitDepth(eBitDepthFloat);
1422
1423 desc.setSingleInstance(false);
1424 desc.setHostFrameThreading(false);
1425 desc.setSupportsMultiResolution(kSupportsMultiResolution);
1426 desc.setSupportsTiles(kSupportsTiles);
1427 desc.setTemporalClipAccess(false);
1428 desc.setRenderTwiceAlways(false);
1429 desc.setSupportsMultipleClipPARs(kSupportsMultipleClipPARs);
1430 desc.setSupportsMultipleClipDepths(kSupportsMultipleClipDepths);
1431 desc.setRenderThreadSafety(kRenderThreadSafety);
1432 // returning an error here crashes Nuke
1433 //if (!getImageEffectHostDescription()->supportsParametricParameter) {
1434 // throwHostMissingSuiteException(kOfxParametricParameterSuite);
1435 //}
1436 #ifdef OFX_EXTENSIONS_NATRON
1437 desc.setChannelSelector(ePixelComponentNone);
1438 #endif
1439 }
1440
1441 void
describeInContext(ImageEffectDescriptor & desc,ContextEnum)1442 HueKeyerPluginFactory::describeInContext(ImageEffectDescriptor &desc,
1443 ContextEnum /*context*/)
1444 {
1445 const ImageEffectHostDescription &gHostDescription = *getImageEffectHostDescription();
1446 const bool supportsParametricParameter = ( gHostDescription.supportsParametricParameter &&
1447 !(gHostDescription.hostName == "uk.co.thefoundry.nuke" &&
1448 8 <= gHostDescription.versionMajor && gHostDescription.versionMajor <= 10) ); // Nuke 8-10 are known to *not* support Parametric
1449
1450 if (!supportsParametricParameter) {
1451 throwHostMissingSuiteException(kOfxParametricParameterSuite);
1452 }
1453
1454 ClipDescriptor *srcClip = desc.defineClip(kOfxImageEffectSimpleSourceClipName);
1455 assert(srcClip);
1456 srcClip->addSupportedComponent(ePixelComponentRGBA);
1457 //srcClip->addSupportedComponent(ePixelComponentRGB);
1458 #ifdef OFX_EXTENSIONS_NATRON
1459 //srcClip->addSupportedComponent(ePixelComponentXY);
1460 #endif
1461 //srcClip->addSupportedComponent(ePixelComponentAlpha);
1462 srcClip->setTemporalClipAccess(false);
1463 srcClip->setSupportsTiles(kSupportsTiles);
1464 srcClip->setIsMask(false);
1465
1466 ClipDescriptor *dstClip = desc.defineClip(kOfxImageEffectOutputClipName);
1467 assert(dstClip);
1468 dstClip->addSupportedComponent(ePixelComponentRGBA);
1469 //dstClip->addSupportedComponent(ePixelComponentRGB);
1470 #ifdef OFX_EXTENSIONS_NATRON
1471 //dstClip->addSupportedComponent(ePixelComponentXY);
1472 #endif
1473 //dstClip->addSupportedComponent(ePixelComponentAlpha);
1474 dstClip->setSupportsTiles(kSupportsTiles);
1475
1476
1477 // make some pages and to things in
1478 PageParamDescriptor *page = desc.definePageParam("Controls");
1479
1480 // define it
1481 {
1482 ParametricParamDescriptor* param = desc.defineParametricParam(kParamKeyerHue);
1483 assert(param);
1484 param->setPeriodic(true);
1485 param->setLabel(kParamKeyerHueLabel);
1486 param->setHint(kParamKeyerHueHint);
1487 {
1488 HueCorrectInteractDescriptor* interact = new HueCorrectInteractDescriptor;
1489 param->setInteractDescriptor(interact);
1490 }
1491
1492 // define it as three dimensional
1493 param->setDimension(kCurveKeyerNb);
1494
1495 // label our dimensions are r/g/b
1496 param->setDimensionLabel("amount", kCurveKeyerAmount);
1497 param->setDimensionLabel("sat_thrsh", kCurveKeyerSatThrsh);
1498
1499 // set the UI colour for each dimension
1500 //const OfxRGBColourD master = {0.9, 0.9, 0.9};
1501 // the following are magic colors, they all have the same Rec709 luminance
1502 //const OfxRGBColourD red = {0.711519527404004, 0.164533420851110, 0.164533420851110}; //set red color to red curve
1503 //const OfxRGBColourD green = {0., 0.546986106552894, 0.}; //set green color to green curve
1504 //const OfxRGBColourD blue = {0.288480472595996, 0.288480472595996, 0.835466579148890}; //set blue color to blue curve
1505 const OfxRGBColourD alpha = {0.398979, 0.398979, 0.398979};
1506 const OfxRGBColourD yellow = {0.711519527404004, 0.711519527404004, 0.164533420851110};
1507 param->setUIColour( kCurveKeyerAmount, alpha );
1508 param->setUIColour( kCurveKeyerSatThrsh, yellow );
1509
1510 // set the min/max parametric range to 0..6
1511 param->setRange(0.0, 6.0);
1512 // set the default Y range to 0..1 for all dimensions
1513 param->setDimensionDisplayRange(0., 1., kCurveKeyerAmount);
1514 param->setDimensionDisplayRange(0., 1., kCurveKeyerSatThrsh);
1515
1516
1517 int plast = param->supportsPeriodic() ? 5 : 6;
1518 // set a default curve
1519 for (int c = 0; c < kCurveKeyerNb; ++c) {
1520 // minimum/maximum: are these supported by OpenFX?
1521 param->setDimensionRange(0., 1., c);
1522 param->setDimensionDisplayRange(0., 1., c);
1523 for (int p = 0; p <= plast; ++p) {
1524 // add a control point at p
1525 param->addControlPoint(c, // curve to set
1526 0.0, // time, ignored in this case, as we are not adding a key
1527 p, // parametric position, zero
1528 (c == kCurveKeyerSatThrsh) ? 0.1 : (double)(p == 3 || p == 4), // value to be
1529 false); // don't add a key
1530 }
1531 }
1532
1533 if (page) {
1534 page->addChild(*param);
1535 }
1536 }
1537 } // HueKeyerPluginFactory::describeInContext
1538
1539 ImageEffect*
createInstance(OfxImageEffectHandle handle,ContextEnum)1540 HueKeyerPluginFactory::createInstance(OfxImageEffectHandle handle,
1541 ContextEnum /*context*/)
1542 {
1543 return new HueKeyerPlugin(handle);
1544 }
1545
1546 static HueKeyerPluginFactory p1(kPluginKeyerIdentifier, kPluginVersionMajor, kPluginVersionMinor);
1547 mRegisterPluginFactoryInstance(p1)
1548
1549 OFXS_NAMESPACE_ANONYMOUS_EXIT
1550