You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-06-08 22:17:28 -07:00
Adding new Denoise effect
- Adds a new DenoiseImage video effect for reducing luma grain and color speckles with adaptive spatial filtering, conservative temporal blending, motion protection, and brightness response controls. - Added to openshot-benchmark - New unit tests
This commit is contained in:
@@ -223,6 +223,7 @@ typedef struct OpenShotByteBuffer {
|
||||
%include "QtPlayer.h"
|
||||
%include "QtTextReader.h"
|
||||
%include "KeyFrame.h"
|
||||
%include "AnimatedCurve.h"
|
||||
%include "RendererBase.h"
|
||||
%include "Settings.h"
|
||||
%include "TimelineBase.h"
|
||||
@@ -256,6 +257,7 @@ typedef struct OpenShotByteBuffer {
|
||||
%include "effects/Sharpen.h"
|
||||
%include "effects/Shift.h"
|
||||
%include "effects/SphericalProjection.cpp"
|
||||
%include "effects/DenoiseImage.h"
|
||||
%include "effects/Wave.h"
|
||||
#ifdef USE_OPENCV
|
||||
%include "effects/Stabilizer.h"
|
||||
|
||||
@@ -505,6 +505,7 @@ static int openshot_swig_is_qwidget(PyObject *obj) {
|
||||
%include "QtPlayer.h"
|
||||
%include "QtTextReader.h"
|
||||
%include "KeyFrame.h"
|
||||
%include "AnimatedCurve.h"
|
||||
%include "RendererBase.h"
|
||||
%include "Settings.h"
|
||||
%include "TimelineBase.h"
|
||||
@@ -544,6 +545,7 @@ static int openshot_swig_is_qwidget(PyObject *obj) {
|
||||
%include "effects/Sharpen.h"
|
||||
%include "effects/Shift.h"
|
||||
%include "effects/SphericalProjection.cpp"
|
||||
%include "effects/DenoiseImage.h"
|
||||
%include "effects/Wave.h"
|
||||
#ifdef USE_OPENCV
|
||||
%include "effects/Stabilizer.h"
|
||||
|
||||
@@ -251,6 +251,7 @@ typedef struct OpenShotByteBuffer {
|
||||
%include "QtPlayer.h"
|
||||
%include "QtTextReader.h"
|
||||
%include "KeyFrame.h"
|
||||
%include "AnimatedCurve.h"
|
||||
%include "RendererBase.h"
|
||||
%include "Settings.h"
|
||||
%include "TimelineBase.h"
|
||||
@@ -281,5 +282,5 @@ typedef struct OpenShotByteBuffer {
|
||||
%include "effects/Pixelate.h"
|
||||
%include "effects/Saturation.h"
|
||||
%include "effects/Shift.h"
|
||||
%include "effects/DenoiseImage.h"
|
||||
%include "effects/Wave.h"
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ set(EFFECTS_SOURCES
|
||||
effects/Shadow.cpp
|
||||
effects/Shift.cpp
|
||||
effects/SphericalProjection.cpp
|
||||
effects/DenoiseImage.cpp
|
||||
effects/Wave.cpp
|
||||
audio_effects/STFT.cpp
|
||||
audio_effects/Noise.cpp
|
||||
|
||||
@@ -104,6 +104,9 @@ EffectBase* EffectInfo::CreateEffect(std::string effect_type) {
|
||||
else if (effect_type == "SphericalProjection")
|
||||
return new SphericalProjection();
|
||||
|
||||
else if (effect_type == "DenoiseImage")
|
||||
return new DenoiseImage();
|
||||
|
||||
else if (effect_type == "Wave")
|
||||
return new Wave();
|
||||
|
||||
@@ -184,6 +187,7 @@ Json::Value EffectInfo::JsonValue() {
|
||||
root.append(Shadow().JsonInfo());
|
||||
root.append(Shift().JsonInfo());
|
||||
root.append(SphericalProjection().JsonInfo());
|
||||
root.append(DenoiseImage().JsonInfo());
|
||||
root.append(Wave().JsonInfo());
|
||||
/* Audio */
|
||||
root.append(Noise().JsonInfo());
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "effects/Shadow.h"
|
||||
#include "effects/SphericalProjection.h"
|
||||
#include "effects/Shift.h"
|
||||
#include "effects/DenoiseImage.h"
|
||||
#include "effects/Wave.h"
|
||||
|
||||
/* Audio Effects */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief Header file for DenoiseImage effect
|
||||
* @author Jonathan Thomas <jonathan@openshot.org>
|
||||
*
|
||||
* @ref License
|
||||
*/
|
||||
|
||||
// Copyright (c) 2008-2026 OpenShot Studios, LLC
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
#ifndef OPENSHOT_DENOISE_IMAGE_EFFECT_H
|
||||
#define OPENSHOT_DENOISE_IMAGE_EFFECT_H
|
||||
|
||||
#include "../AnimatedCurve.h"
|
||||
#include "../EffectBase.h"
|
||||
#include "../Frame.h"
|
||||
#include "../Json.h"
|
||||
#include "../KeyFrame.h"
|
||||
|
||||
#include <QImage>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace openshot
|
||||
{
|
||||
class DenoiseImage : public EffectBase
|
||||
{
|
||||
private:
|
||||
QImage previous_input_;
|
||||
int64_t last_frame_;
|
||||
|
||||
void init_effect_details();
|
||||
void reset_temporal_history();
|
||||
|
||||
public:
|
||||
Keyframe strength;
|
||||
Keyframe detail;
|
||||
Keyframe temporal;
|
||||
Keyframe motion_safety;
|
||||
Keyframe color_noise;
|
||||
AnimatedCurve response_curve;
|
||||
|
||||
DenoiseImage();
|
||||
DenoiseImage(Keyframe new_strength, Keyframe new_detail, Keyframe new_temporal,
|
||||
Keyframe new_motion_safety, Keyframe new_color_noise);
|
||||
|
||||
std::shared_ptr<openshot::Frame> GetFrame(int64_t frame_number) override {
|
||||
return GetFrame(std::make_shared<openshot::Frame>(), frame_number);
|
||||
}
|
||||
std::shared_ptr<openshot::Frame> GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number) override;
|
||||
|
||||
std::string Json() const override;
|
||||
Json::Value JsonValue() const override;
|
||||
void SetJson(const std::string value) override;
|
||||
void SetJsonValue(const Json::Value root) override;
|
||||
std::string PropertiesJSON(int64_t requested_frame) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -92,6 +92,10 @@ extern "C" {
|
||||
# include "effects/Sharpen.h"
|
||||
# define OPENSHOT_HAS_SHARPEN
|
||||
#endif
|
||||
#if __has_include("effects/DenoiseImage.h")
|
||||
# include "effects/DenoiseImage.h"
|
||||
# define OPENSHOT_HAS_DENOISEIMAGE
|
||||
#endif
|
||||
#if __has_include("effects/Hue.h")
|
||||
# include "effects/Hue.h"
|
||||
# define OPENSHOT_HAS_HUE
|
||||
@@ -602,6 +606,21 @@ int main(int argc, char* argv[]) {
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifdef OPENSHOT_HAS_DENOISEIMAGE
|
||||
trials.emplace_back("Effect_DenoiseImage", [&]() -> TrialResult {
|
||||
FFmpegReader r(video);
|
||||
r.Open();
|
||||
Clip clip(&r);
|
||||
clip.Open();
|
||||
DenoiseImage d;
|
||||
clip.AddEffect(&d);
|
||||
TrialResult result = timed_read(clip);
|
||||
clip.Close();
|
||||
r.Close();
|
||||
return result;
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifdef OPENSHOT_HAS_HUE
|
||||
trials.emplace_back("Effect_Hue", [&]() -> TrialResult {
|
||||
FFmpegReader r(video);
|
||||
|
||||
@@ -69,6 +69,7 @@ set(OPENSHOT_TESTS
|
||||
Sharpen
|
||||
Shadow
|
||||
SphericalEffect
|
||||
DenoiseImage
|
||||
WaveEffect
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief Unit tests for DenoiseImage effect
|
||||
*
|
||||
* @ref License
|
||||
*/
|
||||
|
||||
// Copyright (c) 2008-2026 OpenShot Studios, LLC
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
|
||||
#include <QColor>
|
||||
#include <QImage>
|
||||
|
||||
#include "EffectInfo.h"
|
||||
#include "Frame.h"
|
||||
#include "effects/DenoiseImage.h"
|
||||
#include "openshot_catch.h"
|
||||
|
||||
using namespace openshot;
|
||||
|
||||
static std::shared_ptr<Frame> makeDenoiseFrame()
|
||||
{
|
||||
QImage img(5, 5, QImage::Format_ARGB32);
|
||||
img.fill(QColor(42, 42, 42, 255));
|
||||
img.setPixelColor(2, 2, QColor(84, 20, 90, 128));
|
||||
auto frame = std::make_shared<Frame>();
|
||||
*frame->GetImage() = img;
|
||||
return frame;
|
||||
}
|
||||
|
||||
static std::shared_ptr<Frame> makeFlatFrame(const QColor& color)
|
||||
{
|
||||
QImage img(5, 5, QImage::Format_ARGB32);
|
||||
img.fill(color);
|
||||
auto frame = std::make_shared<Frame>();
|
||||
*frame->GetImage() = img;
|
||||
return frame;
|
||||
}
|
||||
|
||||
static std::shared_ptr<Frame> makeNativeDenoiseFrame()
|
||||
{
|
||||
QImage img(5, 5, QImage::Format_RGBA8888_Premultiplied);
|
||||
img.fill(QColor(42, 42, 42, 255));
|
||||
img.setPixelColor(2, 2, QColor(84, 20, 90, 255));
|
||||
auto frame = std::make_shared<Frame>();
|
||||
frame->AddImage(std::make_shared<QImage>(img));
|
||||
return frame;
|
||||
}
|
||||
|
||||
static AnimatedCurve makeShadowOnlyCurve()
|
||||
{
|
||||
AnimatedCurve curve;
|
||||
curve.Nodes().clear();
|
||||
curve.Nodes().emplace_back(0, 0.0, 1.0, LINEAR);
|
||||
curve.Nodes().emplace_back(1, 0.5, 0.0, LINEAR);
|
||||
curve.Nodes().emplace_back(2, 1.0, 0.0, LINEAR);
|
||||
return curve;
|
||||
}
|
||||
|
||||
TEST_CASE("DenoiseImage zero strength leaves pixels unchanged", "[effect][denoiseimage]")
|
||||
{
|
||||
DenoiseImage effect;
|
||||
effect.strength = Keyframe(0.0);
|
||||
|
||||
auto frame = makeDenoiseFrame();
|
||||
const QColor before = frame->GetImage()->pixelColor(2, 2);
|
||||
auto out = effect.GetFrame(frame, 1);
|
||||
const QColor after = out->GetImage()->pixelColor(2, 2);
|
||||
|
||||
CHECK(after == before);
|
||||
}
|
||||
|
||||
TEST_CASE("DenoiseImage preserves alpha while reducing color speckles", "[effect][denoiseimage]")
|
||||
{
|
||||
DenoiseImage effect;
|
||||
effect.strength = Keyframe(1.0);
|
||||
effect.detail = Keyframe(0.0);
|
||||
effect.temporal = Keyframe(0.0);
|
||||
effect.color_noise = Keyframe(1.0);
|
||||
|
||||
auto frame = makeDenoiseFrame();
|
||||
const QColor before = frame->GetImage()->pixelColor(2, 2);
|
||||
auto out = effect.GetFrame(frame, 1);
|
||||
const QColor after = out->GetImage()->pixelColor(2, 2);
|
||||
|
||||
CHECK(after.alpha() == before.alpha());
|
||||
const int before_delta = std::abs(before.red() - 42) +
|
||||
std::abs(before.green() - 42) +
|
||||
std::abs(before.blue() - 42);
|
||||
const int after_delta = std::abs(after.red() - 42) +
|
||||
std::abs(after.green() - 42) +
|
||||
std::abs(after.blue() - 42);
|
||||
CHECK(after_delta < before_delta);
|
||||
}
|
||||
|
||||
TEST_CASE("DenoiseImage default settings visibly reduce isolated speckles", "[effect][denoiseimage]")
|
||||
{
|
||||
DenoiseImage effect;
|
||||
|
||||
auto frame = makeDenoiseFrame();
|
||||
const QColor before = frame->GetImage()->pixelColor(2, 2);
|
||||
auto out = effect.GetFrame(frame, 1);
|
||||
const QColor after = out->GetImage()->pixelColor(2, 2);
|
||||
|
||||
const int before_delta = std::abs(before.red() - 42) +
|
||||
std::abs(before.green() - 42) +
|
||||
std::abs(before.blue() - 42);
|
||||
const int after_delta = std::abs(after.red() - 42) +
|
||||
std::abs(after.green() - 42) +
|
||||
std::abs(after.blue() - 42);
|
||||
|
||||
CHECK(after_delta < before_delta);
|
||||
}
|
||||
|
||||
TEST_CASE("DenoiseImage changes native OpenShot frame format through ProcessFrame", "[effect][denoiseimage][integration]")
|
||||
{
|
||||
std::unique_ptr<EffectBase> effect(EffectInfo().CreateEffect("DenoiseImage"));
|
||||
REQUIRE(effect != nullptr);
|
||||
|
||||
auto frame = makeNativeDenoiseFrame();
|
||||
REQUIRE(frame->GetImage()->format() == QImage::Format_RGBA8888_Premultiplied);
|
||||
const QColor before = frame->GetImage()->pixelColor(2, 2);
|
||||
|
||||
auto out = effect->ProcessFrame(frame, 1);
|
||||
const QColor after = out->GetImage()->pixelColor(2, 2);
|
||||
|
||||
CHECK(out->GetImage()->format() == QImage::Format_RGBA8888_Premultiplied);
|
||||
CHECK(after != before);
|
||||
CHECK(std::abs(after.red() - 42) < std::abs(before.red() - 42));
|
||||
}
|
||||
|
||||
TEST_CASE("DenoiseImage response curve favors shadows", "[effect][denoiseimage][curve]")
|
||||
{
|
||||
DenoiseImage effect;
|
||||
effect.strength = Keyframe(1.0);
|
||||
effect.detail = Keyframe(0.0);
|
||||
effect.temporal = Keyframe(0.0);
|
||||
effect.color_noise = Keyframe(1.0);
|
||||
effect.response_curve = makeShadowOnlyCurve();
|
||||
|
||||
QImage img(5, 5, QImage::Format_ARGB32);
|
||||
img.fill(QColor(40, 40, 40, 255));
|
||||
img.setPixelColor(2, 2, QColor(90, 10, 90, 255));
|
||||
img.setPixelColor(4, 4, QColor(240, 240, 240, 255));
|
||||
img.setPixelColor(3, 3, QColor(255, 210, 255, 255));
|
||||
auto frame = std::make_shared<Frame>();
|
||||
*frame->GetImage() = img;
|
||||
|
||||
const QColor shadow_before = frame->GetImage()->pixelColor(2, 2);
|
||||
const QColor highlight_before = frame->GetImage()->pixelColor(3, 3);
|
||||
auto out = effect.GetFrame(frame, 1);
|
||||
const QColor shadow_after = out->GetImage()->pixelColor(2, 2);
|
||||
const QColor highlight_after = out->GetImage()->pixelColor(3, 3);
|
||||
|
||||
const int shadow_delta = std::abs(shadow_after.red() - shadow_before.red()) +
|
||||
std::abs(shadow_after.green() - shadow_before.green()) +
|
||||
std::abs(shadow_after.blue() - shadow_before.blue());
|
||||
const int highlight_delta = std::abs(highlight_after.red() - highlight_before.red()) +
|
||||
std::abs(highlight_after.green() - highlight_before.green()) +
|
||||
std::abs(highlight_after.blue() - highlight_before.blue());
|
||||
|
||||
CHECK(shadow_delta > highlight_delta);
|
||||
}
|
||||
|
||||
TEST_CASE("DenoiseImage temporal only applies to sequential frames", "[effect][denoiseimage][temporal]")
|
||||
{
|
||||
DenoiseImage effect;
|
||||
effect.strength = Keyframe(1.0);
|
||||
effect.detail = Keyframe(0.0);
|
||||
effect.temporal = Keyframe(1.0);
|
||||
effect.motion_safety = Keyframe(1.0);
|
||||
effect.color_noise = Keyframe(1.0);
|
||||
|
||||
auto first = makeFlatFrame(QColor(80, 80, 80, 255));
|
||||
effect.GetFrame(first, 1);
|
||||
|
||||
auto sequential = makeFlatFrame(QColor(65, 65, 65, 255));
|
||||
auto sequential_out = effect.GetFrame(sequential, 2);
|
||||
const QColor sequential_color = sequential_out->GetImage()->pixelColor(2, 2);
|
||||
|
||||
auto jumped = makeFlatFrame(QColor(65, 65, 65, 255));
|
||||
auto jumped_out = effect.GetFrame(jumped, 10);
|
||||
const QColor jumped_color = jumped_out->GetImage()->pixelColor(2, 2);
|
||||
|
||||
CHECK(sequential_color.red() > jumped_color.red());
|
||||
}
|
||||
|
||||
TEST_CASE("DenoiseImage JSON and PropertiesJSON expose V1 controls", "[effect][denoiseimage][json]")
|
||||
{
|
||||
DenoiseImage effect;
|
||||
effect.strength = Keyframe(0.25);
|
||||
effect.detail = Keyframe(0.75);
|
||||
effect.temporal = Keyframe(0.2);
|
||||
effect.motion_safety = Keyframe(0.8);
|
||||
effect.color_noise = Keyframe(0.9);
|
||||
effect.response_curve = makeShadowOnlyCurve();
|
||||
|
||||
DenoiseImage copy;
|
||||
copy.SetJson(effect.Json());
|
||||
|
||||
CHECK(copy.strength.GetValue(1) == Approx(0.25));
|
||||
CHECK(copy.detail.GetValue(1) == Approx(0.75));
|
||||
CHECK(copy.temporal.GetValue(1) == Approx(0.2));
|
||||
CHECK(copy.motion_safety.GetValue(1) == Approx(0.8));
|
||||
CHECK(copy.color_noise.GetValue(1) == Approx(0.9));
|
||||
CHECK(copy.response_curve.Nodes().size() == 3);
|
||||
|
||||
std::istringstream props(effect.PropertiesJSON(1));
|
||||
Json::CharReaderBuilder rb;
|
||||
Json::Value root;
|
||||
std::string errs;
|
||||
REQUIRE(Json::parseFromStream(rb, props, &root, &errs));
|
||||
CHECK(root["strength"]["name"].asString() == "Strength");
|
||||
CHECK(root["color_noise"]["name"].asString() == "Color Noise");
|
||||
CHECK(root["response_curve"]["type"].asString() == "colorgrade_curve");
|
||||
CHECK(root["response_curve"].isMember("curve"));
|
||||
}
|
||||
Reference in New Issue
Block a user