mirror of
https://git.ffmpeg.org/ffmpeg.git
synced 2026-10-24 02:09:03 +03:00
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
341 lines
12 KiB
C++
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"
|