1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio/blob/master/LICENSE.md
4 
5 /// \file
6 /// ImageBufAlgo functions for filtered transformations
7 
8 
9 #include <cmath>
10 #include <memory>
11 
12 #include "imageio_pvt.h"
13 #include <OpenImageIO/dassert.h>
14 #include <OpenImageIO/filter.h>
15 #include <OpenImageIO/imagebuf.h>
16 #include <OpenImageIO/imagebufalgo.h>
17 #include <OpenImageIO/imagebufalgo_util.h>
18 #include <OpenImageIO/thread.h>
19 
20 #if OIIO_USING_IMATH >= 3
21 #    include <Imath/ImathBox.h>
22 #else
23 #    include <OpenEXR/ImathBox.h>
24 #endif
25 
26 OIIO_NAMESPACE_BEGIN
27 
28 
29 namespace {
30 
31 // Define a templated Accumulator type that's float, except in the case
32 // of double input, in which case it's double.
33 template<typename T> struct Accum_t {
34     typedef float type;
35 };
36 template<> struct Accum_t<double> {
37     typedef double type;
38 };
39 
40 
41 
42 // Poor man's Dual2<float> makes it easy to compute with differentials. For
43 // a rich man's implementation and full documentation, see
44 // OpenShadingLanguage (dual2.h).
45 class Dual2 {
46 public:
val() const47     float val() const { return m_val; }
dx() const48     float dx() const { return m_dx; }
dy() const49     float dy() const { return m_dy; }
Dual2(float val)50     Dual2(float val)
51         : m_val(val)
52         , m_dx(0.0f)
53         , m_dy(0.0f)
54     {
55     }
Dual2(float val,float dx,float dy)56     Dual2(float val, float dx, float dy)
57         : m_val(val)
58         , m_dx(dx)
59         , m_dy(dy)
60     {
61     }
operator =(float f)62     Dual2& operator=(float f)
63     {
64         m_val = f;
65         m_dx  = 0.0f;
66         m_dy  = 0.0f;
67         return *this;
68     }
operator +(const Dual2 & a,const Dual2 & b)69     friend Dual2 operator+(const Dual2& a, const Dual2& b)
70     {
71         return Dual2(a.m_val + b.m_val, a.m_dx + b.m_dx, a.m_dy + b.m_dy);
72     }
operator +(const Dual2 & a,float b)73     friend Dual2 operator+(const Dual2& a, float b)
74     {
75         return Dual2(a.m_val + b, a.m_dx, a.m_dy);
76     }
operator *(const Dual2 & a,float b)77     friend Dual2 operator*(const Dual2& a, float b)
78     {
79         return Dual2(a.m_val * b, a.m_dx * b, a.m_dy * b);
80     }
operator *(const Dual2 & a,const Dual2 & b)81     friend Dual2 operator*(const Dual2& a, const Dual2& b)
82     {
83         // Use the chain rule
84         return Dual2(a.m_val * b.m_val, a.m_val * b.m_dx + a.m_dx * b.m_val,
85                      a.m_val * b.m_dy + a.m_dy * b.m_val);
86     }
operator /(const Dual2 & a,const Dual2 & b)87     friend Dual2 operator/(const Dual2& a, const Dual2& b)
88     {
89         float bvalinv   = 1.0f / b.m_val;
90         float aval_bval = a.m_val * bvalinv;
91         return Dual2(aval_bval, bvalinv * (a.m_dx - aval_bval * b.m_dx),
92                      bvalinv * (a.m_dy - aval_bval * b.m_dy));
93     }
94 
95 private:
96     float m_val, m_dx, m_dy;
97 };
98 
99 /// Transform a 2D point (x,y) with derivatives by a 3x3 affine matrix to
100 /// obtain a transformed point with derivatives.
101 inline void
robust_multVecMatrix(const Imath::M33f & M,const Dual2 & x,const Dual2 & y,Dual2 & outx,Dual2 & outy)102 robust_multVecMatrix(const Imath::M33f& M, const Dual2& x, const Dual2& y,
103                      Dual2& outx, Dual2& outy)
104 {
105     Dual2 a = x * M[0][0] + y * M[1][0] + M[2][0];
106     Dual2 b = x * M[0][1] + y * M[1][1] + M[2][1];
107     Dual2 w = x * M[0][2] + y * M[1][2] + M[2][2];
108 
109     if (w.val() != 0.0f) {
110         outx = a / w;
111         outy = b / w;
112     } else {
113         outx = 0.0f;
114         outy = 0.0f;
115     }
116 }
117 
118 
119 
120 // Transform an ROI by an affine matrix.
121 ROI
transform(const Imath::M33f & M,ROI roi)122 transform(const Imath::M33f& M, ROI roi)
123 {
124     Imath::V2f ul(roi.xbegin + 0.5f, roi.ybegin + 0.5f);
125     Imath::V2f ur(roi.xend - 0.5f, roi.ybegin + 0.5f);
126     Imath::V2f ll(roi.xbegin + 0.5f, roi.yend - 0.5f);
127     Imath::V2f lr(roi.xend - 0.5f, roi.yend - 0.5f);
128     M.multVecMatrix(ul, ul);
129     M.multVecMatrix(ur, ur);
130     M.multVecMatrix(ll, ll);
131     M.multVecMatrix(lr, lr);
132     Imath::Box2f box(ul);
133     box.extendBy(ll);
134     box.extendBy(ur);
135     box.extendBy(lr);
136     int xmin = int(floorf(box.min.x));
137     int ymin = int(floorf(box.min.y));
138     int xmax = int(floorf(box.max.x)) + 1;
139     int ymax = int(floorf(box.max.y)) + 1;
140     return ROI(xmin, xmax, ymin, ymax, roi.zbegin, roi.zend, roi.chbegin,
141                roi.chend);
142 }
143 
144 
145 
146 // Given s,t image space coordinates and their derivatives, compute a
147 // filtered sample using the derivatives to guide the size of the filter
148 // footprint.
149 template<typename SRCTYPE>
150 inline void
filtered_sample(const ImageBuf & src,float s,float t,float dsdx,float dtdx,float dsdy,float dtdy,const Filter2D * filter,ImageBuf::WrapMode wrap,float * result)151 filtered_sample(const ImageBuf& src, float s, float t, float dsdx, float dtdx,
152                 float dsdy, float dtdy, const Filter2D* filter,
153                 ImageBuf::WrapMode wrap, float* result)
154 {
155     OIIO_DASSERT(filter);
156     // Just use isotropic filtering
157     float ds          = std::max(1.0f, std::max(fabsf(dsdx), fabsf(dsdy)));
158     float dt          = std::max(1.0f, std::max(fabsf(dtdx), fabsf(dtdy)));
159     float ds_inv      = 1.0f / ds;
160     float dt_inv      = 1.0f / dt;
161     float filterrad_s = 0.5f * ds * filter->width();
162     float filterrad_t = 0.5f * dt * filter->width();
163     ImageBuf::ConstIterator<SRCTYPE> samp(src, (int)floorf(s - filterrad_s),
164                                           (int)ceilf(s + filterrad_s),
165                                           (int)floorf(t - filterrad_t),
166                                           (int)ceilf(t + filterrad_t), 0, 1,
167                                           wrap);
168     int nc     = src.nchannels();
169     float* sum = OIIO_ALLOCA(float, nc);
170     memset(sum, 0, nc * sizeof(float));
171     float total_w = 0.0f;
172     for (; !samp.done(); ++samp) {
173         float w = (*filter)(ds_inv * (samp.x() + 0.5f - s),
174                             dt_inv * (samp.y() + 0.5f - t));
175         for (int c = 0; c < nc; ++c)
176             sum[c] += w * samp[c];
177         total_w += w;
178     }
179     if (total_w != 0.0f)
180         for (int c = 0; c < nc; ++c)
181             result[c] = sum[c] / total_w;
182     else
183         for (int c = 0; c < nc; ++c)
184             result[c] = 0.0f;
185 }
186 
187 }  // namespace
188 
189 
190 
191 template<typename DSTTYPE, typename SRCTYPE>
192 static bool
resize_(ImageBuf & dst,const ImageBuf & src,Filter2D * filter,ROI roi,int nthreads)193 resize_(ImageBuf& dst, const ImageBuf& src, Filter2D* filter, ROI roi,
194         int nthreads)
195 {
196     ImageBufAlgo::parallel_image(roi, nthreads, [&](ROI roi) {
197         const ImageSpec& srcspec(src.spec());
198         const ImageSpec& dstspec(dst.spec());
199         int nchannels = dstspec.nchannels;
200 
201         // Local copies of the source image window, converted to float
202         float srcfx = srcspec.full_x;
203         float srcfy = srcspec.full_y;
204         float srcfw = srcspec.full_width;
205         float srcfh = srcspec.full_height;
206 
207         // Ratios of dst/src size.  Values larger than 1 indicate that we
208         // are maximizing (enlarging the image), and thus want to smoothly
209         // interpolate.  Values less than 1 indicate that we are minimizing
210         // (shrinking the image), and thus want to properly filter out the
211         // high frequencies.
212         float xratio = float(dstspec.full_width)
213                        / srcfw;  // 2 upsize, 0.5 downsize
214         float yratio = float(dstspec.full_height) / srcfh;
215 
216         float dstfx          = float(dstspec.full_x);
217         float dstfy          = float(dstspec.full_y);
218         float dstfw          = float(dstspec.full_width);
219         float dstfh          = float(dstspec.full_height);
220         float dstpixelwidth  = 1.0f / dstfw;
221         float dstpixelheight = 1.0f / dstfh;
222         float filterrad      = filter->width() / 2.0f;
223 
224         // radi,radj is the filter radius, as an integer, in source pixels.  We
225         // will filter the source over [x-radi, x+radi] X [y-radj,y+radj].
226         int radi        = (int)ceilf(filterrad / xratio);
227         int radj        = (int)ceilf(filterrad / yratio);
228         int xtaps       = 2 * radi + 1;
229         int ytaps       = 2 * radj + 1;
230         bool separable  = filter->separable();
231         float* yfiltval = OIIO_ALLOCA(float, ytaps);
232         std::unique_ptr<float[]> xfiltval_all;
233         if (separable) {
234             // For separable filters, horizontal tap weights will be the same
235             // for every column. So we precompute all the tap weights for every
236             // x position we'll need. We do the same thing in y, but row by row
237             // inside the loop (since we never revisit a y row). This
238             // substantially speeds up resize.
239             xfiltval_all.reset(new float[xtaps * roi.width()]);
240             for (int x = roi.xbegin; x < roi.xend; ++x) {
241                 float* xfiltval = xfiltval_all.get() + (x - roi.xbegin) * xtaps;
242                 float s         = (x - dstfx + 0.5f) * dstpixelwidth;
243                 float src_xf    = srcfx + s * srcfw;
244                 int src_x;
245                 float src_xf_frac   = floorfrac(src_xf, &src_x);
246                 float totalweight_x = 0.0f;
247                 for (int i = 0; i < xtaps; ++i) {
248                     float w = filter->xfilt(
249                         xratio * (i - radi - (src_xf_frac - 0.5f)));
250                     xfiltval[i] = w;
251                     totalweight_x += w;
252                 }
253                 if (totalweight_x != 0.0f)
254                     for (int i = 0; i < xtaps; ++i)    // normalize x filter
255                         xfiltval[i] /= totalweight_x;  // weights
256             }
257         }
258 
259 #if 0
260     std::cerr << "Resizing " << srcspec.full_width << "x" << srcspec.full_height
261               << " to " << dstspec.full_width << "x" << dstspec.full_height << "\n";
262     std::cerr << "ratios = " << xratio << ", " << yratio << "\n";
263     std::cerr << "examining src filter " << filter->name()
264               << " support radius of " << radi << " x " << radj << " pixels\n";
265     std::cout << "  " << xtaps << "x" << ytaps << " filter taps\n";
266     std::cerr << "dst range " << roi << "\n";
267     std::cerr << "separable filter\n";
268 #endif
269 
270         // Accumulate the weighted results in pel[]. We select a type big
271         // enough to hold with required precision.
272         typedef typename Accum_t<DSTTYPE>::type Acc_t;
273         Acc_t* pel = OIIO_ALLOCA(Acc_t, nchannels);
274 
275 #define USE_SPECIAL 0
276 #if USE_SPECIAL
277         // Special case: src and dst are local memory, float buffers, and we're
278         // operating on all channels, <= 4.
279         bool special
280             = ((is_same<DSTTYPE, float>::value || is_same<DSTTYPE, half>::value)
281                && (is_same<SRCTYPE, float>::value
282                    || is_same<SRCTYPE, half>::value)
283                // && dst.localpixels() // has to be, because it's writable
284                && src.localpixels()
285                // && R.contains_roi(roi)  // has to be, because IBAPrep
286                && src.contains_roi(roi) && roi.chbegin == 0
287                && roi.chend == dst.nchannels() && roi.chend == src.nchannels()
288                && roi.chend <= 4 && separable);
289 #endif
290 
291         // We're going to loop over all output pixels we're interested in.
292         //
293         // (s,t) = NDC space coordinates of the output sample we are computing.
294         //     This is the "sample point".
295         // (src_xf, src_xf) = source pixel space float coordinates of the
296         //     sample we're computing. We want to compute the weighted sum
297         //     of all the source image pixels that fall under the filter when
298         //     centered at that location.
299         // (src_x, src_y) = image space integer coordinates of the floor,
300         //     i.e., the closest pixel in the source image.
301         // src_xf_frac and src_yf_frac are the position within that pixel
302         //     of our sample.
303         //
304         // Separate cases for separable and non-separable filters.
305         if (separable) {
306             ImageBuf::Iterator<DSTTYPE> out(dst, roi);
307             ImageBuf::ConstIterator<SRCTYPE> srcpel(src, ImageBuf::WrapClamp);
308             for (int y = roi.ybegin; y < roi.yend; ++y) {
309                 float t      = (y - dstfy + 0.5f) * dstpixelheight;
310                 float src_yf = srcfy + t * srcfh;
311                 int src_y;
312                 float src_yf_frac = floorfrac(src_yf, &src_y);
313                 // If using separable filters, our vertical set of filter tap
314                 // weights will be the same for the whole scanline we're on.  Just
315                 // compute and normalize them once.
316                 float totalweight_y = 0.0f;
317                 for (int j = 0; j < ytaps; ++j) {
318                     float w = filter->yfilt(
319                         yratio * (j - radj - (src_yf_frac - 0.5f)));
320                     yfiltval[j] = w;
321                     totalweight_y += w;
322                 }
323                 if (totalweight_y != 0.0f)
324                     for (int i = 0; i < ytaps; ++i)
325                         yfiltval[i] /= totalweight_y;
326 
327                 for (int x = roi.xbegin; x < roi.xend; ++x, ++out) {
328                     float s      = (x - dstfx + 0.5f) * dstpixelwidth;
329                     float src_xf = srcfx + s * srcfw;
330                     int src_x    = ifloor(src_xf);
331                     for (int c = 0; c < nchannels; ++c)
332                         pel[c] = 0.0f;
333                     const float* xfiltval = xfiltval_all.get()
334                                             + (x - roi.xbegin) * xtaps;
335                     float totalweight_x = 0.0f;
336                     for (int i = 0; i < xtaps; ++i)
337                         totalweight_x += xfiltval[i];
338                     if (totalweight_x != 0.0f) {
339                         srcpel.rerange(src_x - radi, src_x + radi + 1,
340                                        src_y - radj, src_y + radj + 1, 0, 1,
341                                        ImageBuf::WrapClamp);
342                         for (int j = -radj; j <= radj; ++j) {
343                             float wy = yfiltval[j + radj];
344                             if (wy == 0.0f) {
345                                 // 0 weight for this y tap -- move to next line
346                                 srcpel.pos(srcpel.x(), srcpel.y() + 1,
347                                            srcpel.z());
348                                 continue;
349                             }
350                             for (int i = 0; i < xtaps; ++i, ++srcpel) {
351                                 float w = wy * xfiltval[i];
352                                 if (w)
353                                     for (int c = 0; c < nchannels; ++c)
354                                         pel[c] += w * srcpel[c];
355                             }
356                         }
357                     }
358                     // Copy the pixel value (already normalized) to the output.
359                     OIIO_DASSERT(out.x() == x && out.y() == y);
360                     if (totalweight_y == 0.0f) {
361                         // zero it out
362                         for (int c = 0; c < nchannels; ++c)
363                             out[c] = 0.0f;
364                     } else {
365                         for (int c = 0; c < nchannels; ++c)
366                             out[c] = pel[c];
367                     }
368                 }
369             }
370 
371         } else {
372             // Non-separable filter
373             ImageBuf::Iterator<DSTTYPE> out(dst, roi);
374             ImageBuf::ConstIterator<SRCTYPE> srcpel(src, ImageBuf::WrapClamp);
375             for (int y = roi.ybegin; y < roi.yend; ++y) {
376                 float t      = (y - dstfy + 0.5f) * dstpixelheight;
377                 float src_yf = srcfy + t * srcfh;
378                 int src_y;
379                 float src_yf_frac = floorfrac(src_yf, &src_y);
380                 for (int x = roi.xbegin; x < roi.xend; ++x, ++out) {
381                     float s      = (x - dstfx + 0.5f) * dstpixelwidth;
382                     float src_xf = srcfx + s * srcfw;
383                     int src_x;
384                     float src_xf_frac = floorfrac(src_xf, &src_x);
385                     for (int c = 0; c < nchannels; ++c)
386                         pel[c] = 0.0f;
387                     float totalweight = 0.0f;
388                     srcpel.rerange(src_x - radi, src_x + radi + 1, src_y - radi,
389                                    src_y + radi + 1, 0, 1, ImageBuf::WrapClamp);
390                     for (int j = -radj; j <= radj; ++j) {
391                         for (int i = -radi; i <= radi; ++i, ++srcpel) {
392                             OIIO_DASSERT(!srcpel.done());
393                             float w
394                                 = (*filter)(xratio * (i - (src_xf_frac - 0.5f)),
395                                             yratio
396                                                 * (j - (src_yf_frac - 0.5f)));
397                             if (w) {
398                                 totalweight += w;
399                                 for (int c = 0; c < nchannels; ++c)
400                                     pel[c] += w * srcpel[c];
401                             }
402                         }
403                     }
404                     OIIO_DASSERT(srcpel.done());
405                     // Rescale pel to normalize the filter and write it to the
406                     // output image.
407                     OIIO_DASSERT(out.x() == x && out.y() == y);
408                     if (totalweight == 0.0f) {
409                         // zero it out
410                         for (int c = 0; c < nchannels; ++c)
411                             out[c] = 0.0f;
412                     } else {
413                         for (int c = 0; c < nchannels; ++c)
414                             out[c] = pel[c] / totalweight;
415                     }
416                 }
417             }
418         }
419     });  // end of parallel_image
420     return true;
421 }
422 
423 
424 
425 static std::shared_ptr<Filter2D>
get_resize_filter(string_view filtername,float fwidth,ImageBuf & dst,float wratio,float hratio)426 get_resize_filter(string_view filtername, float fwidth, ImageBuf& dst,
427                   float wratio, float hratio)
428 {
429     // Set up a shared pointer with custom deleter to make sure any
430     // filter we allocate here is properly destroyed.
431     std::shared_ptr<Filter2D> filter((Filter2D*)nullptr, Filter2D::destroy);
432     if (filtername.empty()) {
433         // No filter name supplied -- pick a good default
434         if (wratio > 1.0f || hratio > 1.0f)
435             filtername = "blackman-harris";
436         else
437             filtername = "lanczos3";
438     }
439     for (int i = 0, e = Filter2D::num_filters(); i < e; ++i) {
440         FilterDesc fd;
441         Filter2D::get_filterdesc(i, &fd);
442         if (fd.name == filtername) {
443             float w = fwidth > 0.0f ? fwidth
444                                     : fd.width * std::max(1.0f, wratio);
445             float h = fwidth > 0.0f ? fwidth
446                                     : fd.width * std::max(1.0f, hratio);
447             filter.reset(Filter2D::create(filtername, w, h));
448             break;
449         }
450     }
451     if (!filter) {
452         dst.errorf("Filter \"%s\" not recognized", filtername);
453     }
454     return filter;
455 }
456 
457 
458 
459 bool
resize(ImageBuf & dst,const ImageBuf & src,Filter2D * filter,ROI roi,int nthreads)460 ImageBufAlgo::resize(ImageBuf& dst, const ImageBuf& src, Filter2D* filter,
461                      ROI roi, int nthreads)
462 {
463     pvt::LoggedTimer logtime("IBA::resize");
464     if (!IBAprep(roi, &dst, &src,
465                  IBAprep_NO_SUPPORT_VOLUME | IBAprep_NO_COPY_ROI_FULL))
466         return false;
467 
468     // Set up a shared pointer with custom deleter to make sure any
469     // filter we allocate here is properly destroyed.
470     std::shared_ptr<Filter2D> filterptr((Filter2D*)NULL, Filter2D::destroy);
471     if (!filter) {
472         // If no filter was provided, punt and just linearly interpolate.
473         const ImageSpec& srcspec(src.spec());
474         const ImageSpec& dstspec(dst.spec());
475         float wratio = float(dstspec.full_width) / float(srcspec.full_width);
476         float hratio = float(dstspec.full_height) / float(srcspec.full_height);
477         float w      = 2.0f * std::max(1.0f, wratio);
478         float h      = 2.0f * std::max(1.0f, hratio);
479         filter       = Filter2D::create("triangle", w, h);
480         filterptr.reset(filter);
481     }
482 
483     bool ok;
484     OIIO_DISPATCH_COMMON_TYPES2(ok, "resize", resize_, dst.spec().format,
485                                 src.spec().format, dst, src, filter, roi,
486                                 nthreads);
487     return ok;
488 }
489 
490 
491 
492 bool
resize(ImageBuf & dst,const ImageBuf & src,string_view filtername,float fwidth,ROI roi,int nthreads)493 ImageBufAlgo::resize(ImageBuf& dst, const ImageBuf& src, string_view filtername,
494                      float fwidth, ROI roi, int nthreads)
495 {
496     pvt::LoggedTimer logtime("IBA::resize");
497     if (!IBAprep(roi, &dst, &src,
498                  IBAprep_NO_SUPPORT_VOLUME | IBAprep_NO_COPY_ROI_FULL))
499         return false;
500     const ImageSpec& srcspec(src.spec());
501     const ImageSpec& dstspec(dst.spec());
502 
503     // Resize ratios
504     float wratio = float(dstspec.full_width) / float(srcspec.full_width);
505     float hratio = float(dstspec.full_height) / float(srcspec.full_height);
506 
507     // Set up a shared pointer with custom deleter to make sure any
508     // filter we allocate here is properly destroyed.
509     auto filter = get_resize_filter(filtername, fwidth, dst, wratio, hratio);
510     if (!filter)
511         return false;  // error issued in get_resize_filter
512 
513     logtime.stop();  // it will be picked up again by the next call...
514     return resize(dst, src, filter.get(), roi, nthreads);
515 }
516 
517 
518 
519 ImageBuf
resize(const ImageBuf & src,Filter2D * filter,ROI roi,int nthreads)520 ImageBufAlgo::resize(const ImageBuf& src, Filter2D* filter, ROI roi,
521                      int nthreads)
522 {
523     ImageBuf result;
524     bool ok = resize(result, src, filter, roi, nthreads);
525     if (!ok && !result.has_error())
526         result.errorf("ImageBufAlgo::resize() error");
527     return result;
528 }
529 
530 
531 ImageBuf
resize(const ImageBuf & src,string_view filtername,float filterwidth,ROI roi,int nthreads)532 ImageBufAlgo::resize(const ImageBuf& src, string_view filtername,
533                      float filterwidth, ROI roi, int nthreads)
534 {
535     ImageBuf result;
536     bool ok = resize(result, src, filtername, filterwidth, roi, nthreads);
537     if (!ok && !result.has_error())
538         result.errorf("ImageBufAlgo::resize() error");
539     return result;
540 }
541 
542 
543 
544 bool
fit(ImageBuf & dst,const ImageBuf & src,Filter2D * filter,bool exact,ROI roi,int nthreads)545 ImageBufAlgo::fit(ImageBuf& dst, const ImageBuf& src, Filter2D* filter,
546                   bool exact, ROI roi, int nthreads)
547 {
548     // No time logging, it will be accounted in the underlying warp/resize
549     if (!IBAprep(roi, &dst, &src,
550                  IBAprep_NO_SUPPORT_VOLUME | IBAprep_NO_COPY_ROI_FULL))
551         return false;
552 
553     const ImageSpec& srcspec(src.spec());
554 
555     // Compute scaling factors and use action_resize to do the heavy lifting
556     int fit_full_width     = roi.width();
557     int fit_full_height    = roi.height();
558     int fit_full_x         = roi.xbegin;
559     int fit_full_y         = roi.ybegin;
560     float oldaspect        = float(srcspec.full_width) / srcspec.full_height;
561     float newaspect        = float(fit_full_width) / fit_full_height;
562     int resize_full_width  = fit_full_width;
563     int resize_full_height = fit_full_height;
564     int xoffset = 0, yoffset = 0;
565     float xoff = 0.0f, yoff = 0.0f;
566     float scale = 1.0f;
567 
568     if (newaspect >= oldaspect) {  // same or wider than original
569         resize_full_width = int(resize_full_height * oldaspect + 0.5f);
570         xoffset           = (fit_full_width - resize_full_width) / 2;
571         scale             = float(fit_full_height) / float(srcspec.full_height);
572         xoff = float(fit_full_width - scale * srcspec.full_width) / 2.0f;
573     } else {  // narrower than original
574         resize_full_height = int(resize_full_width / oldaspect + 0.5f);
575         yoffset            = (fit_full_height - resize_full_height) / 2;
576         scale              = float(fit_full_width) / float(srcspec.full_width);
577         yoff = float(fit_full_height - scale * srcspec.full_height) / 2.0f;
578     }
579 
580     ROI newroi(fit_full_x, fit_full_x + fit_full_width, fit_full_y,
581                fit_full_y + fit_full_height, 0, 1, 0, srcspec.nchannels);
582     // std::cout << "  Fitting " << srcspec.roi()
583     //           << " into " << newroi << "\n";
584     // std::cout << "  Fit scale factor " << scale << "\n";
585 
586     // Set up a shared pointer with custom deleter to make sure any
587     // filter we allocate here is properly destroyed.
588     std::shared_ptr<Filter2D> filterptr((Filter2D*)NULL, Filter2D::destroy);
589     if (!filter) {
590         // If no filter was provided, punt and just linearly interpolate.
591         float wratio = float(resize_full_width) / float(srcspec.full_width);
592         float hratio = float(resize_full_height) / float(srcspec.full_height);
593         float w      = 2.0f * std::max(1.0f, wratio);
594         float h      = 2.0f * std::max(1.0f, hratio);
595         filter       = Filter2D::create("triangle", w, h);
596         filterptr.reset(filter);
597     }
598 
599     bool ok = true;
600     if (exact) {
601         // Full partial-pixel filtered resize -- exactly preserves aspect
602         // ratio and exactly centers the padded image, but might make the
603         // edges of the resized area blurry because it's not a whole number
604         // of pixels.
605         Imath::M33f M(scale, 0.0f, 0.0f, 0.0f, scale, 0.0f, xoff, yoff, 1.0f);
606         // std::cout << "   Fit performing warp with " << M << "\n";
607         ImageSpec newspec = srcspec;
608         newspec.set_roi(newroi);
609         newspec.set_roi_full(newroi);
610         dst.reset(newspec);
611         ImageBuf::WrapMode wrap = ImageBuf::WrapMode_from_string("black");
612         ok &= ImageBufAlgo::warp(dst, src, M, filter, false, wrap, {},
613                                  nthreads);
614     } else {
615         // Full pixel resize -- gives the sharpest result, but for odd-sized
616         // destination resolution, may not be exactly centered and will only
617         // preserve the aspect ratio to the nearest integer pixel size.
618         if (resize_full_width != srcspec.full_width
619             || resize_full_height != srcspec.full_height
620             || fit_full_x != srcspec.full_x || fit_full_y != srcspec.full_y) {
621             ROI resizeroi(fit_full_x, fit_full_x + resize_full_width,
622                           fit_full_y, fit_full_y + resize_full_height, 0, 1, 0,
623                           srcspec.nchannels);
624             ImageSpec newspec = srcspec;
625             newspec.set_roi(resizeroi);
626             newspec.set_roi_full(resizeroi);
627             dst.reset(newspec);
628             ok &= ImageBufAlgo::resize(dst, src, filter, resizeroi, nthreads);
629         } else {
630             ok &= dst.copy(src);  // no resize is necessary
631         }
632         dst.specmod().full_width  = fit_full_width;
633         dst.specmod().full_height = fit_full_height;
634         dst.specmod().full_x      = fit_full_x;
635         dst.specmod().full_y      = fit_full_y;
636         dst.specmod().x           = xoffset;
637         dst.specmod().y           = yoffset;
638     }
639     return ok;
640 }
641 
642 
643 
644 bool
fit(ImageBuf & dst,const ImageBuf & src,string_view filtername,float fwidth,bool exact,ROI roi,int nthreads)645 ImageBufAlgo::fit(ImageBuf& dst, const ImageBuf& src, string_view filtername,
646                   float fwidth, bool exact, ROI roi, int nthreads)
647 {
648     pvt::LoggedTimer logtime("IBA::fit");
649     if (!IBAprep(roi, &dst, &src,
650                  IBAprep_NO_SUPPORT_VOLUME | IBAprep_NO_COPY_ROI_FULL))
651         return false;
652     const ImageSpec& srcspec(src.spec());
653     const ImageSpec& dstspec(dst.spec());
654     // Resize ratios
655     float wratio = float(dstspec.full_width) / float(srcspec.full_width);
656     float hratio = float(dstspec.full_height) / float(srcspec.full_height);
657 
658     // Set up a shared pointer with custom deleter to make sure any
659     // filter we allocate here is properly destroyed.
660     auto filter = get_resize_filter(filtername, fwidth, dst, wratio, hratio);
661     if (!filter)
662         return false;  // error issued in get_resize_filter
663 
664     logtime.stop();  // it will be picked up again by the next call...
665     return fit(dst, src, filter.get(), exact, roi, nthreads);
666 }
667 
668 
669 
670 ImageBuf
fit(const ImageBuf & src,Filter2D * filter,bool exact,ROI roi,int nthreads)671 ImageBufAlgo::fit(const ImageBuf& src, Filter2D* filter, bool exact, ROI roi,
672                   int nthreads)
673 {
674     ImageBuf result;
675     bool ok = fit(result, src, filter, exact, roi, nthreads);
676     if (!ok && !result.has_error())
677         result.errorf("ImageBufAlgo::fit() error");
678     return result;
679 }
680 
681 
682 ImageBuf
fit(const ImageBuf & src,string_view filtername,float filterwidth,bool exact,ROI roi,int nthreads)683 ImageBufAlgo::fit(const ImageBuf& src, string_view filtername,
684                   float filterwidth, bool exact, ROI roi, int nthreads)
685 {
686     ImageBuf result;
687     bool ok = fit(result, src, filtername, filterwidth, exact, roi, nthreads);
688     if (!ok && !result.has_error())
689         result.errorf("ImageBufAlgo::fit() error");
690     return result;
691 }
692 
693 
694 
695 template<typename DSTTYPE, typename SRCTYPE>
696 static bool
resample_(ImageBuf & dst,const ImageBuf & src,bool interpolate,ROI roi,int nthreads)697 resample_(ImageBuf& dst, const ImageBuf& src, bool interpolate, ROI roi,
698           int nthreads)
699 {
700     OIIO_ASSERT(src.deep() == dst.deep());
701     ImageBufAlgo::parallel_image(roi, nthreads, [&](ROI roi) {
702         const ImageSpec& srcspec(src.spec());
703         const ImageSpec& dstspec(dst.spec());
704         int nchannels = src.nchannels();
705         bool deep     = src.deep();
706 
707         // Local copies of the source image window, converted to float
708         float srcfx = srcspec.full_x;
709         float srcfy = srcspec.full_y;
710         float srcfw = srcspec.full_width;
711         float srcfh = srcspec.full_height;
712 
713         float dstfx          = dstspec.full_x;
714         float dstfy          = dstspec.full_y;
715         float dstfw          = dstspec.full_width;
716         float dstfh          = dstspec.full_height;
717         float dstpixelwidth  = 1.0f / dstfw;
718         float dstpixelheight = 1.0f / dstfh;
719         float* pel           = OIIO_ALLOCA(float, nchannels);
720 
721         ImageBuf::Iterator<DSTTYPE> out(dst, roi);
722         ImageBuf::ConstIterator<SRCTYPE> srcpel(src);
723         for (int y = roi.ybegin; y < roi.yend; ++y) {
724             // s,t are NDC space
725             float t = (y - dstfy + 0.5f) * dstpixelheight;
726             // src_xf, src_xf are image space float coordinates
727             float src_yf = srcfy + t * srcfh;
728             // src_x, src_y are image space integer coordinates of the floor
729             int src_y = ifloor(src_yf);
730             for (int x = roi.xbegin; x < roi.xend; ++x, ++out) {
731                 float s      = (x - dstfx + 0.5f) * dstpixelwidth;
732                 float src_xf = srcfx + s * srcfw;
733                 int src_x    = ifloor(src_xf);
734                 if (deep) {
735                     srcpel.pos(src_x, src_y, 0);
736                     int nsamps = srcpel.deep_samples();
737                     OIIO_DASSERT(nsamps == out.deep_samples());
738                     if (!nsamps || nsamps != out.deep_samples())
739                         continue;
740                     for (int c = 0; c < nchannels; ++c) {
741                         if (dstspec.channelformat(c) == TypeDesc::UINT32)
742                             for (int samp = 0; samp < nsamps; ++samp)
743                                 out.set_deep_value(
744                                     c, samp, srcpel.deep_value_uint(c, samp));
745                         else
746                             for (int samp = 0; samp < nsamps; ++samp)
747                                 out.set_deep_value(c, samp,
748                                                    srcpel.deep_value(c, samp));
749                     }
750                 } else if (interpolate) {
751                     // Non-deep image, bilinearly interpolate
752                     src.interppixel(src_xf, src_yf, pel, ImageBuf::WrapClamp);
753                     for (int c = roi.chbegin; c < roi.chend; ++c)
754                         out[c] = pel[c];
755                 } else {
756                     // Non-deep image, just copy closest pixel
757                     srcpel.pos(src_x, src_y, 0);
758                     for (int c = roi.chbegin; c < roi.chend; ++c)
759                         out[c] = srcpel[c];
760                 }
761             }
762         }
763     });
764     return true;
765 }
766 
767 
768 
769 bool
resample(ImageBuf & dst,const ImageBuf & src,bool interpolate,ROI roi,int nthreads)770 ImageBufAlgo::resample(ImageBuf& dst, const ImageBuf& src, bool interpolate,
771                        ROI roi, int nthreads)
772 {
773     pvt::LoggedTimer logtime("IBA::resample");
774     if (!IBAprep(roi, &dst, &src,
775                  IBAprep_NO_SUPPORT_VOLUME | IBAprep_NO_COPY_ROI_FULL
776                      | IBAprep_SUPPORT_DEEP))
777         return false;
778 
779     if (dst.deep()) {
780         // If it's deep, figure out the sample allocations first, because
781         // it's not thread-safe to do that simultaneously with copying the
782         // values.
783         const ImageSpec& srcspec(src.spec());
784         const ImageSpec& dstspec(dst.spec());
785         float srcfx          = srcspec.full_x;
786         float srcfy          = srcspec.full_y;
787         float srcfw          = srcspec.full_width;
788         float srcfh          = srcspec.full_height;
789         float dstpixelwidth  = 1.0f / dstspec.full_width;
790         float dstpixelheight = 1.0f / dstspec.full_height;
791         ImageBuf::ConstIterator<float> srcpel(src, roi);
792         ImageBuf::Iterator<float> dstpel(dst, roi);
793         for (; !dstpel.done(); ++dstpel, ++srcpel) {
794             float s   = (dstpel.x() - dstspec.full_x + 0.5f) * dstpixelwidth;
795             float t   = (dstpel.y() - dstspec.full_y + 0.5f) * dstpixelheight;
796             int src_y = ifloor(srcfy + t * srcfh);
797             int src_x = ifloor(srcfx + s * srcfw);
798             srcpel.pos(src_x, src_y, 0);
799             dstpel.set_deep_samples(srcpel.deep_samples());
800         }
801     }
802 
803     bool ok;
804     OIIO_DISPATCH_COMMON_TYPES2(ok, "resample", resample_, dst.spec().format,
805                                 src.spec().format, dst, src, interpolate, roi,
806                                 nthreads);
807     return ok;
808 }
809 
810 
811 
812 ImageBuf
resample(const ImageBuf & src,bool interpolate,ROI roi,int nthreads)813 ImageBufAlgo::resample(const ImageBuf& src, bool interpolate, ROI roi,
814                        int nthreads)
815 {
816     ImageBuf result;
817     bool ok = resample(result, src, interpolate, roi, nthreads);
818     if (!ok && !result.has_error())
819         result.errorf("ImageBufAlgo::resample() error");
820     return result;
821 }
822 
823 
824 
825 #if 0
826 template<typename DSTTYPE, typename SRCTYPE>
827 static bool
828 affine_resample_ (ImageBuf &dst, const ImageBuf &src, const Imath::M33f &Minv,
829                   ROI roi, int nthreads)
830 {
831     ImageBufAlgo::parallel_image (roi, nthreads, [&](ROI roi){
832         ImageBuf::Iterator<DSTTYPE,DSTTYPE> d (dst, roi);
833         ImageBuf::ConstIterator<SRCTYPE,DSTTYPE> s (src);
834         for (  ;  ! d.done();  ++d) {
835             Imath::V2f P (d.x()+0.5f, d.y()+0.5f);
836             Minv.multVecMatrix (P, P);
837             s.pos (int(floorf(P.x)), int(floorf(P.y)), d.z());
838             for (int c = roi.chbegin;  c < roi.chend;  ++c)
839                 d[c] = s[c];
840         }
841     });
842     return true;
843 }
844 #endif
845 
846 
847 
848 template<typename DSTTYPE, typename SRCTYPE>
849 static bool
warp_(ImageBuf & dst,const ImageBuf & src,const Imath::M33f & M,const Filter2D * filter,ImageBuf::WrapMode wrap,ROI roi,int nthreads)850 warp_(ImageBuf& dst, const ImageBuf& src, const Imath::M33f& M,
851       const Filter2D* filter, ImageBuf::WrapMode wrap, ROI roi, int nthreads)
852 {
853     ImageBufAlgo::parallel_image(roi, nthreads, [&](ROI roi) {
854         int nc     = dst.nchannels();
855         float* pel = OIIO_ALLOCA(float, nc);
856         memset(pel, 0, nc * sizeof(float));
857         Imath::M33f Minv = M.inverse();
858         ImageBuf::Iterator<DSTTYPE> out(dst, roi);
859         for (; !out.done(); ++out) {
860             Dual2 x(out.x() + 0.5f, 1.0f, 0.0f);
861             Dual2 y(out.y() + 0.5f, 0.0f, 1.0f);
862             robust_multVecMatrix(Minv, x, y, x, y);
863             filtered_sample<SRCTYPE>(src, x.val(), y.val(), x.dx(), y.dx(),
864                                      x.dy(), y.dy(), filter, wrap, pel);
865             for (int c = roi.chbegin; c < roi.chend; ++c)
866                 out[c] = pel[c];
867         }
868     });
869     return true;
870 }
871 
872 
873 
874 bool
warp(ImageBuf & dst,const ImageBuf & src,const Imath::M33f & M,const Filter2D * filter,bool recompute_roi,ImageBuf::WrapMode wrap,ROI roi,int nthreads)875 ImageBufAlgo::warp(ImageBuf& dst, const ImageBuf& src, const Imath::M33f& M,
876                    const Filter2D* filter, bool recompute_roi,
877                    ImageBuf::WrapMode wrap, ROI roi, int nthreads)
878 {
879     pvt::LoggedTimer logtime("IBA::warp");
880     ROI src_roi_full = src.roi_full();
881     ROI dst_roi, dst_roi_full;
882     if (dst.initialized()) {
883         dst_roi      = roi.defined() ? roi : dst.roi();
884         dst_roi_full = dst.roi_full();
885     } else {
886         dst_roi      = roi.defined()
887                            ? roi
888                            : (recompute_roi ? transform(M, src.roi()) : src.roi());
889         dst_roi_full = src_roi_full;
890     }
891     dst_roi.chend      = std::min(dst_roi.chend, src.nchannels());
892     dst_roi_full.chend = std::min(dst_roi_full.chend, src.nchannels());
893 
894     if (!IBAprep(dst_roi, &dst, &src, IBAprep_NO_SUPPORT_VOLUME))
895         return false;
896 
897     // Set up a shared pointer with custom deleter to make sure any
898     // filter we allocate here is properly destroyed.
899     std::shared_ptr<Filter2D> filterptr((Filter2D*)NULL, Filter2D::destroy);
900     if (filter == NULL) {
901         // If no filter was provided, punt and just linearly interpolate.
902         filterptr.reset(Filter2D::create("lanczos3", 6.0f, 6.0f));
903         filter = filterptr.get();
904     }
905 
906     bool ok;
907     OIIO_DISPATCH_COMMON_TYPES2(ok, "warp", warp_, dst.spec().format,
908                                 src.spec().format, dst, src, M, filter, wrap,
909                                 dst_roi, nthreads);
910     return ok;
911 }
912 
913 
914 
915 bool
warp(ImageBuf & dst,const ImageBuf & src,const Imath::M33f & M,string_view filtername_,float filterwidth,bool recompute_roi,ImageBuf::WrapMode wrap,ROI roi,int nthreads)916 ImageBufAlgo::warp(ImageBuf& dst, const ImageBuf& src, const Imath::M33f& M,
917                    string_view filtername_, float filterwidth,
918                    bool recompute_roi, ImageBuf::WrapMode wrap, ROI roi,
919                    int nthreads)
920 {
921     // Set up a shared pointer with custom deleter to make sure any
922     // filter we allocate here is properly destroyed.
923     std::shared_ptr<Filter2D> filter((Filter2D*)NULL, Filter2D::destroy);
924     std::string filtername = filtername_.size() ? filtername_ : "lanczos3";
925     for (int i = 0, e = Filter2D::num_filters(); i < e; ++i) {
926         FilterDesc fd;
927         Filter2D::get_filterdesc(i, &fd);
928         if (fd.name == filtername) {
929             float w = filterwidth > 0.0f ? filterwidth : fd.width;
930             float h = filterwidth > 0.0f ? filterwidth : fd.width;
931             filter.reset(Filter2D::create(filtername, w, h));
932             break;
933         }
934     }
935     if (!filter) {
936         dst.errorf("Filter \"%s\" not recognized", filtername);
937         return false;
938     }
939 
940     return warp(dst, src, M, filter.get(), recompute_roi, wrap, roi, nthreads);
941 }
942 
943 
944 
945 ImageBuf
warp(const ImageBuf & src,const Imath::M33f & M,const Filter2D * filter,bool recompute_roi,ImageBuf::WrapMode wrap,ROI roi,int nthreads)946 ImageBufAlgo::warp(const ImageBuf& src, const Imath::M33f& M,
947                    const Filter2D* filter, bool recompute_roi,
948                    ImageBuf::WrapMode wrap, ROI roi, int nthreads)
949 {
950     ImageBuf result;
951     bool ok = warp(result, src, M, filter, recompute_roi, wrap, roi, nthreads);
952     if (!ok && !result.has_error())
953         result.errorf("ImageBufAlgo::warp() error");
954     return result;
955 }
956 
957 
958 
959 ImageBuf
warp(const ImageBuf & src,const Imath::M33f & M,string_view filtername,float filterwidth,bool recompute_roi,ImageBuf::WrapMode wrap,ROI roi,int nthreads)960 ImageBufAlgo::warp(const ImageBuf& src, const Imath::M33f& M,
961                    string_view filtername, float filterwidth,
962                    bool recompute_roi, ImageBuf::WrapMode wrap, ROI roi,
963                    int nthreads)
964 {
965     ImageBuf result;
966     bool ok = warp(result, src, M, filtername, filterwidth, recompute_roi, wrap,
967                    roi, nthreads);
968     if (!ok && !result.has_error())
969         result.errorf("ImageBufAlgo::warp() error");
970     return result;
971 }
972 
973 
974 
975 bool
rotate(ImageBuf & dst,const ImageBuf & src,float angle,float center_x,float center_y,Filter2D * filter,bool recompute_roi,ROI roi,int nthreads)976 ImageBufAlgo::rotate(ImageBuf& dst, const ImageBuf& src, float angle,
977                      float center_x, float center_y, Filter2D* filter,
978                      bool recompute_roi, ROI roi, int nthreads)
979 {
980     // Calculate the rotation matrix
981     Imath::M33f M;
982     M.translate(Imath::V2f(-center_x, -center_y));
983     M.rotate(angle);
984     M *= Imath::M33f().translate(Imath::V2f(center_x, center_y));
985     return ImageBufAlgo::warp(dst, src, M, filter, recompute_roi,
986                               ImageBuf::WrapBlack, roi, nthreads);
987 }
988 
989 
990 
991 bool
rotate(ImageBuf & dst,const ImageBuf & src,float angle,float center_x,float center_y,string_view filtername,float filterwidth,bool recompute_roi,ROI roi,int nthreads)992 ImageBufAlgo::rotate(ImageBuf& dst, const ImageBuf& src, float angle,
993                      float center_x, float center_y, string_view filtername,
994                      float filterwidth, bool recompute_roi, ROI roi,
995                      int nthreads)
996 {
997     // Calculate the rotation matrix
998     Imath::M33f M;
999     M.translate(Imath::V2f(-center_x, -center_y));
1000     M.rotate(angle);
1001     M *= Imath::M33f().translate(Imath::V2f(center_x, center_y));
1002     return ImageBufAlgo::warp(dst, src, M, filtername, filterwidth,
1003                               recompute_roi, ImageBuf::WrapBlack, roi,
1004                               nthreads);
1005 }
1006 
1007 
1008 
1009 bool
rotate(ImageBuf & dst,const ImageBuf & src,float angle,Filter2D * filter,bool recompute_roi,ROI roi,int nthreads)1010 ImageBufAlgo::rotate(ImageBuf& dst, const ImageBuf& src, float angle,
1011                      Filter2D* filter, bool recompute_roi, ROI roi,
1012                      int nthreads)
1013 {
1014     ROI src_roi_full = src.roi_full();
1015     float center_x   = 0.5f * (src_roi_full.xbegin + src_roi_full.xend);
1016     float center_y   = 0.5f * (src_roi_full.ybegin + src_roi_full.yend);
1017     return ImageBufAlgo::rotate(dst, src, angle, center_x, center_y, filter,
1018                                 recompute_roi, roi, nthreads);
1019 }
1020 
1021 
1022 
1023 bool
rotate(ImageBuf & dst,const ImageBuf & src,float angle,string_view filtername,float filterwidth,bool recompute_roi,ROI roi,int nthreads)1024 ImageBufAlgo::rotate(ImageBuf& dst, const ImageBuf& src, float angle,
1025                      string_view filtername, float filterwidth,
1026                      bool recompute_roi, ROI roi, int nthreads)
1027 {
1028     ROI src_roi_full = src.roi_full();
1029     float center_x   = 0.5f * (src_roi_full.xbegin + src_roi_full.xend);
1030     float center_y   = 0.5f * (src_roi_full.ybegin + src_roi_full.yend);
1031     return ImageBufAlgo::rotate(dst, src, angle, center_x, center_y, filtername,
1032                                 filterwidth, recompute_roi, roi, nthreads);
1033 }
1034 
1035 
1036 
1037 ImageBuf
rotate(const ImageBuf & src,float angle,float center_x,float center_y,Filter2D * filter,bool recompute_roi,ROI roi,int nthreads)1038 ImageBufAlgo::rotate(const ImageBuf& src, float angle, float center_x,
1039                      float center_y, Filter2D* filter, bool recompute_roi,
1040                      ROI roi, int nthreads)
1041 {
1042     ImageBuf result;
1043     bool ok = rotate(result, src, angle, center_x, center_y, filter,
1044                      recompute_roi, roi, nthreads);
1045     if (!ok && !result.has_error())
1046         result.errorf("ImageBufAlgo::rotate() error");
1047     return result;
1048 }
1049 
1050 
1051 
1052 ImageBuf
rotate(const ImageBuf & src,float angle,float center_x,float center_y,string_view filtername,float filterwidth,bool recompute_roi,ROI roi,int nthreads)1053 ImageBufAlgo::rotate(const ImageBuf& src, float angle, float center_x,
1054                      float center_y, string_view filtername, float filterwidth,
1055                      bool recompute_roi, ROI roi, int nthreads)
1056 {
1057     ImageBuf result;
1058     bool ok = rotate(result, src, angle, center_x, center_y, filtername,
1059                      filterwidth, recompute_roi, roi, nthreads);
1060     if (!ok && !result.has_error())
1061         result.errorf("ImageBufAlgo::rotate() error");
1062     return result;
1063 }
1064 
1065 
1066 
1067 ImageBuf
rotate(const ImageBuf & src,float angle,Filter2D * filter,bool recompute_roi,ROI roi,int nthreads)1068 ImageBufAlgo::rotate(const ImageBuf& src, float angle, Filter2D* filter,
1069                      bool recompute_roi, ROI roi, int nthreads)
1070 {
1071     ImageBuf result;
1072     bool ok = rotate(result, src, angle, filter, recompute_roi, roi, nthreads);
1073     if (!ok && !result.has_error())
1074         result.errorf("ImageBufAlgo::rotate() error");
1075     return result;
1076 }
1077 
1078 
1079 
1080 ImageBuf
rotate(const ImageBuf & src,float angle,string_view filtername,float filterwidth,bool recompute_roi,ROI roi,int nthreads)1081 ImageBufAlgo::rotate(const ImageBuf& src, float angle, string_view filtername,
1082                      float filterwidth, bool recompute_roi, ROI roi,
1083                      int nthreads)
1084 {
1085     ImageBuf result;
1086     bool ok = rotate(result, src, angle, filtername, filterwidth, recompute_roi,
1087                      roi, nthreads);
1088     if (!ok && !result.has_error())
1089         result.errorf("ImageBufAlgo::rotate() error");
1090     return result;
1091 }
1092 
1093 
1094 OIIO_NAMESPACE_END
1095