Skip to content

OIIO 2.0 Porting Guide

Larry Gritz edited this page Dec 10, 2018 · 5 revisions

OpenImageIO 2.0 makes several (minor) non-backwards compatible API changes that will require some minor changes to client applications.

Required: ImageInput/Output::create() and open() now return unique_ptr<>

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.

Possible break: ustring cast to int has been removed

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()) { ... }

Python changes

The Python bindings have undergone a significant overhaul. Here are the things that will require changes for OIIO 2.0:

Additional, More Pythonic ImageBufAlgo returns results directly

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)

You can refer to types using strings

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.

Passing arrays of pixels is now done with NumPy ndarray

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).

Improve your C++ life with ImageBufAlgo

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";