diff --git a/src/doc/oiiotool.rst b/src/doc/oiiotool.rst index 3dc1510f11..cfe07c57c7 100644 --- a/src/doc/oiiotool.rst +++ b/src/doc/oiiotool.rst @@ -3081,6 +3081,41 @@ current top image. oiiotool mandrill.tif --warp "0.707,0.707,0,-0.707,0.707,0,128,-53.02,1" -o warped.tif +.. option:: --st_warp + + Use the top image as a set of normalized `st` image coordinates to warp the + second image in the stack. + + Optional appended modifiers include: + + `filter=` *name* + Filter name. The default is `lanczos3`. + + `chan_s=` *index* + The index of the `s` channel in the `st` image (default=0). + + `chan_t=` *index* + The index of the `t` channel in the `st` image (default=1). + + `flip_s=` *bool* + If nonzero, mirror the `s` coordinates along the horizontal axis. Useful + for coordinates authored in terms of an origin other than the upper-left + corner (default=0). + + `flip_t=` *bool* + If nonzero, mirror the `t` coordinates along the vertical axis. Useful + for coordinates authored in terms of an origin other than the upper-left + corner (default=0). + + Examples:: + + oiiotool mandrill.tif distortion_st.tif --st_warp -o mandrill_distorted.tif + + # Using an `st` map authored in terms of a lower-left origin (e.g. by + # Nuke), so flip the vertical (`t`) coordinate. + oiiotool mandrill.tif st_from_nuke.tif --st_warp:filter=triangle:flip_t=1 -o mandrill_distorted.tif + + .. option:: --convolve Use the top image as a kernel to convolve the next image farther down diff --git a/src/include/OpenImageIO/imagebufalgo.h b/src/include/OpenImageIO/imagebufalgo.h index e5868dd67a..c7b3b5118b 100644 --- a/src/include/OpenImageIO/imagebufalgo.h +++ b/src/include/OpenImageIO/imagebufalgo.h @@ -789,6 +789,68 @@ bool OIIO_API warp (ImageBuf &dst, const ImageBuf &src, const Imath::M33f &M, /// @} +/// @defgroup st_warp (st_warp: warp an image using per-pixel st coordinates) +/// @{ +/// +/// Warp the `src` image using "st" coordinates from a secondary `stbuf` image. +/// +/// Each pixel in the `stbuf` image is used as a normalized image-space +/// coordinate in the `src` image, which is then sampled at that position using +/// the given reconstruction filter to produce an output pixel. +/// +/// The transform is only defined over the area of the `stbuf` image, and thus +/// the given `roi` argument will be intersected with its geometry. +/// +/// \b NOTE: The current behavior of this transform is modeled to match Nuke's +/// STMap node. +/// +/// @param dst +/// The output ImageBuf. If an initialized buffer is provided, its +/// full-size dimensions must match those of `stbuf`. +/// @param src +/// The source ImageBuf to warp. +/// @param stbuf +/// The ImageBuf holding the st coordinates. This must be holding +/// a floating-point pixel data type. +/// @param chan_s +/// The index of the "s" channel in the `stbuf` image. This defaults +/// to its first channel. +/// @param chan_t +/// The index of the "t" channel in the `stbuf` image. This defaults +/// to its second channel. +/// @param flip_s +/// Whether to mirror the "s" coordinate along the horizontal axis +/// when computing source pixel positions. This is useful if the +/// coordinates are defined in terms of a different image origin +/// than OpenImageIO's. +/// @param flip_t +/// Whether to mirror the "t" coordinate along the vertical axis +/// when computing source pixel positions. This is useful if the +/// coordinates are defined in terms of a different image origin +/// than OpenImageIO's. + +ImageBuf OIIO_API st_warp (const ImageBuf &src, const ImageBuf& stbuf, + string_view filtername=string_view(), + float filterwidth=0.0f, int chan_s=0, int chan_t=1, + bool flip_s=false, bool flip_t=false, ROI roi={}, + int nthreads=0); +ImageBuf OIIO_API st_warp (const ImageBuf &src, const ImageBuf& stbuf, + const Filter2D *filter, int chan_s=0, int chan_t=1, + bool flip_s=false, bool flip_t=false, ROI roi={}, + int nthreads=0); +bool OIIO_API st_warp (ImageBuf &dst, const ImageBuf &src, + const ImageBuf& stbuf, + string_view filtername=string_view(), + float filterwidth=0.0f, int chan_s=0, int chan_t=1, + bool flip_s=false, bool flip_t=false, ROI roi={}, + int nthreads=0); +bool OIIO_API st_warp (ImageBuf &dst, const ImageBuf &src, + const ImageBuf& stbuf, const Filter2D *filter, + int chan_s=0, int chan_t=1, bool flip_s=false, + bool flip_t=false, ROI roi={}, int nthreads=0); +/// @} + + /// Compute per-pixel sum `A + B`, returning the result image. /// /// `A` and `B` may each either be an `ImageBuf&`, or a `cspan` diff --git a/src/libOpenImageIO/imagebufalgo_test.cpp b/src/libOpenImageIO/imagebufalgo_test.cpp index a1f3736aa2..b203e4f11e 100644 --- a/src/libOpenImageIO/imagebufalgo_test.cpp +++ b/src/libOpenImageIO/imagebufalgo_test.cpp @@ -872,6 +872,43 @@ test_IBAprep() +// Test extra validation checks done by `st_warp` +void +test_validate_st_warp_checks() +{ + // using namespace ImageBufAlgo; + std::cout << "test st_warp validation checks" << std::endl; + + const int size = 16; + ImageSpec srcSpec(size, size, 3, TypeDesc::FLOAT); + ImageBuf SRC(srcSpec); + ImageBuf ST; + ImageBuf DST; + + ImageBufAlgo::zero(SRC); + + // Fail: Uninitialized ST buffer + OIIO_CHECK_ASSERT(!ImageBufAlgo::st_warp(DST, SRC, ST)); + + ROI disjointROI(size, size, size * 2, size * 2, 0, 1, 0, 2); + ImageSpec stSpec(disjointROI, TypeDesc::HALF); + ST.reset(stSpec); + // Fail: Non-intersecting ST and output ROIs + OIIO_CHECK_ASSERT(!ImageBufAlgo::st_warp(DST, SRC, ST)); + + stSpec = ImageSpec(size, size, 2, TypeDesc::HALF); + ST.reset(stSpec); + + DST.reset(); + // Fail: Out-of-range chan_s + OIIO_CHECK_ASSERT(!ImageBufAlgo::st_warp(DST, SRC, ST, nullptr, 2)); + // Fail: Out-of-range chan_t + OIIO_CHECK_ASSERT(!ImageBufAlgo::st_warp(DST, SRC, ST, nullptr, 0, 2)); + // Success + OIIO_CHECK_ASSERT(ImageBufAlgo::st_warp(DST, SRC, ST, nullptr)); +} + + void benchmark_parallel_image(int res, int iters) { @@ -988,6 +1025,7 @@ main(int argc, char** argv) histogram_computation_test(); test_maketx_from_imagebuf(); test_IBAprep(); + test_validate_st_warp_checks(); test_opencv(); benchmark_parallel_image(64, iterations * 64); diff --git a/src/libOpenImageIO/imagebufalgo_xform.cpp b/src/libOpenImageIO/imagebufalgo_xform.cpp index 496a055f40..fefd8b234d 100644 --- a/src/libOpenImageIO/imagebufalgo_xform.cpp +++ b/src/libOpenImageIO/imagebufalgo_xform.cpp @@ -203,6 +203,30 @@ filtered_sample(const ImageBuf& src, float s, float t, float dsdx, float dtdx, +static std::shared_ptr +get_warp_filter(string_view filtername_, float filterwidth, ImageBuf& dst) +{ + // Set up a shared pointer with custom deleter to make sure any + // filter we allocate here is properly destroyed. + std::shared_ptr filter((Filter2D*)nullptr, Filter2D::destroy); + std::string filtername = filtername_.size() ? filtername_ : "lanczos3"; + for (int i = 0, e = Filter2D::num_filters(); i < e; ++i) { + FilterDesc fd; + Filter2D::get_filterdesc(i, &fd); + if (fd.name == filtername) { + float w = filterwidth > 0.0f ? filterwidth : fd.width; + filter.reset(Filter2D::create(filtername, w, w)); + break; + } + } + if (!filter) { + dst.errorfmt("Filter \"{}\" not recognized", filtername); + } + return filter; +} + + + template static bool warp_(ImageBuf& dst, const ImageBuf& src, const Imath::M33f& M, @@ -285,29 +309,16 @@ ImageBufAlgo::warp(ImageBuf& dst, const ImageBuf& src, const Imath::M33f& M, bool ImageBufAlgo::warp(ImageBuf& dst, const ImageBuf& src, const Imath::M33f& M, - string_view filtername_, float filterwidth, + string_view filtername, float filterwidth, bool recompute_roi, ImageBuf::WrapMode wrap, ROI roi, int nthreads) { // Set up a shared pointer with custom deleter to make sure any // filter we allocate here is properly destroyed. - std::shared_ptr filter((Filter2D*)NULL, Filter2D::destroy); - std::string filtername = filtername_.size() ? filtername_ : "lanczos3"; - for (int i = 0, e = Filter2D::num_filters(); i < e; ++i) { - FilterDesc fd; - Filter2D::get_filterdesc(i, &fd); - if (fd.name == filtername) { - float w = filterwidth > 0.0f ? filterwidth : fd.width; - float h = filterwidth > 0.0f ? filterwidth : fd.width; - filter.reset(Filter2D::create(filtername, w, h)); - break; - } - } + auto filter = get_warp_filter(filtername, filterwidth, dst); if (!filter) { - dst.errorfmt("Filter \"{}\" not recognized", filtername); - return false; + return false; // error issued in get_warp_filter } - return warp(dst, src, M, filter.get(), recompute_roi, wrap, roi, nthreads); } @@ -1143,4 +1154,232 @@ ImageBufAlgo::rotate(const ImageBuf& src, float angle, string_view filtername, } + +template +static bool +st_warp_(ImageBuf& dst, const ImageBuf& src, const ImageBuf& stbuf, int chan_s, + int chan_t, bool flip_s, bool flip_t, const Filter2D* filter, ROI roi, + int nthreads) +{ + OIIO_DASSERT(filter); + OIIO_DASSERT(dst.spec().nchannels >= roi.chend); + + ImageBufAlgo::parallel_image(roi, nthreads, [&](ROI roi) { + const ImageSpec& srcspec(src.spec()); + const ImageSpec& dstspec(dst.spec()); + const int src_width = srcspec.full_width; + const int src_height = srcspec.full_height; + + const float xscale = float(dstspec.full_width) / src_width; + const float yscale = float(dstspec.full_height) / src_height; + + const int xbegin = src.xbegin(); + const int xend = src.xend(); + const int ybegin = src.ybegin(); + const int yend = src.yend(); + + // The horizontal and vertical filter radii, in source pixels. + // We will sample and filter the source over + // [x-filterrad_x, x+filterrad_x] X [y-filterrad_y,y+filterrad_y]. + const int filterrad_x = (int)ceilf(filter->width() / 2.0f / xscale); + const int filterrad_y = (int)ceilf(filter->height() / 2.0f / yscale); + + // Accumulation buffer for filter samples, typed to maintain the + // necessary precision. + typedef typename Accum_t::type Acc_t; + const int nchannels = roi.chend - roi.chbegin; + Acc_t* sample_accum = OIIO_ALLOCA(Acc_t, nchannels); + + ImageBuf::ConstIterator src_iter(src); + ImageBuf::ConstIterator st_iter(stbuf, roi); + ImageBuf::Iterator out_iter(dst, roi); + + // The ST buffer defines the output dimensions, and thus the bounds of + // the outer loop. + // XXX: Sampling of the source buffer can be entirely random, so there + // are probably some opportunities for optimization in here... + for (; !st_iter.done(); ++st_iter, ++out_iter) { + // Look up source coordinates from ST channels. + float src_s = st_iter[chan_s]; + float src_t = st_iter[chan_t]; + + if (flip_s) { + src_s = 1.0f - src_s; + } + if (flip_t) { + src_t = 1.0f - src_t; + } + + const float src_x = src_s * src_width; + const float src_y = src_t * src_height; + + // Set up source iterator range + const int x_min = clamp((int)floorf(src_x - filterrad_x), xbegin, + xend); + const int x_max = clamp((int)ceilf(src_x + filterrad_x), xbegin, + xend); + const int y_min = clamp((int)floorf(src_y - filterrad_y), ybegin, + yend); + const int y_max = clamp((int)ceilf(src_y + filterrad_y), ybegin, + yend); + + src_iter.rerange(x_min, x_max + 1, y_min, y_max + 1, 0, 1); + + memset(sample_accum, 0, nchannels * sizeof(Acc_t)); + float total_weight = 0.0f; + for (; !src_iter.done(); ++src_iter) { + const float weight = (*filter)(src_iter.x() - src_x + 0.5f, + src_iter.y() - src_y + 0.5f); + total_weight += weight; + for (int idx = 0, chan = roi.chbegin; chan < roi.chend; + ++chan, ++idx) { + sample_accum[idx] += src_iter[chan] * weight; + } + } + + if (total_weight > 0.0f) { + for (int idx = 0, chan = roi.chbegin; chan < roi.chend; + ++chan, ++idx) { + out_iter[chan] = sample_accum[idx] / total_weight; + } + } else { + for (int chan = roi.chbegin; chan < roi.chend; ++chan) { + out_iter[chan] = 0; + } + } + } + }); // end of parallel_image + return true; +} + + + +static bool +check_st_warp_args(ImageBuf& dst, const ImageBuf& src, const ImageBuf& stbuf, + int chan_s, int chan_t, ROI& roi) +{ + // Validate ST buffer + if (!stbuf.initialized()) { + dst.error("ImageBufAlgo::st_warp : Uninitialized ST buffer"); + return false; + } + + const ImageSpec& stSpec = stbuf.spec(); + // XXX: Wanted to use `uint32_t` for channel indices, but I don't want to + // break from the rest of the API and introduce a bunch of compile warnings. + if (chan_s >= stSpec.nchannels) { + dst.errorfmt("ImageBufAlgo::st_warp : Out-of-range S channel index: {}", + chan_s); + return false; + } + if (chan_t >= stSpec.nchannels) { + dst.errorfmt("ImageBufAlgo::st_warp : Out-of-range T channel index: {}", + chan_t); + return false; + } + + // Prep the output buffer, and then intersect the resulting ROI with the ST + // buffer's ROI, since the ST warp is only defined for pixels in the latter. + bool res + = ImageBufAlgo::IBAprep(roi, &dst, &src, + ImageBufAlgo::IBAprep_NO_SUPPORT_VOLUME + | ImageBufAlgo::IBAprep_NO_COPY_ROI_FULL); + if (res) { + const int chbegin = roi.chbegin; + const int chend = roi.chend; + roi = roi_intersection(roi, stSpec.roi()); + if (roi.npixels() <= 0) { + dst.errorfmt("ImageBufAlgo::st_warp : Output ROI does not " + "intersect ST buffer."); + return false; + } + // Make sure to preserve the channel range determined by `IBAprep`. + roi.chbegin = chbegin; + roi.chend = chend; + } + return res; +} + + + +bool +ImageBufAlgo::st_warp(ImageBuf& dst, const ImageBuf& src, const ImageBuf& stbuf, + const Filter2D* filter, int chan_s, int chan_t, + bool flip_s, bool flip_t, ROI roi, int nthreads) +{ + pvt::LoggedTimer logtime("IBA::st_warp"); + + if (!check_st_warp_args(dst, src, stbuf, chan_s, chan_t, roi)) { + return false; + } + + // Set up a shared pointer with custom deleter to make sure any + // filter we allocate here is properly destroyed. + std::shared_ptr filterptr((Filter2D*)nullptr, Filter2D::destroy); + if (!filter) { + // If a null filter was provided, fall back to a reasonable default. + filterptr.reset(Filter2D::create("lanczos3", 6.0f, 6.0f)); + filter = filterptr.get(); + } + + bool ok; + OIIO_DISPATCH_COMMON_TYPES3(ok, "st_warp", st_warp_, dst.spec().format, + src.spec().format, stbuf.spec().format, dst, + src, stbuf, chan_s, chan_t, flip_s, flip_t, + filter, roi, nthreads); + return ok; +} + + + +bool +ImageBufAlgo::st_warp(ImageBuf& dst, const ImageBuf& src, const ImageBuf& stbuf, + string_view filtername, float filterwidth, int chan_s, + int chan_t, bool flip_s, bool flip_t, ROI roi, + int nthreads) +{ + // Set up a shared pointer with custom deleter to make sure any + // filter we allocate here is properly destroyed. + auto filter = get_warp_filter(filtername, filterwidth, dst); + if (!filter) { + return false; // Error issued in `get_warp_filter`. + } + return st_warp(dst, src, stbuf, filter.get(), chan_s, chan_t, flip_s, + flip_t, roi, nthreads); +} + + + +ImageBuf +ImageBufAlgo::st_warp(const ImageBuf& src, const ImageBuf& stbuf, + const Filter2D* filter, int chan_s, int chan_t, + bool flip_s, bool flip_t, ROI roi, int nthreads) +{ + ImageBuf result; + bool ok = st_warp(result, src, stbuf, filter, chan_s, chan_t, flip_s, + flip_t, roi, nthreads); + if (!ok && !result.has_error()) { + result.error("ImageBufAlgo::st_warp : Unknown error"); + } + return result; +} + + + +ImageBuf +ImageBufAlgo::st_warp(const ImageBuf& src, const ImageBuf& stbuf, + string_view filtername, float filterwidth, int chan_s, + int chan_t, bool flip_s, bool flip_t, ROI roi, + int nthreads) +{ + ImageBuf result; + bool ok = st_warp(result, src, stbuf, filtername, filterwidth, chan_s, + chan_t, flip_s, flip_t, roi, nthreads); + if (!ok && !result.has_error()) { + result.error("ImageBufAlgo::st_warp : Unknown error"); + } + return result; +} + + OIIO_NAMESPACE_END diff --git a/src/oiiotool/oiiotool.cpp b/src/oiiotool/oiiotool.cpp index 1412383788..d17016067a 100644 --- a/src/oiiotool/oiiotool.cpp +++ b/src/oiiotool/oiiotool.cpp @@ -3247,6 +3247,19 @@ OIIOTOOL_OP(warp, 1, [](OiiotoolOp& op, span img) { +// --st_warp +OIIOTOOL_OP(st_warp, 2, [](OiiotoolOp& op, span img) { + std::string filtername = op.options()["filter"]; + int chan_s = op.options().get_int("chan_s"); + int chan_t = op.options().get_int("chan_t", 1); + bool flip_s = static_cast(op.options().get_int("flip_s")); + bool flip_t = static_cast(op.options().get_int("flip_t")); + return ImageBufAlgo::st_warp(*img[0], *img[1], *img[2], filtername, 0.0f, + chan_s, chan_t, flip_s, flip_t); +}); + + + // --cshift OIIOTOOL_OP(cshift, 1, [](OiiotoolOp& op, span img) { int xyz[3] = { 0, 0, 0 }; @@ -6076,6 +6089,9 @@ getargs(int argc, char* argv[]) ap.arg("--warp %s:MATRIX") .help("Warp pixels (argument is a 3x3 matrix, separated by commas) (options: filter=%s, recompute_roi=%d)") .action(action_warp); + ap.arg("--st_warp") + .help("Warp the first image using normalized \"st\" coordinates from the second image (options: filter=%s, chan_s=0, chan_t=1, flip_s=0, flip_t=0)") + .action(action_st_warp); ap.arg("--convolve") .help("Convolve with a kernel") .action(action_convolve); diff --git a/src/python/py_imagebufalgo.cpp b/src/python/py_imagebufalgo.cpp index a8b7f49c16..4b80d76616 100644 --- a/src/python/py_imagebufalgo.cpp +++ b/src/python/py_imagebufalgo.cpp @@ -1622,6 +1622,29 @@ IBA_resample_ret(const ImageBuf& src, bool interpolate, ROI roi, int nthreads) } +bool +IBA_st_warp(ImageBuf& dst, const ImageBuf& src, const ImageBuf& stbuf, + const std::string& filtername = "", float filterwidth = 0.0f, + int chan_s = 0, int chan_t = 1, bool flip_s = false, + bool flip_t = false, ROI roi = ROI::All(), int nthreads = 0) +{ + py::gil_scoped_release gil; + return ImageBufAlgo::st_warp(dst, src, stbuf, filtername, filterwidth, + chan_s, chan_t, flip_s, flip_t, roi, nthreads); +} + +ImageBuf +IBA_st_warp_ret(const ImageBuf& src, const ImageBuf& stbuf, + const std::string& filtername = "", float filterwidth = 0.0f, + int chan_s = 0, int chan_t = 1, bool flip_s = false, + bool flip_t = false, ROI roi = ROI::All(), int nthreads = 0) +{ + py::gil_scoped_release gil; + return ImageBufAlgo::st_warp(src, stbuf, filtername, filterwidth, chan_s, + chan_t, flip_s, flip_t, roi, nthreads); +} + + bool IBA_fit(ImageBuf& dst, const ImageBuf& src, const std::string& filtername = "", @@ -2935,6 +2958,15 @@ declare_imagebufalgo(py::module& m) "interpolate"_a = true, "roi"_a = ROI::All(), "nthreads"_a = 0) + .def_static("st_warp", &IBA_st_warp, "dst"_a, "src"_a, "stbuf"_a, + "filtername"_a = "", "filterwidth"_a = 0.0f, "chan_s"_a = 0, + "chan_t"_a = 1, "flip_s"_a = false, "flip_t"_a = false, + "roi"_a = ROI::All(), "nthreads"_a = 0) + .def_static("st_warp", &IBA_st_warp_ret, "src"_a, "stbuf"_a, + "filtername"_a = "", "filterwidth"_a = 0.0f, "chan_s"_a = 0, + "chan_t"_a = 1, "flip_s"_a = false, "flip_t"_a = false, + "roi"_a = ROI::All(), "nthreads"_a = 0) + .def_static("fit", &IBA_fit, "dst"_a, "src"_a, "filtername"_a = "", "filterwidth"_a = 0.0f, "fillmode"_a = "letterbox", "exact"_a = false, "roi"_a = ROI::All(), "nthreads"_a = 0) diff --git a/testsuite/oiiotool-xform/ref/out.txt b/testsuite/oiiotool-xform/ref/out.txt index c43ab7e100..f9161fdbbd 100644 --- a/testsuite/oiiotool-xform/ref/out.txt +++ b/testsuite/oiiotool-xform/ref/out.txt @@ -46,6 +46,8 @@ Comparing "pixelaspect.tif" and "ref/pixelaspect.tif" PASS Comparing "warped.tif" and "ref/warped.tif" PASS +Comparing "st_warped.tif" and "ref/st_warped.tif" +PASS Comparing "rotated.tif" and "ref/rotated.tif" PASS Comparing "rotated-offcenter.tif" and "ref/rotated-offcenter.tif" diff --git a/testsuite/oiiotool-xform/ref/st_warped.tif b/testsuite/oiiotool-xform/ref/st_warped.tif new file mode 100644 index 0000000000..213ae689e6 Binary files /dev/null and b/testsuite/oiiotool-xform/ref/st_warped.tif differ diff --git a/testsuite/oiiotool-xform/run.py b/testsuite/oiiotool-xform/run.py index 77760f0f6c..5987481473 100755 --- a/testsuite/oiiotool-xform/run.py +++ b/testsuite/oiiotool-xform/run.py @@ -84,6 +84,12 @@ def make_test_pattern1 (filename, xres=288, yres=216) : # test warp command += oiiotool ("resize.tif --warp 0.7071068,0.7071068,0,-0.7071068,0.7071068,0,128,-53.01933,1 -o warped.tif") +# test st_warp +# We use an identity ST pattern with a bit of gamma to simulate some warping. +command += oiiotool ("resize.tif " + "--pattern fill:topleft=0,0,0:topright=1,0,0:bottomleft=0,1,0:bottomright=1,1,0 256x256 3 " + "--powc 1.2 --st_warp -o st_warped.tif") + # test flip command += oiiotool ("image.tif --flip -o flip.tif") command += oiiotool ("image.tif --crop 180x140+30+30 --flip -o flip-crop.tif") @@ -144,6 +150,7 @@ def make_test_pattern1 (filename, xres=288, yres=216) : "fith-height-300x300.exr", "pixelaspect.tif", "warped.tif", + "st_warped.tif", "rotated.tif", "rotated-offcenter.tif", "rotated360.tif", "flip.tif", "flip-crop.tif", "flop.tif", "flop-crop.tif",