OpenShot Library | libopenshot 1.1.1
Loading...
Searching...
No Matches
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 "Logger.h"
19
20#include <array>
21#include <cmath>
22#include <omp.h>
23
24using namespace openshot;
25
26namespace {
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
72Displace::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)
80}
81
82// Init effect settings
83void 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
94std::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
164std::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
236std::string Displace::Json() const {
237 return JsonValue().toStyledString();
238}
239
240// Generate Json::Value for this object
241Json::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
259void 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
272void 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
307std::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
330void 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}
Header file for Displace effect class.
Header file for all Exception classes.
Header file for ReaderBase class.
Header file for Timeline class.
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:136
virtual openshot::TimelineBase * ParentTimeline()
Get the associated Timeline pointer (if any)
Definition ClipBase.h:94
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:100
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
Keyframe contrast
Contrast adjustment for the displacement map.
Definition Displace.h:58
Displace()
Blank constructor, useful when using Json to load the effect properties.
Definition Displace.cpp:66
Json::Value JsonValue() const override
Generate Json::Value for this object.
Definition Displace.cpp:241
bool invert
Invert the displacement map before converting it to offsets.
Definition Displace.h:53
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
Definition Displace.cpp:272
Keyframe vertical
Vertical displacement amount as a percentage of image height.
Definition Displace.h:56
~Displace() override
Definition Displace.cpp:347
Keyframe brightness
Brightness adjustment for the displacement map.
Definition Displace.h:57
std::string Json() const override
Generate JSON string of this object.
Definition Displace.cpp:236
void SetJson(const std::string value) override
Load JSON string into this object.
Definition Displace.cpp:259
Keyframe horizontal
Horizontal displacement amount as a percentage of image width.
Definition Displace.h:55
std::string PropertiesJSON(int64_t requested_frame) const override
Definition Displace.cpp:307
bool replace_image
Replace the frame image with the processed displacement map for debugging.
Definition Displace.h:52
ReaderBase * Reader()
Get the reader object of the displacement map.
Definition Displace.h:104
Keyframe strength
Overall strength multiplier for the displacement effect.
Definition Displace.h:54
ReaderBase * CreateReaderFromJson(const Json::Value &reader_json) const
Create a reader instance from reader JSON.
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
openshot::ClipBase * ParentClip()
Parent clip object of this effect (which can be unparented and NULL)
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:114
int num
Numerator for the fraction.
Definition Fraction.h:32
double ToDouble() const
Return this fraction as a double (i.e. 1/2 = 0.5)
Definition Fraction.cpp:40
int den
Denominator for the fraction.
Definition Fraction.h:33
Exception for invalid JSON.
Definition Exceptions.h:224
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
void Log(std::string message, Level level=LevelDebug)
Definition Logger.cpp:146
static Logger * Instance()
Definition Logger.cpp:55
This abstract class is the base class, used by all readers in libopenshot.
Definition ReaderBase.h:76
virtual bool IsOpen()=0
Determine if reader is open or closed.
openshot::ReaderInfo info
Information about the current media file.
Definition ReaderBase.h:91
virtual std::string Json() const =0
Generate JSON string of this object.
virtual Json::Value JsonValue() const =0
Generate Json::Value for this object.
virtual void Open()=0
Open the reader (and start consuming resources, such as images or video files)
virtual std::shared_ptr< openshot::Frame > GetFrame(int64_t number)=0
openshot::ClipBase * ParentClip()
Parent clip object of this reader (which can be unparented and NULL)
virtual void Close()=0
Close the reader (and any resources it was consuming)
This class represents a timeline.
Definition Timeline.h:154
This namespace is the default namespace for all code in the openshot library.
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:43
bool has_audio
Determines if this effect manipulates the audio of a frame.
Definition EffectBase.h:44
std::string class_name
The class name of the effect.
Definition EffectBase.h:39
std::string name
The name of the effect.
Definition EffectBase.h:40
std::string description
The description of this effect and what it does.
Definition EffectBase.h:41
bool has_single_image
Determines if this file only contains a single image.
Definition ReaderBase.h:42
openshot::Fraction fps
Frames per second, as a fraction (i.e. 24/1 = 24 fps)
Definition ReaderBase.h:48
int64_t video_length
The number of frames in the video stream.
Definition ReaderBase.h:53
bool has_video
Determines if this file has a video stream.
Definition ReaderBase.h:40