24 constexpr
float kInv255 = 1.0f / 255.0f;
26 struct WheelBiasParams {
32 static float clamp01(
float value) {
33 return std::max(0.0f, std::min(1.0f, value));
36 static int clampByte(
float value) {
41 return static_cast<int>(std::round(value));
44 static float smooth_midtones(
float luma) {
45 const float centered = std::abs(luma - 0.5f) * 2.0f;
46 return clamp01(1.0f - centered * centered);
49 static std::string color_to_hex(
const QColor& color) {
50 return color.name(QColor::HexRgb).toStdString();
53 static std::array<float, 256> build_curve_lut(
const AnimatedCurve& curve, int64_t frame_number) {
54 std::array<float, 256> lut{};
56 for (
size_t i = 0; i < lut.size(); ++i)
57 lut[i] =
static_cast<float>(i) * kInv255;
62 for (
size_t i = 0; i < lut.size(); ++i) {
63 lut[i] = clamp01(
static_cast<float>(sampled_curve.
GetValue(
static_cast<int64_t
>(i))));
68 static float sample_curve_lut(
const std::array<float, 256>& lut,
float value) {
69 return lut[clampByte(clamp01(value) * 255.0f)];
72 static WheelBiasParams build_wheel_bias(
const ColorGradeWheelEntry& wheel, int64_t frame_number) {
73 const QColor color = wheel.
GetColor(frame_number);
74 const float cr = color.redF();
75 const float cg = color.greenF();
76 const float cb = color.blueF();
77 const float avg = (cr + cg + cb) / 3.0f;
78 const float amount = wheel.
GetAmount(frame_number);
79 const float luma = wheel.
GetLuma(frame_number);
81 ((cr - avg) * amount) + luma,
82 ((cg - avg) * amount) + luma,
83 ((cb - avg) * amount) + luma,
89 : color(QColor(Qt::white)), amount(0.0f), luma(0.0f) {}
92 Json::Value root(Json::objectValue);
93 root[
"color"] = color_to_hex(QColor(
107 if (!root[
"color_keyframes"].isNull()) {
109 }
else if (!root[
"color"].isNull()) {
110 const QColor parsed(QString::fromStdString(root[
"color"].asString()));
111 if (parsed.isValid())
114 if (!root[
"amount_keyframes"].isNull())
116 else if (!root[
"amount"].isNull())
117 amount =
Keyframe(std::max(-1.0f, std::min(1.0f, root[
"amount"].asFloat())));
118 if (!root[
"luma_keyframes"].isNull())
120 else if (!root[
"luma"].isNull())
121 luma =
Keyframe(std::max(-1.0f, std::min(1.0f, root[
"luma"].asFloat())));
133 return std::max(-1.0f, std::min(1.0f,
static_cast<float>(
amount.
GetValue(frame_number))));
137 return std::max(-1.0f, std::min(1.0f,
static_cast<float>(
luma.
GetValue(frame_number))));
144 Json::Value root(Json::objectValue);
155 if (root[
"enabled"].isBool())
157 else if (!root[
"enabled_keyframes"].isNull())
159 if (!root[
"global"].isNull())
161 if (!root[
"shadows"].isNull())
163 if (!root[
"midtones"].isNull())
165 if (!root[
"highlights"].isNull())
170 return IsEnabled(frame_number) ?
"Global / Shadows / Midtones / Highlights"
192 init_effect_details();
195 void ColorGrade::init_effect_details() {
199 info.
description =
"Unified color grading effect with curves, wheels and LUT support.";
204 float ColorGrade::Clamp01(
float value) {
205 return clamp01(value);
208 void ColorGrade::sync_lut_effect() {
212 Json::Value payload(Json::objectValue);
213 payload[
"lut_path"] = lut_path;
222 std::shared_ptr<openshot::Frame>
ColorGrade::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number) {
223 std::shared_ptr<QImage> frame_image = frame->GetImage();
227 const float temperature_value = std::max(-1.0f, std::min(1.0f,
static_cast<float>(
temperature.
GetValue(frame_number))));
228 const float tint_value = std::max(-1.0f, std::min(1.0f,
static_cast<float>(
tint.
GetValue(frame_number))));
229 const float exposure_value = std::max(-4.0f, std::min(4.0f,
static_cast<float>(
exposure.
GetValue(frame_number))));
230 const float contrast_value = std::max(-1.0f, std::min(2.0f,
static_cast<float>(
contrast.
GetValue(frame_number))));
231 const float highlights_value = std::max(-1.0f, std::min(1.0f,
static_cast<float>(
highlights.
GetValue(frame_number))));
232 const float shadows_value = std::max(-1.0f, std::min(1.0f,
static_cast<float>(
shadows.
GetValue(frame_number))));
233 const float saturation_value = std::max(0.0f, std::min(4.0f,
static_cast<float>(
saturation.
GetValue(frame_number))));
234 const float vibrance_value = std::max(-1.0f, std::min(1.0f,
static_cast<float>(
vibrance.
GetValue(frame_number))));
235 const float mix_value = std::max(0.0f, std::min(1.0f,
static_cast<float>(
mix.
GetValue(frame_number))));
236 const float inverse_mix = 1.0f - mix_value;
237 const float exposure_gain = std::pow(2.0f, exposure_value);
238 const float contrast_factor = std::max(0.0f, 1.0f + contrast_value);
239 const std::array<float, 256> curve_all_lut = build_curve_lut(
curve_all, frame_number);
240 const std::array<float, 256> curve_red_lut = build_curve_lut(
curve_red, frame_number);
241 const std::array<float, 256> curve_green_lut = build_curve_lut(
curve_green, frame_number);
242 const std::array<float, 256> curve_blue_lut = build_curve_lut(
curve_blue, frame_number);
244 const WheelBiasParams global_wheel = wheels_enabled ? build_wheel_bias(
wheels.
global, frame_number) : WheelBiasParams{0.0f, 0.0f, 0.0f};
245 const WheelBiasParams shadows_wheel = wheels_enabled ? build_wheel_bias(
wheels.
shadows, frame_number) : WheelBiasParams{0.0f, 0.0f, 0.0f};
246 const WheelBiasParams midtones_wheel = wheels_enabled ? build_wheel_bias(
wheels.
midtones, frame_number) : WheelBiasParams{0.0f, 0.0f, 0.0f};
247 const WheelBiasParams highlights_wheel = wheels_enabled ? build_wheel_bias(
wheels.
highlights, frame_number) : WheelBiasParams{0.0f, 0.0f, 0.0f};
249 static const std::array<float, 256> inv_alpha = [] {
250 std::array<float, 256> lut{};
252 for (
int i = 1; i < 256; ++i)
253 lut[i] = 255.0f /
static_cast<float>(i);
257 const auto apply_wheel_bias = [](
const WheelBiasParams& wheel,
float weight,
float& r,
float& g,
float& b) {
258 if (std::abs(weight) <= 0.00001f)
260 r += wheel.red_delta * weight;
261 g += wheel.green_delta * weight;
262 b += wheel.blue_delta * weight;
265 unsigned char* pixels =
reinterpret_cast<unsigned char*
>(frame_image->bits());
266 const int pixel_count = frame_image->width() * frame_image->height();
268 #pragma omp parallel for if(pixel_count >= 16384) schedule(static)
269 for (
int pixel = 0; pixel < pixel_count; ++pixel) {
270 const int idx = pixel * 4;
271 const int A = pixels[idx + 3];
275 const float alpha_percent =
static_cast<float>(A) * kInv255;
281 R = pixels[idx + 0] * kInv255;
282 G = pixels[idx + 1] * kInv255;
283 B = pixels[idx + 2] * kInv255;
285 const float inv_alpha_percent = inv_alpha[A];
286 R = (pixels[idx + 0] * inv_alpha_percent) * kInv255;
287 G = (pixels[idx + 1] * inv_alpha_percent) * kInv255;
288 B = (pixels[idx + 2] * inv_alpha_percent) * kInv255;
291 const float original_r = R;
292 const float original_g = G;
293 const float original_b = B;
296 R = Clamp01(R + (temperature_value * 0.125f));
297 B = Clamp01(B - (temperature_value * 0.125f));
298 G = Clamp01(G - (tint_value * 0.1f));
299 R = Clamp01(R + (tint_value * 0.05f));
300 B = Clamp01(B + (tint_value * 0.05f));
303 R = Clamp01(R * exposure_gain);
304 G = Clamp01(G * exposure_gain);
305 B = Clamp01(B * exposure_gain);
307 R = Clamp01(((R - 0.5f) * contrast_factor) + 0.5f);
308 G = Clamp01(((G - 0.5f) * contrast_factor) + 0.5f);
309 B = Clamp01(((B - 0.5f) * contrast_factor) + 0.5f);
311 float luma = (0.299f * R) + (0.587f * G) + (0.114f * B);
312 const float shadow_weight = (1.0f - luma) * (1.0f - luma);
313 const float highlight_weight = luma * luma;
316 const float shadow_adjust = shadows_value * shadow_weight * 0.35f;
317 const float highlight_adjust = highlights_value * highlight_weight * 0.35f;
318 R = Clamp01(R + shadow_adjust + highlight_adjust);
319 G = Clamp01(G + shadow_adjust + highlight_adjust);
320 B = Clamp01(B + shadow_adjust + highlight_adjust);
323 luma = (0.299f * R) + (0.587f * G) + (0.114f * B);
327 if (wheels_enabled) {
328 apply_wheel_bias(global_wheel, 1.0f, wheel_r, wheel_g, wheel_b);
329 apply_wheel_bias(shadows_wheel, (1.0f - luma) * (1.0f - luma), wheel_r, wheel_g, wheel_b);
330 apply_wheel_bias(midtones_wheel, smooth_midtones(luma), wheel_r, wheel_g, wheel_b);
331 apply_wheel_bias(highlights_wheel, luma * luma, wheel_r, wheel_g, wheel_b);
333 R = Clamp01(wheel_r);
334 G = Clamp01(wheel_g);
335 B = Clamp01(wheel_b);
338 luma = (0.299f * R) + (0.587f * G) + (0.114f * B);
339 const float max_channel = std::max(R, std::max(G, B));
340 const float min_channel = std::min(R, std::min(G, B));
341 const float colorfulness = max_channel - min_channel;
342 const float vibrance_factor = 1.0f + (vibrance_value * (1.0f - colorfulness));
343 const float sat_factor = saturation_value * std::max(0.0f, vibrance_factor);
344 R = Clamp01(luma + ((R - luma) * sat_factor));
345 G = Clamp01(luma + ((G - luma) * sat_factor));
346 B = Clamp01(luma + ((B - luma) * sat_factor));
349 R = sample_curve_lut(curve_all_lut, R);
350 G = sample_curve_lut(curve_all_lut, G);
351 B = sample_curve_lut(curve_all_lut, B);
352 R = sample_curve_lut(curve_red_lut, R);
353 G = sample_curve_lut(curve_green_lut, G);
354 B = sample_curve_lut(curve_blue_lut, B);
357 R = Clamp01((original_r * inverse_mix) + (R * mix_value));
358 G = Clamp01((original_g * inverse_mix) + (G * mix_value));
359 B = Clamp01((original_b * inverse_mix) + (B * mix_value));
362 pixels[idx + 0] =
static_cast<unsigned char>(clampByte(R * 255.0f));
363 pixels[idx + 1] =
static_cast<unsigned char>(clampByte(G * 255.0f));
364 pixels[idx + 2] =
static_cast<unsigned char>(clampByte(B * 255.0f));
366 pixels[idx + 0] =
static_cast<unsigned char>(clampByte(R * 255.0f * alpha_percent));
367 pixels[idx + 1] =
static_cast<unsigned char>(clampByte(G * 255.0f * alpha_percent));
368 pixels[idx + 2] =
static_cast<unsigned char>(clampByte(B * 255.0f * alpha_percent));
374 frame = lut_effect.
GetFrame(frame, frame_number);
401 root[
"lut_path"] = lut_path;
411 throw InvalidJSON(
"Invalid JSON for ColorGrade effect");
418 if (!root[
"temperature"].isNull())
420 if (!root[
"tint"].isNull())
422 if (!root[
"exposure"].isNull())
424 if (!root[
"contrast"].isNull())
426 if (!root[
"highlights"].isNull())
428 if (!root[
"shadows"].isNull())
430 if (!root[
"saturation"].isNull())
432 if (!root[
"vibrance"].isNull())
434 if (!root[
"mix"].isNull())
436 if (!root[
"wheels"].isNull())
438 if (!root[
"curve_all"].isNull())
440 if (!root[
"curve_red"].isNull())
442 if (!root[
"curve_green"].isNull())
444 if (!root[
"curve_blue"].isNull())
446 if (!root[
"lut_path"].isNull()) {
447 lut_path = root[
"lut_path"].asString();
450 if (!root[
"lut_intensity"].isNull()) {
469 root[
"wheels"] =
add_property_json(
"Color Wheels", 0.0,
"colorgrade_wheels",
wheels.
Summary(requested_frame), NULL, 0.0, 1.0,
false, requested_frame);
475 root[
"curve_all"][
"channel"] =
"all";
480 root[
"curve_red"][
"channel"] =
"red";
485 root[
"curve_green"][
"channel"] =
"green";
490 root[
"curve_blue"][
"channel"] =
"blue";
493 root[
"lut_path"] =
add_property_json(
"LUT File", 0.0,
"string", lut_path, NULL, 0, 0,
false, requested_frame);
496 return root.toStyledString();