1
0
mirror of https://git.ffmpeg.org/ffmpeg.git synced 2026-10-24 02:09:03 +03:00
Files
ffmpeg/libavfilter/ocio_wrapper.cpp
Sam.Richards@taurich.org 26a05be656 avfilter/vf_opencolorio: negotiate the output format properly
The filter picked its output format in config_props(), after format
negotiation had already settled the output link. The frames it emitted
could therefore disagree with the negotiated link format, for example
when a downstream zscale had negotiated something else.

Switch to FILTER_QUERY_FUNC2() and decide the output format during
negotiation instead:

- with format=, the output is constrained to that single format, which
  is now also validated in init() against the supported output list;
- otherwise, when no half-float candidate remains on the input, the
  output list is the input list, so the filter passes the format
  through and downstream constraints can still narrow the input;
- otherwise the output is restricted to float, and negotiation picks
  GBRPF32 or GBRAPF32 to match the input's alpha.

Half-float input has to be promoted because swscale cannot read or
write half-float in its new backend, and its legacy code clamps
half-float to [0,1], which would destroy values outside that range.
Half-float is therefore no longer offered as an output format.

The default output depends on the input format, so the query waits for
the input list to be merged with the upstream one, which it detects by
comparing it against the input link's other end. The merged list need
not be a singleton. Waiting for a singleton would reject valid graphs,
because a filter such as zscale offers half-float and integer formats
on its output independently of its input, so that list never narrows
to one of the two.

While the query is delayed it publishes the input color space, color
range, alpha mode and chroma location lists, because a converter
inserted on the input requires them, but the matching output lists are
only published together with the output pixel formats, as a converter
inserted on the output would otherwise find them without a format list
and crash.

Finally, supply opaque input alpha when the output format has alpha and
the input format does not. Prepend an alpha-initialization matrix to the
resolved OCIO processor, allowing OCIO to optimize it with the user's
transform. This avoids a separate per-frame alpha fill and ensures that
transforms which use alpha in RGB calculations or modify alpha receive
the opaque value before processing and retain their transformed alpha.
Filling output alpha after processing would instead leave alpha-dependent
RGB incorrect and overwrite alpha produced by the user's transform.

Note, this includes Fairys fairy/pr21799-replace patch (Thank-you Fairy!).

Signed-off-by: Sam Richards <Sam.Richards@taurich.org>
Assisted-by: Fairy
2026-10-18 00:45:18 +00:00

341 lines
12 KiB
C++

/*
* 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;
OCIO::ConstProcessorRcPtr processor = s->processor;
const AVPixFmtDescriptor *in_desc =
av_pix_fmt_desc_get((AVPixelFormat)input_format);
const AVPixFmtDescriptor *out_desc =
av_pix_fmt_desc_get((AVPixelFormat)output_format);
if (!(in_desc->flags & AV_PIX_FMT_FLAG_ALPHA) &&
(out_desc->flags & AV_PIX_FMT_FLAG_ALPHA)) {
// Supply opaque input alpha before the user's transform so it
// can use alpha in RGB calculations and transform alpha itself.
const double matrix[16] = { 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 0 };
const double offset[4] = { 0, 0, 0, 1 };
OCIO::MatrixTransformRcPtr opaque = OCIO::MatrixTransform::Create();
opaque->setMatrix(matrix);
opaque->setOffset(offset);
OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create();
group->appendTransform(opaque);
group->appendTransform(s->processor->createGroupTransform());
processor = s->config->getProcessor(group);
}
s->cpu = 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"