diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4fed42b6b4..e9d284141b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -287,7 +287,7 @@ endif() # List all the individual testsuite tests here: oiio_add_tests (ico gpsread misnamed-file nonwhole-tiles - oiiotool oiiotool-fixnan + oiiotool oiiotool-composite oiiotool-fixnan perchannel sgi rla psd dpx png texture-fill texture-filtersize diff --git a/src/doc/oiiotool.tex b/src/doc/oiiotool.tex index e53c2b4235..b9b1df2dcd 100644 --- a/src/doc/oiiotool.tex +++ b/src/doc/oiiotool.tex @@ -638,7 +638,7 @@ \section{\oiiotool commands that make new images} \apiitem{--sub} Replace the \emph{two} top images with a new image that is the difference -between the next-to-top and the top image. +between the first and second images. \apiend \apiitem{--abs} @@ -646,6 +646,12 @@ \section{\oiiotool commands that make new images} consisting of the \emph{absolute value} of he old pixel value. \apiend +\apiitem{--over} +Replace the \emph{two} top images with a new image that is the +Porter/Duff ``over'' composite of the first image over the second +image. +\apiend + \apiitem{--flip} Replace the current image with a new image that is flipped vertically, with the top scanline becoming the bottom, and vice versa. diff --git a/src/include/imagebuf.h b/src/include/imagebuf.h index 1f6984091c..7a5219a8ab 100644 --- a/src/include/imagebuf.h +++ b/src/include/imagebuf.h @@ -52,6 +52,61 @@ OIIO_NAMESPACE_ENTER { +class ImageBuf; + + + +/// Helper struct describing a region of interest in an image. +/// The region is [xbegin,xend) x [begin,yend) x [zbegin,zend), +/// with the "end" designators signifying one past the last pixel, +/// a la STL style. +struct ROI { + int xbegin, xend, ybegin, yend, zbegin, zend; + bool defined; + + /// Default constructor is an undefined region. + /// + ROI () : defined(false) { } + + /// Constructor with an explicitly defined region. + /// + ROI (int xbegin, int xend, int ybegin, int yend, int zbegin=0, int zend=1) + : xbegin(xbegin), xend(xend), ybegin(ybegin), yend(yend), + zbegin(zbegin), zend(zend), defined(true) + { } + + // Region dimensions. + int width () const { return xend - xbegin; } + int height () const { return yend - ybegin; } + int depth () const { return zend - zbegin; } + /// Total number of pixels in the region. + imagesize_t npixels () const { + imagesize_t w = width(), h = height(), d = depth(); + return w*h*d; + } +}; + + +/// Union of two regions, the smallest region containing both. +ROI roi_union (const ROI &A, const ROI &B); + +/// Intersection of two regions. +ROI roi_intersection (const ROI &A, const ROI &B); + +/// Return pixel data window for this ImageSpec as a ROI. +ROI get_roi (const ImageSpec &spec); + +/// Return full/display window for this ImageSpec as a ROI. +ROI get_roi_full (const ImageSpec &spec); + +/// Set pixel data window for this ImageSpec to a ROI. +void set_roi (ImageSpec &spec, const ROI &newroi); + +/// Set full/display window for this ImageSpec to a ROI. +void set_roi_full (ImageSpec &spec, const ROI &newroi); + + + /// An ImageBuf is a simple in-memory representation of a 2D image. It /// uses ImageInput and ImageOutput underneath for its file I/O, and has /// simple routines for setting and getting individual pixels, that @@ -424,6 +479,8 @@ class DLLPUBLIC ImageBuf { /// aren't local. void *pixeladdr (int x, int y, int z); + /// Is this ImageBuf object initialized? + bool initialized () const { return m_spec_valid || m_pixels_valid; } /// Templated class for referring to an individual pixel in an /// ImageBuf, iterating over the pixels of an ImageBuf, or iterating @@ -480,6 +537,20 @@ class DLLPUBLIC ImageBuf { m_rng_zend = std::min (zend, m_img_zend); pos (m_rng_xbegin, m_rng_ybegin, m_rng_zbegin); } + /// Construct read-write clamped valid iteration region from + /// ImageBuf and ROI. + Iterator (ImageBuf &ib, const ROI &roi) + : m_ib(&ib), m_tile(NULL) + { + init_ib (); + m_rng_xbegin = std::max (roi.xbegin, m_img_xbegin); + m_rng_xend = std::min (roi.xend, m_img_xend); + m_rng_ybegin = std::max (roi.ybegin, m_img_ybegin); + m_rng_yend = std::min (roi.yend, m_img_yend); + m_rng_zbegin = std::max (roi.zbegin, m_img_zbegin); + m_rng_zend = std::min (roi.zend, m_img_zend); + pos (m_rng_xbegin, m_rng_ybegin, m_rng_zbegin); + } /// Construct from an ImageBuf and designated region -- iterate /// over region, starting with the upper left pixel, and do NOT /// clamp the region to the valid image pixels. If "unclamped" @@ -745,6 +816,20 @@ class DLLPUBLIC ImageBuf { m_rng_zend = std::min (zend, m_img_zend); pos (m_rng_xbegin, m_rng_ybegin, m_rng_zbegin); } + /// Construct read-only clamped valid iteration region + /// from ImageBuf and ROI. + ConstIterator (ImageBuf &ib, const ROI &roi) + : m_ib(&ib), m_tile(NULL) + { + init_ib (); + m_rng_xbegin = std::max (roi.xbegin, m_img_xbegin); + m_rng_xend = std::min (roi.xend, m_img_xend); + m_rng_ybegin = std::max (roi.ybegin, m_img_ybegin); + m_rng_yend = std::min (roi.yend, m_img_yend); + m_rng_zbegin = std::max (roi.zbegin, m_img_zbegin); + m_rng_zend = std::min (roi.zend, m_img_zend); + pos (m_rng_xbegin, m_rng_ybegin, m_rng_zbegin); + } /// Construct from an ImageBuf and designated region -- iterate /// over region, starting with the upper left pixel, and do NOT /// clamp the region to the valid image pixels. If "unclamped" diff --git a/src/include/imagebufalgo.h b/src/include/imagebufalgo.h index 617a3b9497..17f1f55d10 100644 --- a/src/include/imagebufalgo.h +++ b/src/include/imagebufalgo.h @@ -42,6 +42,7 @@ #include "imagebuf.h" #include "fmath.h" #include "color.h" +#include "thread.h" #ifndef __OPENCV_CORE_TYPES_H__ @@ -305,6 +306,100 @@ bool DLLPUBLIC capture_image (ImageBuf &dst, int cameranum = 0, TypeDesc convert=TypeDesc::UNKNOWN); + +/// Set R to the composite of A over B using the Porter/Duff definition +/// of "over", returning true upon success and false for any of a +/// variety of failures (as described below). +/// +/// A and B must have valid alpha channels identified by their ImageSpec +/// alpha_channel field, with the following two exceptions: (a) a +/// 3-channel image with no identified alpha will be assumed to be RGB, +/// alpha == 1.0; (b) a 4-channel image with no identified alpha will be +/// assumed to be RGBA with alpha in channel [3]. If A or B do not have +/// alpha channels (as determined by those rules) or if the number of +/// non-alpha channels do not match between A and B, over() will fail, +/// returning false. +/// +/// R is not already an initialized ImageBuf, it will be sized to +/// encompass the minimal rectangular pixel region containing the union +/// of the defined pixels of A and B, and with a number of channels +/// equal to the number of non-alpha channels of A and B, plus an alpha +/// channel. However, if R is already initialized, it will not be +/// resized, and the "over" operation will apply to its existing pixel +/// data window. In this case, R must have an alpha channel designated +/// and must have the same number of non-alpha channels as A and B, +/// otherwise it will fail, returning false. +/// +/// 'roi' specifies the region of R's pixels which will be computed; +/// existing pixels outside this range will not be altered. If not +/// specified, the default ROI value will be interpreted as a request to +/// apply "A over B" to the entire region of R's pixel data. +/// +/// A, B, and R need not perfectly overlap in their pixel data windows; +/// pixel values of A or B that are outside their respective pixel data +/// window will be treated as having "zero" (0,0,0...) value. +/// +/// threads == 0, the default, indicates that over() should use as many +/// CPU threads as are specified by the global OIIO "threads" attribute. +/// Note that this is not a guarantee, for example, the implementation +/// may choose to spawn fewer threads for images too small to make a +/// large number of threads to be worthwhile. Values of threads > 0 are +/// a request for that specific number of threads, with threads == 1 +/// guaranteed to not spawn additional threads (this is especially +/// useful if over() is being called from one thread of an +/// already-multithreaded program). +bool DLLPUBLIC over (ImageBuf &R, const ImageBuf &A, const ImageBuf &B, + ROI roi = ROI(), int threads = 0); + + + + +/// Helper template for generalized multithreading for image processing +/// functions. Some function/functor f is applied to every pixel the +/// region of interest roi, dividing the region into multiple threads if +/// threads != 1. Note that threads == 0 indicates that the number of +/// threads should be as set by the global OIIO "threads" attribute. +/// +/// Most image operations will require additional arguments, including +/// additional input and output images or other parameters. The +/// parallel_image template can still be used by employing the +/// boost::bind (or std::bind, for C++11). For example, suppose you +/// have an image operation defined as: +/// void my_image_op (ImageBuf &out, const ImageBuf &in, +/// float scale, ROI roi); +/// Then you can parallelize it as follows: +/// ImageBuf R /*result*/, A /*input*/; +/// ROI roi = get_roi (R); +/// parallel_image (boost::bind(my_image_op,boost::ref(R), +/// boost::cref(A),3.14,_1), roi); +/// +template +void +parallel_image (Func f, ROI roi, int nthreads=0) +{ + // Special case: threads <= 0 means to use the "threads" attribute + if (nthreads <= 0) + OIIO::getattribute ("threads", nthreads); + + if (nthreads <= 1 || roi.npixels() < 1000) { + // Just one thread, or a small image region: use this thread only + f (roi); + } else { + // Spawn threads by dividing the region into y bands. + boost::thread_group threads; + int blocksize = std::max (1, (roi.height() + nthreads - 1) / nthreads); + int roi_ybegin = roi.ybegin; + int roi_yend = roi.yend; + for (int i = 0; i < nthreads; i++) { + roi.ybegin = roi_ybegin + i * blocksize; + roi.yend = std::min (roi.ybegin + blocksize, roi_yend); + threads.add_thread (new boost::thread (f, roi)); + } + threads.join_all (); + } +} + + }; // end namespace ImageBufAlgo diff --git a/src/libOpenImageIO/imagebuf.cpp b/src/libOpenImageIO/imagebuf.cpp index bc49e8022e..62233d1505 100644 --- a/src/libOpenImageIO/imagebuf.cpp +++ b/src/libOpenImageIO/imagebuf.cpp @@ -50,6 +50,74 @@ OIIO_NAMESPACE_ENTER { + + +ROI +get_roi (const ImageSpec &spec) +{ + return ROI (spec.x, spec.x + spec.width, + spec.y, spec.y + spec.height, + spec.z, spec.z + spec.depth); +} + + + +ROI +get_roi_full (const ImageSpec &spec) +{ + return ROI (spec.full_x, spec.full_x + spec.full_width, + spec.full_y, spec.full_y + spec.full_height, + spec.full_z, spec.full_z + spec.full_depth); +} + + + +void +set_roi (ImageSpec &spec, const ROI &newroi) +{ + spec.x = newroi.xbegin; + spec.y = newroi.ybegin; + spec.z = newroi.zbegin; + spec.width = newroi.width(); + spec.height = newroi.height(); + spec.depth = newroi.depth(); +} + + + +void +set_roi_full (ImageSpec &spec, const ROI &newroi) +{ + spec.full_x = newroi.xbegin; + spec.full_y = newroi.ybegin; + spec.full_z = newroi.zbegin; + spec.full_width = newroi.width(); + spec.full_height = newroi.height(); + spec.full_depth = newroi.depth(); +} + + + +ROI +roi_union (const ROI &A, const ROI &B) +{ + return ROI (std::min (A.xbegin, B.xbegin), std::max (A.xend, B.xend), + std::min (A.ybegin, B.ybegin), std::max (A.yend, B.yend), + std::min (A.zbegin, B.zbegin), std::max (A.zend, B.zend)); +} + + + +ROI +roi_intersection (const ROI &A, const ROI &B) +{ + return ROI (std::max (A.xbegin, B.xbegin), std::min (A.xend, B.xend), + std::max (A.ybegin, B.ybegin), std::min (A.yend, B.yend), + std::max (A.zbegin, B.zbegin), std::min (A.zend, B.zend)); +} + + + ImageBuf::ImageBuf (const std::string &filename, ImageCache *imagecache) : m_name(filename), m_nsubimages(0), diff --git a/src/libOpenImageIO/imagebufalgo.cpp b/src/libOpenImageIO/imagebufalgo.cpp index e34ac0d265..3f605f7397 100644 --- a/src/libOpenImageIO/imagebufalgo.cpp +++ b/src/libOpenImageIO/imagebufalgo.cpp @@ -37,6 +37,9 @@ /// \file /// Implementation of ImageBufAlgo algorithms. +#include +#include + #include #include @@ -50,6 +53,7 @@ #include "dassert.h" #include "sysutil.h" #include "filter.h" +#include "thread.h" OIIO_NAMESPACE_ENTER @@ -1119,5 +1123,218 @@ ImageBufAlgo::fixNonFinite (ImageBuf &dst, const ImageBuf &src, } + +namespace { // anonymous namespace + +// Fully type-specialized version of over. +template +void +over_RAB (ImageBuf &R, const ImageBuf &A, const ImageBuf &B, ROI roi) +{ + // Output image R. + const ImageSpec &specR = R.spec(); + int channels_R = specR.nchannels; + + // Input image A. + const ImageSpec &specA = A.spec(); + int alpha_index_A = specA.alpha_channel; + int has_alpha_A = (alpha_index_A >= 0); + int channels_A = specA.nchannels; + + // Input image B. + const ImageSpec &specB = B.spec(); + int alpha_index_B = specB.alpha_channel; + int has_alpha_B = (alpha_index_B >= 0); + int channels_B = specB.nchannels; + + int channels_AB = std::min (channels_A, channels_B); + + ImageBuf::ConstIterator a (A); + ImageBuf::ConstIterator b (B); + ImageBuf::Iterator r (R, roi); + for ( ; ! r.done(); r++) { + a.pos (r.x(), r.y(), r.z()); + b.pos (r.x(), r.y(), r.z()); + + if (! a.valid()) { + if (! b.valid()) { + // a and b invalid. + for (int c = 0; c < channels_R; c++) { r[c] = 0.0f; } + } else { + // a invalid, b valid. + for (int c = 0; c < channels_B; c++) { r[c] = b[c]; } + if (! has_alpha_B) { r[3] = 1.0f; } + } + continue; + } + + if (! b.valid()) { + // a valid, b invalid. + for (int c = 0; c < channels_A; c++) { r[c] = a[c]; } + if (! has_alpha_A) { r[3] = 1.0f; } + continue; + } + + // At this point, a and b are valid. + float alpha_A = has_alpha_A + ? clamp (a[alpha_index_A], 0.0f, 1.0f) : 1.0f; + float one_minus_alpha_A = 1.0f - alpha_A; + for (int c = 0; c < channels_AB; c++) + r[c] = a[c] + one_minus_alpha_A * b[c]; + if (channels_R != channels_AB) { + // R has 4 channels, A or B has 3 channels -> alpha channel is 3. + r[3] = alpha_A + one_minus_alpha_A * (has_alpha_B ? b[3] : 1.0f); + } + } +} + + + +// Partially type-specialized version of over -- return and A types are +// known, we still need to specialize based on B's type. +template +bool +over_RA (ImageBuf &R, const ImageBuf &A, const ImageBuf &B, ROI roi, + int nthreads) +{ +// Shorten text below with a macro +#define RUN_OP(type) \ + ImageBufAlgo::parallel_image ( \ + boost::bind (over_RAB, boost::ref(R), \ + boost::cref(A), boost::cref(B), _1), \ + roi, nthreads) + + switch (B.spec().format.basetype) { + case TypeDesc::FLOAT : RUN_OP (float); return true; + case TypeDesc::UINT8 : RUN_OP (unsigned char); return true; + case TypeDesc::UINT16 : RUN_OP (unsigned short); return true; + case TypeDesc::HALF : RUN_OP (half); return true; + } + return false; // unsupported type +#undef RUN_OP +} + + + +// Partially type-specialized version of over -- return type known, +// need to specialize on A and B. +template +bool +over_R (ImageBuf &R, const ImageBuf &A, const ImageBuf &B, ROI roi, + int nthreads) +{ + switch (A.spec().format.basetype) { + case TypeDesc::FLOAT : + return over_RA (R, A, B, roi, nthreads); + case TypeDesc::UINT8 : + return over_RA (R, A, B, roi, nthreads); + case TypeDesc::UINT16 : + return over_RA (R, A, B, roi, nthreads); + case TypeDesc::HALF : + return over_RA (R, A, B, roi, nthreads); + } + return false; // unsupported type +} + +} // anonymous namespace + + +bool +ImageBufAlgo::over (ImageBuf &R, const ImageBuf &A, const ImageBuf &B, ROI roi, + int nthreads) +{ + // Output image R. + const ImageSpec &specR = R.spec(); + int alpha_R = specR.alpha_channel; + int has_alpha_R = (alpha_R >= 0); + int channels_R = specR.nchannels; + int non_alpha_R = channels_R - has_alpha_R; + bool initialized_R = R.initialized(); + + // Input image A. + const ImageSpec &specA = A.spec(); + int alpha_A = specA.alpha_channel; + int has_alpha_A = (alpha_A >= 0); + int channels_A = specA.nchannels; + int non_alpha_A = has_alpha_A ? (channels_A - 1) : 3; + bool A_not_34 = channels_A != 3 && channels_A != 4; + + // Input image B. + const ImageSpec &specB = B.spec(); + int alpha_B = specB.alpha_channel; + int has_alpha_B = (alpha_B >= 0); + int channels_B = specB.nchannels; + int non_alpha_B = has_alpha_B ? (channels_B - 1) : 3; + bool B_not_34 = channels_B != 3 && channels_B != 4; + + // Fail if the input images have a Z channel. + if (specA.z_channel >= 0 || specB.z_channel >= 0) + return false; + + // If input images A and B have different number of non-alpha channels + // then return false. + if (non_alpha_A != non_alpha_B) + return false; + + // A or B has number of channels different than 3 and 4, and it does + // not have an alpha channel. + if ((A_not_34 && !has_alpha_A) || (B_not_34 && !has_alpha_B)) + return false; + + // A or B has zero or one channel -> return false. + if (channels_A <= 1 || channels_B <= 1) + return false; + + // Initialized R -> use as allocated. + // Uninitialized R -> size it to the union of A and B. + ImageSpec newspec = ImageSpec (); + ROI union_AB = roi_union (get_roi(specA), get_roi(specB)); + set_roi (newspec, union_AB); + if ((! has_alpha_A && ! has_alpha_B) + || (has_alpha_A && ! has_alpha_B && alpha_A == channels_A - 1) + || (! has_alpha_A && has_alpha_B && alpha_B == channels_B - 1)) { + if (! initialized_R) { + newspec.nchannels = 4; + newspec.alpha_channel = 3; + R.reset ("over", newspec); + } else { + if (non_alpha_R != 3 || alpha_R != 3) + return false; + } + } else if (has_alpha_A && has_alpha_B && alpha_A == alpha_B) { + if (! initialized_R) { + newspec.nchannels = channels_A; + newspec.alpha_channel = alpha_A; + R.reset ("over", newspec); + } else { + if (non_alpha_R != non_alpha_A || alpha_R != alpha_A) + return false; + } + } else { + return false; + } + + // Specified ROI -> use it. Unspecified ROI -> initialize from R. + if (! roi.defined) { + roi = get_roi (R.spec()); + } + + // Call over_R, specialize by return type (which will in turn + // specialize by source image types. + switch (R.spec().format.basetype) { + case TypeDesc::FLOAT : + return over_R (R, A, B, roi, nthreads); + case TypeDesc::UINT8 : + return over_R (R, A, B, roi, nthreads); + case TypeDesc::UINT16 : + return over_R (R, A, B, roi, nthreads); + case TypeDesc::HALF : + return over_R (R, A, B, roi, nthreads); + } + return false; // unsupported type +} + + + } OIIO_NAMESPACE_EXIT diff --git a/src/libOpenImageIO/imageio.cpp b/src/libOpenImageIO/imageio.cpp index 2ecba06e29..f487e27872 100644 --- a/src/libOpenImageIO/imageio.cpp +++ b/src/libOpenImageIO/imageio.cpp @@ -49,7 +49,7 @@ OIIO_NAMESPACE_ENTER // Global private data namespace pvt { -int oiio_threads = 1; +int oiio_threads = boost::thread::hardware_concurrency(); ustring plugin_searchpath; }; diff --git a/src/oiiotool/oiiotool.cpp b/src/oiiotool/oiiotool.cpp index 3758142b1e..cf59dfae51 100644 --- a/src/oiiotool/oiiotool.cpp +++ b/src/oiiotool/oiiotool.cpp @@ -1396,6 +1396,37 @@ action_fixnan (int argc, const char *argv[]) +static int +action_over (int argc, const char *argv[]) +{ + if (ot.postpone_callback (2, action_over, argc, argv)) + return 0; + + ImageRecRef B (ot.pop()); + ImageRecRef A (ot.pop()); + ot.read (A); + ot.read (B); + const ImageBuf &Aib ((*A)()); + const ImageBuf &Bib ((*B)()); + const ImageSpec &specA = Aib.spec(); + const ImageSpec &specB = Bib.spec(); + + // Create output image specification. + ImageSpec specR = specA; + set_roi (specR, roi_union (get_roi(specA), get_roi(specB))); + specR.nchannels = std::max (specA.nchannels, specB.nchannels); + if (specR.alpha_channel < 0 && specR.nchannels == 4) + specR.alpha_channel = 3; + + ot.push (new ImageRec ("irec", specR, ot.imagecache)); + ImageBuf &Rib ((*ot.curimg)()); + + ImageBufAlgo::over (Rib, Aib, Bib); + return 0; +} + + + static void getargs (int argc, char *argv[]) { @@ -1470,6 +1501,7 @@ getargs (int argc, char *argv[]) "--add %@", action_add, NULL, "Add two images", "--sub %@", action_sub, NULL, "Subtract two images", "--abs %@", action_abs, NULL, "Take the absolute value of the image pixels", + "--over %@", action_over, NULL, "'Over' composite of two images", "--flip %@", action_flip, NULL, "Flip the image vertically (top<->bottom)", "--flop %@", action_flop, NULL, "Flop the image horizontally (left<->right)", "--flipflop %@", action_flipflop, NULL, "Flip and flop the image (180 degree rotation)", diff --git a/testsuite/oiiotool-composite/a.exr b/testsuite/oiiotool-composite/a.exr new file mode 100644 index 0000000000..5d17dbe4af Binary files /dev/null and b/testsuite/oiiotool-composite/a.exr differ diff --git a/testsuite/oiiotool-composite/b.exr b/testsuite/oiiotool-composite/b.exr new file mode 100644 index 0000000000..ec46edac6f Binary files /dev/null and b/testsuite/oiiotool-composite/b.exr differ diff --git a/testsuite/oiiotool-composite/ref/a_over_b.exr b/testsuite/oiiotool-composite/ref/a_over_b.exr new file mode 100644 index 0000000000..764dfc9609 Binary files /dev/null and b/testsuite/oiiotool-composite/ref/a_over_b.exr differ diff --git a/testsuite/oiiotool-composite/ref/out.txt b/testsuite/oiiotool-composite/ref/out.txt new file mode 100644 index 0000000000..6483c48316 --- /dev/null +++ b/testsuite/oiiotool-composite/ref/out.txt @@ -0,0 +1,2 @@ +Comparing "a_over_b.exr" and "ref/a_over_b.exr" +PASS diff --git a/testsuite/oiiotool-composite/run.py b/testsuite/oiiotool-composite/run.py new file mode 100755 index 0000000000..d93d98ea42 --- /dev/null +++ b/testsuite/oiiotool-composite/run.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +# Test for oiiotool application of the Porter/Duff compositing operations +# + + +# test over +command += (oiio_app("oiiotool") + + " a.exr --over b.exr -o a_over_b.exr >> out.txt ;\n") + +# future: test in, out, etc., the other Porter/Duff operations + +# Outputs to check against references +outputs = [ "a_over_b.exr", "out.txt" ] +