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:
@@ -18,8 +18,10 @@ add_subdirectory(colormanager)
|
||||
add_subdirectory(displaytransform)
|
||||
add_subdirectory(ociobase)
|
||||
add_subdirectory(ociogradingtransformlinear)
|
||||
add_subdirectory(ociogradingtransformlog)
|
||||
add_subdirectory(ociolut)
|
||||
add_subdirectory(threewaycolor)
|
||||
add_subdirectory(whitebalance)
|
||||
|
||||
set(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);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
return project()->get_color_reference_space();
|
||||
|
||||
@@ -65,6 +65,16 @@ public:
|
||||
|
||||
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);
|
||||
|
||||
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(),
|
||||
InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable));
|
||||
set_input_property(
|
||||
k_file_input, QStringLiteral("filter"),
|
||||
tr("LUT Files (*.cube *.3dl);;Cube LUT (*.cube);;3DL LUT (*.3dl);;All Files (*)"));
|
||||
const QString all_luts =
|
||||
QStringLiteral("*.") +
|
||||
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"),
|
||||
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
|
||||
set_input_property(k_file_input, QStringLiteral("lut_library"), true);
|
||||
|
||||
@@ -257,10 +259,8 @@ bool OCIOLutNode::create_processor_from_inputs() const
|
||||
last_path_.clear();
|
||||
last_direction_ = -1;
|
||||
processor_dirty_ = false;
|
||||
set_last_error(
|
||||
tr("OCIO LUT: unsupported LUT file extension (expected .cube or "
|
||||
".3dl): %1")
|
||||
.arg(path));
|
||||
set_last_error(tr("OCIO LUT: unsupported LUT file extension: %1")
|
||||
.arg(path));
|
||||
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 "color/displaytransform/displaytransform.h"
|
||||
#include "color/ociogradingtransformlinear/ociogradingtransformlinear.h"
|
||||
#include "color/ociogradingtransformlog/ociogradingtransformlog.h"
|
||||
#include "color/ociolut/ociolut.h"
|
||||
#include "color/threewaycolor/threewaycolor.h"
|
||||
#include "color/whitebalance/whitebalance.h"
|
||||
#include "common/current.h"
|
||||
#include "distort/cornerpin/cornerpindistortnode.h"
|
||||
#include "distort/crop/cropdistortnode.h"
|
||||
@@ -375,6 +377,10 @@ Node *NodeFactory::create_from_factory_index(const NodeFactory::InternalID &id)
|
||||
return new DisplayTransformNode();
|
||||
case k_ocio_grading_transform_linear:
|
||||
return new OCIOGradingTransformLinearNode();
|
||||
case k_ocio_grading_transform_log:
|
||||
return new OCIOGradingTransformLogNode();
|
||||
case k_white_balance:
|
||||
return new WhiteBalanceNode();
|
||||
case k_ocio_lut:
|
||||
return new OCIOLutNode();
|
||||
case k_three_way_color:
|
||||
|
||||
@@ -84,6 +84,8 @@ public:
|
||||
k_tile_distort,
|
||||
k_swirl_distort,
|
||||
k_multicam_node,
|
||||
k_ocio_grading_transform_log,
|
||||
k_white_balance,
|
||||
|
||||
// Count value
|
||||
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
|
||||
{
|
||||
if (params.colorspace().isEmpty()) {
|
||||
return project()->color_manager()->get_default_input_color_space();
|
||||
} else {
|
||||
if (!params.colorspace().isEmpty()) {
|
||||
// The user explicitly set this stream's 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()
|
||||
|
||||
Reference in New Issue
Block a user