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