This is an automated email from the git hooks/post-receive script.
Git pushed a commit to branch master
in repository ffmpeg.
The following commit(s) were added to refs/heads/master by this push:
new 677cf95ea4 Initial checkin of OCIO filter.
677cf95ea4 is described below
commit 677cf95ea4be805fa326adb082eff666a2e790ea
Author: [email protected] <[email protected]>
AuthorDate: Wed Jan 7 20:50:17 2026 +0000
Commit: Lynne <[email protected]>
CommitDate: Sat Feb 14 12:21:10 2026 +0000
Initial checkin of OCIO filter.
Initial checkin of OCIO filter.
Initial checkin of OCIO filter.
Signed-off-by: [email protected] <[email protected]>
Change for the right C++ library, should work on linux too.
Signed-off-by: [email protected] <[email protected]>
Adding inverse when using display/view.
Removed comments.
Removed code that was setting the CICP values. Hopefully this can be done
through OCIO at some point.
Config cleanup - need a modified require_cpp to handle namespacing.
Switch to using require_cpp so that namespace can be used.
Adding documentation.
Sadly a bit of linting went in here, but more importantly added a threads
option to split the image into horizontal tiles, since OCIO was running rather
slow.
Signed-off-by: [email protected] <[email protected]>
Adding context parameters.
Signed-off-by: [email protected] <[email protected]>
Add the OCIO config parameter.
Signed-off-by: [email protected] <[email protected]>
Make the min threads 1 for now, reserve 0 for later if we can automatically
pick something.
Also added a few comments.
Signed-off-by: [email protected] <[email protected]>
This is using ffmpeg-slicing.
Signed-off-by: [email protected] <[email protected]>
Adding OCIO filetransform.
Making sure everything is using av_log rather than std::cerr.
Signed-off-by: [email protected] <[email protected]>
Updating the tests so they would work without additional files.
Signed-off-by: [email protected] <[email protected]>
Adding the file-transform documentation.
Signed-off-by: [email protected] <[email protected]>
Adding copyright/license info.
Signed-off-by: [email protected] <[email protected]>
Removing tests, since this is optional code.
Signed-off-by: [email protected] <[email protected]>
Code cleanup.
Signed-off-by: [email protected] <[email protected]>
Typo.
Signed-off-by: [email protected] <[email protected]>
I went the wrong way, av_log is expecting \n
Signed-off-by: [email protected] <[email protected]>
Fix indenting to 4 spaces.
Signed-off-by: [email protected] <[email protected]>
Fixing lint issues and a spelling mistake.
Signed-off-by: [email protected] <[email protected]>
Code formatting cleanup to match conventions.
Signed-off-by: [email protected] <[email protected]>
Whitespace removal.
Signed-off-by: [email protected] <[email protected]>
---
configure | 28 ++++
doc/filters.texi | 63 +++++++++
libavfilter/Makefile | 1 +
libavfilter/allfilters.c | 1 +
libavfilter/ocio_wrapper.cpp | 316 +++++++++++++++++++++++++++++++++++++++++++
libavfilter/ocio_wrapper.hpp | 72 ++++++++++
libavfilter/vf_opencolorio.c | 289 +++++++++++++++++++++++++++++++++++++++
7 files changed, 770 insertions(+)
diff --git a/configure b/configure
index 31e1bf3600..48f0cfb967 100755
--- a/configure
+++ b/configure
@@ -259,6 +259,7 @@ External library support:
--enable-libopenh264 enable H.264 encoding via OpenH264 [no]
--enable-libopenjpeg enable JPEG 2000 encoding via OpenJPEG [no]
--enable-libopenmpt enable decoding tracked files via libopenmpt [no]
+ --enable-libopencolorio enable color management via OpenColorIO [no]
--enable-libopenvino enable OpenVINO as a DNN module backend
for DNN based filters like dnn_processing [no]
--enable-libopus enable Opus de/encoding via libopus [no]
@@ -1579,6 +1580,17 @@ check_lib(){
enable $name && eval ${name}_extralibs="\$@"
}
+check_lib_cpp(){
+ log check_lib_cpp "$@"
+ name="$1"
+ headers="$2"
+ code="$3"
+ shift 3
+ disable $name
+ test_code ld "$headers" "$code" cxx "$@" &&
+ enable $name && eval ${name}_extralibs="\$@"
+}
+
check_lib_cxx(){
log check_lib_cxx "$@"
name="$1"
@@ -1765,6 +1777,14 @@ require_cxx(){
check_lib_cxx "$name" "$@" || die "ERROR: $name_version not found"
}
+require_cpp(){
+ log require_cpp "$@"
+ name_version="$1"
+ name="${1%% *}"
+ shift
+ check_lib_cpp "$name" "$@" || die "ERROR: $name_version not found"
+}
+
require_headers(){
log require_headers "$@"
headers="$1"
@@ -2028,6 +2048,8 @@ EXTERNAL_LIBRARY_LIST="
libmysofa
liboapv
libopencv
+ libopencolorio
+ libopenimage
libopenh264
libopenjpeg
libopenmpt
@@ -4119,6 +4141,8 @@ openclsrc_filter_deps="opencl"
qrencode_filter_deps="libqrencode"
qrencodesrc_filter_deps="libqrencode"
quirc_filter_deps="libquirc"
+ocio_filter_deps="libopencolorio"
+libopencolorio_filter_deps="libopencolorio"
overlay_opencl_filter_deps="opencl"
overlay_qsv_filter_deps="libmfx"
overlay_qsv_filter_select="qsvvpp"
@@ -7281,6 +7305,10 @@ enabled libopencore_amrnb && require libopencore_amrnb
opencore-amrnb/interf_dec
enabled libopencore_amrwb && require libopencore_amrwb opencore-amrwb/dec_if.h
D_IF_init -lopencore-amrwb
enabled libopencv && { check_pkg_config libopencv opencv4
opencv2/core/core_c.h cvCreateImageHeader ||
require libopencv opencv2/core/core_c.h
cvCreateImageHeader -lopencv_core -lopencv_imgproc; }
+enabled libopencolorio && add_cxxflags $(pkg-config --cflags OpenColorIO) &&
+ OCIO_LIBS=$($pkg_config --libs OpenColorIO) &&
+ require_cpp OpenColorIO OpenColorIO/OpenColorIO.h
"namespace OCIO = OCIO_NAMESPACE; OCIO::ConfigRcPtr cfg =
OCIO::Config::Create();" $OCIO_LIBS -lstdc++ &&
+ append libopencolorio_extralibs "$OCIO_LIBS
-lstdc++"
enabled libopenh264 && require_pkg_config libopenh264 "openh264 >=
1.3.0" wels/codec_api.h WelsGetCodecVersion
enabled libopenjpeg && { check_pkg_config libopenjpeg "libopenjp2 >=
2.1.0" openjpeg.h opj_version ||
{ require_pkg_config libopenjpeg "libopenjp2 >=
2.1.0" openjpeg.h opj_version -DOPJ_STATIC && add_cppflags -DOPJ_STATIC; } }
diff --git a/doc/filters.texi b/doc/filters.texi
index 71bc1e8686..80a1bda322 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -18793,6 +18793,69 @@ normalize=blackpt=red:whitept=cyan
Pass the video source unchanged to the output.
+@section ocio
+OpenColorIO library filter
+
+This filter allows you to do color management using the OpenColorIO library.
+See https://opencolorio.org/ for more details. To Enable
+compilation of this filter, you need to configure FFmpeg with
@code{--enable-libopencolorio}.
+
+It accepts the following options:
+
+@table @option
+@item config
+By default the filter will use the OCIO config defined by the OCIO environment
variable, but this parameter allows you to explicitly specify its location.
+If you are getting started, you can use
config=ocio://studio-config-v1.0.0_aces-v1.3_ocio-v2.1 which specifies one of
the built in defaults.
+
+@item input
+Set the input colorspace.
+
+@item output
+Set the output colorspace.
+
+@item display
+Set the display colorspace, used in combination with view.
+
+@item view
+Set the view colorspace, used in combination with display.
+
+@item inverse
+When used in combination with display and view, this inverts the transform, so
going from a display/view to the "input colorspace".
+
+@item filetransform
+Allows you to specify an external file-transform to use instead of the OCIO
config file. This is useful for applying a single transform without needing a
full OCIO config file.
+
+@item format
+Allow you to specify the output pix_fmt of the OCIO filter. This *has* to be a
RGB colorspace, so you really are limited to rgb24, rgba, rgb48, rgba48,
gbrp10, gbrp12, gbrpf32le, gbrapf32le, for most encoding we would recommend
rgb48
+
+@item context_params
+Allow you to specify additional context parameters for the OCIO filter. This
is a list of key=value pairs, separated by colons.
+
+@end table
+
+@subsection Examples
+
+Map from ACEScg to ACEScct, this assumes the OCIO file is defined with the
OCIO environment variable.
+@example
+input=ACEScg:output=ACEScct:format=rgb48
+@end example
+
+Map from ACEScg to a sRGB display using the "ACES 1.0 - SDR Video" view
transform, this assumes the OCIO file is defined with the OCIO environment
variable. Note you will need to wrap the argument in quotes to ensure that the
spaces are interpreted correctly.
+@example
+input=ACEScg:display=sRGB - Display:view=ACES 1.0 - SDR Video:format=rgb48
+@end example
+
+
+As above but using the OCIO file
studio-config-v1.0.0_aces-v1.3_ocio-v2.1_ns.ocio rather than the OCIO
environment variable.
+@example
+config=studio-config-v1.0.0_aces-v1.3_ocio-v2.1_ns.ocio:input=ACEScg:display=sRGB
- Display:view=ACES 1.0 - SDR Video:format=rgb48
+@end example
+
+If you are converting to YCrCb you still will want to set the color matrix for
the conversion. This is a good example of combining the two.
+@example
+ffmpeg -y -i SOURCEFRAMES.%05d.exr -c:v libx265 -vf
"ocio=input=ACEScg:output=ACEScct:format=rgb48,scale=in_color_matrix=bt709:out_color_matrix=bt709,format=yuv444p10"
OUTPUTFILE.mov
+@end example
+
@section ocr
Optical Character Recognition
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 4e128ed109..6ecacc346b 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -416,6 +416,7 @@ OBJS-$(CONFIG_NORMALIZE_FILTER) +=
vf_normalize.o
OBJS-$(CONFIG_NULL_FILTER) += vf_null.o
OBJS-$(CONFIG_OCR_FILTER) += vf_ocr.o
OBJS-$(CONFIG_OCV_FILTER) += vf_libopencv.o
+OBJS-$(CONFIG_OCIO_FILTER) += vf_opencolorio.o ocio_wrapper.o
OBJS-$(CONFIG_OSCILLOSCOPE_FILTER) += vf_datascope.o
OBJS-$(CONFIG_OVERLAY_FILTER) += vf_overlay.o framesync.o
OBJS-$(CONFIG_OVERLAY_CUDA_FILTER) += vf_overlay_cuda.o framesync.o
vf_overlay_cuda.ptx.o \
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 33cd637706..458f8c5373 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -391,6 +391,7 @@ extern const FFFilter ff_vf_null;
extern const FFFilter ff_vf_ocr;
extern const FFFilter ff_vf_ocv;
extern const FFFilter ff_vf_oscilloscope;
+extern const FFFilter ff_vf_ocio;
extern const FFFilter ff_vf_overlay;
extern const FFFilter ff_vf_overlay_opencl;
extern const FFFilter ff_vf_overlay_qsv;
diff --git a/libavfilter/ocio_wrapper.cpp b/libavfilter/ocio_wrapper.cpp
new file mode 100644
index 0000000000..5e3b1e2d97
--- /dev/null
+++ b/libavfilter/ocio_wrapper.cpp
@@ -0,0 +1,316 @@
+/*
+ * Copyright (c) 2026 Sam Richards
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+
+#include <OpenColorIO/OpenColorIO.h>
+#include <exception>
+
+namespace OCIO = OCIO_NAMESPACE;
+
+struct OCIOState {
+ OCIO::ConstConfigRcPtr config;
+ OCIO::ConstProcessorRcPtr processor;
+ OCIO::ConstCPUProcessorRcPtr cpu;
+ int channels;
+};
+
+extern "C" {
+
+#include "formats.h"
+#include "ocio_wrapper.hpp"
+#include <libavutil/frame.h>
+#include <libavutil/pixdesc.h>
+#include <libavutil/dict.h>
+
+// Helper to map AV_PIX_FMT to OCIO BitDepth
+static OCIO::BitDepth get_ocio_depth(int format)
+{
+ switch (format) {
+ case AV_PIX_FMT_RGB24:
+ case AV_PIX_FMT_RGBA:
+ return OCIO::BIT_DEPTH_UINT8;
+
+ case AV_PIX_FMT_RGB48:
+ case AV_PIX_FMT_RGBA64:
+ return OCIO::BIT_DEPTH_UINT16;
+
+ case AV_PIX_FMT_GBRP10:
+ case AV_PIX_FMT_GBRAP10:
+ return OCIO::BIT_DEPTH_UINT10;
+
+ case AV_PIX_FMT_GBRP12:
+ case AV_PIX_FMT_GBRAP12:
+ return OCIO::BIT_DEPTH_UINT12;
+
+ // Note: FFmpeg treats half-float as specific types often requiring casts.
+ // For this snippet we map F16 directly if your system supports it,
+ // otherwise, standard float (F32) is safer.
+ case AV_PIX_FMT_GBRPF16:
+ case AV_PIX_FMT_GBRAPF16:
+ return OCIO::BIT_DEPTH_F16;
+
+ case AV_PIX_FMT_GBRPF32:
+ case AV_PIX_FMT_GBRAPF32:
+ return OCIO::BIT_DEPTH_F32;
+
+ default:
+ return OCIO::BIT_DEPTH_UNKNOWN;
+ }
+}
+
+static OCIO::ConstContextRcPtr add_context_params(OCIO::ConstConfigRcPtr
config, AVDictionary *params)
+{
+
+ OCIO::ConstContextRcPtr context = config->getCurrentContext();
+ if (!params)
+ return context;
+ if (!context)
+ return nullptr;
+
+ OCIO::ContextRcPtr ctx = context->createEditableCopy();
+ if (!ctx) {
+ return context;
+ }
+ const AVDictionaryEntry *e = NULL;
+ while ((e = av_dict_iterate(params, e))) {
+ ctx->setStringVar(e->key, e->value);
+ }
+ return ctx;
+}
+
+OCIOHandle
+ocio_create_output_colorspace_processor(AVFilterContext *ctx, const char
*config_path,
+ const char *input_color_space,
+ const char *output_color_space,
+ AVDictionary *params)
+{
+ try {
+ OCIOState *s = new OCIOState();
+ if (!config_path)
+ s->config = OCIO::Config::CreateFromEnv();
+ else
+ s->config = OCIO::Config::CreateFromFile(config_path);
+
+ if (!s->config || !input_color_space || !output_color_space) {
+ av_log(ctx, AV_LOG_ERROR, "Error: Config or color spaces
invalid.\n");
+ if (!s->config) av_log(ctx, AV_LOG_ERROR, "Config is null\n");
+ if (!input_color_space) av_log(ctx, AV_LOG_ERROR, "Input color
space is null\n");
+ if (!output_color_space) av_log(ctx, AV_LOG_ERROR, "Output color
space is null\n");
+ delete s;
+ return nullptr;
+ }
+
+ // ColorSpace Transform: InputSpace -> OutputSpace
+ OCIO::ColorSpaceTransformRcPtr cst =
OCIO::ColorSpaceTransform::Create();
+ cst->setSrc(input_color_space);
+ cst->setDst(output_color_space);
+ auto context = add_context_params(s->config, params);
+ s->processor = s->config->getProcessor(context, cst,
OCIO::TRANSFORM_DIR_FORWARD);
+
+ return (OCIOHandle)s;
+ } catch (OCIO::Exception &e) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO Filter: Error in
create_output_colorspace_processor: %s\n", e.what());
+ return nullptr;
+ } catch (...) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO Filter: Unknown Error in
create_output_colorspace_processor\n");
+ return nullptr;
+ }
+}
+
+OCIOHandle ocio_create_display_view_processor(AVFilterContext *ctx,
+ const char *config_path,
+ const char *input_color_space,
+ const char *display,
+ const char *view, int inverse,
+ AVDictionary *params)
+{
+ try {
+
+ OCIOState *s = new OCIOState();
+ if (!config_path)
+ s->config = OCIO::Config::CreateFromEnv();
+ else
+ s->config = OCIO::Config::CreateFromFile(config_path);
+
+ if (!s->config || !input_color_space || !display || !view) {
+ av_log(ctx, AV_LOG_ERROR, "Error: Config or arguments invalid.\n");
+ if (!s->config) av_log(ctx, AV_LOG_ERROR, "Config is null\n");
+ if (!input_color_space) av_log(ctx, AV_LOG_ERROR, "Input color
space is null\n");
+ if (!display) av_log(ctx, AV_LOG_ERROR, "Display is null\n");
+ if (!view) av_log(ctx, AV_LOG_ERROR, "View is null\n");
+ delete s;
+ return nullptr;
+ }
+
+ // Display/View Transform: InputSpace -> Display/View
+ OCIO::DisplayViewTransformRcPtr dv =
OCIO::DisplayViewTransform::Create();
+ dv->setSrc(input_color_space);
+ dv->setDisplay(display);
+ dv->setView(view);
+ OCIO::TransformDirection direction = OCIO::TRANSFORM_DIR_FORWARD;
+ if (inverse)
+ direction = OCIO::TRANSFORM_DIR_INVERSE;
+ OCIO::ConstContextRcPtr context = add_context_params(s->config,
params);
+ s->processor = s->config->getProcessor(context, dv, direction);
+
+ return (OCIOHandle)s;
+ } catch (OCIO::Exception &e) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO Error in
create_display_view_processor: %s\n", e.what());
+ return nullptr;
+ } catch (...) {
+ av_log(ctx, AV_LOG_ERROR, "Unknown Error in
create_display_view_processor\n");
+ return nullptr;
+ }
+}
+
+OCIOHandle ocio_create_file_transform_processor(AVFilterContext *ctx,
+ const char *file_transform,
+ int inverse)
+{
+ try {
+ if (!file_transform) {
+ av_log(ctx, AV_LOG_ERROR, "File transform is null\n");
+ return nullptr;
+ }
+ OCIOState *s = new OCIOState();
+
+ // File Transform: InputSpace -> FileTransform -> OutputSpace
+ OCIO::FileTransformRcPtr ft = OCIO::FileTransform::Create();
+ ft->setSrc(file_transform);
+ OCIO::TransformDirection direction = OCIO::TRANSFORM_DIR_FORWARD;
+ if (inverse)
+ direction = OCIO::TRANSFORM_DIR_INVERSE;
+ s->config = OCIO::Config::Create();
+ s->processor = s->config->getProcessor(ft, direction);
+
+ return (OCIOHandle)s;
+ } catch (OCIO::Exception &e) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO Error in
create_file_transform_processor: %s\n", e.what());
+ return nullptr;
+ } catch (...) {
+ av_log(ctx, AV_LOG_ERROR, "Unknown Error in
create_file_transform_processor\n");
+ return nullptr;
+ }
+}
+
+// In ocio_wrapper.cpp
+int ocio_finalize_processor(AVFilterContext *ctx, OCIOHandle handle, int
input_format,
+ int output_format)
+{
+ try {
+ OCIOState *s = (OCIOState *)handle;
+ if (!s || !s->processor)
+ return -1;
+
+ s->cpu = s->processor->getOptimizedCPUProcessor(
+ get_ocio_depth(input_format), get_ocio_depth(output_format),
+ OCIO::OPTIMIZATION_DEFAULT);
+
+ return 0;
+ } catch (OCIO::Exception &e) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO error: %s\n", e.what());
+ return -1;
+ } catch (...) {
+ av_log(ctx, AV_LOG_ERROR, "Unknown error in
ocio_finalize_processor\n");
+ return -1;
+ }
+}
+
+static OCIO::ImageDesc *AVFrame2ImageDescSlice(AVFrame *frame, int y_start,
+ int height)
+{
+ OCIO::BitDepth ocio_bitdepth = get_ocio_depth(frame->format);
+ if (ocio_bitdepth == OCIO::BIT_DEPTH_UNKNOWN) {
+ throw std::runtime_error("Unsupported pixel format for OCIO
processing");
+ }
+
+ int stridex = frame->linesize[0];
+ const AVPixFmtDescriptor *desc =
+ av_pix_fmt_desc_get((enum AVPixelFormat)frame->format);
+ if (!desc) {
+ throw std::runtime_error("Invalid pixel format descriptor");
+ }
+
+ bool is_planar = desc && (desc->flags & AV_PIX_FMT_FLAG_PLANAR);
+
+ if (is_planar) {
+ // For planar, we need to offset each plane
+ uint8_t *red = frame->data[2] + y_start * frame->linesize[2];
+ uint8_t *green = frame->data[0] + y_start * frame->linesize[0];
+ uint8_t *blue = frame->data[1] + y_start * frame->linesize[1];
+ uint8_t *alpha = (desc->nb_components == 4)
+ ? (frame->data[3] + y_start * frame->linesize[3])
+ : nullptr;
+
+ return new OCIO::PlanarImageDesc(
+ (void *)red, (void *)green, (void *)blue, (void *)alpha,
frame->width,
+ height, ocio_bitdepth, desc->comp[0].step, stridex);
+ }
+
+ uint8_t *data = frame->data[0] + y_start * frame->linesize[0];
+ // Note we are assuming that these are RGB or RGBA channel ordering.
+ // And are also likely to be integer.
+ return new OCIO::PackedImageDesc(
+ (void *)data, frame->width, height, desc->nb_components, ocio_bitdepth,
+ desc->comp[0].depth / 8, desc->comp[0].step, frame->linesize[0]);
+}
+
+int ocio_apply(AVFilterContext *ctx, OCIOHandle handle, AVFrame *input_frame,
AVFrame *output_frame,
+ int y_start, int height)
+{
+ OCIOState *s = (OCIOState *)handle;
+ if (!s || !s->cpu)
+ return -1;
+
+ try {
+ if (input_frame == output_frame) {
+ OCIO::ImageDesc *imgDesc = AVFrame2ImageDescSlice(input_frame,
y_start, height);
+ s->cpu->apply(*imgDesc);
+ delete imgDesc;
+ return 0;
+ }
+
+ OCIO::ImageDesc *input = AVFrame2ImageDescSlice(input_frame, y_start,
height);
+ OCIO::ImageDesc *output = AVFrame2ImageDescSlice(output_frame,
y_start, height);
+ s->cpu->apply(*input, *output);
+
+ delete input;
+ delete output;
+ return 0;
+ } catch (const OCIO::Exception &ex) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO error: %s\n", ex.what());
+ return -2; // or another error code
+ } catch (const std::exception &ex) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO error: Standard exception: %s\n",
ex.what());
+ return -3;
+ } catch (...) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO error: Unknown error in OCIO
processing.\n");
+ return -4;
+ }
+}
+
+void ocio_destroy_processor(AVFilterContext *ctx, OCIOHandle handle)
+{
+ if (!handle)
+ return;
+ delete (OCIOState *)handle;
+}
+
+} // extern "C"
diff --git a/libavfilter/ocio_wrapper.hpp b/libavfilter/ocio_wrapper.hpp
new file mode 100644
index 0000000000..900e70dc72
--- /dev/null
+++ b/libavfilter/ocio_wrapper.hpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2026 Sam Richards
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma once
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void *OCIOHandle;
+
+// Create an OCIO processor for Display/View transform.
+// Returns NULL on failure.
+OCIOHandle ocio_create_display_view_processor(AVFilterContext *ctx,
+ const char *config_path,
+ const char *input_color_space,
+ const char *display,
+ const char *view, int inverse,
+ AVDictionary *params);
+
+// Create an OCIO processor for output colorspace transform.
+// Returns NULL on failure.
+OCIOHandle
+ocio_create_output_colorspace_processor(AVFilterContext *ctx,
+ const char *config_path,
+ const char *input_color_space,
+ const char *output_color_space,
+ AVDictionary *params);
+
+// Create an OCIO processor for file transform.
+// Returns NULL on failure.
+OCIOHandle ocio_create_file_transform_processor(AVFilterContext *ctx,
+ const char *file_transform,
+ int inverse);
+
+// Finalize OCIO processor for given bit depth.
+// is_half_float: true for half-float, false for float
+int ocio_finalize_processor(AVFilterContext *ctx, OCIOHandle handle, int
input_format,
+ int output_format);
+
+// Apply processor to planar float RGB(A).
+// pixels: pointer to float samples
+// w,h: image dimensions
+// channels: 3 or 4
+// stride_bytes: bytes between row starts (use 0 for tightly packed)
+int ocio_apply(AVFilterContext *ctx, OCIOHandle handle, AVFrame *input_frame,
AVFrame *output_frame,
+ int y_start, int height);
+
+// Destroy OCIO processor.
+void ocio_destroy_processor(AVFilterContext *ctx, OCIOHandle handle);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libavfilter/vf_opencolorio.c b/libavfilter/vf_opencolorio.c
new file mode 100644
index 0000000000..d34922deca
--- /dev/null
+++ b/libavfilter/vf_opencolorio.c
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2026 Sam Richards
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "avfilter.h"
+#include "formats.h"
+#include "libavutil/half2float.h"
+#include "libavutil/opt.h"
+#include "libavutil/pixdesc.h"
+#include "libavutil/time.h"
+#include "ocio_wrapper.hpp"
+#include "video.h"
+
+typedef struct {
+ const AVClass *class;
+ char *config_path;
+ char *input_space;
+ char *output_space;
+ char *display;
+ char *view;
+ char *filetransform;
+ int inverse;
+ OCIOHandle ocio;
+ int output_format;
+ char *out_format_string; // e.g. "rgb48le" which is converted to
AVPixelFormat
+ // as output_format
+ int channels; // 3 or 4 depending on pixfmt
+ AVDictionary *context_params;
+} OCIOContext;
+
+typedef struct ThreadData {
+ AVFrame *in, *out;
+} ThreadData;
+
+static int ocio_filter_slice(AVFilterContext *ctx, void *arg, int jobnr, int
nb_jobs)
+{
+ OCIOContext *s = ctx->priv;
+ ThreadData *td = arg;
+ AVFrame *in = td->in;
+ AVFrame *out = td->out;
+ const int height = out->height;
+ const int slice_start = (height * jobnr) / nb_jobs;
+ const int slice_end = (height * (jobnr + 1)) / nb_jobs;
+ const int slice_h = slice_end - slice_start;
+
+ return ocio_apply(ctx, s->ocio, in, out, slice_start, slice_h);
+}
+
+static int query_formats(AVFilterContext *ctx) {
+ static const enum AVPixelFormat pix_fmts[] = {
+ // 8-bit
+ AV_PIX_FMT_RGBA, AV_PIX_FMT_RGB24,
+ // 16-bit
+ AV_PIX_FMT_RGBA64, AV_PIX_FMT_RGB48,
+ // 10-bit
+ AV_PIX_FMT_GBRP10, AV_PIX_FMT_GBRAP10,
+ // 12-bit
+ AV_PIX_FMT_GBRP12, AV_PIX_FMT_GBRAP12,
+ // Half-float and float
+ AV_PIX_FMT_GBRPF16, AV_PIX_FMT_GBRAPF16,
+ // Float
+ AV_PIX_FMT_GBRPF32, AV_PIX_FMT_GBRAPF32, AV_PIX_FMT_NONE};
+ return ff_set_common_formats(ctx, ff_make_format_list(pix_fmts));
+}
+
+static int config_props(AVFilterLink *inlink)
+{
+ AVFilterContext *ctx = inlink->dst;
+ OCIOContext *s = ctx->priv;
+ const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
+
+ if (!desc) {
+ av_log(ctx, AV_LOG_ERROR, "Invalid pixel format\n");
+ return AVERROR(EINVAL);
+ }
+
+ int is_half =
+ (desc->comp[0].depth == 16 && desc->flags & AV_PIX_FMT_FLAG_FLOAT);
+ if (s->output_format == AV_PIX_FMT_NONE) {
+ // Need to set the output format now, if not known.
+ if (is_half) {
+ // If its half-float, we output float, due to a bug in ffmpeg with
+ // half-float frames
+ s->output_format = AV_PIX_FMT_GBRAPF32;
+ } else {
+ // If output format not set, use same as input
+ s->output_format = inlink->format;
+ }
+ }
+
+ s->channels = desc->nb_components; // 3 or 4
+
+ av_log(ctx, AV_LOG_INFO,
+ "Configuring OCIO for %s (bit depth: %d, channels: %d), output "
+ "format: (%s)\n",
+ av_get_pix_fmt_name(inlink->format), desc->comp[0].depth, s->channels,
+ av_get_pix_fmt_name(s->output_format));
+
+ // Now finalize the OCIO processor with the correct bit depth
+ int ret = ocio_finalize_processor(ctx, s->ocio, inlink->format,
s->output_format);
+ if (ret < 0) {
+ av_log(ctx, AV_LOG_ERROR,
+ "Failed to finalize OCIO processor for bit depth\n");
+ return AVERROR_EXTERNAL;
+ }
+
+ return 0;
+}
+
+static av_cold int init(AVFilterContext *ctx)
+{
+ OCIOContext *s = ctx->priv;
+ if (s->out_format_string != NULL)
+ s->output_format = av_get_pix_fmt(s->out_format_string);
+ else
+ s->output_format = AV_PIX_FMT_NONE; // Default to same as input format
(see later).
+
+ if (s->filetransform && strlen(s->filetransform) > 0) {
+ s->ocio = ocio_create_file_transform_processor(
+ ctx, s->filetransform, s->inverse);
+ av_log(ctx, AV_LOG_INFO,
+ "Creating OCIO processor with FileTransform: %s, Inverse: %d\n",
+ s->filetransform, s->inverse);
+ } else if (s->output_space && strlen(s->output_space) > 0) {
+ s->ocio = ocio_create_output_colorspace_processor(
+ ctx, s->config_path, s->input_space, s->output_space,
s->context_params);
+ av_log(ctx, AV_LOG_INFO,
+ "Creating OCIO processor with config: %s, input: %s, output: %s\n",
+ s->config_path, s->input_space, s->output_space);
+ } else {
+ s->ocio = ocio_create_display_view_processor(
+ ctx, s->config_path, s->input_space, s->display, s->view,
s->inverse, s->context_params);
+ av_log(ctx, AV_LOG_INFO,
+ "Creating OCIO processor with config: %s, input: %s, display: %s, "
+ "view: %s, Inverse: %d\n",
+ s->config_path, s->input_space, s->display, s->view, s->inverse);
+ }
+ if (!s->ocio) {
+ av_log(ctx, AV_LOG_ERROR, "Failed to create OCIO processor.\n");
+ return AVERROR(EINVAL);
+ }
+
+ return 0;
+}
+
+static int filter_frame(AVFilterLink *inlink, AVFrame *frame)
+{
+ AVFilterContext *ctx = inlink->dst;
+ OCIOContext *s = ctx->priv;
+ const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame->format);
+
+ if (!desc)
+ return AVERROR(EINVAL);
+
+ int ret;
+ AVFrame *output_frame;
+ ThreadData td;
+
+ if (s->output_format == inlink->format) {
+ /* No pixel-format conversion needed. If the input frame is
+ * writable we can apply OCIO in-place, otherwise allocate a
+ * separate output frame to avoid mutating shared buffers. */
+ if (av_frame_is_writable(frame)) {
+ output_frame = frame;
+ } else {
+ output_frame = av_frame_alloc();
+ if (!output_frame) {
+ av_frame_free(&frame);
+ return AVERROR(ENOMEM);
+ }
+ output_frame->format = s->output_format;
+ output_frame->width = frame->width;
+ output_frame->height = frame->height;
+ ret = av_frame_get_buffer(output_frame, 32);
+
+ if (ret < 0) {
+ av_frame_free(&output_frame);
+ av_frame_free(&frame);
+ return ret;
+ }
+ av_frame_copy_props(output_frame, frame);
+ }
+ } else {
+ // Allocate new output frame
+ output_frame = av_frame_alloc();
+ if (!output_frame) {
+ av_frame_free(&frame);
+ return AVERROR(ENOMEM);
+ }
+ output_frame->format = s->output_format;
+ output_frame->width = frame->width;
+ output_frame->height = frame->height;
+ ret = av_frame_get_buffer(output_frame, 32);
+
+ if (ret < 0) {
+ av_frame_free(&output_frame);
+ av_frame_free(&frame);
+ return ret;
+ }
+ av_frame_copy_props(output_frame, frame);
+ }
+
+ td.in = frame;
+ td.out = output_frame;
+
+ // Use threads from context if set, otherwise let ffmpeg decide based on
global settings or defaults
+ // Note: ctx->graph->nb_threads is usually the global thread count.
+ // ff_filter_get_nb_threads(ctx) gives the number of threads available for
this filter.
+
+ int nb_jobs = ff_filter_get_nb_threads(ctx);
+
+ ret = ff_filter_execute(ctx, ocio_filter_slice, &td, NULL,
FFMIN(output_frame->height, nb_jobs));
+
+ if (frame != output_frame)
+ av_frame_free(&frame);
+
+ if (ret < 0) {
+ av_log(ctx, AV_LOG_ERROR, "OCIO apply failed.\n");
+ return AVERROR(EINVAL);
+ }
+
+ return ff_filter_frame(ctx->outputs[0], output_frame);
+}
+
+static av_cold void uninit(AVFilterContext *ctx)
+{
+ OCIOContext *s = ctx->priv;
+ if (s->ocio) {
+ ocio_destroy_processor(ctx, s->ocio);
+ s->ocio = NULL;
+ }
+}
+
+#define OFFSET(x) offsetof(OCIOContext, x)
+#define FLAGS AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM
+
+static const AVOption ocio_options[] = {
+ { "config", "OCIO config path, overriding OCIO environment variable.",
OFFSET(config_path), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS },
+ { "input", "Input color space", OFFSET(input_space), AV_OPT_TYPE_STRING,
{.str=NULL}, 0, 0, FLAGS },
+ { "output", "Output color space", OFFSET(output_space),
AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS },
+ { "filetransform", "Specify a File Transform", OFFSET(filetransform),
AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS },
+ { "display", "Output display, used instead of output color space.",
OFFSET(display), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS },
+ { "view", "View, output view transform, used in combination with
display.", OFFSET(view), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS },
+ { "inverse", "Invert output display/view transform.", OFFSET(inverse),
AV_OPT_TYPE_INT, {.i64=0}, 0, 1, FLAGS },
+ { "format", "Output video format", OFFSET(out_format_string),
AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS },
+ { "context_params", "OCIO context parameters", OFFSET(context_params),
AV_OPT_TYPE_DICT, { .str = NULL }, 0, 0, FLAGS },{ NULL }
+};
+
+AVFILTER_DEFINE_CLASS(ocio);
+
+static const AVFilterPad inputs[] = {
+ {
+ .name = "default",
+ .type = AVMEDIA_TYPE_VIDEO,
+ .filter_frame = filter_frame,
+ .config_props = config_props,
+ },
+};
+
+static const AVFilterPad outputs[] = {
+ {.name = "default", .type = AVMEDIA_TYPE_VIDEO}};
+
+const FFFilter ff_vf_ocio = {
+ .p.name = "ocio",
+ .p.description = NULL_IF_CONFIG_SMALL("Apply OCIO Display/View transform"),
+ .p.priv_class = &ocio_class,
+ .p.flags = AVFILTER_FLAG_SLICE_THREADS,
+ .priv_size = sizeof(OCIOContext),
+ .init = init,
+ .uninit = uninit,
+ FILTER_INPUTS(inputs),
+ FILTER_OUTPUTS(outputs),
+ FILTER_QUERY_FUNC(query_formats)};
_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]