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 CImgDistance plugin.
21  */
22 
23 #include <memory>
24 #include <cmath>
25 #include <cstring>
26 #include <algorithm>
27 #if defined(_WIN32) || defined(__WIN32__) || defined(WIN32)
28 #include <windows.h>
29 #endif
30 
31 #include "ofxsProcessing.H"
32 #include "ofxsMaskMix.h"
33 #include "ofxsMacros.h"
34 #include "ofxsCoords.h"
35 #include "ofxsCopier.h"
36 
37 #include "CImgFilter.h"
38 
39 using namespace OFX;
40 using namespace cimg_library;
41 
42 using std::min; using std::max; using std::floor; using std::ceil; using std::sqrt;
43 
44 OFXS_NAMESPACE_ANONYMOUS_ENTER
45 
46 #ifdef DEBUG
47 #define EXPERIMENTAL // turn on experimental not-for-everyone code
48 #endif
49 
50 #define kPluginName          "DistanceCImg"
51 #define kPluginGrouping      "Filter"
52 #define kPluginDescription \
53 "Compute at each pixel the distance to pixels that have a value of zero.\n" \
54 "The distance is normalized with respect to the largest image dimension, so that it is between 0 and 1.\n" \
55 "Optionally, a signed distance to the frontier between zero and nonzero values can be computed.\n" \
56 "The distance transform can then be thresholded using the Threshold effect, or transformed using the ColorLookup effect, in order to generate a mask for another effect.\n" \
57 "See alse https://en.wikipedia.org/wiki/Distance_transform\n" \
58 "Uses the 'distance' function from the CImg library.\n" \
59 "CImg is a free, open-source library distributed under the CeCILL-C " \
60 "(close to the GNU LGPL) or CeCILL (compatible with the GNU GPL) licenses. " \
61 "It can be used in commercial applications (see http://cimg.eu)."
62 
63 #define kPluginIdentifier    "eu.cimg.Distance"
64 // History:
65 // version 1.0: initial version
66 #define kPluginVersionMajor 1 // Incrementing this number means that you have broken backwards compatibility of the plug-in.
67 #define kPluginVersionMinor 0 // Increment this when you have fixed a bug or made it faster.
68 
69 #define kSupportsComponentRemapping 1
70 #define kSupportsTiles 0 // requires the whole image to compute distance
71 #define kSupportsMultiResolution 1
72 #define kSupportsRenderScale 1
73 #define kSupportsMultipleClipPARs false
74 #define kSupportsMultipleClipDepths false
75 #define kRenderThreadSafety eRenderFullySafe
76 #ifdef cimg_use_openmp
77 #define kHostFrameThreading false
78 #else
79 #define kHostFrameThreading true
80 #endif
81 #define kSupportsRGBA true
82 #define kSupportsRGB true
83 #define kSupportsXY true
84 #define kSupportsAlpha true
85 
86 #define kParamMetric "metric"
87 #define kParamMetricLabel "Metric"
88 #define kParamMetricHint "Type of metric."
89 #define kParamMetricOptionChebyshev "Chebyshev", "max(abs(x-xborder),abs(y-yborder))", "chebyshev"
90 #define kParamMetricOptionManhattan "Manhattan", "abs(x-xborder) + abs(y-yborder)", "manhattan"
91 #define kParamMetricOptionEuclidean "Euclidean", "sqrt(sqr(x-xborder) + sqr(y-yborder))", "euclidean"
92 //#define kParamMetricOptionSquaredEuclidean "Squared Euclidean", "sqr(x-xborder) + sqr(y-yborder)", "squaredeuclidean"
93 #ifdef EXPERIMENTAL
94 #define kParamMetricOptionSpherical "Spherical", "Compute the Euclidean distance, and draw a 2.5D sphere at each point with the distance as radius. Gives a round shape rather than a conical shape to the distance function.", "spherical"
95 #endif
96 enum MetricEnum {
97     eMetricChebyshev = 0,
98     eMetricManhattan,
99     eMetricEuclidean,
100 //    eMetricSquaredEuclidean,
101 #ifdef EXPERIMENTAL
102     eMetricSpherical,
103 #endif
104 };
105 #define kParamMetricDefault eMetricEuclidean
106 
107 #define kParamSigned "signed"
108 #define kParamSignedLabel "Signed Distance"
109 #define kParamSignedHint "Instead of computing the distance to pixels with a value of zero, compute the signed distance to the contour between zero and non-zero pixels. On output, non-zero-valued pixels have a positive signed distance, zero-valued pixels have a negative signed distance."
110 
111 /// Distance plugin
112 struct CImgDistanceParams
113 {
114     MetricEnum metric;
115     bool signedDistance;
116 };
117 
118 class CImgDistancePlugin
119 : public CImgFilterPluginHelper<CImgDistanceParams, false>
120 {
121 public:
122 
CImgDistancePlugin(OfxImageEffectHandle handle)123     CImgDistancePlugin(OfxImageEffectHandle handle)
124     : CImgFilterPluginHelper<CImgDistanceParams, false>(handle, /*usesMask=*/false, kSupportsComponentRemapping, kSupportsTiles, kSupportsMultiResolution, kSupportsRenderScale, /*defaultUnpremult=*/ false)
125     {
126         _metric  = fetchChoiceParam(kParamMetric);
127         _signed  = fetchBooleanParam(kParamSigned);
128         assert(_metric && _signed);
129     }
130 
getValuesAtTime(double time,CImgDistanceParams & params)131     virtual void getValuesAtTime(double time,
132                                  CImgDistanceParams& params) OVERRIDE FINAL
133     {
134         params.metric = (MetricEnum)_metric->getValueAtTime(time);
135         params.signedDistance = _signed->getValueAtTime(time);
136     }
137 
138     // compute the roi required to compute rect, given params. This roi is then intersected with the image rod.
139     // only called if mix != 0.
getRoI(const OfxRectI & rect,const OfxPointD &,const CImgDistanceParams &,OfxRectI * roi)140     virtual void getRoI(const OfxRectI& rect,
141                         const OfxPointD& /*renderScale*/,
142                         const CImgDistanceParams& /*params*/,
143                         OfxRectI* roi) OVERRIDE FINAL
144     {
145         int delta_pix = 0; // does not support tiles anyway
146         assert(!kSupportsTiles);
147 
148         roi->x1 = rect.x1 - delta_pix;
149         roi->x2 = rect.x2 + delta_pix;
150         roi->y1 = rect.y1 - delta_pix;
151         roi->y2 = rect.y2 + delta_pix;
152     }
153 
render(const RenderArguments & args,const CImgDistanceParams & params,int,int,CImg<cimgpix_t> &,CImg<cimgpix_t> & cimg,int)154     virtual void render(const RenderArguments &args,
155                         const CImgDistanceParams& params,
156                         int /*x1*/,
157                         int /*y1*/,
158                         CImg<cimgpix_t>& /*mask*/,
159                         CImg<cimgpix_t>& cimg,
160                         int /*alphaChannel*/) OVERRIDE FINAL
161     {
162         // PROCESSING.
163         // This is the only place where the actual processing takes place
164 
165         // first compute the maximum dimension, which is used to normalize the distance so that it is between 0 and 1
166         // - of the format, if it is defined
167         // - else use the cimg dimension, since this plugin does not support ties anyway
168 
169         double maxdim = max( cimg.width(), cimg.height() );
170 #ifdef OFX_EXTENSIONS_NATRON
171         OfxRectI srcFormat;
172         _srcClip->getFormat(srcFormat);
173         OfxRectD srcFormatD;
174         srcFormatD.x1 = srcFormat.x1 * args.renderScale.x;
175         srcFormatD.x2 = srcFormat.x2 * args.renderScale.x;
176         srcFormatD.y1 = srcFormat.y1 * args.renderScale.y;
177         srcFormatD.y2 = srcFormat.y2 * args.renderScale.y;
178         if ( !Coords::rectIsEmpty(srcFormatD) ) {
179             maxdim = max( srcFormatD.x2 - srcFormatD.x1, srcFormatD.y2 - srcFormatD.y1 );
180         }
181 #endif
182 #ifdef EXPERIMENTAL
183         int m = (params.metric == eMetricSpherical) ? /*(int)eMetricSquaredEuclidean*/3 : (int)params.metric;
184 #else
185         int m = (int)params.metric;
186 #endif
187         int niter = 1;
188         CImg<cimgpix_t> cimg_save;
189 
190         if (params.signedDistance) {
191             // to compute the signed distance, first compute the distance A to zero-valued pixels, then B to non-zero
192             // valued pixels, then the result is A - 0.5 - (B - 0.5)
193             niter = 2;
194             // copy image to compute the non-zero part afterwards part
195             cimg_save.assign(cimg, /*is_shared=*/false);
196             cimg.swap(cimg_save);
197             cimg_pragma_openmp(parallel for cimg_openmp_if(cimg.size()>=8192))
198             // compute the negative part first
199             cimg_rof(cimg,ptrd,cimgpix_t) *ptrd = (*ptrd == 0);
200         }
201 
202         for (int i = 0; i < niter; ++i) {
203             cimg.distance(0, m);
204 
205 #ifdef EXPERIMENTAL
206             if (params.metric == eMetricSpherical) {
207                 bool finished = false;
208                 CImg<cimgpix_t> distance(cimg, /*is_shared=*/false);
209 
210                 // TODO: perform a MAT (medial axis transform) first to reduce the number of points.
211                 //  E. Remy and E. Thiel. Exact Medial Axis with Euclidean Distance. Image and Vision Computing, 23(2):167-175, 2005.
212                 // see http://pageperso.lif.univ-mrs.fr/~edouard.thiel/IVC2004/
213 
214                 while (!finished) {
215                     cimg_abort_test;
216                     int max_x = 0, max_y = 0, max_z = 0, max_c = 0;
217                     cimgpix_t dmax = distance(0,0,0,0);
218                     // TODO: if we start from the MAT, we can process each sphere center sequentially: no need to extract the maximum to get the next center (this is only useful to prune points that are within the Z-cone). The main loop would thus be on the non-zero MAT pixels.
219                     cimg_forXYZC(cimg, x, y, z, c) {
220                         if (distance(x,y,z,c) > dmax) {
221                             dmax = distance(x,y,z,c);
222                             max_x = x;
223                             max_y = y;
224                             max_z = z;
225                             max_c = c;
226                         }
227                     }//image loop
228                     //printf("dmax=%g\n", dmax);
229                     if (dmax <= 0) {
230                         // no more sphere to draw
231                         finished = true;
232                     } else {
233                         distance(max_x, max_y, max_z, max_c) = 0;
234                         // draw a Z-sphere in the zmap and prune points in
235                         // the cimg corresponding to occluded spheres
236                         cimgpix_t r2 = dmax;
237                         cimgpix_t r = sqrt(r2);
238                         int xmin = (int)floor(max((cimgpix_t)0, max_x - r));
239                         int xmax = (int)ceil(min((cimgpix_t)cimg.width(), max_x + r));
240                         int ymin = (int)floor(max((cimgpix_t)0, max_y - r));
241                         int ymax = (int)ceil(min((cimgpix_t)cimg.height(), max_y + r));
242                         // loop on all pixels in the bounding box
243                         cimg_for_inXY(cimg, xmin, ymin, xmax, ymax, x, y) {
244                             cimgpix_t pr2 = (x - max_x)*(x - max_x) + (y - max_y)*(y - max_y);
245                             if (pr2 < r2) {
246                                 // draw the Z-sphere point
247                                 cimgpix_t z = r2 - pr2;
248                                 if (cimg(x,y, max_z, max_c) < z) {
249                                     cimg(x,y, max_z, max_c) = z;
250                                 }
251                                 // prune points below the Z-cone (should not be necessary if we do the MAT first)
252                                 if (distance(x, y, max_z, max_c) > 0 && distance(x, y, max_z, max_c) < /*(r - sqrt(pr2))^2=*/(r2 + pr2 - 2 * r * sqrt(pr2)) ) {
253                                     distance(x, y, max_z, max_c) = 0;
254                                 }
255                             }
256                         }
257                     }
258                 }
259                 // now compute the square root
260                 cimg.sqrt();
261             }
262 #endif
263             if (params.signedDistance) {
264                 if (i == 0) {
265                     cimg.swap(cimg_save);
266                     // now cimg_save contains the negative part
267                 }
268                 if (i == 1) {
269                     // now cimg_save contains the negative part
270                     // now cimg contains the positive part
271                     cimgpix_t *ptrd = cimg.data();
272                     for (const cimgpix_t *ptrs = cimg_save.data(), *ptrs_end = ptrs + cimg_save.size();
273                          ptrs < ptrs_end;
274                          ++ptrd, ++ptrs) {
275                         *ptrd = *ptrd > 0 ? (*ptrd - 0.5) : (0.5 - *ptrs);
276 
277                     }
278 
279                 }
280             }
281         }
282         //if (params.metric == eMetricSquaredEuclidean) {
283         //    cimg /= maxdim * maxdim/* * args.renderScale.x*/;
284         //} else {
285         cimg /= maxdim/* * args.renderScale.x*/;
286         //}
287     }
288 
289 private:
290 
291     // params
292     ChoiceParam *_metric;
293     BooleanParam *_signed;
294 };
295 
296 
297 mDeclarePluginFactory(CImgDistancePluginFactory, {ofxsThreadSuiteCheck();}, {});
298 
299 void
describe(ImageEffectDescriptor & desc)300 CImgDistancePluginFactory::describe(ImageEffectDescriptor& desc)
301 {
302     // basic labels
303     desc.setLabel(kPluginName);
304     desc.setPluginGrouping(kPluginGrouping);
305     desc.setPluginDescription(kPluginDescription);
306 
307     // add supported context
308     desc.addSupportedContext(eContextFilter);
309     desc.addSupportedContext(eContextGeneral);
310 
311     // add supported pixel depths
312     //desc.addSupportedBitDepth(eBitDepthUByte);
313     //desc.addSupportedBitDepth(eBitDepthUShort);
314     desc.addSupportedBitDepth(eBitDepthFloat);
315 
316     // set a few flags
317     desc.setSingleInstance(false);
318     desc.setHostFrameThreading(kHostFrameThreading);
319     desc.setSupportsMultiResolution(kSupportsMultiResolution);
320     desc.setSupportsTiles(kSupportsTiles);
321     desc.setTemporalClipAccess(false);
322     desc.setRenderTwiceAlways(true);
323     desc.setSupportsMultipleClipPARs(kSupportsMultipleClipPARs);
324     desc.setSupportsMultipleClipDepths(kSupportsMultipleClipDepths);
325     desc.setRenderThreadSafety(kRenderThreadSafety);
326 }
327 
328 void
describeInContext(ImageEffectDescriptor & desc,ContextEnum context)329 CImgDistancePluginFactory::describeInContext(ImageEffectDescriptor& desc,
330                                            ContextEnum context)
331 {
332     // create the clips and params
333     PageParamDescriptor *page = CImgDistancePlugin::describeInContextBegin(desc, context,
334                                                                          kSupportsRGBA,
335                                                                          kSupportsRGB,
336                                                                          kSupportsXY,
337                                                                          kSupportsAlpha,
338                                                                          kSupportsTiles,
339                                                                          /*processRGB=*/ false,
340                                                                          /*processAlpha*/ true,      // Enable alpha by default, so it works OK on masks
341                                                                          /*processIsSecret=*/ false);
342 
343     {
344         ChoiceParamDescriptor *param = desc.defineChoiceParam(kParamMetric);
345         param->setLabel(kParamMetricLabel);
346         param->setHint(kParamMetricHint);
347         assert(param->getNOptions() == eMetricChebyshev);
348         param->appendOption(kParamMetricOptionChebyshev);
349         assert(param->getNOptions() == eMetricManhattan);
350         param->appendOption(kParamMetricOptionManhattan);
351         assert(param->getNOptions() == eMetricEuclidean);
352         param->appendOption(kParamMetricOptionEuclidean);
353         //assert(param->getNOptions() == eMetricSquaredEuclidean);
354         //param->appendOption(kParamMetricOptionSquaredEuclidean);
355 #ifdef EXPERIMENTAL
356         assert(param->getNOptions() == eMetricSpherical);
357         param->appendOption(kParamMetricOptionSpherical);
358 #endif
359         param->setDefault(kParamMetricDefault);
360         if (page) {
361             page->addChild(*param);
362         }
363     }
364     {
365         BooleanParamDescriptor *param = desc.defineBooleanParam(kParamSigned);
366         param->setLabel(kParamSignedLabel);
367         param->setHint(kParamSignedHint);
368     }
369 
370     CImgDistancePlugin::describeInContextEnd(desc, context, page);
371 }
372 
373 ImageEffect*
createInstance(OfxImageEffectHandle handle,ContextEnum)374 CImgDistancePluginFactory::createInstance(OfxImageEffectHandle handle,
375                                         ContextEnum /*context*/)
376 {
377     return new CImgDistancePlugin(handle);
378 }
379 
380 static CImgDistancePluginFactory p(kPluginIdentifier, kPluginVersionMajor, kPluginVersionMinor);
381 mRegisterPluginFactoryInstance(p)
382 
383 OFXS_NAMESPACE_ANONYMOUS_EXIT
384