Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #390 from lgritz/over

ImageBufAlgo::over() Porter/Duff compositing operation and oiiotool --over
by Steven Stavrev (with minor LG mods)
  • Loading branch information...
commit b5b28a9debaec1f270171025b6074d2f342b2183 2 parents 322245d + 93681dc
@lgritz lgritz authored
View
2  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
View
8 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.
View
85 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"
View
95 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 <class Func>
+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
View
68 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),
View
217 src/libOpenImageIO/imagebufalgo.cpp
@@ -37,6 +37,9 @@
/// \file
/// Implementation of ImageBufAlgo algorithms.
+#include <boost/version.hpp>
+#include <boost/bind.hpp>
+
#include <OpenEXR/ImathFun.h>
#include <OpenEXR/half.h>
@@ -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<class Rtype, class Atype, class Btype>
+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<Atype, float> a (A);
+ ImageBuf::ConstIterator<Btype, float> b (B);
+ ImageBuf::Iterator<Rtype, float> 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<class Rtype, class Atype>
+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<Rtype,Atype,type>, 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<class Rtype>
+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<Rtype, float> (R, A, B, roi, nthreads);
+ case TypeDesc::UINT8 :
+ return over_RA<Rtype, unsigned char> (R, A, B, roi, nthreads);
+ case TypeDesc::UINT16 :
+ return over_RA<Rtype, unsigned short> (R, A, B, roi, nthreads);
+ case TypeDesc::HALF :
+ return over_RA<Rtype, half> (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<float> (R, A, B, roi, nthreads);
+ case TypeDesc::UINT8 :
+ return over_R<unsigned char> (R, A, B, roi, nthreads);
+ case TypeDesc::UINT16 :
+ return over_R<unsigned short> (R, A, B, roi, nthreads);
+ case TypeDesc::HALF :
+ return over_R<half> (R, A, B, roi, nthreads);
+ }
+ return false; // unsupported type
+}
+
+
+
}
OIIO_NAMESPACE_EXIT
View
2  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;
};
View
32 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)",
View
BIN  testsuite/oiiotool-composite/a.exr
Binary file not shown
View
BIN  testsuite/oiiotool-composite/b.exr
Binary file not shown
View
BIN  testsuite/oiiotool-composite/ref/a_over_b.exr
Binary file not shown
View
2  testsuite/oiiotool-composite/ref/out.txt
@@ -0,0 +1,2 @@
+Comparing "a_over_b.exr" and "ref/a_over_b.exr"
+PASS
View
15 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" ]
+
Please sign in to comment.
Something went wrong with that request. Please try again.