OpenShot Library | libopenshot  0.5.0
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 
19 using namespace openshot;
20 
21 void 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 
139 void ColorMap::init_effect_details()
140 {
141  InitEffectInfo();
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 
158 ColorMap::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 
177 std::shared_ptr<openshot::Frame>
178 ColorMap::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 
321 std::string ColorMap::Json() const
322 {
323  return JsonValue().toStyledString();
324 }
325 
326 Json::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 
338 void 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 
349 void 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 
367 std::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 }
openshot::ClipBase::add_property_json
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
openshot::stringToJson
const Json::Value stringToJson(const std::string value)
Definition: Json.cpp:16
openshot::EffectBase::info
EffectInfoStruct info
Information about the current effect.
Definition: EffectBase.h:69
openshot
This namespace is the default namespace for all code in the openshot library.
Definition: Compressor.h:28
openshot::EffectBase::JsonValue
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
Definition: EffectBase.cpp:79
openshot::ColorMap::GetFrame
std::shared_ptr< openshot::Frame > GetFrame(int64_t frame_number) override
Apply effect to a new frame.
Definition: ColorMap.h:80
openshot::ColorMap::ColorMap
ColorMap()
Blank constructor (used by JSON loader)
Definition: ColorMap.cpp:149
openshot::ColorMap::JsonValue
Json::Value JsonValue() const override
Generate Json::Value for this object.
Definition: ColorMap.cpp:326
openshot::Keyframe::SetJsonValue
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Definition: KeyFrame.cpp:372
openshot::ColorMap::intensity
Keyframe intensity
Overall intensity 0–1 (affects all channels)
Definition: ColorMap.h:55
openshot::Keyframe::JsonValue
Json::Value JsonValue() const
Generate Json::Value for this object.
Definition: KeyFrame.cpp:339
openshot::EffectBase::BasePropertiesJSON
Json::Value BasePropertiesJSON(int64_t requested_frame) const
Generate JSON object of base properties (recommended to be used by all effects)
Definition: EffectBase.cpp:179
openshot::ColorMap::intensity_g
Keyframe intensity_g
Blend 0–1 for green channel.
Definition: ColorMap.h:57
openshot::Keyframe
A Keyframe is a collection of Point instances, which is used to vary a number or property over time.
Definition: KeyFrame.h:53
openshot::InvalidJSON
Exception for invalid JSON.
Definition: Exceptions.h:217
openshot::ColorMap::SetJsonValue
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
Definition: ColorMap.cpp:349
openshot::ColorMap::SetJson
void SetJson(const std::string value) override
Load JSON string into this object.
Definition: ColorMap.cpp:338
openshot::EffectBase::InitEffectInfo
void InitEffectInfo()
Definition: EffectBase.cpp:24
openshot::EffectInfoStruct::has_audio
bool has_audio
Determines if this effect manipulates the audio of a frame.
Definition: EffectBase.h:41
path
path
Definition: FFmpegWriter.cpp:1469
openshot::ColorMap::Json
std::string Json() const override
Generate JSON string of this object.
Definition: ColorMap.cpp:321
openshot::EffectInfoStruct::class_name
std::string class_name
The class name of the effect.
Definition: EffectBase.h:36
openshot::EffectInfoStruct::description
std::string description
The description of this effect and what it does.
Definition: EffectBase.h:38
openshot::EffectInfoStruct::has_video
bool has_video
Determines if this effect manipulates the image of a frame.
Definition: EffectBase.h:40
openshot::ColorMap::PropertiesJSON
std::string PropertiesJSON(int64_t requested_frame) const override
Expose properties (for UI)
Definition: ColorMap.cpp:367
openshot::ColorMap::intensity_b
Keyframe intensity_b
Blend 0–1 for blue channel.
Definition: ColorMap.h:58
openshot::EffectBase::constrain
int constrain(int color_value)
Constrain a color value from 0 to 255.
Definition: EffectBase.cpp:60
openshot::ColorMap::intensity_r
Keyframe intensity_r
Blend 0–1 for red channel.
Definition: ColorMap.h:56
openshot::EffectInfoStruct::name
std::string name
The name of the effect.
Definition: EffectBase.h:37
ColorMap.h
Header file for ColorMap (LUT) effect.
Exceptions.h
Header file for all Exception classes.
openshot::EffectBase::SetJsonValue
virtual void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Definition: EffectBase.cpp:115
openshot::Keyframe::GetValue
double GetValue(int64_t index) const
Get the value at a specific index.
Definition: KeyFrame.cpp:258