OpenShot Library | libopenshot 0.5.0
Loading...
Searching...
No Matches
Sharpen.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
14#include "Sharpen.h"
15#include "Exceptions.h"
16#include <algorithm>
17#include <cmath>
18#include <vector>
19#include <omp.h>
20
21using namespace openshot;
22
23// Constructor with default keyframes
25 : amount(10.0)
26 , radius(3.0)
27 , threshold(0.0)
28 , mode(0)
29 , channel(1)
30{
31 init_effect_details();
32}
33
34// Constructor from keyframes
36 : amount(a)
37 , radius(r)
38 , threshold(t)
39 , mode(0)
40 , channel(1)
41{
42 init_effect_details();
43}
44
45// Initialize effect metadata
46void Sharpen::init_effect_details()
47{
49 info.class_name = "Sharpen";
50 info.name = "Sharpen";
51 info.description = "Boost edge contrast to make video details look crisper.";
52 info.has_audio = false;
53 info.has_video = true;
54}
55
56// Compute three box sizes to approximate a Gaussian of sigma
57static void boxes_for_gauss(double sigma, int b[3])
58{
59 const int n = 3;
60 double wi = std::sqrt((12.0 * sigma * sigma / n) + 1.0);
61 int wl = int(std::floor(wi));
62 if (!(wl & 1)) --wl;
63 int wu = wl + 2;
64 double mi = (12.0 * sigma * sigma - n*wl*wl - 4.0*n*wl - 3.0*n)
65 / (-4.0*wl - 4.0);
66 int m = int(std::round(mi));
67 for (int i = 0; i < n; ++i)
68 b[i] = i < m ? wl : wu;
69}
70
71// Blur one axis with an edge-replicate sliding window
72static void blur_axis(const QImage& src, QImage& dst, int r, bool vertical)
73{
74 if (r <= 0) {
75 dst = src.copy();
76 return;
77 }
78
79 int W = src.width();
80 int H = src.height();
81 int bpl = src.bytesPerLine();
82 const uchar* in = src.bits();
83 uchar* out = dst.bits();
84 int window = 2*r + 1;
85
86 if (!vertical) {
87 #pragma omp parallel for
88 for (int y = 0; y < H; ++y) {
89 const uchar* rowIn = in + y*bpl;
90 uchar* rowOut = out + y*bpl;
91 double sB = rowIn[0]*(r+1), sG = rowIn[1]*(r+1),
92 sR = rowIn[2]*(r+1), sA = rowIn[3]*(r+1);
93 for (int x = 1; x <= r; ++x) {
94 const uchar* p = rowIn + std::min(x, W-1)*4;
95 sB += p[0]; sG += p[1]; sR += p[2]; sA += p[3];
96 }
97 for (int x = 0; x < W; ++x) {
98 uchar* o = rowOut + x*4;
99 o[0] = uchar(sB / window + 0.5);
100 o[1] = uchar(sG / window + 0.5);
101 o[2] = uchar(sR / window + 0.5);
102 o[3] = uchar(sA / window + 0.5);
103
104 const uchar* addP = rowIn + std::min(x+r+1, W-1)*4;
105 const uchar* subP = rowIn + std::max(x-r, 0)*4;
106 sB += addP[0] - subP[0];
107 sG += addP[1] - subP[1];
108 sR += addP[2] - subP[2];
109 sA += addP[3] - subP[3];
110 }
111 }
112 }
113 else {
114 #pragma omp parallel for
115 for (int x = 0; x < W; ++x) {
116 double sB = 0, sG = 0, sR = 0, sA = 0;
117 const uchar* p0 = in + x*4;
118 sB = p0[0]*(r+1); sG = p0[1]*(r+1);
119 sR = p0[2]*(r+1); sA = p0[3]*(r+1);
120 for (int y = 1; y <= r; ++y) {
121 const uchar* p = in + std::min(y, H-1)*bpl + x*4;
122 sB += p[0]; sG += p[1]; sR += p[2]; sA += p[3];
123 }
124 for (int y = 0; y < H; ++y) {
125 uchar* o = out + y*bpl + x*4;
126 o[0] = uchar(sB / window + 0.5);
127 o[1] = uchar(sG / window + 0.5);
128 o[2] = uchar(sR / window + 0.5);
129 o[3] = uchar(sA / window + 0.5);
130
131 const uchar* addP = in + std::min(y+r+1, H-1)*bpl + x*4;
132 const uchar* subP = in + std::max(y-r, 0)*bpl + x*4;
133 sB += addP[0] - subP[0];
134 sG += addP[1] - subP[1];
135 sR += addP[2] - subP[2];
136 sA += addP[3] - subP[3];
137 }
138 }
139 }
140}
141
142// Wrapper to handle fractional radius by blending two integer passes
143static void box_blur(const QImage& src, QImage& dst, double rf, bool vertical)
144{
145 int r0 = int(std::floor(rf));
146 int r1 = r0 + 1;
147 double f = rf - r0;
148 if (f < 1e-4) {
149 blur_axis(src, dst, r0, vertical);
150 }
151 else {
152 QImage a(src.size(), QImage::Format_ARGB32);
153 QImage b(src.size(), QImage::Format_ARGB32);
154 blur_axis(src, a, r0, vertical);
155 blur_axis(src, b, r1, vertical);
156
157 int pixels = src.width() * src.height();
158 const uchar* pa = a.bits();
159 const uchar* pb = b.bits();
160 uchar* pd = dst.bits();
161 #pragma omp parallel for
162 for (int i = 0; i < pixels; ++i) {
163 for (int c = 0; c < 4; ++c) {
164 pd[i*4+c] = uchar((1.0 - f) * pa[i*4+c]
165 + f * pb[i*4+c]
166 + 0.5);
167 }
168 }
169 }
170}
171
172// Apply three sequential box blurs to approximate Gaussian
173static void gauss_blur(const QImage& src, QImage& dst, double sigma)
174{
175 int b[3];
176 boxes_for_gauss(sigma, b);
177 QImage t1(src.size(), QImage::Format_ARGB32);
178 QImage t2(src.size(), QImage::Format_ARGB32);
179
180 double r = 0.5 * (b[0] - 1);
181 box_blur(src , t1, r, false);
182 box_blur(t1, t2, r, true);
183
184 r = 0.5 * (b[1] - 1);
185 box_blur(t2, t1, r, false);
186 box_blur(t1, t2, r, true);
187
188 r = 0.5 * (b[2] - 1);
189 box_blur(t2, t1, r, false);
190 box_blur(t1, dst, r, true);
191}
192
193// Main frame processing
194std::shared_ptr<Frame> Sharpen::GetFrame(
195 std::shared_ptr<Frame> frame, int64_t frame_number)
196{
197 auto img = frame->GetImage();
198 if (!img || img->isNull())
199 return frame;
200 if (img->format() != QImage::Format_ARGB32)
201 *img = img->convertToFormat(QImage::Format_ARGB32);
202
203 int W = img->width();
204 int H = img->height();
205 if (W <= 0 || H <= 0)
206 return frame;
207
208 // Retrieve keyframe values
209 double amt = amount.GetValue(frame_number); // 0–40
210 double rpx = radius.GetValue(frame_number); // px
211 double thrUI = threshold.GetValue(frame_number); // 0–1
212
213 // Sigma scaled against 720p reference
214 double sigma = std::max(0.1, rpx * H / 720.0);
215
216 // Generate blurred image
217 QImage blur(W, H, QImage::Format_ARGB32);
218 gauss_blur(*img, blur, sigma);
219
220 // Precompute maximum luma difference for adaptive threshold
221 int bplS = img->bytesPerLine();
222 int bplB = blur.bytesPerLine();
223 uchar* sBits = img->bits();
224 uchar* bBits = blur.bits();
225
226 double maxDY = 0.0;
227 #pragma omp parallel for reduction(max:maxDY)
228 for (int y = 0; y < H; ++y) {
229 uchar* sRow = sBits + y * bplS;
230 uchar* bRow = bBits + y * bplB;
231 for (int x = 0; x < W; ++x) {
232 double dB = double(sRow[x*4+0]) - double(bRow[x*4+0]);
233 double dG = double(sRow[x*4+1]) - double(bRow[x*4+1]);
234 double dR = double(sRow[x*4+2]) - double(bRow[x*4+2]);
235 double dY = std::abs(0.114*dB + 0.587*dG + 0.299*dR);
236 maxDY = std::max(maxDY, dY);
237 }
238 }
239
240 // Compute actual threshold in luma units
241 double thr = thrUI * maxDY;
242
243 // Process pixels
244 #pragma omp parallel for
245 for (int y = 0; y < H; ++y) {
246 uchar* sRow = sBits + y * bplS;
247 uchar* bRow = bBits + y * bplB;
248 for (int x = 0; x < W; ++x) {
249 uchar* sp = sRow + x*4;
250 uchar* bp = bRow + x*4;
251
252 // Detail per channel
253 double dB = double(sp[0]) - double(bp[0]);
254 double dG = double(sp[1]) - double(bp[1]);
255 double dR = double(sp[2]) - double(bp[2]);
256 double dY = 0.114*dB + 0.587*dG + 0.299*dR;
257
258 // Skip if below adaptive threshold
259 if (std::abs(dY) < thr)
260 continue;
261
262 // Halo limiter
263 auto halo = [](double d) {
264 return (255.0 - std::abs(d)) / 255.0;
265 };
266
267 double outC[3];
268
269 if (mode == 1) {
270 // HighPass: base = blurred image
271 // detail = original – blurred
272 // no halo limiter
273
274 // precompute normalized luma weights
275 const double wB = 0.114, wG = 0.587, wR = 0.299;
276
277 if (channel == 1) {
278 // Luma only: add back luma detail weighted per channel
279 double lumaInc = amt * dY;
280 outC[0] = bp[0] + lumaInc * wB;
281 outC[1] = bp[1] + lumaInc * wG;
282 outC[2] = bp[2] + lumaInc * wR;
283 }
284 else if (channel == 2) {
285 // Chroma only: subtract luma from detail, add chroma back
286 double lumaDetail = dY;
287 double chromaB = dB - lumaDetail * wB;
288 double chromaG = dG - lumaDetail * wG;
289 double chromaR = dR - lumaDetail * wR;
290 outC[0] = bp[0] + amt * chromaB;
291 outC[1] = bp[1] + amt * chromaG;
292 outC[2] = bp[2] + amt * chromaR;
293 }
294 else {
295 // All channels: add full per-channel detail
296 outC[0] = bp[0] + amt * dB;
297 outC[1] = bp[1] + amt * dG;
298 outC[2] = bp[2] + amt * dR;
299 }
300 }
301 else {
302 // Unsharp-Mask: base = original + amt * detail * halo(detail)
303 if (channel == 1) {
304 // Luma only
305 double inc = amt * dY * halo(dY);
306 for (int c = 0; c < 3; ++c)
307 outC[c] = sp[c] + inc;
308 }
309 else if (channel == 2) {
310 // Chroma only
311 double l = dY;
312 double chroma[3] = { dB - l, dG - l, dR - l };
313 for (int c = 0; c < 3; ++c)
314 outC[c] = sp[c] + amt * chroma[c] * halo(chroma[c]);
315 }
316 else {
317 // All channels
318 outC[0] = sp[0] + amt * dB * halo(dB);
319 outC[1] = sp[1] + amt * dG * halo(dG);
320 outC[2] = sp[2] + amt * dR * halo(dR);
321 }
322 }
323
324 // Write back clamped
325 for (int c = 0; c < 3; ++c) {
326 sp[c] = uchar(std::clamp(outC[c], 0.0, 255.0) + 0.5);
327 }
328 }
329 }
330
331 return frame;
332}
333
334// JSON serialization
335std::string Sharpen::Json() const
336{
337 return JsonValue().toStyledString();
338}
339
340Json::Value Sharpen::JsonValue() const
341{
342 Json::Value root = EffectBase::JsonValue();
343 root["type"] = info.class_name;
344 root["amount"] = amount.JsonValue();
345 root["radius"] = radius.JsonValue();
346 root["threshold"] = threshold.JsonValue();
347 root["mode"] = mode;
348 root["channel"] = channel;
349 return root;
350}
351
352// JSON deserialization
353void Sharpen::SetJson(std::string value)
354{
355 auto root = openshot::stringToJson(value);
357}
358
359void Sharpen::SetJsonValue(Json::Value root)
360{
362 if (!root["amount"].isNull())
363 amount.SetJsonValue(root["amount"]);
364 if (!root["radius"].isNull())
365 radius.SetJsonValue(root["radius"]);
366 if (!root["threshold"].isNull())
367 threshold.SetJsonValue(root["threshold"]);
368 if (!root["mode"].isNull())
369 mode = root["mode"].asInt();
370 if (!root["channel"].isNull())
371 channel = root["channel"].asInt();
372}
373
374// UI property definitions
375std::string Sharpen::PropertiesJSON(int64_t t) const
376{
377 Json::Value root = BasePropertiesJSON(t);
378 root["amount"] = add_property_json(
379 "Amount", amount.GetValue(t), "float", "", &amount, 0, 40, false, t);
380 root["radius"] = add_property_json(
381 "Radius", radius.GetValue(t), "float", "pixels", &radius, 0, 10, false, t);
382 root["threshold"] = add_property_json(
383 "Threshold", threshold.GetValue(t), "float", "ratio", &threshold, 0, 1, false, t);
384 root["mode"] = add_property_json(
385 "Mode", mode, "int", "", nullptr, 0, 1, false, t);
386 root["mode"]["choices"].append(add_property_choice_json("UnsharpMask", 0, mode));
387 root["mode"]["choices"].append(add_property_choice_json("HighPassBlend", 1, mode));
388 root["channel"] = add_property_json(
389 "Channel", channel, "int", "", nullptr, 0, 2, false, t);
390 root["channel"]["choices"].append(add_property_choice_json("All", 0, channel));
391 root["channel"]["choices"].append(add_property_choice_json("Luma", 1, channel));
392 root["channel"]["choices"].append(add_property_choice_json("Chroma", 2, channel));
393 return root.toStyledString();
394}
Header file for all Exception classes.
Header file for Sharpen effect 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:132
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
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
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
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
int mode
Sharpening mode (0 = UnsharpMask, 1 = HighPassBlend)
Definition Sharpen.h:47
std::string Json() const override
Get and Set JSON methods.
Definition Sharpen.cpp:335
Keyframe radius
Radius of the blur used in sharpening (0 to 10 pixels for 1080p)
Definition Sharpen.h:41
std::shared_ptr< Frame > GetFrame(std::shared_ptr< Frame > frame, int64_t frame_number) override
This method is required for all derived classes of EffectBase, and returns a modified openshot::Frame...
Definition Sharpen.cpp:194
Json::Value JsonValue() const override
Generate Json::Value for this object.
Definition Sharpen.cpp:340
Sharpen()
Default constructor.
Definition Sharpen.cpp:24
void SetJson(const std::string value) override
Load JSON string into this object.
Definition Sharpen.cpp:353
int channel
Channel to apply sharpening to (0 = All, 1 = Luma, 2 = Chroma)
Definition Sharpen.h:50
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
Definition Sharpen.cpp:359
Keyframe amount
Amount of sharpening to apply (0 to 2)
Definition Sharpen.h:38
std::string PropertiesJSON(int64_t requested_frame) const override
Definition Sharpen.cpp:375
Keyframe threshold
Threshold for applying sharpening (0 to 1)
Definition Sharpen.h:44
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