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