OpenShot Library | libopenshot 0.5.0
Loading...
Searching...
No Matches
ColorMap.cpp
Go to the documentation of this file.
1
9// Copyright (c) 2008-2025 OpenShot Studios, LLC
10//
11// SPDX-License-Identifier: LGPL-3.0-or-later
12
13#include "ColorMap.h"
14#include "Exceptions.h"
15#include <algorithm>
16#include <omp.h>
17#include <QRegularExpression>
18
19using namespace openshot;
20
21void ColorMap::load_cube_file()
22{
23 if (lut_path.empty()) {
24 lut_data.clear();
25 lut_size = 0;
26 lut_type = LUTType::None;
27 needs_refresh = false;
28 return;
29 }
30
31 int parsed_size = 0;
32 std::vector<float> parsed_data;
33 bool parsed_is_3d = false;
34 std::array<float, 3> parsed_domain_min{0.0f, 0.0f, 0.0f};
35 std::array<float, 3> parsed_domain_max{1.0f, 1.0f, 1.0f};
36
37 #pragma omp critical(load_lut)
38 {
39 QFile file(QString::fromStdString(lut_path));
40 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
41 // leave parsed_size == 0
42 } else {
43 QTextStream in(&file);
44 QRegularExpression ws_re("\\s+");
45 auto parse_domain_line = [&](const QString &line) {
46 if (!line.startsWith("DOMAIN_MIN") && !line.startsWith("DOMAIN_MAX"))
47 return;
48 auto parts = line.split(ws_re);
49 if (parts.size() < 4)
50 return;
51 auto assign_values = [&](std::array<float, 3> &target) {
52 target[0] = parts[1].toFloat();
53 target[1] = parts[2].toFloat();
54 target[2] = parts[3].toFloat();
55 };
56 if (line.startsWith("DOMAIN_MIN"))
57 assign_values(parsed_domain_min);
58 else
59 assign_values(parsed_domain_max);
60 };
61
62 auto try_parse = [&](const QString &keyword, bool want3d) -> bool {
63 if (!file.seek(0) || !in.seek(0))
64 return false;
65
66 QString line;
67 int detected_size = 0;
68 while (!in.atEnd()) {
69 line = in.readLine().trimmed();
70 parse_domain_line(line);
71 if (line.startsWith(keyword)) {
72 auto parts = line.split(ws_re);
73 if (parts.size() >= 2) {
74 detected_size = parts[1].toInt();
75 }
76 break;
77 }
78 }
79 if (detected_size <= 0)
80 return false;
81
82 const int total_entries = want3d
83 ? detected_size * detected_size * detected_size
84 : detected_size;
85 std::vector<float> data;
86 data.reserve(size_t(total_entries * 3));
87 while (!in.atEnd() && int(data.size()) < total_entries * 3) {
88 line = in.readLine().trimmed();
89 if (line.isEmpty() ||
90 line.startsWith("#") ||
91 line.startsWith("TITLE"))
92 {
93 continue;
94 }
95 if (line.startsWith("DOMAIN_MIN") ||
96 line.startsWith("DOMAIN_MAX"))
97 {
98 parse_domain_line(line);
99 continue;
100 }
101 auto vals = line.split(ws_re);
102 if (vals.size() >= 3) {
103 data.push_back(vals[0].toFloat());
104 data.push_back(vals[1].toFloat());
105 data.push_back(vals[2].toFloat());
106 }
107 }
108 if (int(data.size()) != total_entries * 3)
109 return false;
110
111 parsed_size = detected_size;
112 parsed_is_3d = want3d;
113 parsed_data.swap(data);
114 return true;
115 };
116
117 if (!try_parse("LUT_3D_SIZE", true)) {
118 try_parse("LUT_1D_SIZE", false);
119 }
120 }
121 }
122
123 if (parsed_size > 0) {
124 lut_size = parsed_size;
125 lut_data.swap(parsed_data);
126 lut_type = parsed_is_3d ? LUTType::LUT3D : LUTType::LUT1D;
127 lut_domain_min = parsed_domain_min;
128 lut_domain_max = parsed_domain_max;
129 } else {
130 lut_data.clear();
131 lut_size = 0;
132 lut_type = LUTType::None;
133 lut_domain_min = std::array<float, 3>{0.0f, 0.0f, 0.0f};
134 lut_domain_max = std::array<float, 3>{1.0f, 1.0f, 1.0f};
135 }
136 needs_refresh = false;
137}
138
139void ColorMap::init_effect_details()
140{
142 info.class_name = "ColorMap";
143 info.name = "Color Map / Lookup";
144 info.description = "Adjust colors using 3D LUT lookup tables (.cube format)";
145 info.has_video = true;
146 info.has_audio = false;
147}
148
150 : lut_path(""), lut_size(0), lut_type(LUTType::None), needs_refresh(true),
151 lut_domain_min{0.0f, 0.0f, 0.0f}, lut_domain_max{1.0f, 1.0f, 1.0f},
152 intensity(1.0), intensity_r(1.0), intensity_g(1.0), intensity_b(1.0)
153{
154 init_effect_details();
155 load_cube_file();
156}
157
158ColorMap::ColorMap(const std::string &path,
159 const Keyframe &i,
160 const Keyframe &iR,
161 const Keyframe &iG,
162 const Keyframe &iB)
163 : lut_path(path),
164 lut_size(0),
165 lut_type(LUTType::None),
166 needs_refresh(true),
167 lut_domain_min{0.0f, 0.0f, 0.0f}, lut_domain_max{1.0f, 1.0f, 1.0f},
168 intensity(i),
169 intensity_r(iR),
170 intensity_g(iG),
171 intensity_b(iB)
172{
173 init_effect_details();
174 load_cube_file();
175}
176
177std::shared_ptr<openshot::Frame>
178ColorMap::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
179{
180 // Reload LUT when its path changed; no locking here
181 if (needs_refresh) {
182 load_cube_file();
183 needs_refresh = false;
184 }
185
186 if (lut_data.empty() || lut_size <= 0 || lut_type == LUTType::None)
187 return frame;
188
189 auto image = frame->GetImage();
190 int w = image->width(), h = image->height();
191 unsigned char *pixels = image->bits();
192
193 float overall = float(intensity.GetValue(frame_number));
194 float tR = float(intensity_r.GetValue(frame_number)) * overall;
195 float tG = float(intensity_g.GetValue(frame_number)) * overall;
196 float tB = float(intensity_b.GetValue(frame_number)) * overall;
197
198 const bool use3d = (lut_type == LUTType::LUT3D);
199 const bool use1d = (lut_type == LUTType::LUT1D);
200 const int lut_dim = lut_size;
201 const std::vector<float> &table = lut_data;
202 const int data_count = int(table.size());
203
204 auto sample1d = [&](float value, int channel) -> float {
205 if (lut_dim <= 1) {
206 int base = std::min(channel, data_count - 1);
207 return table[base];
208 }
209 float scaled = value * float(lut_dim - 1);
210 int i0 = int(floor(scaled));
211 int i1 = std::min(i0 + 1, lut_dim - 1);
212 float t = scaled - i0;
213 int base0 = std::max(0, std::min(i0 * 3 + channel, data_count - 1));
214 int base1 = std::max(0, std::min(i1 * 3 + channel, data_count - 1));
215 float v0 = table[base0];
216 float v1 = table[base1];
217 return v0 * (1.0f - t) + v1 * t;
218 };
219
220 int pixel_count = w * h;
221 #pragma omp parallel for
222 for (int i = 0; i < pixel_count; ++i) {
223 int idx = i * 4;
224 int A = pixels[idx + 3];
225 float alpha = A / 255.0f;
226 if (alpha == 0.0f) continue;
227
228 // demultiply premultiplied RGBA
229 float R = pixels[idx + 0] / alpha;
230 float G = pixels[idx + 1] / alpha;
231 float B = pixels[idx + 2] / alpha;
232
233 // normalize to [0,1]
234 float Rn = R * (1.0f / 255.0f);
235 float Gn = G * (1.0f / 255.0f);
236 float Bn = B * (1.0f / 255.0f);
237
238 auto normalize_to_domain = [&](float value, int channel) -> float {
239 float min_val = lut_domain_min[channel];
240 float max_val = lut_domain_max[channel];
241 float range = max_val - min_val;
242 if (range <= 0.0f)
243 return std::clamp(value, 0.0f, 1.0f);
244 float normalized = (value - min_val) / range;
245 return std::clamp(normalized, 0.0f, 1.0f);
246 };
247 float Rdn = normalize_to_domain(Rn, 0);
248 float Gdn = normalize_to_domain(Gn, 1);
249 float Bdn = normalize_to_domain(Bn, 2);
250
251 float lr = Rn;
252 float lg = Gn;
253 float lb = Bn;
254
255 if (use3d) {
256 float rf = Rdn * (lut_dim - 1);
257 float gf = Gdn * (lut_dim - 1);
258 float bf = Bdn * (lut_dim - 1);
259
260 int r0 = int(floor(rf)), r1 = std::min(r0 + 1, lut_dim - 1);
261 int g0 = int(floor(gf)), g1 = std::min(g0 + 1, lut_dim - 1);
262 int b0 = int(floor(bf)), b1 = std::min(b0 + 1, lut_dim - 1);
263
264 float dr = rf - r0;
265 float dg = gf - g0;
266 float db = bf - b0;
267
268 int base000 = ((b0 * lut_dim + g0) * lut_dim + r0) * 3;
269 int base100 = ((b0 * lut_dim + g0) * lut_dim + r1) * 3;
270 int base010 = ((b0 * lut_dim + g1) * lut_dim + r0) * 3;
271 int base110 = ((b0 * lut_dim + g1) * lut_dim + r1) * 3;
272 int base001 = ((b1 * lut_dim + g0) * lut_dim + r0) * 3;
273 int base101 = ((b1 * lut_dim + g0) * lut_dim + r1) * 3;
274 int base011 = ((b1 * lut_dim + g1) * lut_dim + r0) * 3;
275 int base111 = ((b1 * lut_dim + g1) * lut_dim + r1) * 3;
276
277 float c00 = table[base000 + 0] * (1 - dr) + table[base100 + 0] * dr;
278 float c01 = table[base001 + 0] * (1 - dr) + table[base101 + 0] * dr;
279 float c10 = table[base010 + 0] * (1 - dr) + table[base110 + 0] * dr;
280 float c11 = table[base011 + 0] * (1 - dr) + table[base111 + 0] * dr;
281 float c0 = c00 * (1 - dg) + c10 * dg;
282 float c1 = c01 * (1 - dg) + c11 * dg;
283 lr = c0 * (1 - db) + c1 * db;
284
285 c00 = table[base000 + 1] * (1 - dr) + table[base100 + 1] * dr;
286 c01 = table[base001 + 1] * (1 - dr) + table[base101 + 1] * dr;
287 c10 = table[base010 + 1] * (1 - dr) + table[base110 + 1] * dr;
288 c11 = table[base011 + 1] * (1 - dr) + table[base111 + 1] * dr;
289 c0 = c00 * (1 - dg) + c10 * dg;
290 c1 = c01 * (1 - dg) + c11 * dg;
291 lg = c0 * (1 - db) + c1 * db;
292
293 c00 = table[base000 + 2] * (1 - dr) + table[base100 + 2] * dr;
294 c01 = table[base001 + 2] * (1 - dr) + table[base101 + 2] * dr;
295 c10 = table[base010 + 2] * (1 - dr) + table[base110 + 2] * dr;
296 c11 = table[base011 + 2] * (1 - dr) + table[base111 + 2] * dr;
297 c0 = c00 * (1 - dg) + c10 * dg;
298 c1 = c01 * (1 - dg) + c11 * dg;
299 lb = c0 * (1 - db) + c1 * db;
300 } else if (use1d) {
301 lr = sample1d(Rdn, 0);
302 lg = sample1d(Gdn, 1);
303 lb = sample1d(Bdn, 2);
304 }
305
306 // blend per-channel, re-premultiply alpha
307 float outR = (lr * tR + Rn * (1 - tR)) * alpha;
308 float outG = (lg * tG + Gn * (1 - tG)) * alpha;
309 float outB = (lb * tB + Bn * (1 - tB)) * alpha;
310
311 pixels[idx + 0] = constrain(outR * 255.0f);
312 pixels[idx + 1] = constrain(outG * 255.0f);
313 pixels[idx + 2] = constrain(outB * 255.0f);
314 // alpha left unchanged
315 }
316
317 return frame;
318}
319
320
321std::string ColorMap::Json() const
322{
323 return JsonValue().toStyledString();
324}
325
326Json::Value ColorMap::JsonValue() const
327{
328 Json::Value root = EffectBase::JsonValue();
329 root["type"] = info.class_name;
330 root["lut_path"] = lut_path;
331 root["intensity"] = intensity.JsonValue();
332 root["intensity_r"] = intensity_r.JsonValue();
333 root["intensity_g"] = intensity_g.JsonValue();
334 root["intensity_b"] = intensity_b.JsonValue();
335 return root;
336}
337
338void ColorMap::SetJson(const std::string value)
339{
340 try {
341 const Json::Value root = openshot::stringToJson(value);
342 SetJsonValue(root);
343 }
344 catch (...) {
345 throw InvalidJSON("Invalid JSON for ColorMap effect");
346 }
347}
348
349void ColorMap::SetJsonValue(const Json::Value root)
350{
352 if (!root["lut_path"].isNull())
353 {
354 lut_path = root["lut_path"].asString();
355 needs_refresh = true;
356 }
357 if (!root["intensity"].isNull())
358 intensity.SetJsonValue(root["intensity"]);
359 if (!root["intensity_r"].isNull())
360 intensity_r.SetJsonValue(root["intensity_r"]);
361 if (!root["intensity_g"].isNull())
362 intensity_g.SetJsonValue(root["intensity_g"]);
363 if (!root["intensity_b"].isNull())
364 intensity_b.SetJsonValue(root["intensity_b"]);
365}
366
367std::string ColorMap::PropertiesJSON(int64_t requested_frame) const
368{
369 Json::Value root = BasePropertiesJSON(requested_frame);
370
371 root["lut_path"] = add_property_json(
372 "LUT File", 0.0, "string", lut_path, nullptr, 0, 0, false, requested_frame);
373
374 root["intensity"] = add_property_json(
375 "Overall Intensity",
376 intensity.GetValue(requested_frame),
377 "float", "", &intensity, 0.0, 1.0, false, requested_frame);
378
379 root["intensity_r"] = add_property_json(
380 "Red Intensity",
381 intensity_r.GetValue(requested_frame),
382 "float", "", &intensity_r, 0.0, 1.0, false, requested_frame);
383
384 root["intensity_g"] = add_property_json(
385 "Green Intensity",
386 intensity_g.GetValue(requested_frame),
387 "float", "", &intensity_g, 0.0, 1.0, false, requested_frame);
388
389 root["intensity_b"] = add_property_json(
390 "Blue Intensity",
391 intensity_b.GetValue(requested_frame),
392 "float", "", &intensity_b, 0.0, 1.0, false, requested_frame);
393
394 return root.toStyledString();
395}
Header file for ColorMap (LUT) effect.
Header file for all Exception classes.
Json::Value add_property_json(std::string name, float value, std::string type, std::string memo, const Keyframe *keyframe, float min_value, float max_value, bool readonly, int64_t requested_frame) const
Generate JSON for a property.
Definition ClipBase.cpp:96
std::string Json() const override
Generate JSON string of this object.
Definition ColorMap.cpp:321
Keyframe intensity_r
Blend 0–1 for red channel.
Definition ColorMap.h:56
Keyframe intensity_b
Blend 0–1 for blue channel.
Definition ColorMap.h:58
Json::Value JsonValue() const override
Generate Json::Value for this object.
Definition ColorMap.cpp:326
Keyframe intensity_g
Blend 0–1 for green channel.
Definition ColorMap.h:57
std::shared_ptr< openshot::Frame > GetFrame(int64_t frame_number) override
Apply effect to a new frame.
Definition ColorMap.h:80
Keyframe intensity
Overall intensity 0–1 (affects all channels)
Definition ColorMap.h:55
ColorMap()
Blank constructor (used by JSON loader)
Definition ColorMap.cpp:149
void SetJson(const std::string value) override
Load JSON string into this object.
Definition ColorMap.cpp:338
std::string PropertiesJSON(int64_t requested_frame) const override
Expose properties (for UI)
Definition ColorMap.cpp:367
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
Definition ColorMap.cpp:349
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
int constrain(int color_value)
Constrain a color value from 0 to 255.
Json::Value BasePropertiesJSON(int64_t requested_frame) const
Generate JSON object of base properties (recommended to be used by all effects)
virtual void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
EffectInfoStruct info
Information about the current effect.
Definition EffectBase.h:69
Exception for invalid JSON.
Definition Exceptions.h:218
A Keyframe is a collection of Point instances, which is used to vary a number or property over time.
Definition KeyFrame.h:53
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Definition KeyFrame.cpp:372
double GetValue(int64_t index) const
Get the value at a specific index.
Definition KeyFrame.cpp:258
Json::Value JsonValue() const
Generate Json::Value for this object.
Definition KeyFrame.cpp:339
This namespace is the default namespace for all code in the openshot library.
Definition Compressor.h:29
const Json::Value stringToJson(const std::string value)
Definition Json.cpp:16
bool has_video
Determines if this effect manipulates the image of a frame.
Definition EffectBase.h:40
bool has_audio
Determines if this effect manipulates the audio of a frame.
Definition EffectBase.h:41
std::string class_name
The class name of the effect.
Definition EffectBase.h:36
std::string name
The name of the effect.
Definition EffectBase.h:37
std::string description
The description of this effect and what it does.
Definition EffectBase.h:38