24 constexpr
float kInv255 = 1.0f / 255.0f;
26 struct WheelBiasParams {
32 static inline float clamp01(
float value) {
33 return std::max(0.0f, std::min(1.0f, value));
36 static inline int clampByte(
float value) {
41 return static_cast<int>(value + 0.5f);
44 static inline 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[
static_cast<int>(value * 255.0f + 0.5f)];
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;
238 if (temperature_value == 0.0f && tint_value == 0.0f &&
239 exposure_value == 0.0f && contrast_value == 0.0f &&
240 highlights_value == 0.0f && shadows_value == 0.0f &&
241 saturation_value == 1.0f && vibrance_value == 0.0f &&
242 mix_value == 1.0f && !wheels_enabled &&
249 return lut_effect.
GetFrame(frame, frame_number);
253 const float exposure_gain = std::pow(2.0f, exposure_value);
254 const float contrast_factor = std::max(0.0f, 1.0f + contrast_value);
255 const std::array<float, 256> curve_all_lut = build_curve_lut(
curve_all, frame_number);
256 const std::array<float, 256> curve_red_lut = build_curve_lut(
curve_red, frame_number);
257 const std::array<float, 256> curve_green_lut = build_curve_lut(
curve_green, frame_number);
258 const std::array<float, 256> curve_blue_lut = build_curve_lut(
curve_blue, frame_number);
259 const WheelBiasParams global_wheel = wheels_enabled ? build_wheel_bias(
wheels.
global, frame_number) : WheelBiasParams{0.0f, 0.0f, 0.0f};
260 const WheelBiasParams shadows_wheel = wheels_enabled ? build_wheel_bias(
wheels.
shadows, frame_number) : WheelBiasParams{0.0f, 0.0f, 0.0f};
261 const WheelBiasParams midtones_wheel = wheels_enabled ? build_wheel_bias(
wheels.
midtones, frame_number) : WheelBiasParams{0.0f, 0.0f, 0.0f};
262 const WheelBiasParams highlights_wheel = wheels_enabled ? build_wheel_bias(
wheels.
highlights, frame_number) : WheelBiasParams{0.0f, 0.0f, 0.0f};
264 static const std::array<float, 256> inv_alpha = [] {
265 std::array<float, 256> lut{};
267 for (
int i = 1; i < 256; ++i)
268 lut[i] = 255.0f /
static_cast<float>(i);
272 const auto apply_wheel_bias = [](
const WheelBiasParams& wheel,
float weight,
float& r,
float& g,
float& b) {
273 if (std::abs(weight) <= 0.00001f)
275 r += wheel.red_delta * weight;
276 g += wheel.green_delta * weight;
277 b += wheel.blue_delta * weight;
280 unsigned char* pixels =
reinterpret_cast<unsigned char*
>(frame_image->bits());
281 const int pixel_count = frame_image->width() * frame_image->height();
283 #pragma omp parallel for if(pixel_count >= 16384) schedule(static)
284 for (
int pixel = 0; pixel < pixel_count; ++pixel) {
285 const int idx = pixel * 4;
286 const int A = pixels[idx + 3];
290 const float alpha_percent =
static_cast<float>(A) * kInv255;
296 R = pixels[idx + 0] * kInv255;
297 G = pixels[idx + 1] * kInv255;
298 B = pixels[idx + 2] * kInv255;
300 const float inv_alpha_percent = inv_alpha[A];
301 R = (pixels[idx + 0] * inv_alpha_percent) * kInv255;
302 G = (pixels[idx + 1] * inv_alpha_percent) * kInv255;
303 B = (pixels[idx + 2] * inv_alpha_percent) * kInv255;
306 const float original_r = R;
307 const float original_g = G;
308 const float original_b = B;
311 R = Clamp01(R + (temperature_value * 0.125f));
312 B = Clamp01(B - (temperature_value * 0.125f));
313 G = Clamp01(G - (tint_value * 0.1f));
314 R = Clamp01(R + (tint_value * 0.05f));
315 B = Clamp01(B + (tint_value * 0.05f));
318 R = Clamp01(R * exposure_gain);
319 G = Clamp01(G * exposure_gain);
320 B = Clamp01(B * exposure_gain);
322 R = Clamp01(((R - 0.5f) * contrast_factor) + 0.5f);
323 G = Clamp01(((G - 0.5f) * contrast_factor) + 0.5f);
324 B = Clamp01(((B - 0.5f) * contrast_factor) + 0.5f);
326 float luma = (0.299f * R) + (0.587f * G) + (0.114f * B);
327 const float shadow_weight = (1.0f - luma) * (1.0f - luma);
328 const float highlight_weight = luma * luma;
331 const float shadow_adjust = shadows_value * shadow_weight * 0.35f;
332 const float highlight_adjust = highlights_value * highlight_weight * 0.35f;
333 R = Clamp01(R + shadow_adjust + highlight_adjust);
334 G = Clamp01(G + shadow_adjust + highlight_adjust);
335 B = Clamp01(B + shadow_adjust + highlight_adjust);
338 luma = (0.299f * R) + (0.587f * G) + (0.114f * B);
342 if (wheels_enabled) {
343 apply_wheel_bias(global_wheel, 1.0f, wheel_r, wheel_g, wheel_b);
344 apply_wheel_bias(shadows_wheel, (1.0f - luma) * (1.0f - luma), wheel_r, wheel_g, wheel_b);
345 apply_wheel_bias(midtones_wheel, smooth_midtones(luma), wheel_r, wheel_g, wheel_b);
346 apply_wheel_bias(highlights_wheel, luma * luma, wheel_r, wheel_g, wheel_b);
348 R = Clamp01(wheel_r);
349 G = Clamp01(wheel_g);
350 B = Clamp01(wheel_b);
353 luma = (0.299f * R) + (0.587f * G) + (0.114f * B);
354 const float max_channel = std::max(R, std::max(G, B));
355 const float min_channel = std::min(R, std::min(G, B));
356 const float colorfulness = max_channel - min_channel;
357 const float vibrance_factor = 1.0f + (vibrance_value * (1.0f - colorfulness));
358 const float sat_factor = saturation_value * std::max(0.0f, vibrance_factor);
359 R = Clamp01(luma + ((R - luma) * sat_factor));
360 G = Clamp01(luma + ((G - luma) * sat_factor));
361 B = Clamp01(luma + ((B - luma) * sat_factor));
364 R = sample_curve_lut(curve_all_lut, R);
365 G = sample_curve_lut(curve_all_lut, G);
366 B = sample_curve_lut(curve_all_lut, B);
367 R = sample_curve_lut(curve_red_lut, R);
368 G = sample_curve_lut(curve_green_lut, G);
369 B = sample_curve_lut(curve_blue_lut, B);
372 R = Clamp01((original_r * inverse_mix) + (R * mix_value));
373 G = Clamp01((original_g * inverse_mix) + (G * mix_value));
374 B = Clamp01((original_b * inverse_mix) + (B * mix_value));
377 pixels[idx + 0] =
static_cast<unsigned char>(clampByte(R * 255.0f));
378 pixels[idx + 1] =
static_cast<unsigned char>(clampByte(G * 255.0f));
379 pixels[idx + 2] =
static_cast<unsigned char>(clampByte(B * 255.0f));
381 pixels[idx + 0] =
static_cast<unsigned char>(clampByte(R * 255.0f * alpha_percent));
382 pixels[idx + 1] =
static_cast<unsigned char>(clampByte(G * 255.0f * alpha_percent));
383 pixels[idx + 2] =
static_cast<unsigned char>(clampByte(B * 255.0f * alpha_percent));
389 frame = lut_effect.
GetFrame(frame, frame_number);
416 root[
"lut_path"] = lut_path;
426 throw InvalidJSON(
"Invalid JSON for ColorGrade effect");
433 if (!root[
"temperature"].isNull())
435 if (!root[
"tint"].isNull())
437 if (!root[
"exposure"].isNull())
439 if (!root[
"contrast"].isNull())
441 if (!root[
"highlights"].isNull())
443 if (!root[
"shadows"].isNull())
445 if (!root[
"saturation"].isNull())
447 if (!root[
"vibrance"].isNull())
449 if (!root[
"mix"].isNull())
451 if (!root[
"wheels"].isNull())
453 if (!root[
"curve_all"].isNull())
455 if (!root[
"curve_red"].isNull())
457 if (!root[
"curve_green"].isNull())
459 if (!root[
"curve_blue"].isNull())
461 if (!root[
"lut_path"].isNull()) {
462 lut_path = root[
"lut_path"].asString();
465 if (!root[
"lut_intensity"].isNull()) {
484 root[
"wheels"] =
add_property_json(
"Color Wheels", 0.0,
"colorgrade_wheels",
wheels.
Summary(requested_frame), NULL, 0.0, 1.0,
false, requested_frame);
490 root[
"curve_all"][
"channel"] =
"all";
495 root[
"curve_red"][
"channel"] =
"red";
500 root[
"curve_green"][
"channel"] =
"green";
505 root[
"curve_blue"][
"channel"] =
"blue";
508 root[
"lut_path"] =
add_property_json(
"LUT File", 0.0,
"string", lut_path, NULL, 0, 0,
false, requested_frame);
511 return root.toStyledString();