color: input colorspace auto-detection, HDR export tags, LGG/white balance nodes, more LUT formats, waveform parade
- media color primaries/transfer tags now flow from the FFmpeg probe through VideoParams into Footage::get_colorspace_to_use(); precedence is user override > media tags > project default - export nclc tags derive from the output colorspace (PQ/HLG/BT.2020, P3, sRGB, Rec.601, Rec.709) instead of hardcoded BT.709 - new OCIO Color Grading (Log) node (lift/gamma/gain) and White Balance node (kelvin temperature + tint, HDR-safe) - LUT whitelist extended to 9 OCIO-supported formats - waveform scope gains an RGB parade mode (GPU and software paths) - tests updated for the new colorspace precedence
This commit is contained in:
@@ -677,6 +677,8 @@ FootageDescription FFmpegDecoder::probe(const QString &filename,
|
|||||||
stream.set_color_range(info.color_range == fb_color_range_jpeg ?
|
stream.set_color_range(info.color_range == fb_color_range_jpeg ?
|
||||||
VideoParams::k_color_range_full :
|
VideoParams::k_color_range_full :
|
||||||
VideoParams::k_color_range_limited);
|
VideoParams::k_color_range_limited);
|
||||||
|
stream.set_color_primaries(info.color_primaries);
|
||||||
|
stream.set_color_transfer(info.color_trc);
|
||||||
stream.set_premultiplied_alpha(false);
|
stream.set_premultiplied_alpha(false);
|
||||||
|
|
||||||
desc.add_video_stream(stream);
|
desc.add_video_stream(stream);
|
||||||
|
|||||||
@@ -38,6 +38,73 @@ FFmpegEncoder::FFmpegEncoder(const EncodingParams ¶ms)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FFmpegEncoder::get_color_tags_for_colorspace(const QString &colorspace,
|
||||||
|
int *primaries, int *trc,
|
||||||
|
int *matrix)
|
||||||
|
{
|
||||||
|
const QString name = colorspace.toLower();
|
||||||
|
|
||||||
|
if (name.contains(QStringLiteral("pq")) ||
|
||||||
|
name.contains(QStringLiteral("2084"))) {
|
||||||
|
*primaries = fb_color_primaries_bt2020;
|
||||||
|
*trc = fb_color_trc_pq;
|
||||||
|
*matrix = fb_col_spc_b_t2020_ncl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.contains(QStringLiteral("hlg"))) {
|
||||||
|
*primaries = fb_color_primaries_bt2020;
|
||||||
|
*trc = fb_color_trc_hlg;
|
||||||
|
*matrix = fb_col_spc_b_t2020_ncl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.contains(QStringLiteral("2020"))) {
|
||||||
|
*primaries = fb_color_primaries_bt2020;
|
||||||
|
*trc = fb_color_trc_bt709;
|
||||||
|
*matrix = fb_col_spc_b_t2020_ncl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.contains(QStringLiteral("p3"))) {
|
||||||
|
*primaries = fb_color_primaries_smpte432;
|
||||||
|
*trc = fb_color_trc_srgb;
|
||||||
|
*matrix = fb_col_spc_b_t709;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.contains(QStringLiteral("srgb"))) {
|
||||||
|
*primaries = fb_color_primaries_bt709;
|
||||||
|
*trc = fb_color_trc_srgb;
|
||||||
|
*matrix = fb_col_spc_b_t709;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.contains(QStringLiteral("pal"))) {
|
||||||
|
*primaries = fb_color_primaries_bt470bg;
|
||||||
|
*trc = fb_color_trc_gamma28;
|
||||||
|
*matrix = fb_col_spc_b_t470_bg;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.contains(QStringLiteral("ntsc"))) {
|
||||||
|
*primaries = fb_color_primaries_smpte170m;
|
||||||
|
*trc = fb_color_trc_smpte170m;
|
||||||
|
*matrix = fb_col_spc_smpt_e170_m;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.contains(QStringLiteral("1886")) ||
|
||||||
|
name.contains(QStringLiteral("709"))) {
|
||||||
|
*primaries = fb_color_primaries_bt709;
|
||||||
|
*trc = fb_color_trc_bt709;
|
||||||
|
*matrix = fb_col_spc_b_t709;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
QStringList FFmpegEncoder::get_pixel_formats_for_codec(ExportCodec::Codec c) const
|
QStringList FFmpegEncoder::get_pixel_formats_for_codec(ExportCodec::Codec c) const
|
||||||
{
|
{
|
||||||
QStringList pix_fmts;
|
QStringList pix_fmts;
|
||||||
@@ -187,6 +254,19 @@ bool FFmpegEncoder::open()
|
|||||||
1 :
|
1 :
|
||||||
0;
|
0;
|
||||||
|
|
||||||
|
// Derive explicit nclc tags (HDR etc.) from the export colorspace;
|
||||||
|
// the bridge falls back to the legacy sRGB/Rec.709 logic when these
|
||||||
|
// are unspecified
|
||||||
|
int color_primaries = fb_color_primaries_unspec;
|
||||||
|
int color_trc = fb_color_trc_unspec;
|
||||||
|
int color_matrix = fb_col_spc_unspec;
|
||||||
|
get_color_tags_for_colorspace(params().color_transform().output(),
|
||||||
|
&color_primaries, &color_trc,
|
||||||
|
&color_matrix);
|
||||||
|
config.video_color_primaries = color_primaries;
|
||||||
|
config.video_color_trc = color_trc;
|
||||||
|
config.video_colorspace = color_matrix;
|
||||||
|
|
||||||
// Custom options (skip Olive-internal keys)
|
// Custom options (skip Olive-internal keys)
|
||||||
for (auto i = params().video_opts().begin();
|
for (auto i = params().video_opts().begin();
|
||||||
i != params().video_opts().end(); i++) {
|
i != params().video_opts().end(); i++) {
|
||||||
|
|||||||
@@ -66,6 +66,18 @@ public:
|
|||||||
return video_conversion_fmt_;
|
return video_conversion_fmt_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Derives nclc color tags from an output colorspace name
|
||||||
|
*
|
||||||
|
* Extracted for testability. Returns true when the name maps to
|
||||||
|
* explicit tags (PQ/HLG/BT.2020, sRGB, P3, Rec.601, Rec.709); returns
|
||||||
|
* false for unknown names, in which case the bridge's legacy
|
||||||
|
* Rec.709/sRGB inference applies.
|
||||||
|
*/
|
||||||
|
static bool get_color_tags_for_colorspace(const QString &colorspace,
|
||||||
|
int *primaries, int *trc,
|
||||||
|
int *matrix);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* @brief Copy the last error message from the bridge into the encoder error state
|
* @brief Copy the last error message from the bridge into the encoder error state
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ void Config::set_defaults()
|
|||||||
set_entry_internal(QStringLiteral("HoverFocus"), NodeValue::k_boolean, false);
|
set_entry_internal(QStringLiteral("HoverFocus"), NodeValue::k_boolean, false);
|
||||||
set_entry_internal(QStringLiteral("AudioScrubbing"), NodeValue::k_boolean,
|
set_entry_internal(QStringLiteral("AudioScrubbing"), NodeValue::k_boolean,
|
||||||
true);
|
true);
|
||||||
|
set_entry_internal(QStringLiteral("WaveformRgbParade"), NodeValue::k_boolean,
|
||||||
|
false);
|
||||||
set_entry_internal(QStringLiteral("AutorecoveryEnabled"), NodeValue::k_boolean,
|
set_entry_internal(QStringLiteral("AutorecoveryEnabled"), NodeValue::k_boolean,
|
||||||
true);
|
true);
|
||||||
set_entry_internal(QStringLiteral("AutorecoveryInterval"), NodeValue::k_int,
|
set_entry_internal(QStringLiteral("AutorecoveryInterval"), NodeValue::k_int,
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ add_subdirectory(colormanager)
|
|||||||
add_subdirectory(displaytransform)
|
add_subdirectory(displaytransform)
|
||||||
add_subdirectory(ociobase)
|
add_subdirectory(ociobase)
|
||||||
add_subdirectory(ociogradingtransformlinear)
|
add_subdirectory(ociogradingtransformlinear)
|
||||||
|
add_subdirectory(ociogradingtransformlog)
|
||||||
add_subdirectory(ociolut)
|
add_subdirectory(ociolut)
|
||||||
add_subdirectory(threewaycolor)
|
add_subdirectory(threewaycolor)
|
||||||
|
add_subdirectory(whitebalance)
|
||||||
|
|
||||||
set(OLIVE_SOURCES
|
set(OLIVE_SOURCES
|
||||||
${OLIVE_SOURCES}
|
${OLIVE_SOURCES}
|
||||||
|
|||||||
@@ -175,6 +175,53 @@ void ColorManager::set_default_input_color_space(const QString &s)
|
|||||||
project()->set_default_input_color_space(s);
|
project()->set_default_input_color_space(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ColorManager::get_colorspace_for_ffmpeg_tags(int primaries,
|
||||||
|
int trc) const
|
||||||
|
{
|
||||||
|
// FFmpeg AVColorPrimaries/AVColorTransferCharacteristic values mapped to
|
||||||
|
// candidate colorspace names, in order of preference
|
||||||
|
struct TagMapping {
|
||||||
|
int primaries;
|
||||||
|
int trc;
|
||||||
|
const char *candidates[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
static const TagMapping k_tag_mappings[] = {
|
||||||
|
{ 1, 1, { "Rec.709 OETF", "Rec.709", "BT.709" } },
|
||||||
|
{ 1, 13, { "sRGB OETF", "sRGB", nullptr } },
|
||||||
|
{ 6, 6, { "Rec.601 OETF (NTSC)", "Rec.601 NTSC", nullptr } },
|
||||||
|
{ 5, 5, { "Rec.601 OETF (PAL)", "Rec.601 PAL", nullptr } },
|
||||||
|
{ 5, 6, { "Rec.601 OETF (PAL)", "Rec.601 PAL", nullptr } },
|
||||||
|
{ 9, 16, { "Rec.2020 PQ", "BT.2020 PQ", "ST 2084 PQ" } },
|
||||||
|
{ 9, 18, { "Rec.2020 HLG", "BT.2020 HLG", "HLG" } },
|
||||||
|
{ 9, 1, { "Rec.2020", "BT.2020", nullptr } },
|
||||||
|
{ 9, 14, { "Rec.2020", "BT.2020", nullptr } },
|
||||||
|
{ 9, 15, { "Rec.2020", "BT.2020", nullptr } },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 0 = unset, 2 = AVCOL_PRI/TRC_UNSPECIFIED
|
||||||
|
if (primaries == 0 || primaries == 2 || trc == 0 || trc == 2) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList available = list_available_colorspaces();
|
||||||
|
|
||||||
|
for (const TagMapping &mapping : k_tag_mappings) {
|
||||||
|
if (mapping.primaries == primaries && mapping.trc == trc) {
|
||||||
|
for (const char *candidate : mapping.candidates) {
|
||||||
|
if (candidate &&
|
||||||
|
available.contains(QString::fromUtf8(candidate))) {
|
||||||
|
return QString::fromUtf8(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Known tag pair, but the config has no matching colorspace
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
QString ColorManager::get_reference_color_space() const
|
QString ColorManager::get_reference_color_space() const
|
||||||
{
|
{
|
||||||
return project()->get_color_reference_space();
|
return project()->get_color_reference_space();
|
||||||
|
|||||||
@@ -65,6 +65,16 @@ public:
|
|||||||
|
|
||||||
QString get_default_input_color_space() const;
|
QString get_default_input_color_space() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Auto-detects an input colorspace from media color tags
|
||||||
|
*
|
||||||
|
* Maps raw FFmpeg color primaries/transfer values (as exposed on
|
||||||
|
* VideoParams) to a colorspace of the active OCIO config. Returns an
|
||||||
|
* empty string when the tags are unknown or the config has no matching
|
||||||
|
* colorspace, in which case the default input colorspace applies.
|
||||||
|
*/
|
||||||
|
QString get_colorspace_for_ffmpeg_tags(int primaries, int trc) const;
|
||||||
|
|
||||||
void set_default_input_color_space(const QString &s);
|
void set_default_input_color_space(const QString &s);
|
||||||
|
|
||||||
QString get_reference_color_space() const;
|
QString get_reference_color_space() const;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Olive - Non-Linear Video Editor
|
||||||
|
# Copyright (C) 2022 Olive Team
|
||||||
|
# Modifications Copyright (C) 2026 Oak Team
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
set(OLIVE_SOURCES
|
||||||
|
${OLIVE_SOURCES}
|
||||||
|
node/color/ociogradingtransformlog/ociogradingtransformlog.cpp
|
||||||
|
node/color/ociogradingtransformlog/ociogradingtransformlog.h
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
Olive - Non-Linear Video Editor
|
||||||
|
Copyright (C) 2022 Olive Team
|
||||||
|
Modifications Copyright (C) 2026 Oak Team
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***/
|
||||||
|
|
||||||
|
#include "ociogradingtransformlog.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "common/ocioutils.h"
|
||||||
|
#include "node/project.h"
|
||||||
|
#include "render/colorprocessor.h"
|
||||||
|
#include "widget/slider/floatslider.h"
|
||||||
|
|
||||||
|
namespace olive
|
||||||
|
{
|
||||||
|
|
||||||
|
// These ids double as the OCIO GPU uniform names for the dynamic
|
||||||
|
// GradingPrimaryTransform; do not rename them. OCIO's log style maps the
|
||||||
|
// classic wheels as: brightness = lift, contrast = gain, gamma = gamma.
|
||||||
|
const QString OCIOGradingTransformLogNode::k_lift_input =
|
||||||
|
QStringLiteral("ocio_grading_primary_brightness");
|
||||||
|
const QString OCIOGradingTransformLogNode::k_gain_input =
|
||||||
|
QStringLiteral("ocio_grading_primary_contrast");
|
||||||
|
const QString OCIOGradingTransformLogNode::k_gamma_input =
|
||||||
|
QStringLiteral("ocio_grading_primary_gamma");
|
||||||
|
const QString OCIOGradingTransformLogNode::k_saturation_input =
|
||||||
|
QStringLiteral("ocio_grading_primary_saturation");
|
||||||
|
const QString OCIOGradingTransformLogNode::k_pivot_input =
|
||||||
|
QStringLiteral("ocio_grading_primary_pivot");
|
||||||
|
const QString OCIOGradingTransformLogNode::k_clamp_black_enable_input =
|
||||||
|
QStringLiteral("clamp_black_enable_in");
|
||||||
|
const QString OCIOGradingTransformLogNode::k_clamp_black_input =
|
||||||
|
QStringLiteral("ocio_grading_primary_clampBlack");
|
||||||
|
const QString OCIOGradingTransformLogNode::k_clamp_white_enable_input =
|
||||||
|
QStringLiteral("clamp_white_enable_in");
|
||||||
|
const QString OCIOGradingTransformLogNode::k_clamp_white_input =
|
||||||
|
QStringLiteral("ocio_grading_primary_clampWhite");
|
||||||
|
|
||||||
|
#define super OCIOBaseNode
|
||||||
|
|
||||||
|
OCIOGradingTransformLogNode::OCIOGradingTransformLogNode()
|
||||||
|
{
|
||||||
|
add_input(k_lift_input, NodeValue::k_vec4, QVector4D{ 0.0, 0.0, 0.0, 0.0 });
|
||||||
|
set_input_property(k_lift_input, QStringLiteral("base"), 0.01);
|
||||||
|
set_vec4_input_colors(k_lift_input);
|
||||||
|
|
||||||
|
add_input(k_gain_input, NodeValue::k_vec4, QVector4D{ 1.0, 1.0, 1.0, 1.0 });
|
||||||
|
set_input_property(k_gain_input, QStringLiteral("base"), 0.01);
|
||||||
|
set_vec4_input_colors(k_gain_input);
|
||||||
|
|
||||||
|
add_input(k_gamma_input, NodeValue::k_vec4, QVector4D{ 1.0, 1.0, 1.0, 1.0 });
|
||||||
|
set_input_property(k_gamma_input, QStringLiteral("base"), 0.01);
|
||||||
|
set_vec4_input_colors(k_gamma_input);
|
||||||
|
|
||||||
|
add_input(k_saturation_input, NodeValue::k_float, 1.0);
|
||||||
|
set_input_property(k_saturation_input, QStringLiteral("view"),
|
||||||
|
FloatSlider::k_percentage);
|
||||||
|
set_input_property(k_saturation_input, QStringLiteral("min"), 0.0);
|
||||||
|
|
||||||
|
add_input(k_pivot_input, NodeValue::k_float,
|
||||||
|
-0.2); // Default for GRADING_LOG listed in ocio::GradingPrimary
|
||||||
|
set_input_property(k_pivot_input, QStringLiteral("base"), 0.01);
|
||||||
|
|
||||||
|
add_input(k_clamp_black_enable_input, NodeValue::k_boolean, false);
|
||||||
|
|
||||||
|
add_input(k_clamp_black_input, NodeValue::k_float, 0.0);
|
||||||
|
set_input_property(k_clamp_black_input, QStringLiteral("enabled"),
|
||||||
|
get_standard_value(k_clamp_black_enable_input).toBool());
|
||||||
|
set_input_property(k_clamp_black_input, QStringLiteral("base"), 0.01);
|
||||||
|
|
||||||
|
add_input(k_clamp_white_enable_input, NodeValue::k_boolean, false);
|
||||||
|
|
||||||
|
add_input(k_clamp_white_input, NodeValue::k_float, 1.0);
|
||||||
|
set_input_property(k_clamp_white_input, QStringLiteral("enabled"),
|
||||||
|
get_standard_value(k_clamp_white_enable_input).toBool());
|
||||||
|
set_input_property(k_clamp_white_input, QStringLiteral("base"), 0.01);
|
||||||
|
|
||||||
|
// Constrain the white clamp minimum to just above the (static) black clamp
|
||||||
|
// as per ocio::GradingPrimary::validate. When the black clamp is keyframed
|
||||||
|
// or connected, Value() enforces the invariant per frame instead.
|
||||||
|
update_clamp_white_minimum();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OCIOGradingTransformLogNode::name() const
|
||||||
|
{
|
||||||
|
return tr("OCIO Color Grading (Log)");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OCIOGradingTransformLogNode::id() const
|
||||||
|
{
|
||||||
|
return QStringLiteral("org.olivevideoeditor.Olive.ociogradingtransformlog");
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<Node::CategoryID> OCIOGradingTransformLogNode::category() const
|
||||||
|
{
|
||||||
|
return { k_category_color };
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OCIOGradingTransformLogNode::description() const
|
||||||
|
{
|
||||||
|
return tr("Lift/gamma/gain color grading using OpenColorIO.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::retranslate()
|
||||||
|
{
|
||||||
|
super::retranslate();
|
||||||
|
|
||||||
|
set_input_name(k_texture_input, tr("Input"));
|
||||||
|
set_input_name(k_lift_input, tr("Lift"));
|
||||||
|
set_input_name(k_gain_input, tr("Gain"));
|
||||||
|
set_input_name(k_gamma_input, tr("Gamma"));
|
||||||
|
set_input_name(k_saturation_input, tr("Saturation"));
|
||||||
|
set_input_name(k_pivot_input, tr("Pivot"));
|
||||||
|
set_input_name(k_clamp_black_enable_input, tr("Enable Black Clamp"));
|
||||||
|
set_input_name(k_clamp_black_input, tr("Black Clamp"));
|
||||||
|
set_input_name(k_clamp_white_enable_input, tr("Enable White Clamp"));
|
||||||
|
set_input_name(k_clamp_white_input, tr("White Clamp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::InputValueChangedEvent(const QString &input,
|
||||||
|
int element)
|
||||||
|
{
|
||||||
|
Q_UNUSED(element);
|
||||||
|
|
||||||
|
if (input == k_clamp_white_enable_input) {
|
||||||
|
set_input_property(k_clamp_white_input, QStringLiteral("enabled"),
|
||||||
|
get_standard_value(k_clamp_white_enable_input).toBool());
|
||||||
|
} else if (input == k_clamp_black_enable_input) {
|
||||||
|
set_input_property(k_clamp_black_input, QStringLiteral("enabled"),
|
||||||
|
get_standard_value(k_clamp_black_enable_input).toBool());
|
||||||
|
} else if (input == k_clamp_black_input) {
|
||||||
|
// Ensure the white clamp is always greater than the black clamp as per
|
||||||
|
// ocio::GradingPrimary::validate
|
||||||
|
update_clamp_white_minimum();
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_processor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::InputConnectedEvent(const QString &input,
|
||||||
|
int element, Node *output)
|
||||||
|
{
|
||||||
|
super::InputConnectedEvent(input, element, output);
|
||||||
|
|
||||||
|
if (input == k_clamp_black_input) {
|
||||||
|
update_clamp_white_minimum();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::InputDisconnectedEvent(const QString &input,
|
||||||
|
int element,
|
||||||
|
Node *output)
|
||||||
|
{
|
||||||
|
super::InputDisconnectedEvent(input, element, output);
|
||||||
|
|
||||||
|
if (input == k_clamp_black_input) {
|
||||||
|
update_clamp_white_minimum();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::update_clamp_white_minimum()
|
||||||
|
{
|
||||||
|
// A static UI minimum cannot follow an animated black clamp; for keyframed
|
||||||
|
// or connected values the white>black invariant is enforced per frame in
|
||||||
|
// Value() instead
|
||||||
|
if (is_input_keyframing(k_clamp_black_input) ||
|
||||||
|
is_input_connected(k_clamp_black_input)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_input_property(k_clamp_white_input, QStringLiteral("min"),
|
||||||
|
get_standard_value(k_clamp_black_input).toDouble() + 0.000001);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::generate_processor()
|
||||||
|
{
|
||||||
|
if (manager()) {
|
||||||
|
ocio::GradingPrimaryTransformRcPtr gp =
|
||||||
|
ocio::GradingPrimaryTransform::Create(ocio::GRADING_LOG);
|
||||||
|
gp->makeDynamic();
|
||||||
|
gp->setDirection(ocio::TransformDirection::TRANSFORM_DIR_FORWARD);
|
||||||
|
|
||||||
|
try {
|
||||||
|
set_processor(ColorProcessor::create(
|
||||||
|
manager()->get_config()->getProcessor(gp)));
|
||||||
|
} catch (const ocio::Exception &e) {
|
||||||
|
std::cerr << std::endl << e.what() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::value(const NodeValueRow &value,
|
||||||
|
const NodeGlobals &globals,
|
||||||
|
NodeValueTable *table) const
|
||||||
|
{
|
||||||
|
if (TexturePtr tex = value[k_texture_input].to_texture()) {
|
||||||
|
if (processor()) {
|
||||||
|
ColorTransformJob job(value);
|
||||||
|
|
||||||
|
job.set_color_processor(processor());
|
||||||
|
job.set_input_texture(value[k_texture_input]);
|
||||||
|
|
||||||
|
const int master_channel = 0;
|
||||||
|
const int red_channel = 1;
|
||||||
|
const int green_channel = 2;
|
||||||
|
const int blue_channel = 3;
|
||||||
|
|
||||||
|
// OCIO expects vec3s on the GPU but RGBMs (master + RGB) on the
|
||||||
|
// CPU; the per-style master combination below mirrors
|
||||||
|
// ocio::GradingPrimary. Lift is additive, gain/gamma multiply.
|
||||||
|
QVector4D lift = value[k_lift_input].to_vec4();
|
||||||
|
lift[red_channel] += lift[master_channel];
|
||||||
|
lift[green_channel] += lift[master_channel];
|
||||||
|
lift[blue_channel] += lift[master_channel];
|
||||||
|
job.insert(k_lift_input,
|
||||||
|
NodeValue(NodeValue::k_vec3,
|
||||||
|
QVector3D(lift[red_channel], lift[green_channel],
|
||||||
|
lift[blue_channel])));
|
||||||
|
|
||||||
|
QVector4D gain = value[k_gain_input].to_vec4();
|
||||||
|
gain[red_channel] *= gain[master_channel];
|
||||||
|
gain[green_channel] *= gain[master_channel];
|
||||||
|
gain[blue_channel] *= gain[master_channel];
|
||||||
|
job.insert(k_gain_input,
|
||||||
|
NodeValue(NodeValue::k_vec3,
|
||||||
|
QVector3D(gain[red_channel], gain[green_channel],
|
||||||
|
gain[blue_channel])));
|
||||||
|
|
||||||
|
QVector4D gamma = value[k_gamma_input].to_vec4();
|
||||||
|
gamma[red_channel] *= gamma[master_channel];
|
||||||
|
gamma[green_channel] *= gamma[master_channel];
|
||||||
|
gamma[blue_channel] *= gamma[master_channel];
|
||||||
|
job.insert(k_gamma_input,
|
||||||
|
NodeValue(NodeValue::k_vec3,
|
||||||
|
QVector3D(gamma[red_channel],
|
||||||
|
gamma[green_channel],
|
||||||
|
gamma[blue_channel])));
|
||||||
|
|
||||||
|
if (!value[k_clamp_black_enable_input].to_bool()) {
|
||||||
|
job.insert(k_clamp_black_input,
|
||||||
|
NodeValue(NodeValue::k_float,
|
||||||
|
ocio::GradingPrimary::NoClampBlack()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value[k_clamp_white_enable_input].to_bool()) {
|
||||||
|
job.insert(k_clamp_white_input,
|
||||||
|
NodeValue(NodeValue::k_float,
|
||||||
|
ocio::GradingPrimary::NoClampWhite()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value[k_clamp_black_enable_input].to_bool() &&
|
||||||
|
value[k_clamp_white_enable_input].to_bool()) {
|
||||||
|
// ocio::GradingPrimary::validate requires the white clamp to be
|
||||||
|
// greater than the black clamp. Keyframed or connected values
|
||||||
|
// can violate this at arbitrary times, so enforce the invariant
|
||||||
|
// per frame here.
|
||||||
|
const double clamp_black = value[k_clamp_black_input].to_double();
|
||||||
|
const double clamp_white = value[k_clamp_white_input].to_double();
|
||||||
|
if (clamp_white <= clamp_black) {
|
||||||
|
job.insert(k_clamp_white_input,
|
||||||
|
NodeValue(NodeValue::k_float,
|
||||||
|
clamp_black + 0.000001));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table->push(NodeValue::k_texture, tex->to_job(job), this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::config_changed()
|
||||||
|
{
|
||||||
|
generate_processor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OCIOGradingTransformLogNode::set_vec4_input_colors(const QString &input)
|
||||||
|
{
|
||||||
|
set_input_property(input, QStringLiteral("color0"),
|
||||||
|
QColor(192, 192, 192).name());
|
||||||
|
set_input_property(input, QStringLiteral("color1"), QColor(255, 0, 0).name());
|
||||||
|
set_input_property(input, QStringLiteral("color2"), QColor(0, 255, 0).name());
|
||||||
|
set_input_property(input, QStringLiteral("color3"), QColor(0, 0, 255).name());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
Olive - Non-Linear Video Editor
|
||||||
|
Copyright (C) 2022 Olive Team
|
||||||
|
Modifications Copyright (C) 2026 Oak Team
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***/
|
||||||
|
|
||||||
|
#ifndef OAK_OCIOGRADINGTRANSFORMLOGNODE_H
|
||||||
|
#define OAK_OCIOGRADINGTRANSFORMLOGNODE_H
|
||||||
|
|
||||||
|
#include "node/color/ociobase/ociobase.h"
|
||||||
|
#include "render/colorprocessor.h"
|
||||||
|
|
||||||
|
namespace olive
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Lift/gamma/gain grading node built on ocio::GRADING_LOG
|
||||||
|
*
|
||||||
|
* Mirrors OCIOGradingTransformLinearNode for the log grading style. OCIO's
|
||||||
|
* log-style GPU uniforms map to the classic wheels as: brightness = lift,
|
||||||
|
* contrast = gain, gamma = gamma.
|
||||||
|
*/
|
||||||
|
class OCIOGradingTransformLogNode : public OCIOBaseNode {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
OCIOGradingTransformLogNode();
|
||||||
|
|
||||||
|
NODE_DEFAULT_FUNCTIONS(OCIOGradingTransformLogNode)
|
||||||
|
|
||||||
|
virtual QString name() const override;
|
||||||
|
virtual QString id() const override;
|
||||||
|
virtual QVector<CategoryID> category() const override;
|
||||||
|
virtual QString description() const override;
|
||||||
|
|
||||||
|
virtual void retranslate() override;
|
||||||
|
virtual void InputValueChangedEvent(const QString &input,
|
||||||
|
int element) override;
|
||||||
|
virtual void InputConnectedEvent(const QString &input, int element,
|
||||||
|
Node *output) override;
|
||||||
|
virtual void InputDisconnectedEvent(const QString &input, int element,
|
||||||
|
Node *output) override;
|
||||||
|
void generate_processor();
|
||||||
|
|
||||||
|
virtual void value(const NodeValueRow &value, const NodeGlobals &globals,
|
||||||
|
NodeValueTable *table) const override;
|
||||||
|
|
||||||
|
static const QString k_lift_input;
|
||||||
|
static const QString k_gain_input;
|
||||||
|
static const QString k_gamma_input;
|
||||||
|
static const QString k_saturation_input;
|
||||||
|
static const QString k_pivot_input;
|
||||||
|
static const QString k_clamp_black_enable_input;
|
||||||
|
static const QString k_clamp_black_input;
|
||||||
|
static const QString k_clamp_white_enable_input;
|
||||||
|
static const QString k_clamp_white_input;
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
virtual void config_changed() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void set_vec4_input_colors(const QString &input);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Constrains the white clamp UI minimum to just above the black
|
||||||
|
* clamp, as required by ocio::GradingPrimary::validate
|
||||||
|
*
|
||||||
|
* Only applies while the black clamp is a static value; when it is
|
||||||
|
* keyframed or connected the invariant is enforced per frame in Value()
|
||||||
|
* instead.
|
||||||
|
*/
|
||||||
|
void update_clamp_white_minimum();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // olive
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -77,11 +77,13 @@ OCIOLutNode::OCIOLutNode()
|
|||||||
{
|
{
|
||||||
add_input(k_file_input, NodeValue::k_file, QString(),
|
add_input(k_file_input, NodeValue::k_file, QString(),
|
||||||
InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable));
|
InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable));
|
||||||
set_input_property(
|
const QString all_luts =
|
||||||
k_file_input, QStringLiteral("filter"),
|
QStringLiteral("*.") +
|
||||||
tr("LUT Files (*.cube *.3dl);;Cube LUT (*.cube);;3DL LUT (*.3dl);;All Files (*)"));
|
LUTLibrary::supported_extensions().join(QStringLiteral(" *."));
|
||||||
|
set_input_property(k_file_input, QStringLiteral("filter"),
|
||||||
|
tr("LUT Files (%1);;All Files (*)").arg(all_luts));
|
||||||
set_input_property(k_file_input, QStringLiteral("placeholder"),
|
set_input_property(k_file_input, QStringLiteral("placeholder"),
|
||||||
tr("Select a .cube or .3dl LUT file"));
|
tr("Select a LUT file"));
|
||||||
// Allow the UI to offer the global LUT library for this input
|
// Allow the UI to offer the global LUT library for this input
|
||||||
set_input_property(k_file_input, QStringLiteral("lut_library"), true);
|
set_input_property(k_file_input, QStringLiteral("lut_library"), true);
|
||||||
|
|
||||||
@@ -257,9 +259,7 @@ bool OCIOLutNode::create_processor_from_inputs() const
|
|||||||
last_path_.clear();
|
last_path_.clear();
|
||||||
last_direction_ = -1;
|
last_direction_ = -1;
|
||||||
processor_dirty_ = false;
|
processor_dirty_ = false;
|
||||||
set_last_error(
|
set_last_error(tr("OCIO LUT: unsupported LUT file extension: %1")
|
||||||
tr("OCIO LUT: unsupported LUT file extension (expected .cube or "
|
|
||||||
".3dl): %1")
|
|
||||||
.arg(path));
|
.arg(path));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Oak - Non-Linear Video Editor
|
||||||
|
# Copyright (C) 2026 Oak Team
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
set(OLIVE_SOURCES
|
||||||
|
${OLIVE_SOURCES}
|
||||||
|
node/color/whitebalance/whitebalance.cpp
|
||||||
|
node/color/whitebalance/whitebalance.h
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
Oak - Non-Linear Video Editor
|
||||||
|
Copyright (C) 2026 Oak Team
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***/
|
||||||
|
|
||||||
|
#include "whitebalance.h"
|
||||||
|
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
|
#include "common/filefunctions.h"
|
||||||
|
#include "render/job/shaderjob.h"
|
||||||
|
#include "render/texture.h"
|
||||||
|
#include "widget/slider/floatslider.h"
|
||||||
|
|
||||||
|
namespace olive
|
||||||
|
{
|
||||||
|
|
||||||
|
#define super Node
|
||||||
|
|
||||||
|
const QString WhiteBalanceNode::k_texture_input = QStringLiteral("tex_in");
|
||||||
|
const QString WhiteBalanceNode::k_temperature_input =
|
||||||
|
QStringLiteral("temperature_in");
|
||||||
|
const QString WhiteBalanceNode::k_tint_input = QStringLiteral("tint_in");
|
||||||
|
const QString WhiteBalanceNode::k_gain_input = QStringLiteral("wb_gain_in");
|
||||||
|
|
||||||
|
WhiteBalanceNode::WhiteBalanceNode()
|
||||||
|
{
|
||||||
|
add_input(k_texture_input, NodeValue::k_texture,
|
||||||
|
InputFlags(k_input_flag_not_keyframable));
|
||||||
|
|
||||||
|
add_input(k_temperature_input, NodeValue::k_float, 6500.0);
|
||||||
|
set_input_property(k_temperature_input, QStringLiteral("min"), 1000.0);
|
||||||
|
set_input_property(k_temperature_input, QStringLiteral("max"), 40000.0);
|
||||||
|
set_input_property(k_temperature_input, QStringLiteral("view"),
|
||||||
|
FloatSlider::k_normal);
|
||||||
|
|
||||||
|
add_input(k_tint_input, NodeValue::k_float, 0.0);
|
||||||
|
set_input_property(k_tint_input, QStringLiteral("min"), -1.0);
|
||||||
|
set_input_property(k_tint_input, QStringLiteral("max"), 1.0);
|
||||||
|
set_input_property(k_tint_input, QStringLiteral("base"), 0.01);
|
||||||
|
|
||||||
|
set_effect_input(k_texture_input);
|
||||||
|
set_flag(k_video_effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WhiteBalanceNode::name() const
|
||||||
|
{
|
||||||
|
return tr("White Balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WhiteBalanceNode::id() const
|
||||||
|
{
|
||||||
|
return QStringLiteral("org.olivevideoeditor.Olive.whitebalance");
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<Node::CategoryID> WhiteBalanceNode::category() const
|
||||||
|
{
|
||||||
|
return { k_category_color };
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WhiteBalanceNode::description() const
|
||||||
|
{
|
||||||
|
return tr("Adjust white balance by color temperature and tint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WhiteBalanceNode::retranslate()
|
||||||
|
{
|
||||||
|
super::retranslate();
|
||||||
|
|
||||||
|
set_input_name(k_texture_input, tr("Input"));
|
||||||
|
set_input_name(k_temperature_input, tr("Temperature (K)"));
|
||||||
|
set_input_name(k_tint_input, tr("Tint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderCode WhiteBalanceNode::get_shader_code(const ShaderRequest &request) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(request)
|
||||||
|
return ShaderCode(
|
||||||
|
FileFunctions::read_file_as_string(":/shaders/whitebalance.frag"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WhiteBalanceNode::value(const NodeValueRow &value,
|
||||||
|
const NodeGlobals &globals,
|
||||||
|
NodeValueTable *table) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(globals)
|
||||||
|
|
||||||
|
if (TexturePtr tex = value[k_texture_input].to_texture()) {
|
||||||
|
ShaderJob job(value);
|
||||||
|
|
||||||
|
const QVector3D gain = get_gain_for_temperature(
|
||||||
|
value[k_temperature_input].to_double(),
|
||||||
|
value[k_tint_input].to_double());
|
||||||
|
job.insert(k_gain_input, NodeValue(NodeValue::k_vec3, gain));
|
||||||
|
|
||||||
|
table->push(NodeValue::k_texture, tex->to_job(job), this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector3D WhiteBalanceNode::get_gain_for_temperature(double kelvin, double tint)
|
||||||
|
{
|
||||||
|
// Tanner Helland blackbody approximation (1000K-40000K), returning
|
||||||
|
// 0-255 per channel
|
||||||
|
kelvin = qBound(1000.0, kelvin, 40000.0);
|
||||||
|
const double t = kelvin / 100.0;
|
||||||
|
|
||||||
|
double red;
|
||||||
|
if (t <= 66.0) {
|
||||||
|
red = 255.0;
|
||||||
|
} else {
|
||||||
|
red = 329.698727446 * qPow(t - 60.0, -0.1332047592);
|
||||||
|
}
|
||||||
|
|
||||||
|
double green;
|
||||||
|
if (t <= 66.0) {
|
||||||
|
green = 99.4708025861 * qLn(t) - 161.1195681661;
|
||||||
|
} else {
|
||||||
|
green = 288.1221695283 * qPow(t - 60.0, -0.0755148492);
|
||||||
|
}
|
||||||
|
|
||||||
|
double blue;
|
||||||
|
if (t >= 66.0) {
|
||||||
|
blue = 255.0;
|
||||||
|
} else if (t <= 19.0) {
|
||||||
|
blue = 0.0;
|
||||||
|
} else {
|
||||||
|
blue = 138.5177312231 * qLn(t - 10.0) - 305.0447927307;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize to the green channel so temperature shifts do not change
|
||||||
|
// exposure, then let tint move along the green-magenta axis
|
||||||
|
red /= green;
|
||||||
|
blue /= green;
|
||||||
|
green = 1.0;
|
||||||
|
|
||||||
|
const double tint_gain = qBound(0.0, 1.0 + tint, 2.0);
|
||||||
|
|
||||||
|
return QVector3D(float(red), float(green * tint_gain), float(blue));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
Oak - Non-Linear Video Editor
|
||||||
|
Copyright (C) 2026 Oak Team
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***/
|
||||||
|
|
||||||
|
#ifndef OAK_WHITEBALANCENODE_H
|
||||||
|
#define OAK_WHITEBALANCENODE_H
|
||||||
|
|
||||||
|
#include <QVector3D>
|
||||||
|
|
||||||
|
#include "node/node.h"
|
||||||
|
|
||||||
|
namespace olive
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief White balance correction by color temperature and tint
|
||||||
|
*
|
||||||
|
* Converts a scene illuminant temperature (in Kelvin) into per-channel RGB
|
||||||
|
* gains using the Tanner Helland blackbody approximation, normalized so the
|
||||||
|
* green channel is preserved (no exposure shift). Tint shifts the image
|
||||||
|
* along the green-magenta axis.
|
||||||
|
*/
|
||||||
|
class WhiteBalanceNode : public Node {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
WhiteBalanceNode();
|
||||||
|
|
||||||
|
NODE_DEFAULT_FUNCTIONS(WhiteBalanceNode)
|
||||||
|
|
||||||
|
virtual QString name() const override;
|
||||||
|
virtual QString id() const override;
|
||||||
|
virtual QVector<CategoryID> category() const override;
|
||||||
|
virtual QString description() const override;
|
||||||
|
|
||||||
|
virtual void retranslate() override;
|
||||||
|
|
||||||
|
virtual ShaderCode get_shader_code(const ShaderRequest &request) const override;
|
||||||
|
|
||||||
|
virtual void value(const NodeValueRow &value, const NodeGlobals &globals,
|
||||||
|
NodeValueTable *table) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RGB gains for a given illuminant temperature and tint
|
||||||
|
*
|
||||||
|
* Extracted for testability. Kelvin is clamped to [1000, 40000]; the
|
||||||
|
* result is normalized so the green channel gain is 1.0 at tint 0.
|
||||||
|
*/
|
||||||
|
static QVector3D get_gain_for_temperature(double kelvin, double tint);
|
||||||
|
|
||||||
|
static const QString k_texture_input;
|
||||||
|
static const QString k_temperature_input;
|
||||||
|
static const QString k_tint_input;
|
||||||
|
static const QString k_gain_input;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // olive
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -33,8 +33,10 @@
|
|||||||
#include "block/transition/diptocolor/diptocolortransition.h"
|
#include "block/transition/diptocolor/diptocolortransition.h"
|
||||||
#include "color/displaytransform/displaytransform.h"
|
#include "color/displaytransform/displaytransform.h"
|
||||||
#include "color/ociogradingtransformlinear/ociogradingtransformlinear.h"
|
#include "color/ociogradingtransformlinear/ociogradingtransformlinear.h"
|
||||||
|
#include "color/ociogradingtransformlog/ociogradingtransformlog.h"
|
||||||
#include "color/ociolut/ociolut.h"
|
#include "color/ociolut/ociolut.h"
|
||||||
#include "color/threewaycolor/threewaycolor.h"
|
#include "color/threewaycolor/threewaycolor.h"
|
||||||
|
#include "color/whitebalance/whitebalance.h"
|
||||||
#include "common/current.h"
|
#include "common/current.h"
|
||||||
#include "distort/cornerpin/cornerpindistortnode.h"
|
#include "distort/cornerpin/cornerpindistortnode.h"
|
||||||
#include "distort/crop/cropdistortnode.h"
|
#include "distort/crop/cropdistortnode.h"
|
||||||
@@ -375,6 +377,10 @@ Node *NodeFactory::create_from_factory_index(const NodeFactory::InternalID &id)
|
|||||||
return new DisplayTransformNode();
|
return new DisplayTransformNode();
|
||||||
case k_ocio_grading_transform_linear:
|
case k_ocio_grading_transform_linear:
|
||||||
return new OCIOGradingTransformLinearNode();
|
return new OCIOGradingTransformLinearNode();
|
||||||
|
case k_ocio_grading_transform_log:
|
||||||
|
return new OCIOGradingTransformLogNode();
|
||||||
|
case k_white_balance:
|
||||||
|
return new WhiteBalanceNode();
|
||||||
case k_ocio_lut:
|
case k_ocio_lut:
|
||||||
return new OCIOLutNode();
|
return new OCIOLutNode();
|
||||||
case k_three_way_color:
|
case k_three_way_color:
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ public:
|
|||||||
k_tile_distort,
|
k_tile_distort,
|
||||||
k_swirl_distort,
|
k_swirl_distort,
|
||||||
k_multicam_node,
|
k_multicam_node,
|
||||||
|
k_ocio_grading_transform_log,
|
||||||
|
k_white_balance,
|
||||||
|
|
||||||
// Count value
|
// Count value
|
||||||
k_internal_node_count
|
k_internal_node_count
|
||||||
|
|||||||
@@ -126,11 +126,20 @@ Rational Footage::verify_length_internal(Track::Type type) const
|
|||||||
|
|
||||||
QString Footage::get_colorspace_to_use(const VideoParams ¶ms) const
|
QString Footage::get_colorspace_to_use(const VideoParams ¶ms) const
|
||||||
{
|
{
|
||||||
if (params.colorspace().isEmpty()) {
|
if (!params.colorspace().isEmpty()) {
|
||||||
return project()->color_manager()->get_default_input_color_space();
|
// The user explicitly set this stream's colorspace
|
||||||
} else {
|
|
||||||
return params.colorspace();
|
return params.colorspace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No override: try auto-detecting from the media's color tags
|
||||||
|
const QString detected =
|
||||||
|
project()->color_manager()->get_colorspace_for_ffmpeg_tags(
|
||||||
|
params.color_primaries(), params.color_transfer());
|
||||||
|
if (!detected.isEmpty()) {
|
||||||
|
return detected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return project()->color_manager()->get_default_input_color_space();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Footage::clear()
|
void Footage::clear()
|
||||||
|
|||||||
@@ -29,6 +29,17 @@
|
|||||||
namespace olive
|
namespace olive
|
||||||
{
|
{
|
||||||
|
|
||||||
|
const QStringList &LUTLibrary::supported_extensions()
|
||||||
|
{
|
||||||
|
// LUT formats OCIO FileTransform can load
|
||||||
|
static const QStringList extensions = {
|
||||||
|
QStringLiteral("cube"), QStringLiteral("3dl"), QStringLiteral("spi1d"),
|
||||||
|
QStringLiteral("spi3d"), QStringLiteral("spimtx"), QStringLiteral("csp"),
|
||||||
|
QStringLiteral("clf"), QStringLiteral("ctf"), QStringLiteral("cub"),
|
||||||
|
};
|
||||||
|
return extensions;
|
||||||
|
}
|
||||||
|
|
||||||
bool LUTLibrary::is_supported_extension(const QString &suffix)
|
bool LUTLibrary::is_supported_extension(const QString &suffix)
|
||||||
{
|
{
|
||||||
QString s = suffix;
|
QString s = suffix;
|
||||||
@@ -36,8 +47,7 @@ bool LUTLibrary::is_supported_extension(const QString &suffix)
|
|||||||
s.remove(0, 1);
|
s.remove(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString lower = s.toLower();
|
return supported_extensions().contains(s.toLower());
|
||||||
return lower == QStringLiteral("cube") || lower == QStringLiteral("3dl");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList LUTLibrary::get_directories()
|
QStringList LUTLibrary::get_directories()
|
||||||
|
|||||||
@@ -37,9 +37,16 @@ namespace olive
|
|||||||
*/
|
*/
|
||||||
class LUTLibrary {
|
class LUTLibrary {
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* @brief All LUT file extensions supported by the library
|
||||||
|
*
|
||||||
|
* Extensions OCIO FileTransform can load, lowercase, without the dot.
|
||||||
|
*/
|
||||||
|
static const QStringList &supported_extensions();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns true if the given file suffix is a supported LUT
|
* @brief Returns true if the given file suffix is a supported LUT
|
||||||
* extension (.cube or .3dl, case-insensitive, leading dot tolerated)
|
* extension (case-insensitive, leading dot tolerated)
|
||||||
*/
|
*/
|
||||||
static bool is_supported_extension(const QString &suffix);
|
static bool is_supported_extension(const QString &suffix);
|
||||||
|
|
||||||
|
|||||||
@@ -419,6 +419,10 @@ void VideoParams::load(QXmlStreamReader *reader)
|
|||||||
} else if (reader->name() == QStringLiteral("colorrange")) {
|
} else if (reader->name() == QStringLiteral("colorrange")) {
|
||||||
set_color_range(
|
set_color_range(
|
||||||
static_cast<ColorRange>(reader->readElementText().toInt()));
|
static_cast<ColorRange>(reader->readElementText().toInt()));
|
||||||
|
} else if (reader->name() == QStringLiteral("colorprimaries")) {
|
||||||
|
set_color_primaries(reader->readElementText().toInt());
|
||||||
|
} else if (reader->name() == QStringLiteral("colortransfer")) {
|
||||||
|
set_color_transfer(reader->readElementText().toInt());
|
||||||
} else {
|
} else {
|
||||||
reader->skipCurrentElement();
|
reader->skipCurrentElement();
|
||||||
}
|
}
|
||||||
@@ -463,6 +467,10 @@ void VideoParams::save(QXmlStreamWriter *writer) const
|
|||||||
writer->writeTextElement(QStringLiteral("colorspace"), colorspace_);
|
writer->writeTextElement(QStringLiteral("colorspace"), colorspace_);
|
||||||
writer->writeTextElement(QStringLiteral("colorrange"),
|
writer->writeTextElement(QStringLiteral("colorrange"),
|
||||||
QString::number(color_range_));
|
QString::number(color_range_));
|
||||||
|
writer->writeTextElement(QStringLiteral("colorprimaries"),
|
||||||
|
QString::number(color_primaries_));
|
||||||
|
writer->writeTextElement(QStringLiteral("colortransfer"),
|
||||||
|
QString::number(color_transfer_));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,6 +378,33 @@ public:
|
|||||||
color_range_ = color_range;
|
color_range_ = color_range;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Color primaries/transfer as reported by the media
|
||||||
|
*
|
||||||
|
* Raw FFmpeg AVColorPrimaries/AVColorTransferCharacteristic values
|
||||||
|
* (0 = unset, 2 = unspecified). Used to auto-detect the input
|
||||||
|
* colorspace when no explicit colorspace has been set.
|
||||||
|
*/
|
||||||
|
int color_primaries() const
|
||||||
|
{
|
||||||
|
return color_primaries_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_color_primaries(int p)
|
||||||
|
{
|
||||||
|
color_primaries_ = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
int color_transfer() const
|
||||||
|
{
|
||||||
|
return color_transfer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_color_transfer(int t)
|
||||||
|
{
|
||||||
|
color_transfer_ = t;
|
||||||
|
}
|
||||||
|
|
||||||
int64_t get_time_in_timebase_units(const Rational &time) const;
|
int64_t get_time_in_timebase_units(const Rational &time) const;
|
||||||
|
|
||||||
void load(QXmlStreamReader *reader);
|
void load(QXmlStreamReader *reader);
|
||||||
@@ -425,6 +452,8 @@ private:
|
|||||||
float x_;
|
float x_;
|
||||||
float y_;
|
float y_;
|
||||||
ColorRange color_range_;
|
ColorRange color_range_;
|
||||||
|
int color_primaries_ = 0;
|
||||||
|
int color_transfer_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ uniform vec2 viewport;
|
|||||||
uniform vec3 luma_coeffs;
|
uniform vec3 luma_coeffs;
|
||||||
|
|
||||||
uniform float waveform_scale;
|
uniform float waveform_scale;
|
||||||
|
uniform float parade_mode;
|
||||||
|
|
||||||
in vec2 ove_texcoord;
|
in vec2 ove_texcoord;
|
||||||
out vec4 frag_color;
|
out vec4 frag_color;
|
||||||
@@ -16,6 +17,32 @@ void main(void) {
|
|||||||
vec4 cur_col = vec4(0.0);
|
vec4 cur_col = vec4(0.0);
|
||||||
float ratio = 0.0;
|
float ratio = 0.0;
|
||||||
|
|
||||||
|
if (parade_mode > 0.5) {
|
||||||
|
// RGB parade: three zones side by side, one channel per zone
|
||||||
|
float zone = floor(ove_texcoord.x * 3.0);
|
||||||
|
float zone_x = fract(ove_texcoord.x * 3.0);
|
||||||
|
vec3 zone_color = zone < 0.5 ? vec3(1.0, 0.0, 0.0) :
|
||||||
|
(zone < 1.5 ? vec3(0.0, 1.0, 0.0) :
|
||||||
|
vec3(0.0, 0.0, 1.0));
|
||||||
|
|
||||||
|
for (int i = 0; float(i) < waveform_height; i++) {
|
||||||
|
ratio = float(i) / float(waveform_height - 1.0);
|
||||||
|
vec3 sample_rgb = texture(
|
||||||
|
ove_maintex,
|
||||||
|
vec2(zone_x, ratio)
|
||||||
|
).rgb;
|
||||||
|
|
||||||
|
float channel = zone < 0.5 ? sample_rgb.r :
|
||||||
|
(zone < 1.5 ? sample_rgb.g : sample_rgb.b);
|
||||||
|
|
||||||
|
float hit = step(ove_texcoord.y - quantisation, channel) *
|
||||||
|
step(channel, ove_texcoord.y + quantisation) * intensity;
|
||||||
|
hit += step(1.0 - quantisation, ove_texcoord.y) *
|
||||||
|
step(1.0 - quantisation, channel) * intensity;
|
||||||
|
|
||||||
|
col.rgb += hit * zone_color;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
for (int i = 0; float(i) < waveform_height; i++) {
|
for (int i = 0; float(i) < waveform_height; i++) {
|
||||||
ratio = float(i) / float(waveform_height - 1.0);
|
ratio = float(i) / float(waveform_height - 1.0);
|
||||||
cur_col.rgb = texture(
|
cur_col.rgb = texture(
|
||||||
@@ -34,5 +61,7 @@ void main(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
col.rgb += vec3(col.w);
|
col.rgb += vec3(col.w);
|
||||||
|
}
|
||||||
|
|
||||||
frag_color = vec4(col.rgb, 1.0);
|
frag_color = vec4(col.rgb, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
uniform sampler2D tex_in;
|
||||||
|
|
||||||
|
uniform vec3 wb_gain_in;
|
||||||
|
|
||||||
|
in vec2 ove_texcoord;
|
||||||
|
out vec4 frag_color;
|
||||||
|
|
||||||
|
void main(void)
|
||||||
|
{
|
||||||
|
vec4 source = texture(tex_in, ove_texcoord);
|
||||||
|
|
||||||
|
// Deliberately not clamped: white balance must also work on HDR/linear
|
||||||
|
// footage with values above 1.0
|
||||||
|
frag_color = vec4(source.rgb * wb_gain_in, source.a);
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
#include "waveform.h"
|
#include "waveform.h"
|
||||||
|
|
||||||
|
#include <QContextMenuEvent>
|
||||||
|
#include <QMenu>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
@@ -39,9 +41,27 @@ namespace olive
|
|||||||
|
|
||||||
WaveformScope::WaveformScope(QWidget *parent)
|
WaveformScope::WaveformScope(QWidget *parent)
|
||||||
: super(parent)
|
: super(parent)
|
||||||
|
, parade_mode_(OAK_CONFIG("WaveformRgbParade").toBool())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WaveformScope::set_parade_mode(bool enabled)
|
||||||
|
{
|
||||||
|
parade_mode_ = enabled;
|
||||||
|
OAK_CONFIG("WaveformRgbParade") = enabled;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveformScope::contextMenuEvent(QContextMenuEvent *event)
|
||||||
|
{
|
||||||
|
QMenu menu(this);
|
||||||
|
QAction *parade = menu.addAction(tr("RGB Parade"));
|
||||||
|
parade->setCheckable(true);
|
||||||
|
parade->setChecked(parade_mode_);
|
||||||
|
connect(parade, &QAction::triggered, this, &WaveformScope::set_parade_mode);
|
||||||
|
menu.exec(event->globalPos());
|
||||||
|
}
|
||||||
|
|
||||||
ShaderCode WaveformScope::generate_shader_code()
|
ShaderCode WaveformScope::generate_shader_code()
|
||||||
{
|
{
|
||||||
return ShaderCode(
|
return ShaderCode(
|
||||||
@@ -72,6 +92,10 @@ void WaveformScope::draw_scope(TexturePtr managed_tex, QVariant pipeline)
|
|||||||
job.insert(QStringLiteral("waveform_scale"),
|
job.insert(QStringLiteral("waveform_scale"),
|
||||||
NodeValue(NodeValue::k_float, waveform_scale));
|
NodeValue(NodeValue::k_float, waveform_scale));
|
||||||
|
|
||||||
|
// Overlay vs. RGB parade display
|
||||||
|
job.insert(QStringLiteral("parade_mode"),
|
||||||
|
NodeValue(NodeValue::k_float, parade_mode_ ? 1.0f : 0.0f));
|
||||||
|
|
||||||
// Insert source texture
|
// Insert source texture
|
||||||
job.insert(QStringLiteral("ove_maintex"),
|
job.insert(QStringLiteral("ove_maintex"),
|
||||||
NodeValue(NodeValue::k_texture,
|
NodeValue(NodeValue::k_texture,
|
||||||
@@ -156,26 +180,40 @@ void WaveformScope::draw_scope_software(QPainter &p, const QImage &image)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto mark = [&](float value, int add_r, int add_g, int add_b) {
|
auto mark = [&](int x, float value, int add_r, int add_g, int add_b) {
|
||||||
int scope_y =
|
int scope_y =
|
||||||
waveform_start_dim_y + int((1.0f - value) * waveform_dim_y);
|
waveform_start_dim_y + int((1.0f - value) * waveform_dim_y);
|
||||||
if (scope_y < waveform_start_dim_y ||
|
if (x < waveform_start_dim_x || x >= waveform_end_dim_x ||
|
||||||
|
scope_y < waveform_start_dim_y ||
|
||||||
scope_y >= waveform_start_dim_y + waveform_dim_y) {
|
scope_y >= waveform_start_dim_y + waveform_dim_y) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QRgb *dst_line =
|
QRgb *dst_line = reinterpret_cast<QRgb *>(buf.scanLine(scope_y));
|
||||||
reinterpret_cast<QRgb *>(buf.scanLine(scope_y));
|
QRgb cur = dst_line[x];
|
||||||
QRgb cur = dst_line[scope_x];
|
|
||||||
int nr = qMin(255, qRed(cur) + add_r);
|
int nr = qMin(255, qRed(cur) + add_r);
|
||||||
int ng = qMin(255, qGreen(cur) + add_g);
|
int ng = qMin(255, qGreen(cur) + add_g);
|
||||||
int nb = qMin(255, qBlue(cur) + add_b);
|
int nb = qMin(255, qBlue(cur) + add_b);
|
||||||
int na = qMax(qMax(nr, ng), nb);
|
int na = qMax(qMax(nr, ng), nb);
|
||||||
dst_line[scope_x] = qRgba(nr, ng, nb, na);
|
dst_line[x] = qRgba(nr, ng, nb, na);
|
||||||
};
|
};
|
||||||
|
|
||||||
mark(r, 30, 0, 0);
|
if (parade_mode_) {
|
||||||
mark(g, 0, 30, 0);
|
// RGB parade: each channel gets one third of the scope width
|
||||||
mark(b, 0, 0, 30);
|
const float zone_x = float(sx) / float(src_w) / 3.0f;
|
||||||
|
mark(waveform_start_dim_x +
|
||||||
|
int((0.0f + zone_x) * waveform_dim_x),
|
||||||
|
r, 30, 0, 0);
|
||||||
|
mark(waveform_start_dim_x +
|
||||||
|
int((1.0f / 3.0f + zone_x) * waveform_dim_x),
|
||||||
|
g, 0, 30, 0);
|
||||||
|
mark(waveform_start_dim_x +
|
||||||
|
int((2.0f / 3.0f + zone_x) * waveform_dim_x),
|
||||||
|
b, 0, 0, 30);
|
||||||
|
} else {
|
||||||
|
mark(scope_x, r, 30, 0, 0);
|
||||||
|
mark(scope_x, g, 0, 30, 0);
|
||||||
|
mark(scope_x, b, 0, 0, 30);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,12 +34,24 @@ public:
|
|||||||
|
|
||||||
MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR(WaveformScope)
|
MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR(WaveformScope)
|
||||||
|
|
||||||
|
bool parade_mode() const
|
||||||
|
{
|
||||||
|
return parade_mode_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_parade_mode(bool enabled);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual ShaderCode generate_shader_code() override;
|
virtual ShaderCode generate_shader_code() override;
|
||||||
|
|
||||||
virtual void draw_scope(TexturePtr managed_tex, QVariant pipeline) override;
|
virtual void draw_scope(TexturePtr managed_tex, QVariant pipeline) override;
|
||||||
|
|
||||||
virtual void draw_scope_software(QPainter &p, const QImage &image) override;
|
virtual void draw_scope_software(QPainter &p, const QImage &image) override;
|
||||||
|
|
||||||
|
virtual void contextMenuEvent(QContextMenuEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool parade_mode_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -517,6 +517,8 @@ Serialized inside `<standard><track>` for inputs of type `kVideoParams`.
|
|||||||
<premultipliedalpha>0|1</premultipliedalpha>
|
<premultipliedalpha>0|1</premultipliedalpha>
|
||||||
<colorspace>string</colorspace>
|
<colorspace>string</colorspace>
|
||||||
<colorrange>int</colorrange>
|
<colorrange>int</colorrange>
|
||||||
|
<colorprimaries>int</colorprimaries>
|
||||||
|
<colortransfer>int</colortransfer>
|
||||||
```
|
```
|
||||||
|
|
||||||
- `format`: `PixelFormat::Format` integer (`U8`, `U10`, `U16`, `F16`, `F32`).
|
- `format`: `PixelFormat::Format` integer (`U8`, `U10`, `U16`, `F16`, `F32`).
|
||||||
@@ -527,6 +529,8 @@ Serialized inside `<standard><track>` for inputs of type `kVideoParams`.
|
|||||||
- `videotype`:`VideoParams::Type` 整数。
|
- `videotype`:`VideoParams::Type` 整数。
|
||||||
- `colorrange`: `VideoParams::ColorRange` integer (`0` = limited, `1` = full).
|
- `colorrange`: `VideoParams::ColorRange` integer (`0` = limited, `1` = full).
|
||||||
- `colorrange`:`VideoParams::ColorRange` 整数(`0` 为 limited,`1` 为 full)。
|
- `colorrange`:`VideoParams::ColorRange` 整数(`0` 为 limited,`1` 为 full)。
|
||||||
|
- `colorprimaries`, `colortransfer`: raw FFmpeg `AVColorPrimaries` / `AVColorTransferCharacteristic` values reported by the media (`0` = unset, `2` = unspecified). Used to auto-detect the input colorspace when `colorspace` is empty.
|
||||||
|
- `colorprimaries`、`colortransfer`:媒体上报的 FFmpeg 原始 `AVColorPrimaries` / `AVColorTransferCharacteristic` 值(`0` 为未设置,`2` 为未指定)。当 `colorspace` 为空时用于自动检测输入色彩空间。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,27 @@ typedef enum FBColorSpace {
|
|||||||
fb_col_spc_b_t2020_ncl = 9
|
fb_col_spc_b_t2020_ncl = 9
|
||||||
} FBColorSpace;
|
} FBColorSpace;
|
||||||
|
|
||||||
|
/** Color primaries. Values mirror AVColorPrimaries (static_assert'ed). */
|
||||||
|
typedef enum FBColorPrimaries {
|
||||||
|
fb_color_primaries_bt709 = 1,
|
||||||
|
fb_color_primaries_unspec = 2,
|
||||||
|
fb_color_primaries_bt470bg = 5,
|
||||||
|
fb_color_primaries_smpte170m = 6,
|
||||||
|
fb_color_primaries_bt2020 = 9,
|
||||||
|
fb_color_primaries_smpte432 = 12
|
||||||
|
} FBColorPrimaries;
|
||||||
|
|
||||||
|
/** Color transfer characteristics. Values mirror AVColorTransferCharacteristic. */
|
||||||
|
typedef enum FBColorTrc {
|
||||||
|
fb_color_trc_bt709 = 1,
|
||||||
|
fb_color_trc_unspec = 2,
|
||||||
|
fb_color_trc_gamma28 = 5,
|
||||||
|
fb_color_trc_smpte170m = 6,
|
||||||
|
fb_color_trc_srgb = 13,
|
||||||
|
fb_color_trc_pq = 16,
|
||||||
|
fb_color_trc_hlg = 18
|
||||||
|
} FBColorTrc;
|
||||||
|
|
||||||
/** Media types. Values mirror AVMediaType (static_assert'ed). */
|
/** Media types. Values mirror AVMediaType (static_assert'ed). */
|
||||||
typedef enum FBMediaType {
|
typedef enum FBMediaType {
|
||||||
fb_media_type_video = 0,
|
fb_media_type_video = 0,
|
||||||
@@ -322,6 +343,8 @@ typedef struct FBStreamInfo {
|
|||||||
int pixel_format; /* FBPixelFormat */
|
int pixel_format; /* FBPixelFormat */
|
||||||
int field_order; /* FBFieldOrder */
|
int field_order; /* FBFieldOrder */
|
||||||
int color_range; /* FBColorRange */
|
int color_range; /* FBColorRange */
|
||||||
|
int color_primaries; /* raw AVColorPrimaries value (2 = unspecified) */
|
||||||
|
int color_trc; /* raw AVColorTransferCharacteristic value */
|
||||||
|
|
||||||
int sample_rate;
|
int sample_rate;
|
||||||
int sample_format; /* FBSampleFormat */
|
int sample_format; /* FBSampleFormat */
|
||||||
@@ -536,6 +559,13 @@ typedef struct FBEncoderConfig {
|
|||||||
int64_t video_buffer_size;
|
int64_t video_buffer_size;
|
||||||
int video_threads;
|
int video_threads;
|
||||||
int video_color_srgb; /* non-zero: tag nclc as sRGB, else Rec.709 */
|
int video_color_srgb; /* non-zero: tag nclc as sRGB, else Rec.709 */
|
||||||
|
/* Explicit nclc tags; when video_color_primaries is
|
||||||
|
* fb_color_primaries_unspec, tags are inferred from video_color_srgb
|
||||||
|
* instead. Values match the corresponding
|
||||||
|
* AVColorPrimaries/AVColorTransferCharacteristic/AVColorSpace constants. */
|
||||||
|
int video_color_primaries; /* FBColorPrimaries */
|
||||||
|
int video_color_trc; /* FBColorTrc */
|
||||||
|
int video_colorspace; /* FBColorSpaceTag */
|
||||||
const char **video_opt_keys;
|
const char **video_opt_keys;
|
||||||
const char **video_opt_values;
|
const char **video_opt_values;
|
||||||
int video_opt_count;
|
int video_opt_count;
|
||||||
|
|||||||
@@ -435,6 +435,8 @@ int fb_decoder_get_stream_info(const FBDecoder *decoder, FBStreamInfo *out)
|
|||||||
out->field_order = decoder->codec_ctx ? decoder->codec_ctx->field_order :
|
out->field_order = decoder->codec_ctx ? decoder->codec_ctx->field_order :
|
||||||
AV_FIELD_UNKNOWN;
|
AV_FIELD_UNKNOWN;
|
||||||
out->color_range = par->color_range;
|
out->color_range = par->color_range;
|
||||||
|
out->color_primaries = par->color_primaries;
|
||||||
|
out->color_trc = par->color_trc;
|
||||||
out->sample_rate = par->sample_rate;
|
out->sample_rate = par->sample_rate;
|
||||||
out->sample_format = par->format;
|
out->sample_format = par->format;
|
||||||
out->channel_layout_mask = fb::validate_stream_channel_layout_mask(s);
|
out->channel_layout_mask = fb::validate_stream_channel_layout_mask(s);
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ struct FBEncoder {
|
|||||||
int64_t video_buffer_size = 0;
|
int64_t video_buffer_size = 0;
|
||||||
int video_threads = 0;
|
int video_threads = 0;
|
||||||
int video_color_srgb = 0;
|
int video_color_srgb = 0;
|
||||||
|
int video_color_primaries = fb_color_primaries_unspec;
|
||||||
|
int video_color_trc = fb_color_trc_unspec;
|
||||||
|
int video_colorspace = fb_col_spc_unspec;
|
||||||
std::vector<std::pair<std::string, std::string>> video_opts;
|
std::vector<std::pair<std::string, std::string>> video_opts;
|
||||||
|
|
||||||
int audio_enabled = 0;
|
int audio_enabled = 0;
|
||||||
@@ -251,6 +254,9 @@ FBEncoder *fb_encoder_create(const FBEncoderConfig *config)
|
|||||||
e->video_buffer_size = config->video_buffer_size;
|
e->video_buffer_size = config->video_buffer_size;
|
||||||
e->video_threads = config->video_threads;
|
e->video_threads = config->video_threads;
|
||||||
e->video_color_srgb = config->video_color_srgb;
|
e->video_color_srgb = config->video_color_srgb;
|
||||||
|
e->video_color_primaries = config->video_color_primaries;
|
||||||
|
e->video_color_trc = config->video_color_trc;
|
||||||
|
e->video_colorspace = config->video_colorspace;
|
||||||
for (int i = 0; i < config->video_opt_count; i++) {
|
for (int i = 0; i < config->video_opt_count; i++) {
|
||||||
if (config->video_opt_keys[i] && config->video_opt_values[i]) {
|
if (config->video_opt_keys[i] && config->video_opt_values[i]) {
|
||||||
e->video_opts.emplace_back(config->video_opt_keys[i],
|
e->video_opts.emplace_back(config->video_opt_keys[i],
|
||||||
@@ -868,7 +874,14 @@ bool FBEncoder::initialize_stream(AVMediaType type, AVStream **stream_ptr,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nclc tags. See https://ffmpeg.org/doxygen/4.0/pixfmt_8h.html#ad384ee5a840bafd73daef08e6d9cafe7
|
// nclc tags. See https://ffmpeg.org/doxygen/4.0/pixfmt_8h.html#ad384ee5a840bafd73daef08e6d9cafe7
|
||||||
if (video_color_srgb) {
|
if (video_color_primaries != fb_color_primaries_unspec) {
|
||||||
|
// Explicit tags supplied (e.g. derived from the export colorspace)
|
||||||
|
codec_ctx->color_primaries =
|
||||||
|
static_cast<AVColorPrimaries>(video_color_primaries);
|
||||||
|
codec_ctx->color_trc =
|
||||||
|
static_cast<AVColorTransferCharacteristic>(video_color_trc);
|
||||||
|
codec_ctx->colorspace = static_cast<AVColorSpace>(video_colorspace);
|
||||||
|
} else if (video_color_srgb) {
|
||||||
codec_ctx->color_primaries = AVCOL_PRI_BT709;
|
codec_ctx->color_primaries = AVCOL_PRI_BT709;
|
||||||
codec_ctx->color_trc = AVCOL_TRC_IEC61966_2_1;
|
codec_ctx->color_trc = AVCOL_TRC_IEC61966_2_1;
|
||||||
codec_ctx->colorspace = AVCOL_SPC_BT709;
|
codec_ctx->colorspace = AVCOL_SPC_BT709;
|
||||||
|
|||||||
@@ -84,6 +84,21 @@ static_assert(fb_col_spc_smpt_e170_m == AVCOL_SPC_SMPTE170M, "colspace mismatch"
|
|||||||
static_assert(fb_col_spc_smpt_e240_m == AVCOL_SPC_SMPTE240M, "colspace mismatch");
|
static_assert(fb_col_spc_smpt_e240_m == AVCOL_SPC_SMPTE240M, "colspace mismatch");
|
||||||
static_assert(fb_col_spc_b_t2020_ncl == AVCOL_SPC_BT2020_NCL, "colspace mismatch");
|
static_assert(fb_col_spc_b_t2020_ncl == AVCOL_SPC_BT2020_NCL, "colspace mismatch");
|
||||||
|
|
||||||
|
static_assert(fb_color_primaries_unspec == AVCOL_PRI_UNSPECIFIED, "primaries mismatch");
|
||||||
|
static_assert(fb_color_primaries_bt709 == AVCOL_PRI_BT709, "primaries mismatch");
|
||||||
|
static_assert(fb_color_primaries_bt470bg == AVCOL_PRI_BT470BG, "primaries mismatch");
|
||||||
|
static_assert(fb_color_primaries_smpte170m == AVCOL_PRI_SMPTE170M, "primaries mismatch");
|
||||||
|
static_assert(fb_color_primaries_bt2020 == AVCOL_PRI_BT2020, "primaries mismatch");
|
||||||
|
static_assert(fb_color_primaries_smpte432 == AVCOL_PRI_SMPTE432, "primaries mismatch");
|
||||||
|
|
||||||
|
static_assert(fb_color_trc_unspec == AVCOL_TRC_UNSPECIFIED, "trc mismatch");
|
||||||
|
static_assert(fb_color_trc_bt709 == AVCOL_TRC_BT709, "trc mismatch");
|
||||||
|
static_assert(fb_color_trc_gamma28 == AVCOL_TRC_GAMMA28, "trc mismatch");
|
||||||
|
static_assert(fb_color_trc_smpte170m == AVCOL_TRC_SMPTE170M, "trc mismatch");
|
||||||
|
static_assert(fb_color_trc_srgb == AVCOL_TRC_IEC61966_2_1, "trc mismatch");
|
||||||
|
static_assert(fb_color_trc_pq == AVCOL_TRC_SMPTE2084, "trc mismatch");
|
||||||
|
static_assert(fb_color_trc_hlg == AVCOL_TRC_ARIB_STD_B67, "trc mismatch");
|
||||||
|
|
||||||
static_assert(fb_media_type_video == AVMEDIA_TYPE_VIDEO, "mediatype mismatch");
|
static_assert(fb_media_type_video == AVMEDIA_TYPE_VIDEO, "mediatype mismatch");
|
||||||
static_assert(fb_media_type_audio == AVMEDIA_TYPE_AUDIO, "mediatype mismatch");
|
static_assert(fb_media_type_audio == AVMEDIA_TYPE_AUDIO, "mediatype mismatch");
|
||||||
static_assert(fb_media_type_data == AVMEDIA_TYPE_DATA, "mediatype mismatch");
|
static_assert(fb_media_type_data == AVMEDIA_TYPE_DATA, "mediatype mismatch");
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ void fill_stream_info(const AVStream *s, int has_decoder, FBStreamInfo *out)
|
|||||||
out->pixel_format = fb::pix_fmt_from_av(AVPixelFormat(par->format));
|
out->pixel_format = fb::pix_fmt_from_av(AVPixelFormat(par->format));
|
||||||
out->field_order = fb_field_order_unknown;
|
out->field_order = fb_field_order_unknown;
|
||||||
out->color_range = par->color_range;
|
out->color_range = par->color_range;
|
||||||
|
out->color_primaries = par->color_primaries;
|
||||||
|
out->color_trc = par->color_trc;
|
||||||
out->sample_rate = par->sample_rate;
|
out->sample_rate = par->sample_rate;
|
||||||
out->sample_format = par->format;
|
out->sample_format = par->format;
|
||||||
out->channel_layout_mask = fb::validate_stream_channel_layout_mask(s);
|
out->channel_layout_mask = fb::validate_stream_channel_layout_mask(s);
|
||||||
|
|||||||
@@ -41,3 +41,22 @@ TEST(CodecDecoder, RetrieveVideoFrameFromDemoMp4)
|
|||||||
}
|
}
|
||||||
EXPECT_TRUE(has_nonzero_byte);
|
EXPECT_TRUE(has_nonzero_byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CodecDecoder, ProbeReportsColorTags)
|
||||||
|
{
|
||||||
|
const QString path = QDir(QStringLiteral(OAK_TEST_SOURCE_DIR))
|
||||||
|
.filePath(QStringLiteral("tests/demo.mp4"));
|
||||||
|
ASSERT_TRUE(QFileInfo::exists(path));
|
||||||
|
|
||||||
|
olive::DecoderPtr decoder =
|
||||||
|
olive::Decoder::create_from_id(QStringLiteral("ffmpeg"));
|
||||||
|
ASSERT_TRUE(decoder);
|
||||||
|
|
||||||
|
const olive::FootageDescription desc = decoder->probe(path, nullptr);
|
||||||
|
const QVector<olive::VideoParams> &streams = desc.get_video_streams();
|
||||||
|
ASSERT_FALSE(streams.isEmpty());
|
||||||
|
|
||||||
|
// tests/demo.mp4 is tagged BT.709 primaries and transfer
|
||||||
|
EXPECT_EQ(streams.first().color_primaries(), 1);
|
||||||
|
EXPECT_EQ(streams.first().color_transfer(), 1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -118,3 +118,86 @@ TEST(CodecFFmpegEncoder, EncodePngVideoAndProbeBack)
|
|||||||
fb_probe_close(probe);
|
fb_probe_close(probe);
|
||||||
fb_probe_free(&probe);
|
fb_probe_free(&probe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CodecFFmpegEncoder, ColorTagsForColorspace)
|
||||||
|
{
|
||||||
|
int primaries = 0, trc = 0, matrix = 0;
|
||||||
|
|
||||||
|
ASSERT_TRUE(olive::FFmpegEncoder::get_color_tags_for_colorspace(
|
||||||
|
QStringLiteral("Rec.2020 PQ"), &primaries, &trc, &matrix));
|
||||||
|
EXPECT_EQ(primaries, fb_color_primaries_bt2020);
|
||||||
|
EXPECT_EQ(trc, fb_color_trc_pq);
|
||||||
|
EXPECT_EQ(matrix, fb_col_spc_b_t2020_ncl);
|
||||||
|
|
||||||
|
ASSERT_TRUE(olive::FFmpegEncoder::get_color_tags_for_colorspace(
|
||||||
|
QStringLiteral("Rec.2020 HLG"), &primaries, &trc, &matrix));
|
||||||
|
EXPECT_EQ(primaries, fb_color_primaries_bt2020);
|
||||||
|
EXPECT_EQ(trc, fb_color_trc_hlg);
|
||||||
|
EXPECT_EQ(matrix, fb_col_spc_b_t2020_ncl);
|
||||||
|
|
||||||
|
ASSERT_TRUE(olive::FFmpegEncoder::get_color_tags_for_colorspace(
|
||||||
|
QStringLiteral("sRGB OETF"), &primaries, &trc, &matrix));
|
||||||
|
EXPECT_EQ(primaries, fb_color_primaries_bt709);
|
||||||
|
EXPECT_EQ(trc, fb_color_trc_srgb);
|
||||||
|
EXPECT_EQ(matrix, fb_col_spc_b_t709);
|
||||||
|
|
||||||
|
ASSERT_TRUE(olive::FFmpegEncoder::get_color_tags_for_colorspace(
|
||||||
|
QStringLiteral("Rec.709 OETF"), &primaries, &trc, &matrix));
|
||||||
|
EXPECT_EQ(primaries, fb_color_primaries_bt709);
|
||||||
|
EXPECT_EQ(trc, fb_color_trc_bt709);
|
||||||
|
EXPECT_EQ(matrix, fb_col_spc_b_t709);
|
||||||
|
|
||||||
|
// Unknown/linear spaces must not guess tags
|
||||||
|
EXPECT_FALSE(olive::FFmpegEncoder::get_color_tags_for_colorspace(
|
||||||
|
QStringLiteral("Linear"), &primaries, &trc, &matrix));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CodecFFmpegEncoder, WritesHdrColorTags)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
const QString path = dir.filePath(QStringLiteral("hdr_test.mkv"));
|
||||||
|
|
||||||
|
olive::EncodingParams params;
|
||||||
|
params.set_filename(path);
|
||||||
|
olive::VideoParams video_params(64, 64, olive::core::PixelFormat::u8,
|
||||||
|
olive::VideoParams::k_rgba_channel_count);
|
||||||
|
video_params.set_frame_rate(olive::core::Rational(30, 1));
|
||||||
|
params.enable_video(video_params, olive::ExportCodec::k_codec_h264);
|
||||||
|
params.set_video_pix_fmt(QStringLiteral("yuv420p"));
|
||||||
|
params.set_color_transform(
|
||||||
|
olive::ColorTransform(QStringLiteral("Rec.2020 PQ")));
|
||||||
|
|
||||||
|
olive::FFmpegEncoder encoder(params);
|
||||||
|
ASSERT_TRUE(encoder.open()) << encoder.get_error().toStdString();
|
||||||
|
|
||||||
|
for (int f = 0; f < 3; f++) {
|
||||||
|
olive::FramePtr frame = olive::Frame::create();
|
||||||
|
frame->set_video_params(video_params);
|
||||||
|
ASSERT_TRUE(frame->allocate());
|
||||||
|
memset(frame->data(), 128, frame->allocated_size());
|
||||||
|
ASSERT_TRUE(encoder.write_frame(frame, olive::core::Rational(f, 30)))
|
||||||
|
<< encoder.get_error().toStdString();
|
||||||
|
}
|
||||||
|
encoder.close();
|
||||||
|
|
||||||
|
FBProbe *probe = fb_probe_create();
|
||||||
|
ASSERT_NE(probe, nullptr);
|
||||||
|
ASSERT_EQ(fb_probe_open(probe, path.toUtf8().constData()), 0);
|
||||||
|
|
||||||
|
bool found_video = false;
|
||||||
|
const int stream_count = fb_probe_get_stream_count(probe);
|
||||||
|
for (int i = 0; i < stream_count; i++) {
|
||||||
|
FBStreamInfo info;
|
||||||
|
if (fb_probe_get_stream_info(probe, i, &info) == 0 &&
|
||||||
|
info.codec_type == fb_media_type_video) {
|
||||||
|
found_video = true;
|
||||||
|
EXPECT_EQ(info.color_primaries, fb_color_primaries_bt2020);
|
||||||
|
EXPECT_EQ(info.color_trc, fb_color_trc_pq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(found_video);
|
||||||
|
|
||||||
|
fb_probe_close(probe);
|
||||||
|
fb_probe_free(&probe);
|
||||||
|
}
|
||||||
|
|||||||
@@ -826,6 +826,56 @@ TEST(LUTLibrary, SupportsCubeAnd3dlExtensions)
|
|||||||
EXPECT_FALSE(olive::LUTLibrary::is_supported_extension(QString()));
|
EXPECT_FALSE(olive::LUTLibrary::is_supported_extension(QString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(LUTLibrary, SupportsExtendedOcioFormats)
|
||||||
|
{
|
||||||
|
for (const QString &ext :
|
||||||
|
{ QStringLiteral("spi1d"), QStringLiteral("spi3d"),
|
||||||
|
QStringLiteral("spimtx"), QStringLiteral("csp"),
|
||||||
|
QStringLiteral("clf"), QStringLiteral("ctf"), QStringLiteral("cub") }) {
|
||||||
|
EXPECT_TRUE(olive::LUTLibrary::is_supported_extension(ext))
|
||||||
|
<< ext.toStdString();
|
||||||
|
EXPECT_TRUE(olive::LUTLibrary::is_supported_extension(
|
||||||
|
QStringLiteral(".") + ext.toUpper()))
|
||||||
|
<< ext.toStdString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorLut, Spi1dFileTransformConvertsColor)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
const QString path =
|
||||||
|
QDir(dir.path()).filePath(QStringLiteral("invert.spi1d"));
|
||||||
|
QFile file(path);
|
||||||
|
ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||||
|
file.write("Version 1\n"
|
||||||
|
"From 0.0 1.0\n"
|
||||||
|
"Length 2\n"
|
||||||
|
"Components 3\n"
|
||||||
|
"{\n"
|
||||||
|
"1.0 1.0 1.0\n"
|
||||||
|
"0.0 0.0 0.0\n"
|
||||||
|
"}\n");
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
ocio::FileTransformRcPtr transform = ocio::FileTransform::Create();
|
||||||
|
transform->setSrc(path.toUtf8().constData());
|
||||||
|
transform->setInterpolation(ocio::INTERP_LINEAR);
|
||||||
|
transform->setDirection(ocio::TRANSFORM_DIR_FORWARD);
|
||||||
|
|
||||||
|
ocio::ConstConfigRcPtr config = ocio::Config::CreateRaw();
|
||||||
|
olive::ColorProcessorPtr processor =
|
||||||
|
olive::ColorProcessor::create(config->getProcessor(transform));
|
||||||
|
ASSERT_TRUE(processor);
|
||||||
|
|
||||||
|
const olive::Color out =
|
||||||
|
processor->convert_color(olive::Color(0.25f, 0.50f, 0.75f, 1.0f));
|
||||||
|
EXPECT_NEAR(out.red(), 0.75f, 0.02f);
|
||||||
|
EXPECT_NEAR(out.green(), 0.50f, 0.02f);
|
||||||
|
EXPECT_NEAR(out.blue(), 0.25f, 0.02f);
|
||||||
|
EXPECT_NEAR(out.alpha(), 1.0f, 0.001f);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(LUTLibrary, DirectoryRoundTripCleansAndDeduplicates)
|
TEST(LUTLibrary, DirectoryRoundTripCleansAndDeduplicates)
|
||||||
{
|
{
|
||||||
const QString previous =
|
const QString previous =
|
||||||
|
|||||||
@@ -331,9 +331,11 @@ TEST_F(FootageProbeTest, ProbedFootageValuePushesRealStreamJobs)
|
|||||||
ASSERT_NE(texture, nullptr);
|
ASSERT_NE(texture, nullptr);
|
||||||
EXPECT_EQ(texture->params().width(), 1920);
|
EXPECT_EQ(texture->params().width(), 1920);
|
||||||
EXPECT_EQ(texture->params().height(), 1080);
|
EXPECT_EQ(texture->params().height(), 1080);
|
||||||
// The probed stream has no colorspace, so the project default is used
|
// The probed stream has no explicit colorspace. The media is tagged
|
||||||
|
// BT.709, which auto-detects to the config's Rec.709 input space and
|
||||||
|
// takes precedence over the project default
|
||||||
EXPECT_EQ(texture->params().colorspace(),
|
EXPECT_EQ(texture->params().colorspace(),
|
||||||
QStringLiteral("ProbeInputSpace"));
|
QStringLiteral("Rec.709 OETF"));
|
||||||
|
|
||||||
const auto *video_job =
|
const auto *video_job =
|
||||||
static_cast<const olive::FootageJob *>(texture->job());
|
static_cast<const olive::FootageJob *>(texture->job());
|
||||||
|
|||||||
@@ -8,8 +8,13 @@
|
|||||||
#include "node/color/displaytransform/displaytransform.h"
|
#include "node/color/displaytransform/displaytransform.h"
|
||||||
#include "node/color/ociobase/ociobase.h"
|
#include "node/color/ociobase/ociobase.h"
|
||||||
#include "node/color/ociogradingtransformlinear/ociogradingtransformlinear.h"
|
#include "node/color/ociogradingtransformlinear/ociogradingtransformlinear.h"
|
||||||
|
#include "node/color/ociogradingtransformlog/ociogradingtransformlog.h"
|
||||||
#include "node/color/threewaycolor/threewaycolor.h"
|
#include "node/color/threewaycolor/threewaycolor.h"
|
||||||
|
#include "node/color/whitebalance/whitebalance.h"
|
||||||
|
#include "node/factory.h"
|
||||||
#include "node/project.h"
|
#include "node/project.h"
|
||||||
|
#include "node/project/serializer/serializer.h"
|
||||||
|
#include "render/diskmanager.h"
|
||||||
#include "render/job/colortransformjob.h"
|
#include "render/job/colortransformjob.h"
|
||||||
#include "render/job/shaderjob.h"
|
#include "render/job/shaderjob.h"
|
||||||
#include "render/texture.h"
|
#include "render/texture.h"
|
||||||
@@ -79,6 +84,198 @@ TEST(OCIOBaseNode, PushesNothingWhenTextureInputEmpty)
|
|||||||
EXPECT_EQ(table.count(), 0);
|
EXPECT_EQ(table.count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// ColorManager input colorspace auto-detection
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(ColorManager, DetectsColorspaceFromFfmpegTags)
|
||||||
|
{
|
||||||
|
olive::ColorManager::set_up_default_config();
|
||||||
|
olive::ProjectSerializer::initialize();
|
||||||
|
olive::DiskManager::create_instance();
|
||||||
|
|
||||||
|
olive::Project project;
|
||||||
|
project.initialize();
|
||||||
|
olive::ColorManager *cm = project.color_manager();
|
||||||
|
ASSERT_NE(cm, nullptr);
|
||||||
|
|
||||||
|
EXPECT_EQ(cm->get_colorspace_for_ffmpeg_tags(1, 1),
|
||||||
|
QStringLiteral("Rec.709 OETF"));
|
||||||
|
EXPECT_EQ(cm->get_colorspace_for_ffmpeg_tags(1, 13),
|
||||||
|
QStringLiteral("sRGB OETF"));
|
||||||
|
EXPECT_EQ(cm->get_colorspace_for_ffmpeg_tags(6, 6),
|
||||||
|
QStringLiteral("Rec.601 OETF (NTSC)"));
|
||||||
|
EXPECT_EQ(cm->get_colorspace_for_ffmpeg_tags(5, 5),
|
||||||
|
QStringLiteral("Rec.601 OETF (PAL)"));
|
||||||
|
|
||||||
|
// The embedded config has no PQ/HLG/Rec.2020 colorspace
|
||||||
|
EXPECT_TRUE(cm->get_colorspace_for_ffmpeg_tags(9, 16).isEmpty());
|
||||||
|
EXPECT_TRUE(cm->get_colorspace_for_ffmpeg_tags(9, 18).isEmpty());
|
||||||
|
|
||||||
|
// Unknown tags never guess
|
||||||
|
EXPECT_TRUE(cm->get_colorspace_for_ffmpeg_tags(0, 0).isEmpty());
|
||||||
|
EXPECT_TRUE(cm->get_colorspace_for_ffmpeg_tags(2, 2).isEmpty());
|
||||||
|
|
||||||
|
olive::DiskManager::destroy_instance();
|
||||||
|
olive::ProjectSerializer::destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// OCIOGradingTransformLogNode (lift/gamma/gain)
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(OCIOGradingTransformLogNode, InputIdsMatchOcioUniformNames)
|
||||||
|
{
|
||||||
|
olive::OCIOGradingTransformLogNode node;
|
||||||
|
|
||||||
|
// The input ids double as the OCIO GPU uniform names of the dynamic
|
||||||
|
// GRADING_LOG transform (verified against OCIO's generated shader)
|
||||||
|
EXPECT_EQ(olive::OCIOGradingTransformLogNode::k_lift_input,
|
||||||
|
QStringLiteral("ocio_grading_primary_brightness"));
|
||||||
|
EXPECT_EQ(olive::OCIOGradingTransformLogNode::k_gain_input,
|
||||||
|
QStringLiteral("ocio_grading_primary_contrast"));
|
||||||
|
EXPECT_EQ(olive::OCIOGradingTransformLogNode::k_gamma_input,
|
||||||
|
QStringLiteral("ocio_grading_primary_gamma"));
|
||||||
|
|
||||||
|
EXPECT_TRUE(node.has_input_with_id(
|
||||||
|
olive::OCIOGradingTransformLogNode::k_saturation_input));
|
||||||
|
EXPECT_TRUE(
|
||||||
|
node.has_input_with_id(olive::OCIOGradingTransformLogNode::k_pivot_input));
|
||||||
|
|
||||||
|
// GRADING_LOG defaults (see ocio::GradingPrimary)
|
||||||
|
EXPECT_EQ(node.get_standard_value(
|
||||||
|
olive::OCIOGradingTransformLogNode::k_lift_input),
|
||||||
|
QVariant::fromValue(QVector4D(0, 0, 0, 0)));
|
||||||
|
EXPECT_EQ(node.get_standard_value(
|
||||||
|
olive::OCIOGradingTransformLogNode::k_gain_input),
|
||||||
|
QVariant::fromValue(QVector4D(1, 1, 1, 1)));
|
||||||
|
EXPECT_EQ(node.get_standard_value(
|
||||||
|
olive::OCIOGradingTransformLogNode::k_gamma_input),
|
||||||
|
QVariant::fromValue(QVector4D(1, 1, 1, 1)));
|
||||||
|
EXPECT_EQ(node.get_standard_value(
|
||||||
|
olive::OCIOGradingTransformLogNode::k_pivot_input),
|
||||||
|
QVariant::fromValue(-0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(OCIOGradingTransformLogNode, ValueMapsWheelsToOcioUniforms)
|
||||||
|
{
|
||||||
|
olive::ColorManager::set_up_default_config();
|
||||||
|
olive::ProjectSerializer::initialize();
|
||||||
|
olive::DiskManager::create_instance();
|
||||||
|
|
||||||
|
olive::Project project;
|
||||||
|
project.initialize();
|
||||||
|
|
||||||
|
auto *node = new olive::OCIOGradingTransformLogNode();
|
||||||
|
node->setParent(&project);
|
||||||
|
|
||||||
|
olive::NodeValueRow row;
|
||||||
|
row.insert(olive::OCIOBaseNode::k_texture_input,
|
||||||
|
olive::NodeValue(olive::NodeValue::k_texture,
|
||||||
|
make_dummy_texture()));
|
||||||
|
row.insert(olive::OCIOGradingTransformLogNode::k_lift_input,
|
||||||
|
olive::NodeValue(olive::NodeValue::k_vec4,
|
||||||
|
QVector4D(1.0, 0.25, 0.0, 0.0)));
|
||||||
|
row.insert(olive::OCIOGradingTransformLogNode::k_gain_input,
|
||||||
|
olive::NodeValue(olive::NodeValue::k_vec4,
|
||||||
|
QVector4D(2.0, 0.5, 1.0, 1.0)));
|
||||||
|
row.insert(olive::OCIOGradingTransformLogNode::k_gamma_input,
|
||||||
|
olive::NodeValue(olive::NodeValue::k_vec4,
|
||||||
|
QVector4D(2.0, 1.0, 0.5, 1.0)));
|
||||||
|
|
||||||
|
olive::NodeValueTable table;
|
||||||
|
node->value(row, olive::NodeGlobals(), &table);
|
||||||
|
|
||||||
|
ASSERT_EQ(table.count(), 1);
|
||||||
|
olive::TexturePtr tex = table.get(olive::NodeValue::k_texture).to_texture();
|
||||||
|
ASSERT_TRUE(tex);
|
||||||
|
auto *job = dynamic_cast<olive::ColorTransformJob *>(tex->job());
|
||||||
|
ASSERT_NE(job, nullptr);
|
||||||
|
|
||||||
|
// Lift is additive: RGB += master
|
||||||
|
const QVector3D lift =
|
||||||
|
job->get(olive::OCIOGradingTransformLogNode::k_lift_input).to_vec3();
|
||||||
|
EXPECT_FLOAT_EQ(lift.x(), 1.25f);
|
||||||
|
EXPECT_FLOAT_EQ(lift.y(), 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(lift.z(), 1.0f);
|
||||||
|
|
||||||
|
// Gain multiplies: RGB *= master
|
||||||
|
const QVector3D gain =
|
||||||
|
job->get(olive::OCIOGradingTransformLogNode::k_gain_input).to_vec3();
|
||||||
|
EXPECT_FLOAT_EQ(gain.x(), 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(gain.y(), 2.0f);
|
||||||
|
EXPECT_FLOAT_EQ(gain.z(), 2.0f);
|
||||||
|
|
||||||
|
// Gamma multiplies: RGB *= master
|
||||||
|
const QVector3D gamma =
|
||||||
|
job->get(olive::OCIOGradingTransformLogNode::k_gamma_input).to_vec3();
|
||||||
|
EXPECT_FLOAT_EQ(gamma.x(), 2.0f);
|
||||||
|
EXPECT_FLOAT_EQ(gamma.y(), 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(gamma.z(), 2.0f);
|
||||||
|
|
||||||
|
olive::DiskManager::destroy_instance();
|
||||||
|
olive::ProjectSerializer::destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(OCIOGradingTransformLogNode, FactoryCreatesNode)
|
||||||
|
{
|
||||||
|
std::unique_ptr<olive::Node> node(
|
||||||
|
olive::NodeFactory::create_from_factory_index(
|
||||||
|
olive::NodeFactory::k_ocio_grading_transform_log));
|
||||||
|
ASSERT_NE(node, nullptr);
|
||||||
|
EXPECT_EQ(node->id(),
|
||||||
|
QStringLiteral("org.olivevideoeditor.Olive.ociogradingtransformlog"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// WhiteBalanceNode
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(WhiteBalanceNode, NeutralTemperatureKeepsNearUnityGain)
|
||||||
|
{
|
||||||
|
const QVector3D gain =
|
||||||
|
olive::WhiteBalanceNode::get_gain_for_temperature(6500.0, 0.0);
|
||||||
|
EXPECT_NEAR(gain.x(), 1.0f, 0.02f);
|
||||||
|
EXPECT_FLOAT_EQ(gain.y(), 1.0f);
|
||||||
|
EXPECT_NEAR(gain.z(), 1.0f, 0.02f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WhiteBalanceNode, WarmAndCoolShiftAsExpected)
|
||||||
|
{
|
||||||
|
const QVector3D warm =
|
||||||
|
olive::WhiteBalanceNode::get_gain_for_temperature(3000.0, 0.0);
|
||||||
|
EXPECT_GT(warm.x(), warm.z()); // warm light: red over blue
|
||||||
|
EXPECT_FLOAT_EQ(warm.y(), 1.0f);
|
||||||
|
|
||||||
|
const QVector3D cool =
|
||||||
|
olive::WhiteBalanceNode::get_gain_for_temperature(10000.0, 0.0);
|
||||||
|
EXPECT_GT(cool.z(), cool.x()); // cool light: blue over red
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WhiteBalanceNode, TintMovesGreenMagentaAxis)
|
||||||
|
{
|
||||||
|
const QVector3D magenta =
|
||||||
|
olive::WhiteBalanceNode::get_gain_for_temperature(6500.0, -0.5);
|
||||||
|
const QVector3D green =
|
||||||
|
olive::WhiteBalanceNode::get_gain_for_temperature(6500.0, 0.5);
|
||||||
|
EXPECT_LT(magenta.y(), 1.0f);
|
||||||
|
EXPECT_GT(green.y(), 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(WhiteBalanceNode, FactoryCreatesNode)
|
||||||
|
{
|
||||||
|
std::unique_ptr<olive::Node> node(
|
||||||
|
olive::NodeFactory::create_from_factory_index(
|
||||||
|
olive::NodeFactory::k_white_balance));
|
||||||
|
ASSERT_NE(node, nullptr);
|
||||||
|
EXPECT_EQ(node->id(),
|
||||||
|
QStringLiteral("org.olivevideoeditor.Olive.whitebalance"));
|
||||||
|
EXPECT_TRUE(
|
||||||
|
node->has_input_with_id(olive::WhiteBalanceNode::k_temperature_input));
|
||||||
|
EXPECT_TRUE(node->has_input_with_id(olive::WhiteBalanceNode::k_tint_input));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// DisplayTransformNode
|
// DisplayTransformNode
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ TEST(RenderVideoParams, SaveLoadRoundTrip)
|
|||||||
params.set_frame_rate(olive::core::Rational(24, 1));
|
params.set_frame_rate(olive::core::Rational(24, 1));
|
||||||
params.set_pixel_aspect_ratio(olive::core::Rational(1, 1));
|
params.set_pixel_aspect_ratio(olive::core::Rational(1, 1));
|
||||||
params.set_colorspace(QStringLiteral("test"));
|
params.set_colorspace(QStringLiteral("test"));
|
||||||
|
params.set_color_primaries(9);
|
||||||
|
params.set_color_transfer(16);
|
||||||
|
|
||||||
QByteArray xml;
|
QByteArray xml;
|
||||||
QBuffer buffer(&xml);
|
QBuffer buffer(&xml);
|
||||||
@@ -39,4 +41,6 @@ TEST(RenderVideoParams, SaveLoadRoundTrip)
|
|||||||
EXPECT_EQ(loaded.frame_rate(), olive::core::Rational(24, 1));
|
EXPECT_EQ(loaded.frame_rate(), olive::core::Rational(24, 1));
|
||||||
EXPECT_EQ(loaded.pixel_aspect_ratio(), olive::core::Rational(1, 1));
|
EXPECT_EQ(loaded.pixel_aspect_ratio(), olive::core::Rational(1, 1));
|
||||||
EXPECT_EQ(loaded.colorspace(), QStringLiteral("test"));
|
EXPECT_EQ(loaded.colorspace(), QStringLiteral("test"));
|
||||||
|
EXPECT_EQ(loaded.color_primaries(), 9);
|
||||||
|
EXPECT_EQ(loaded.color_transfer(), 16);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user