You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-06-08 22:17:28 -07:00
51ee44ab33
- 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
223 lines
7.0 KiB
C++
223 lines
7.0 KiB
C++
/**
|
|
* @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"));
|
|
}
|