OpenShot Library | libopenshot  0.7.0
Displace.cpp
Go to the documentation of this file.
1 
9 // Copyright (c) 2008-2026 OpenShot Studios, LLC
10 //
11 // SPDX-License-Identifier: LGPL-3.0-or-later
12 
13 #include "Displace.h"
14 
15 #include "Exceptions.h"
16 #include "ReaderBase.h"
17 #include "Timeline.h"
18 #include "ZmqLogger.h"
19 
20 #include <array>
21 #include <cmath>
22 #include <omp.h>
23 
24 using namespace openshot;
25 
26 namespace {
27  inline unsigned char clamp_u8(int value) {
28  if (value < 0)
29  return 0;
30  if (value > 255)
31  return 255;
32  return static_cast<unsigned char>(value);
33  }
34 
35  inline int clamp_i(int value, int min_value, int max_value) {
36  if (value < min_value)
37  return min_value;
38  if (value > max_value)
39  return max_value;
40  return value;
41  }
42 
43  inline float sample_bilinear(const unsigned char* pixels, int width, int height, float x, float y, int channel) {
44  x = std::fmax(0.0f, std::fmin(x, static_cast<float>(width - 1)));
45  y = std::fmax(0.0f, std::fmin(y, static_cast<float>(height - 1)));
46 
47  const int x0 = static_cast<int>(std::floor(x));
48  const int y0 = static_cast<int>(std::floor(y));
49  const int x1 = clamp_i(x0 + 1, 0, width - 1);
50  const int y1 = clamp_i(y0 + 1, 0, height - 1);
51  const float tx = x - x0;
52  const float ty = y - y0;
53 
54  const int idx00 = ((y0 * width) + x0) * 4 + channel;
55  const int idx10 = ((y0 * width) + x1) * 4 + channel;
56  const int idx01 = ((y1 * width) + x0) * 4 + channel;
57  const int idx11 = ((y1 * width) + x1) * 4 + channel;
58 
59  const float top = (pixels[idx00] * (1.0f - tx)) + (pixels[idx10] * tx);
60  const float bottom = (pixels[idx01] * (1.0f - tx)) + (pixels[idx11] * tx);
61  return (top * (1.0f - ty)) + (bottom * ty);
62  }
63 }
64 
67  : replace_image(false), invert(false), strength(1.0), horizontal(0.05), vertical(0.0), brightness(0.0), contrast(0.0) {
68  init_effect_details();
69 }
70 
71 // Default constructor
72 Displace::Displace(ReaderBase *map_reader, Keyframe map_strength, Keyframe map_horizontal,
73  Keyframe map_vertical, Keyframe map_brightness, Keyframe map_contrast)
74  : replace_image(false), invert(false), strength(map_strength), horizontal(map_horizontal), vertical(map_vertical),
75  brightness(map_brightness), contrast(map_contrast) {
76  init_effect_details();
77 
78  if (map_reader)
79  Reader(CreateReaderFromJson(map_reader->JsonValue()));
80 }
81 
82 // Init effect settings
83 void Displace::init_effect_details()
84 {
86 
87  info.class_name = "Displace";
88  info.name = "Displacement Map";
89  info.description = "Use a grayscale image or video to warp the frame in the horizontal and vertical directions.";
90  info.has_audio = false;
91  info.has_video = true;
92 }
93 
94 std::shared_ptr<QImage> Displace::GetMapImage(std::shared_ptr<QImage> target_image, int64_t frame_number) {
95  if (!map_reader || !target_image || target_image->isNull())
96  return {};
97 
98  std::shared_ptr<QImage> source_map;
99  bool used_cached_scaled = false;
100  #pragma omp critical (open_effect_displace_reader)
101  {
102  try {
103  map_reader->ParentClip(ParentClip());
104  if (!map_reader->IsOpen())
105  map_reader->Open();
106 
107  if (map_reader->info.has_single_image &&
108  cached_single_map_image &&
109  cached_single_map_width == target_image->width() &&
110  cached_single_map_height == target_image->height()) {
111  source_map = cached_single_map_image;
112  used_cached_scaled = true;
113  }
114  else {
115  int64_t mapped_frame = frame_number;
116  if (map_reader->info.has_video && map_reader->info.video_length > 0 &&
117  map_reader->info.fps.num > 0 && map_reader->info.fps.den > 0) {
118  double host_fps = 30.0;
119  if (ParentTimeline()) {
120  Timeline* parent_timeline = dynamic_cast<Timeline*>(ParentTimeline());
121  if (parent_timeline && parent_timeline->info.fps.num > 0 && parent_timeline->info.fps.den > 0)
122  host_fps = parent_timeline->info.fps.ToDouble();
123  }
124  double source_fps = map_reader->info.fps.ToDouble();
125  if (host_fps > 0.0 && source_fps > 0.0) {
126  const int64_t requested_index = std::max(int64_t(0), frame_number - 1);
127  const double seconds = static_cast<double>(requested_index) / host_fps;
128  mapped_frame = static_cast<int64_t>(std::floor(seconds * source_fps)) + 1;
129  }
130  mapped_frame = std::min(std::max(int64_t(1), mapped_frame), map_reader->info.video_length);
131  }
132 
133  auto source_frame = map_reader->GetFrame(mapped_frame);
134  if (source_frame && source_frame->GetImage() && !source_frame->GetImage()->isNull())
135  source_map = std::make_shared<QImage>(*source_frame->GetImage());
136  }
137  } catch (const std::exception& e) {
139  std::string("Displace::GetMapImage unable to read displacement frame: ") + e.what());
140  source_map.reset();
141  }
142  }
143 
144  if (!source_map || source_map->isNull())
145  return {};
146 
147  if (used_cached_scaled)
148  return source_map;
149 
150  auto scaled_map = std::make_shared<QImage>(
151  source_map->scaled(
152  target_image->width(), target_image->height(),
153  Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
154  if (map_reader->info.has_single_image) {
155  cached_single_map_image = scaled_map;
156  cached_single_map_width = target_image->width();
157  cached_single_map_height = target_image->height();
158  }
159  return scaled_map;
160 }
161 
162 // This method is required for all derived classes of EffectBase, and returns a
163 // modified openshot::Frame object
164 std::shared_ptr<openshot::Frame> Displace::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number) {
165  std::shared_ptr<QImage> frame_image = frame->GetImage();
166  if (!frame_image || frame_image->isNull())
167  return frame;
168 
169  auto map_image = GetMapImage(frame_image, frame_number);
170  if (!map_image || map_image->isNull())
171  return frame;
172 
173  const int width = frame_image->width();
174  const int height = frame_image->height();
175  const int pixel_count = width * height;
176 
177  QImage original = frame_image->copy();
178  const unsigned char* original_pixels = reinterpret_cast<const unsigned char*>(original.constBits());
179  unsigned char* pixels = reinterpret_cast<unsigned char*>(frame_image->bits());
180  const unsigned char* map_pixels = reinterpret_cast<const unsigned char*>(map_image->constBits());
181 
182  const float strength_value = static_cast<float>(strength.GetValue(frame_number));
183  const float x_amount = static_cast<float>(horizontal.GetValue(frame_number)) * width * strength_value;
184  const float y_amount = static_cast<float>(vertical.GetValue(frame_number)) * height * strength_value;
185  const float contrast_value = static_cast<float>(contrast.GetValue(frame_number));
186  const int brightness_adj = static_cast<int>(255.0f * brightness.GetValue(frame_number));
187  const float contrast_factor = 20.0f / std::max(0.00001f, 20.0f - contrast_value);
188  const bool output_map = replace_image;
189 
190  std::array<unsigned char, 256> adjusted_gray{};
191  for (int gray = 0; gray < 256; ++gray) {
192  const int adjusted = static_cast<int>(contrast_factor * ((gray + brightness_adj) - 128) + 128);
193  adjusted_gray[gray] = clamp_u8(adjusted);
194  }
195 
196  #pragma omp parallel for if(pixel_count >= 8192) schedule(static)
197  for (int i = 0; i < pixel_count; ++i) {
198  const int idx = i * 4;
199  const int map_r = map_pixels[idx + 0];
200  const int map_g = map_pixels[idx + 1];
201  const int map_b = map_pixels[idx + 2];
202  const int map_a = map_pixels[idx + 3];
203  const int gray = ((map_r * 11) + (map_g * 16) + (map_b * 5)) >> 5;
204  int processed = adjusted_gray[gray];
205  if (invert)
206  processed = 255 - processed;
207 
208  // Transparent areas in the displacement map smoothly collapse back to neutral.
209  processed = 128 + (((processed - 128) * map_a) / 255);
210 
211  if (output_map) {
212  const unsigned char debug_value = clamp_u8(processed);
213  pixels[idx + 0] = debug_value;
214  pixels[idx + 1] = debug_value;
215  pixels[idx + 2] = debug_value;
216  pixels[idx + 3] = 255;
217  continue;
218  }
219 
220  const float signed_map = (static_cast<float>(processed) - 127.5f) / 127.5f;
221  const int pixel_x = i % width;
222  const int pixel_y = i / width;
223  const float sample_x = pixel_x + (signed_map * x_amount);
224  const float sample_y = pixel_y + (signed_map * y_amount);
225 
226  pixels[idx + 0] = clamp_u8(static_cast<int>(std::lround(sample_bilinear(original_pixels, width, height, sample_x, sample_y, 0))));
227  pixels[idx + 1] = clamp_u8(static_cast<int>(std::lround(sample_bilinear(original_pixels, width, height, sample_x, sample_y, 1))));
228  pixels[idx + 2] = clamp_u8(static_cast<int>(std::lround(sample_bilinear(original_pixels, width, height, sample_x, sample_y, 2))));
229  pixels[idx + 3] = clamp_u8(static_cast<int>(std::lround(sample_bilinear(original_pixels, width, height, sample_x, sample_y, 3))));
230  }
231 
232  return frame;
233 }
234 
235 // Generate JSON string of this object
236 std::string Displace::Json() const {
237  return JsonValue().toStyledString();
238 }
239 
240 // Generate Json::Value for this object
241 Json::Value Displace::JsonValue() const {
242  Json::Value root = EffectBase::JsonValue();
243  root["type"] = info.class_name;
244  root["replace_image"] = replace_image;
245  root["invert"] = invert;
246  root["strength"] = strength.JsonValue();
247  root["horizontal"] = horizontal.JsonValue();
248  root["vertical"] = vertical.JsonValue();
249  root["brightness"] = brightness.JsonValue();
250  root["contrast"] = contrast.JsonValue();
251  if (map_reader)
252  root["map_reader"] = map_reader->JsonValue();
253  else
254  root["map_reader"] = Json::objectValue;
255  return root;
256 }
257 
258 // Load JSON string into this object
259 void Displace::SetJson(const std::string value) {
260  try
261  {
262  const Json::Value root = openshot::stringToJson(value);
263  SetJsonValue(root);
264  }
265  catch (const std::exception& e)
266  {
267  throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
268  }
269 }
270 
271 // Load Json::Value into this object
272 void Displace::SetJsonValue(const Json::Value root) {
273  Json::Value normalized_root = root;
274  // Keep displacement-map source separate from the shared effect mask reader.
275  // EffectBase still treats a plain "reader" field as a legacy alias for
276  // "mask_reader", so strip displacement-source fields before loading the
277  // common effect-mask state.
278  normalized_root.removeMember("reader");
279  normalized_root.removeMember("map_reader");
280  EffectBase::SetJsonValue(normalized_root);
281 
282  const Json::Value map_reader_json =
283  !root["map_reader"].isNull() ? root["map_reader"] : root["reader"];
284  if (!map_reader_json.isNull()) {
285  if (!map_reader_json["type"].isNull())
286  Reader(CreateReaderFromJson(map_reader_json));
287  else if (map_reader_json.isObject() && map_reader_json.empty())
288  Reader(NULL);
289  }
290  if (!root["replace_image"].isNull())
291  replace_image = root["replace_image"].asBool();
292  if (!root["invert"].isNull())
293  invert = root["invert"].asBool();
294  if (!root["strength"].isNull())
295  strength.SetJsonValue(root["strength"]);
296  if (!root["horizontal"].isNull())
297  horizontal.SetJsonValue(root["horizontal"]);
298  if (!root["vertical"].isNull())
299  vertical.SetJsonValue(root["vertical"]);
300  if (!root["brightness"].isNull())
301  brightness.SetJsonValue(root["brightness"]);
302  if (!root["contrast"].isNull())
303  contrast.SetJsonValue(root["contrast"]);
304 }
305 
306 // Get all properties for a specific frame
307 std::string Displace::PropertiesJSON(int64_t requested_frame) const {
308  Json::Value root = BasePropertiesJSON(requested_frame);
309 
310  root["replace_image"] = add_property_json("Replace Image", replace_image, "int", "", NULL, 0, 1, false, requested_frame);
311  root["replace_image"]["choices"].append(add_property_choice_json("Yes", true, replace_image));
312  root["replace_image"]["choices"].append(add_property_choice_json("No", false, replace_image));
313  root["invert"] = add_property_json("Map: Invert", invert, "int", "", NULL, 0, 1, false, requested_frame);
314  root["invert"]["choices"].append(add_property_choice_json("Yes", true, invert));
315  root["invert"]["choices"].append(add_property_choice_json("No", false, invert));
316  if (map_reader)
317  root["map_reader"] = add_property_json("Map: Source", 0.0, "reader", map_reader->Json(), NULL, 0, 1, false, requested_frame);
318  else
319  root["map_reader"] = add_property_json("Map: Source", 0.0, "reader", "{}", NULL, 0, 1, false, requested_frame);
320 
321  root["strength"] = add_property_json("Strength", strength.GetValue(requested_frame), "float", "", &strength, 0.0, 3.0, false, requested_frame);
322  root["horizontal"] = add_property_json("Horizontal", horizontal.GetValue(requested_frame), "float", "", &horizontal, -1.0, 1.0, false, requested_frame);
323  root["vertical"] = add_property_json("Vertical", vertical.GetValue(requested_frame), "float", "", &vertical, -1.0, 1.0, false, requested_frame);
324  root["brightness"] = add_property_json("Brightness", brightness.GetValue(requested_frame), "float", "", &brightness, -1.0, 1.0, false, requested_frame);
325  root["contrast"] = add_property_json("Contrast", contrast.GetValue(requested_frame), "float", "", &contrast, 0.0, 20.0, false, requested_frame);
326 
327  return root.toStyledString();
328 }
329 
330 void Displace::Reader(ReaderBase *new_reader) {
331  if (map_reader == new_reader)
332  return;
333 
334  if (map_reader) {
335  map_reader->Close();
336  delete map_reader;
337  }
338 
339  map_reader = new_reader;
340  cached_single_map_image.reset();
341  cached_single_map_width = 0;
342  cached_single_map_height = 0;
343  if (map_reader)
344  map_reader->ParentClip(ParentClip());
345 }
346 
348  Reader(NULL);
349 }
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
Displace.h
Header file for Displace effect class.
openshot::EffectBase::info
EffectInfoStruct info
Information about the current effect.
Definition: EffectBase.h:110
openshot::ReaderBase::JsonValue
virtual Json::Value JsonValue() const =0
Generate Json::Value for this object.
Definition: ReaderBase.cpp:109
openshot::Displace::horizontal
Keyframe horizontal
Horizontal displacement amount as a percentage of image width.
Definition: Displace.h:55
openshot::Displace::JsonValue
Json::Value JsonValue() const override
Generate Json::Value for this object.
Definition: Displace.cpp:241
openshot::ReaderBase::GetFrame
virtual std::shared_ptr< openshot::Frame > GetFrame(int64_t number)=0
openshot::ReaderBase::Json
virtual std::string Json() const =0
Generate JSON string of this object.
openshot
This namespace is the default namespace for all code in the openshot library.
Definition: Compressor.h:28
openshot::EffectBase::ParentClip
openshot::ClipBase * ParentClip()
Parent clip object of this effect (which can be unparented and NULL)
Definition: EffectBase.cpp:549
openshot::ClipBase::add_property_choice_json
Json::Value add_property_choice_json(std::string name, int value, int selected_value) const
Generate JSON choice for a property (dropdown properties)
Definition: ClipBase.cpp:132
openshot::ZmqLogger::Log
void Log(std::string message)
Log message to all subscribers of this logger (if any)
Definition: ZmqLogger.cpp:103
openshot::EffectBase::JsonValue
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
Definition: EffectBase.cpp:96
openshot::ReaderBase::info
openshot::ReaderInfo info
Information about the current media file.
Definition: ReaderBase.h:90
Timeline.h
Header file for Timeline class.
openshot::EffectBase::CreateReaderFromJson
ReaderBase * CreateReaderFromJson(const Json::Value &reader_json) const
Create a reader instance from reader JSON.
Definition: EffectBase.cpp:277
openshot::Keyframe::SetJsonValue
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Definition: KeyFrame.cpp:372
openshot::Displace::PropertiesJSON
std::string PropertiesJSON(int64_t requested_frame) const override
Definition: Displace.cpp:307
openshot::ReaderInfo::has_video
bool has_video
Determines if this file has a video stream.
Definition: ReaderBase.h:40
openshot::Displace::~Displace
~Displace() override
Definition: Displace.cpp:347
openshot::Displace::contrast
Keyframe contrast
Contrast adjustment for the displacement map.
Definition: Displace.h:58
openshot::Fraction::ToDouble
double ToDouble() const
Return this fraction as a double (i.e. 1/2 = 0.5)
Definition: Fraction.cpp:40
openshot::Keyframe::JsonValue
Json::Value JsonValue() const
Generate Json::Value for this object.
Definition: KeyFrame.cpp:339
openshot::Displace::strength
Keyframe strength
Overall strength multiplier for the displacement effect.
Definition: Displace.h:54
openshot::ReaderInfo::video_length
int64_t video_length
The number of frames in the video stream.
Definition: ReaderBase.h:53
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:236
openshot::Displace::SetJson
void SetJson(const std::string value) override
Load JSON string into this object.
Definition: Displace.cpp:259
openshot::Fraction::num
int num
Numerator for the fraction.
Definition: Fraction.h:32
ZmqLogger.h
Header file for ZeroMQ-based Logger class.
openshot::Fraction::den
int den
Denominator for the fraction.
Definition: Fraction.h:33
openshot::Displace::SetJsonValue
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
Definition: Displace.cpp:272
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::Displace::Reader
ReaderBase * Reader()
Get the reader object of the displacement map.
Definition: Displace.h:104
openshot::ReaderBase::Open
virtual void Open()=0
Open the reader (and start consuming resources, such as images or video files)
openshot::Displace::vertical
Keyframe vertical
Vertical displacement amount as a percentage of image height.
Definition: Displace.h:56
openshot::ReaderBase::IsOpen
virtual bool IsOpen()=0
Determine if reader is open or closed.
openshot::InvalidJSON
Exception for invalid JSON.
Definition: Exceptions.h:223
openshot::Displace::brightness
Keyframe brightness
Brightness adjustment for the displacement map.
Definition: Displace.h:57
openshot::Timeline
This class represents a timeline.
Definition: Timeline.h:153
openshot::Displace::Displace
Displace()
Blank constructor, useful when using Json to load the effect properties.
Definition: Displace.cpp:66
openshot::EffectBase::InitEffectInfo
void InitEffectInfo()
Definition: EffectBase.cpp:37
openshot::EffectInfoStruct::has_audio
bool has_audio
Determines if this effect manipulates the audio of a frame.
Definition: EffectBase.h:44
openshot::ReaderInfo::has_single_image
bool has_single_image
Determines if this file only contains a single image.
Definition: ReaderBase.h:42
openshot::ZmqLogger::Instance
static ZmqLogger * Instance()
Create or get an instance of this logger singleton (invoke the class with this method)
Definition: ZmqLogger.cpp:35
openshot::EffectInfoStruct::class_name
std::string class_name
The class name of the effect.
Definition: EffectBase.h:39
ReaderBase.h
Header file for ReaderBase class.
openshot::EffectInfoStruct::description
std::string description
The description of this effect and what it does.
Definition: EffectBase.h:41
openshot::EffectInfoStruct::has_video
bool has_video
Determines if this effect manipulates the image of a frame.
Definition: EffectBase.h:43
openshot::Displace::GetFrame
std::shared_ptr< openshot::Frame > GetFrame(int64_t frame_number) override
This method is required for all derived classes of ClipBase, and returns a new openshot::Frame object...
Definition: Displace.h:80
openshot::ReaderInfo::fps
openshot::Fraction fps
Frames per second, as a fraction (i.e. 24/1 = 24 fps)
Definition: ReaderBase.h:48
openshot::ReaderBase
This abstract class is the base class, used by all readers in libopenshot.
Definition: ReaderBase.h:75
openshot::Displace::invert
bool invert
Invert the displacement map before converting it to offsets.
Definition: Displace.h:53
openshot::EffectInfoStruct::name
std::string name
The name of the effect.
Definition: EffectBase.h:40
openshot::ReaderBase::Close
virtual void Close()=0
Close the reader (and any resources it was consuming)
openshot::Displace::Json
std::string Json() const override
Generate JSON string of this object.
Definition: Displace.cpp:236
openshot::Displace::replace_image
bool replace_image
Replace the frame image with the processed displacement map for debugging.
Definition: Displace.h:52
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:139
openshot::Keyframe::GetValue
double GetValue(int64_t index) const
Get the value at a specific index.
Definition: KeyFrame.cpp:258
openshot::ReaderBase::ParentClip
openshot::ClipBase * ParentClip()
Parent clip object of this reader (which can be unparented and NULL)
Definition: ReaderBase.cpp:243