# How OpenImageIO Handles Color Space Conversions in ImageBufAlgo: A Deep Dive into the OCIO Pipeline

> Discover how OpenImageIO's ImageBufAlgo transforms color spaces using the OCIO pipeline. Learn about its multi-threaded, SIMD-optimized pixel engine for efficient image processing.

- Repository: [Academy Software Foundation/openimageio](https://github.com/academysoftwarefoundation/openimageio)
- Tags: deep-dive
- Published: 2026-02-23

---

**OpenImageIO performs color space conversions through the `ImageBufAlgo::colorconvert` function family, which bridges OpenColorIO (OCIO) processors with a multi-threaded, SIMD-optimized pixel engine that handles ROI-based processing and optional un-premultiplication.**

OpenImageIO (OIIO) provides a robust color management pipeline through its ImageBufAlgo utilities, enabling developers to transform images between color spaces using industry-standard OpenColorIO configurations. Understanding how OIIO handles these conversions—from high-level API calls down to pixel-wise SIMD operations—is essential for optimizing rendering workflows and maintaining color accuracy across VFX pipelines. This article examines the three-layer architecture that powers color space transformations in the `academysoftwarefoundation/openimageio` repository.

## API Surface: The colorconvert Interface

The public interface for color space conversions resides in [`src/include/OpenImageIO/imagebufalgo.h`](https://github.com/academysoftwarefoundation/openimageio/blob/main/src/include/OpenImageIO/imagebufalgo.h) (lines 1862–1882), where multiple overloaded signatures accommodate different workflow patterns. The primary methods allow conversion by color space names or by supplying a pre-built `ColorProcessor` object.

```cpp
// Transform between named colour spaces, returning a new ImageBuf.
ImageBuf OIIO_API colorconvert (const ImageBuf &src,
                       string_view fromspace, string_view tospace,
                       bool unpremult = true,
                       string_view context_key = "", string_view context_value = "",
                       const ColorConfig* colorconfig = nullptr,
                       ROI roi = {}, int nthreads = 0);

// In-place version (dst == src allowed)
bool OIIO_API colorconvert (ImageBuf &dst, const ImageBuf &src,
                   string_view fromspace, string_view tospace,
                   bool unpremult = true, …);

```

Key parameters include **unpremult** (controlling whether RGB channels are divided by alpha before conversion), **ROI** (defining a region of interest for partial processing), and **nthreads** (specifying parallel execution threads). Variants accepting a raw `ColorProcessor*` skip the name resolution phase and proceed directly to pixel processing, optimizing batch operations where the same transform applies to multiple images.

## OCIO Bridge: Resolving Color Space Names

When color spaces are specified by name, OIIO acts as a bridge to OpenColorIO. The implementation in [`src/libOpenImageIO/color_ocio.cpp`](https://github.com/academysoftwarefoundation/openimageio/blob/main/src/libOpenImageIO/color_ocio.cpp) (lines 2191–2210) handles the creation of `ColorProcessor` objects through `ColorConfig::createColorProcessor`.

```cpp
if (!processor) {
    if (!colorconfig)
        colorconfig = &ColorConfig::default_colorconfig();

    // Resolve the named spaces and create the OCIO processor.
    processor = colorconfig->createColorProcessor(
                   colorconfig->resolve(from),
                   colorconfig->resolve(to),
                   context_key, context_value);
    if (!processor) { … error handling … }
}

```

The processor encapsulates the complete color transform, including any applied looks or context variables. After creation, the function updates the destination image's metadata to reflect the new color space:

```cpp
if (ok) dst.specmod().set_colorspace(to);

```

This ensures that the resulting `ImageBuf` carries correct color space metadata for subsequent operations.

## Pixel-Wise Engine: SIMD and Multi-Threading

The heavy lifting occurs in [`src/libOpenImageIO/color_ocio.cpp`](https://github.com/academysoftwarefoundation/openimageio/blob/main/src/libOpenImageIO/color_ocio.cpp) (lines 2264–2290) through two specialized implementations: the generic `colorconvert_impl` template and the optimized `colorconvert_impl_float_rgba` specialization.

### Generic Templated Path

The `colorconvert_impl` function handles arbitrary pixel types through a template parameterized on `Rtype` and `Atype`. It processes up to four channels (RGB+A) using `parallel_image` to distribute work across threads:

```cpp
template<class Rtype, class Atype>
static bool
colorconvert_impl(ImageBuf& R, const ImageBuf& A,
                  const ColorProcessor* processor,
                  bool unpremult, ROI roi, int nthreads)
{
    // Process up to the first 4 channels (RGB+A)
    int channelsToCopy = std::min(4, roi.nchannels());
    if (channelsToCopy < 4) unpremult = false;

    parallel_image(roi, paropt(nthreads),
        [&](ROI roi) {
            // Allocate temporary scanline buffers (SIMD-friendly vfloat4)
            vfloat4* scanline;  OIIO_ALLOCATE_STACK_OR_HEAP(scanline, vfloat4, width);
            float*   alpha;     OIIO_ALLOCATE_STACK_OR_HEAP(alpha, float, width);

            // Load, optionally un-premultiply, apply OCIO processor,
            // optionally re-premultiply, and write back.
            …
            processor->apply((float*)&scanline[0], width, 1, 4,
                             sizeof(float), 4*sizeof(float),
                             width*4*sizeof(float));
            …
        });
    return true;
}

```

The engine uses **vfloat4** vectors for SIMD-friendly memory layout, automatically un-premultiplying alpha (when `unpremult=true`) before applying the OCIO transform, then re-premultiplying afterward. This prevents darkening artifacts in transparent regions during linear-to-display conversions.

### Float-RGBA Fast Path

When both source and destination are 32-bit float with exactly four channels, `colorconvert_impl_float_rgba` eliminates iterator overhead through direct scanline memory copies. The dispatcher macro `OIIO_DISPATCH_COMMON_TYPES2` automatically selects this path when possible, providing significant performance gains for standard RGBA workflows.

## Practical Implementation Example

The Python bindings expose these C++ functions directly, enabling color conversions with minimal boilerplate:

```python
import OpenImageIO as oiio

# Load a linear Rec.709 EXR

src = oiio.ImageBuf("lin_rec709.exr")

# Convert to sRGB, automatically un-premultiplying if an alpha channel exists

dst = oiio.ImageBufAlgo.colorconvert(src, "lin_rec709", "sRGB", unpremult=True)

# Save the result

dst.write("srgb.tif")

```

For C++ applications, the equivalent workflow involves calling `ImageBufAlgo::colorconvert()` with either named spaces or a cached `ColorProcessor` for repeated transformations.

## Summary

- **Three-layer architecture**: The pipeline separates high-level API declarations ([`imagebufalgo.h`](https://github.com/academysoftwarefoundation/openimageio/blob/main/imagebufalgo.h)), OCIO configuration management ([`color_ocio.cpp`](https://github.com/academysoftwarefoundation/openimageio/blob/main/color_ocio.cpp)), and SIMD-optimized pixel processing.
- **Automatic optimization**: The dispatcher automatically selects a specialized float-RGBA path when possible, falling back to generic templates for other formats.
- **Alpha handling**: The `unpremult` parameter ensures correct color math in transparent regions by temporarily un-premultiplying before the OCIO transform.
- **Thread safety**: The `parallel_image` utility distributes ROI slices across configurable thread counts for multi-core utilization.
- **Metadata preservation**: Successful conversions update the destination `ImageSpec` with the target color space via `set_colorspace()`.

## Frequently Asked Questions

### How does OpenImageIO handle alpha channels during color space conversion?

When the `unpremult` parameter is true (the default), OIIO divides RGB channels by alpha before applying the OpenColorIO processor, then multiplies back afterward. This occurs in `colorconvert_impl` within [`src/libOpenImageIO/color_ocio.cpp`](https://github.com/academysoftwarefoundation/openimageio/blob/main/src/libOpenImageIO/color_ocio.cpp), preventing dark fringes around transparent edges that would otherwise occur if the transform were applied to premultiplied values.

### Can I reuse a ColorProcessor for multiple images to improve performance?

Yes. While the name-based `colorconvert` overloads create a new processor for each call, you can instantiate a `ColorProcessor` once via `ColorConfig::createColorProcessor`, then pass it to the `colorconvert` overload accepting a `ColorProcessor*` pointer. This eliminates redundant OCIO configuration lookups when batch-processing sequences.

### What file formats preserve color space metadata after conversion?

Any format supporting color space metadata (such as OpenEXR, TIFF, and PNG) will store the target color space in the file header after conversion. The implementation explicitly calls `dst.specmod().set_colorspace(to)` upon successful completion, ensuring downstream applications recognize the correct color space without manual tagging.

### Is the color conversion thread-safe for parallel image processing?

Yes. The pixel engine uses `parallel_image` to partition the ROI into independent scanline ranges processed by separate threads. Each thread maintains its own stack-allocated scanline buffers via `OIIO_ALLOCATE_STACK_OR_HEAP`, preventing data races while maximizing CPU utilization across large images.