-
Notifications
You must be signed in to change notification settings - Fork 602
OIIO 2.0 Porting Guide
OpenImageIO 2.0 makes several (minor) non-backwards compatible API changes that will require some minor changes to client applications.
I will describe this for ImageInput, but the situation with ImageOutput is exactly the same.
The ImageInput::create()
function returned a raw pointer to an ImageInput, which the caller was then responsible for eventually destroying.
Old (OIIO 1.x) code:
ImageInput *in = ImageInput::open(filename);
...
in->close(); // all done!
ImageInput::destroy (in); // correctly destroy it
It was equivalent to call ImageInput::create()
and then separately open()
the resulting ImageInput. The same guidelines apply to create() as to open().
Note also that a common mistake was to call delete in
rather than ImageInput::destroy(in)
. That usually worked, and was harmless on Linux or OSX, but could be problematic on Windows systems where sometimes code that lives in different DLLs have different heaps and you should avoid an allocation and a free happening in different DLLs.
In OIIO 2.0, the create()
function was changed to return a std::unique_ptr<ImageInput>
, which makes resource management a lot easier. The new way is:
auto in = ImageInput::open(filename);
...
in->close();
// in will automatically free when it goes out of scope!
// Optional: in.reset();
In OIIO 2.0, when in
leaves scope, it will automatically release its resources. But if you really want to ensure that it happens right now, you can call in.reset()
.
If you have code that needs to work with both OIIO 1.8 and 2.0, you can use this idiom:
auto in = ImageInput::open(filename);
...
in->close();
#if OIIO_VERSION < 10903
ImageInput::destroy(in);
#endif
The auto
will catch the result of open() (or create()) regardless of it's a raw pointer or a unique_ptr. You only need to destroy() the raw pointer, which is the case if OIIO_VERSION < 10903. But if you are confident that your code will only need to support OIIO 2.0 or newer, then you don't need the #if
or the destroy
at all.
You may or may not run into this, depending on whether you have apps that use the ustring
class, and how you use it.
The ustring
class previously had a operator int()
that returned nonzero if the ustring had characters, 0 if empty. So you could do this:
ustring u; // it hold something, or maybe nothing
if (u) {
// u is not empty
}
This was well intentioned, but by being an int, it led to other problems where if you assigned to a char*
, it could do the int cast and then interpret that int as a pointer, and it just gets worse from there. This possibility was theorized to create risk of unintentional bugs in code that used ustring, and as soon as we took away the operator int()
, which will generate a compile-time error if you try to use it that way, indeed we found places in both OSL and USD that used it incorrectly and could never have worked properly (presumably those lines were on untested code paths or made bugs that nobody correctly diagnosed and fixed).
Anyway, long story short, the fix is to change
ustring u, v;
if (u) { ... }
if (!v) { ... }
to
if (!u.empty()) { ... }
if (v.empty()) { ... }
The Python bindings have undergone a significant overhaul. Here are the things that will require changes for OIIO 2.0:
ImageBufAlgo functions tended to require some very non-Pythonic constructs, by passing a result reference that was already a constructed (but empty) ImageBuf. For example:
# assume imageA and imageB are ImageBuf
result = ImageBuf() # make a placeholder
ImageBufAlgo.add (result, imageA, imageB)
Now, each of these ImageBufAlgo functions additionally has a variant that takes only the input arguments and directly returns the result. So you can do this:
result = ImageBufAlgo.add (imageA, imageB)
This kind of change has been made for every IBA function. The old forms still exist, and are sometimes useful, but we think that most users will prefer the new ones in most situations.
Similarly, ImageBufAlgo.compare()
and computePixelStats()
used to take a CompareResults
or PixelStats
reference, respectively. Now they additionally have varieties that will directly return the object:
# OLD:
comp = OpenImageio.CompareResults()
ImageBufAlgo.compare (imageA, imageB, failthresh, warnthresh, comp)
stats = OpenImageIO.PixelStats()
ImageBufAlgo.computePixelStats (imageA, stats)
This has been changed to the more intuitive:
# NEW:
comp = ImageBufAlgo.compare (imageA, ImageB, failthresh, warnthresh)
stats = ImageBufAlgo.computePixelStats (imageA)
Places that previously took a TypeDesc, like this awkward nonsense:
buf = ImageBuf(...)
buf.read (convert=OpenImageIO.TypeDesc(OpenImageIO.UINT8))
Ick! Now you can name the type with a string
buf.read (convert='uint8')
Other type names you will frequently encounter are: 'float', 'uint16', 'half'.
Pretty much anyplace in the OIIO Python APIs that required a TypeDesc will now alternately accept a string.
Functions that took or returned actual pixel data, such as ImageOutput.write_scanline()
or ImageBuf.get_pixels()
, used to pass this data back and forth using an old fashioned Python array.array
, laying out all the values sequentially.
In OIIO 2.0, any time you pass blocks of pixels, you do it with a NumPy ndarray. For example,
imgin = ImageInput.open (...)
pixels = imgin.read_image()
At this point, pixels[0] would be an array.array, and pixels[0] would contain the first channel of the first pixel in the first row and column, etc.
NEW:
imgin = ImageInput.open (...)
pixels = imgin.read_image()
Yeah, looks the same, right? But pixels
is now a NumPy ndarray, and it is indexed as [y][x][channel]
. So, for example, pixels[13][0][2]
would be the blue (channel 2, remember indexing starts with 0) value for the pixel in the leftmost (0) column of row 13 (remember again that they start with 0).
Just like with Python, the C++ interface for ImageBufAlgo always looked like this:
ImageBuf A, B, Result;
ImageBufAlgo::function (Result, A, B);
But now there is an additional simpler version:
ImageBuf Result = ImageBufAlgo::function (A, B);
Three notes:
(a) No, this is not expensive and does not make any extra copies or additional allocation. Thank you, C++ "move semantics"!
(b) The original form is still very useful if you are accumulating into an image and wish to avoid extra allocations, for example, to add A, B, C, D:
ImageBufAlgo::add (Result, A, B);
ImageBufAlgo::add (Result, Result, C);
ImageBufAlgo::add (Result, Result, D);
If you did the "assignment form each time, you would make some unnecessary allocations and copies. But most of the time, the new simple way is preferable, and certainly makes your code easier to read.
(c) You might be wondering, "but the old kind returned a bool indicating if there was an error, the new form returns the result ImageBuf, so how do I know if there was an error?"
Answer: errors will be registered with the result, so you can check:
ImageBufAlgo::add (Result, A, B);
if (R.has_error())
std::cout << "add error: " << R.geterror() << "\n";