You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-03-02 08:53:52 -08:00
Merge branch 'develop' into lens-flare
This commit is contained in:
@@ -166,6 +166,7 @@
|
||||
%include "effects/Brightness.h"
|
||||
%include "effects/Caption.h"
|
||||
%include "effects/ChromaKey.h"
|
||||
%include "effects/ColorMap.h"
|
||||
%include "effects/ColorShift.h"
|
||||
%include "effects/Crop.h"
|
||||
%include "effects/Deinterlace.h"
|
||||
@@ -175,7 +176,13 @@
|
||||
%include "effects/Negate.h"
|
||||
%include "effects/Pixelate.h"
|
||||
%include "effects/Saturation.h"
|
||||
%include "effects/Sharpen.h"
|
||||
%include "effects/Shift.h"
|
||||
%include "effects/SphericalProjection.cpp"
|
||||
%include "effects/Wave.h"
|
||||
|
||||
|
||||
#ifdef USE_OPENCV
|
||||
%include "effects/Stabilizer.h"
|
||||
%include "effects/Tracker.h"
|
||||
%include "effects/ObjectDetection.h"
|
||||
%include "effects/Outline.h"
|
||||
#endif
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
%include "effects/Brightness.h"
|
||||
%include "effects/Caption.h"
|
||||
%include "effects/ChromaKey.h"
|
||||
%include "effects/ColorMap.h"
|
||||
%include "effects/ColorShift.h"
|
||||
%include "effects/Crop.h"
|
||||
%include "effects/Deinterlace.h"
|
||||
@@ -347,7 +348,9 @@
|
||||
%include "effects/Negate.h"
|
||||
%include "effects/Pixelate.h"
|
||||
%include "effects/Saturation.h"
|
||||
%include "effects/Sharpen.h"
|
||||
%include "effects/Shift.h"
|
||||
%include "effects/SphericalProjection.cpp"
|
||||
%include "effects/Wave.h"
|
||||
#ifdef USE_OPENCV
|
||||
%include "effects/Stabilizer.h"
|
||||
|
||||
35940
examples/example-lut.cube
Normal file
35940
examples/example-lut.cube
Normal file
File diff suppressed because it is too large
Load Diff
@@ -111,6 +111,7 @@ set(EFFECTS_SOURCES
|
||||
effects/Brightness.cpp
|
||||
effects/Caption.cpp
|
||||
effects/ChromaKey.cpp
|
||||
effects/ColorMap.cpp
|
||||
effects/ColorShift.cpp
|
||||
effects/Crop.cpp
|
||||
effects/Deinterlace.cpp
|
||||
|
||||
@@ -40,6 +40,9 @@ EffectBase* EffectInfo::CreateEffect(std::string effect_type) {
|
||||
else if (effect_type == "ChromaKey")
|
||||
return new ChromaKey();
|
||||
|
||||
else if (effect_type == "ColorMap")
|
||||
return new ColorMap();
|
||||
|
||||
else if (effect_type == "ColorShift")
|
||||
return new ColorShift();
|
||||
|
||||
@@ -135,6 +138,7 @@ Json::Value EffectInfo::JsonValue() {
|
||||
root.append(Brightness().JsonInfo());
|
||||
root.append(Caption().JsonInfo());
|
||||
root.append(ChromaKey().JsonInfo());
|
||||
root.append(ColorMap().JsonInfo());
|
||||
root.append(ColorShift().JsonInfo());
|
||||
root.append(Crop().JsonInfo());
|
||||
root.append(Deinterlace().JsonInfo());
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "effects/Brightness.h"
|
||||
#include "effects/Caption.h"
|
||||
#include "effects/ChromaKey.h"
|
||||
#include "effects/ColorMap.h"
|
||||
#include "effects/ColorShift.h"
|
||||
#include "effects/Crop.h"
|
||||
#include "effects/Deinterlace.h"
|
||||
|
||||
307
src/effects/ColorMap.cpp
Normal file
307
src/effects/ColorMap.cpp
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief Source file for ColorMap (LUT) effect
|
||||
* @author Jonathan Thomas <jonathan@openshot.org>
|
||||
*
|
||||
* @ref License
|
||||
*/
|
||||
|
||||
// Copyright (c) 2008-2025 OpenShot Studios, LLC
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
#include "ColorMap.h"
|
||||
#include "Exceptions.h"
|
||||
#include <omp.h>
|
||||
#include <QRegularExpression>
|
||||
|
||||
using namespace openshot;
|
||||
|
||||
void ColorMap::load_cube_file()
|
||||
{
|
||||
if (lut_path.empty()) {
|
||||
lut_data.clear();
|
||||
lut_size = 0;
|
||||
needs_refresh = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int parsed_size = 0;
|
||||
std::vector<float> parsed_data;
|
||||
|
||||
#pragma omp critical(load_lut)
|
||||
{
|
||||
QFile file(QString::fromStdString(lut_path));
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
// leave parsed_size == 0
|
||||
} else {
|
||||
QTextStream in(&file);
|
||||
QString line;
|
||||
QRegularExpression ws_re("\\s+");
|
||||
|
||||
// 1) Find LUT_3D_SIZE
|
||||
while (!in.atEnd()) {
|
||||
line = in.readLine().trimmed();
|
||||
if (line.startsWith("LUT_3D_SIZE")) {
|
||||
auto parts = line.split(ws_re);
|
||||
if (parts.size() >= 2) {
|
||||
parsed_size = parts[1].toInt();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Read N³ lines of R G B floats
|
||||
if (parsed_size > 0) {
|
||||
int total = parsed_size * parsed_size * parsed_size;
|
||||
parsed_data.reserve(size_t(total * 3));
|
||||
while (!in.atEnd() && int(parsed_data.size()) < total * 3) {
|
||||
line = in.readLine().trimmed();
|
||||
if (line.isEmpty() ||
|
||||
line.startsWith("#") ||
|
||||
line.startsWith("TITLE") ||
|
||||
line.startsWith("DOMAIN"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto vals = line.split(ws_re);
|
||||
if (vals.size() >= 3) {
|
||||
// .cube file is R G B
|
||||
parsed_data.push_back(vals[0].toFloat());
|
||||
parsed_data.push_back(vals[1].toFloat());
|
||||
parsed_data.push_back(vals[2].toFloat());
|
||||
}
|
||||
}
|
||||
if (int(parsed_data.size()) != total * 3) {
|
||||
parsed_data.clear();
|
||||
parsed_size = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed_size > 0) {
|
||||
lut_size = parsed_size;
|
||||
lut_data.swap(parsed_data);
|
||||
} else {
|
||||
lut_data.clear();
|
||||
lut_size = 0;
|
||||
}
|
||||
needs_refresh = false;
|
||||
}
|
||||
|
||||
void ColorMap::init_effect_details()
|
||||
{
|
||||
InitEffectInfo();
|
||||
info.class_name = "ColorMap";
|
||||
info.name = "Color Map / Lookup";
|
||||
info.description = "Adjust colors using 3D LUT lookup tables (.cube format)";
|
||||
info.has_video = true;
|
||||
info.has_audio = false;
|
||||
}
|
||||
|
||||
ColorMap::ColorMap()
|
||||
: lut_path(""), lut_size(0), needs_refresh(true),
|
||||
intensity(1.0), intensity_r(1.0), intensity_g(1.0), intensity_b(1.0)
|
||||
{
|
||||
init_effect_details();
|
||||
load_cube_file();
|
||||
}
|
||||
|
||||
ColorMap::ColorMap(const std::string &path,
|
||||
const Keyframe &i,
|
||||
const Keyframe &iR,
|
||||
const Keyframe &iG,
|
||||
const Keyframe &iB)
|
||||
: lut_path(path),
|
||||
lut_size(0),
|
||||
needs_refresh(true),
|
||||
intensity(i),
|
||||
intensity_r(iR),
|
||||
intensity_g(iG),
|
||||
intensity_b(iB)
|
||||
{
|
||||
init_effect_details();
|
||||
load_cube_file();
|
||||
}
|
||||
|
||||
std::shared_ptr<openshot::Frame>
|
||||
ColorMap::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
|
||||
{
|
||||
// Reload LUT when its path changed; no locking here
|
||||
if (needs_refresh) {
|
||||
load_cube_file();
|
||||
needs_refresh = false;
|
||||
}
|
||||
|
||||
if (lut_data.empty())
|
||||
return frame;
|
||||
|
||||
auto image = frame->GetImage();
|
||||
int w = image->width(), h = image->height();
|
||||
unsigned char *pixels = image->bits();
|
||||
|
||||
float overall = float(intensity.GetValue(frame_number));
|
||||
float tR = float(intensity_r.GetValue(frame_number)) * overall;
|
||||
float tG = float(intensity_g.GetValue(frame_number)) * overall;
|
||||
float tB = float(intensity_b.GetValue(frame_number)) * overall;
|
||||
|
||||
int pixel_count = w * h;
|
||||
#pragma omp parallel for
|
||||
for (int i = 0; i < pixel_count; ++i) {
|
||||
int idx = i * 4;
|
||||
int A = pixels[idx + 3];
|
||||
float alpha = A / 255.0f;
|
||||
if (alpha == 0.0f) continue;
|
||||
|
||||
// demultiply premultiplied RGBA
|
||||
float R = pixels[idx + 0] / alpha;
|
||||
float G = pixels[idx + 1] / alpha;
|
||||
float B = pixels[idx + 2] / alpha;
|
||||
|
||||
// normalize to [0,1]
|
||||
float Rn = R * (1.0f / 255.0f);
|
||||
float Gn = G * (1.0f / 255.0f);
|
||||
float Bn = B * (1.0f / 255.0f);
|
||||
|
||||
// map into LUT space [0 .. size-1]
|
||||
float rf = Rn * (lut_size - 1);
|
||||
float gf = Gn * (lut_size - 1);
|
||||
float bf = Bn * (lut_size - 1);
|
||||
|
||||
int r0 = int(floor(rf)), r1 = std::min(r0 + 1, lut_size - 1);
|
||||
int g0 = int(floor(gf)), g1 = std::min(g0 + 1, lut_size - 1);
|
||||
int b0 = int(floor(bf)), b1 = std::min(b0 + 1, lut_size - 1);
|
||||
|
||||
float dr = rf - r0;
|
||||
float dg = gf - g0;
|
||||
float db = bf - b0;
|
||||
|
||||
// compute base offsets with red fastest, then green, then blue
|
||||
int base000 = ((b0 * lut_size + g0) * lut_size + r0) * 3;
|
||||
int base100 = ((b0 * lut_size + g0) * lut_size + r1) * 3;
|
||||
int base010 = ((b0 * lut_size + g1) * lut_size + r0) * 3;
|
||||
int base110 = ((b0 * lut_size + g1) * lut_size + r1) * 3;
|
||||
int base001 = ((b1 * lut_size + g0) * lut_size + r0) * 3;
|
||||
int base101 = ((b1 * lut_size + g0) * lut_size + r1) * 3;
|
||||
int base011 = ((b1 * lut_size + g1) * lut_size + r0) * 3;
|
||||
int base111 = ((b1 * lut_size + g1) * lut_size + r1) * 3;
|
||||
|
||||
// trilinear interpolation
|
||||
// red
|
||||
float c00 = lut_data[base000 + 0] * (1 - dr) + lut_data[base100 + 0] * dr;
|
||||
float c01 = lut_data[base001 + 0] * (1 - dr) + lut_data[base101 + 0] * dr;
|
||||
float c10 = lut_data[base010 + 0] * (1 - dr) + lut_data[base110 + 0] * dr;
|
||||
float c11 = lut_data[base011 + 0] * (1 - dr) + lut_data[base111 + 0] * dr;
|
||||
float c0 = c00 * (1 - dg) + c10 * dg;
|
||||
float c1 = c01 * (1 - dg) + c11 * dg;
|
||||
float lr = c0 * (1 - db) + c1 * db;
|
||||
|
||||
// green
|
||||
c00 = lut_data[base000 + 1] * (1 - dr) + lut_data[base100 + 1] * dr;
|
||||
c01 = lut_data[base001 + 1] * (1 - dr) + lut_data[base101 + 1] * dr;
|
||||
c10 = lut_data[base010 + 1] * (1 - dr) + lut_data[base110 + 1] * dr;
|
||||
c11 = lut_data[base011 + 1] * (1 - dr) + lut_data[base111 + 1] * dr;
|
||||
c0 = c00 * (1 - dg) + c10 * dg;
|
||||
c1 = c01 * (1 - dg) + c11 * dg;
|
||||
float lg = c0 * (1 - db) + c1 * db;
|
||||
|
||||
// blue
|
||||
c00 = lut_data[base000 + 2] * (1 - dr) + lut_data[base100 + 2] * dr;
|
||||
c01 = lut_data[base001 + 2] * (1 - dr) + lut_data[base101 + 2] * dr;
|
||||
c10 = lut_data[base010 + 2] * (1 - dr) + lut_data[base110 + 2] * dr;
|
||||
c11 = lut_data[base011 + 2] * (1 - dr) + lut_data[base111 + 2] * dr;
|
||||
c0 = c00 * (1 - dg) + c10 * dg;
|
||||
c1 = c01 * (1 - dg) + c11 * dg;
|
||||
float lb = c0 * (1 - db) + c1 * db;
|
||||
|
||||
// blend per-channel, re-premultiply alpha
|
||||
float outR = (lr * tR + Rn * (1 - tR)) * alpha;
|
||||
float outG = (lg * tG + Gn * (1 - tG)) * alpha;
|
||||
float outB = (lb * tB + Bn * (1 - tB)) * alpha;
|
||||
|
||||
pixels[idx + 0] = constrain(outR * 255.0f);
|
||||
pixels[idx + 1] = constrain(outG * 255.0f);
|
||||
pixels[idx + 2] = constrain(outB * 255.0f);
|
||||
// alpha left unchanged
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
||||
std::string ColorMap::Json() const
|
||||
{
|
||||
return JsonValue().toStyledString();
|
||||
}
|
||||
|
||||
Json::Value ColorMap::JsonValue() const
|
||||
{
|
||||
Json::Value root = EffectBase::JsonValue();
|
||||
root["type"] = info.class_name;
|
||||
root["lut_path"] = lut_path;
|
||||
root["intensity"] = intensity.JsonValue();
|
||||
root["intensity_r"] = intensity_r.JsonValue();
|
||||
root["intensity_g"] = intensity_g.JsonValue();
|
||||
root["intensity_b"] = intensity_b.JsonValue();
|
||||
return root;
|
||||
}
|
||||
|
||||
void ColorMap::SetJson(const std::string value)
|
||||
{
|
||||
try {
|
||||
const Json::Value root = openshot::stringToJson(value);
|
||||
SetJsonValue(root);
|
||||
}
|
||||
catch (...) {
|
||||
throw InvalidJSON("Invalid JSON for ColorMap effect");
|
||||
}
|
||||
}
|
||||
|
||||
void ColorMap::SetJsonValue(const Json::Value root)
|
||||
{
|
||||
EffectBase::SetJsonValue(root);
|
||||
if (!root["lut_path"].isNull())
|
||||
{
|
||||
lut_path = root["lut_path"].asString();
|
||||
needs_refresh = true;
|
||||
}
|
||||
if (!root["intensity"].isNull())
|
||||
intensity.SetJsonValue(root["intensity"]);
|
||||
if (!root["intensity_r"].isNull())
|
||||
intensity_r.SetJsonValue(root["intensity_r"]);
|
||||
if (!root["intensity_g"].isNull())
|
||||
intensity_g.SetJsonValue(root["intensity_g"]);
|
||||
if (!root["intensity_b"].isNull())
|
||||
intensity_b.SetJsonValue(root["intensity_b"]);
|
||||
}
|
||||
|
||||
std::string ColorMap::PropertiesJSON(int64_t requested_frame) const
|
||||
{
|
||||
Json::Value root = BasePropertiesJSON(requested_frame);
|
||||
|
||||
root["lut_path"] = add_property_json(
|
||||
"LUT File", 0.0, "string", lut_path, nullptr, 0, 0, false, requested_frame);
|
||||
|
||||
root["intensity"] = add_property_json(
|
||||
"Overall Intensity",
|
||||
intensity.GetValue(requested_frame),
|
||||
"float", "", &intensity, 0.0, 1.0, false, requested_frame);
|
||||
|
||||
root["intensity_r"] = add_property_json(
|
||||
"Red Intensity",
|
||||
intensity_r.GetValue(requested_frame),
|
||||
"float", "", &intensity_r, 0.0, 1.0, false, requested_frame);
|
||||
|
||||
root["intensity_g"] = add_property_json(
|
||||
"Green Intensity",
|
||||
intensity_g.GetValue(requested_frame),
|
||||
"float", "", &intensity_g, 0.0, 1.0, false, requested_frame);
|
||||
|
||||
root["intensity_b"] = add_property_json(
|
||||
"Blue Intensity",
|
||||
intensity_b.GetValue(requested_frame),
|
||||
"float", "", &intensity_b, 0.0, 1.0, false, requested_frame);
|
||||
|
||||
return root.toStyledString();
|
||||
}
|
||||
94
src/effects/ColorMap.h
Normal file
94
src/effects/ColorMap.h
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief Header file for ColorMap (LUT) effect
|
||||
* @author Jonathan Thomas <jonathan@openshot.org>
|
||||
*
|
||||
* @ref License
|
||||
*/
|
||||
|
||||
// Copyright (c) 2008-2025 OpenShot Studios, LLC
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
#ifndef OPENSHOT_COLORMAP_EFFECT_H
|
||||
#define OPENSHOT_COLORMAP_EFFECT_H
|
||||
|
||||
#include "../EffectBase.h"
|
||||
#include "../Json.h"
|
||||
#include "../KeyFrame.h"
|
||||
#include <QString>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace openshot
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Applies a 3D LUT (.cube) color transform to each frame.
|
||||
*
|
||||
* Loads a .cube file (LUT_3D_SIZE N × N × N) into memory, then for each pixel
|
||||
* uses nearest‐neighbor lookup and blends the result by keyframable per‐channel intensities.
|
||||
*/
|
||||
class ColorMap : public EffectBase
|
||||
{
|
||||
private:
|
||||
std::string lut_path; ///< Filesystem path to .cube LUT file
|
||||
int lut_size; ///< Dimension N of the cube (LUT_3D_SIZE)
|
||||
std::vector<float> lut_data; ///< Flat array [N³ × 3] RGB lookup table
|
||||
bool needs_refresh; ///< Reload LUT on next frame
|
||||
|
||||
/// Populate info fields (class_name, name, description)
|
||||
void init_effect_details();
|
||||
|
||||
/// Parse the .cube file into lut_size & lut_data
|
||||
void load_cube_file();
|
||||
|
||||
public:
|
||||
Keyframe intensity; ///< Overall intensity 0–1 (affects all channels)
|
||||
Keyframe intensity_r; ///< Blend 0–1 for red channel
|
||||
Keyframe intensity_g; ///< Blend 0–1 for green channel
|
||||
Keyframe intensity_b; ///< Blend 0–1 for blue channel
|
||||
|
||||
/// Blank constructor (used by JSON loader)
|
||||
ColorMap();
|
||||
|
||||
/**
|
||||
* @brief Constructor with LUT path and per‐channel intensities
|
||||
*
|
||||
* @param path Filesystem path to .cube file
|
||||
* @param i Keyframe for overall intensity (0–1)
|
||||
* @param iR Keyframe for red blend (0–1)
|
||||
* @param iG Keyframe for green blend (0–1)
|
||||
* @param iB Keyframe for blue blend (0–1)
|
||||
*/
|
||||
ColorMap(const std::string &path,
|
||||
const Keyframe &i = Keyframe(1.0),
|
||||
const Keyframe &iR = Keyframe(1.0),
|
||||
const Keyframe &iG = Keyframe(1.0),
|
||||
const Keyframe &iB = Keyframe(1.0));
|
||||
|
||||
/// Apply effect to a new frame
|
||||
std::shared_ptr<openshot::Frame>
|
||||
GetFrame(int64_t frame_number) override
|
||||
{ return GetFrame(std::make_shared<openshot::Frame>(), frame_number); }
|
||||
|
||||
/// Apply effect to an existing frame
|
||||
std::shared_ptr<openshot::Frame>
|
||||
GetFrame(std::shared_ptr<openshot::Frame> frame,
|
||||
int64_t frame_number) override;
|
||||
|
||||
// JSON serialization
|
||||
std::string Json() const override;
|
||||
Json::Value JsonValue() const override;
|
||||
void SetJson(const std::string value) override;
|
||||
void SetJsonValue(const Json::Value root) override;
|
||||
|
||||
/// Expose properties (for UI)
|
||||
std::string PropertiesJSON(int64_t requested_frame) const override;
|
||||
};
|
||||
|
||||
} // namespace openshot
|
||||
|
||||
#endif // OPENSHOT_COLORMAP_EFFECT_H
|
||||
@@ -43,6 +43,7 @@ set(OPENSHOT_TESTS
|
||||
SphericalMetadata
|
||||
Timeline
|
||||
# Effects
|
||||
ColorMap
|
||||
ChromaKey
|
||||
Crop
|
||||
LensFlare
|
||||
|
||||
214
tests/ColorMap.cpp
Normal file
214
tests/ColorMap.cpp
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* @file
|
||||
* @brief Unit tests for ColorMap effect
|
||||
* @author Jonathan Thomas
|
||||
*
|
||||
* @ref License
|
||||
*/
|
||||
|
||||
// Copyright (c) 2008-2025 OpenShot Studios, LLC
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
#include <memory>
|
||||
#include <QImage>
|
||||
#include <QColor>
|
||||
#include <sstream>
|
||||
#include "Frame.h"
|
||||
#include "effects/ColorMap.h"
|
||||
#include "openshot_catch.h"
|
||||
|
||||
using namespace openshot;
|
||||
|
||||
// allow Catch2 to print QColor on failure
|
||||
static std::ostream& operator<<(std::ostream& os, QColor const& c)
|
||||
{
|
||||
os << "QColor(" << c.red() << "," << c.green()
|
||||
<< "," << c.blue() << "," << c.alpha() << ")";
|
||||
return os;
|
||||
}
|
||||
|
||||
// Build a simple 2×2 frame with one distinct pixel
|
||||
static std::shared_ptr<Frame> makeTestFrame()
|
||||
{
|
||||
QImage img(2, 2, QImage::Format_ARGB32);
|
||||
img.fill(QColor(50,100,150,255));
|
||||
img.setPixelColor(0,0, QColor(10,20,30,255));
|
||||
auto frame = std::make_shared<Frame>();
|
||||
*frame->GetImage() = img;
|
||||
return frame;
|
||||
}
|
||||
|
||||
// Helper to construct the LUT-path from TEST_MEDIA_PATH
|
||||
static std::string lutPath()
|
||||
{
|
||||
std::stringstream path;
|
||||
path << TEST_MEDIA_PATH << "example-lut.cube";
|
||||
return path.str();
|
||||
}
|
||||
|
||||
TEST_CASE("Default ColorMap with no LUT path leaves image unchanged", "[effect][colormap]")
|
||||
{
|
||||
ColorMap effect;
|
||||
auto in = makeTestFrame();
|
||||
QColor before = in->GetImage()->pixelColor(0,0);
|
||||
|
||||
auto out = effect.GetFrame(in, 0);
|
||||
QColor after = out->GetImage()->pixelColor(0,0);
|
||||
|
||||
CHECK(after == before);
|
||||
}
|
||||
|
||||
TEST_CASE("Overall intensity = 0 leaves image unchanged even when LUT is set", "[effect][colormap]")
|
||||
{
|
||||
ColorMap effect(
|
||||
lutPath(),
|
||||
Keyframe(0.0), // overall off
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0)
|
||||
);
|
||||
|
||||
auto in = makeTestFrame();
|
||||
QColor before = in->GetImage()->pixelColor(0,0);
|
||||
auto out = effect.GetFrame(in, 1);
|
||||
QColor after = out->GetImage()->pixelColor(0,0);
|
||||
|
||||
CHECK(after == before);
|
||||
}
|
||||
|
||||
TEST_CASE("JSON round-trip preserves LUT path and intensity keyframe values", "[effect][colormap][json]")
|
||||
{
|
||||
ColorMap A(
|
||||
lutPath(),
|
||||
Keyframe(0.3), // overall
|
||||
Keyframe(0.4),
|
||||
Keyframe(0.5),
|
||||
Keyframe(0.6)
|
||||
);
|
||||
|
||||
std::string serialized = A.Json();
|
||||
ColorMap B;
|
||||
B.SetJson(serialized);
|
||||
|
||||
CHECK(B.JsonValue()["lut_path"].asString() == lutPath());
|
||||
CHECK( B.intensity. GetValue(0) == Approx(0.3) );
|
||||
CHECK( B.intensity_r.GetValue(0) == Approx(0.4) );
|
||||
CHECK( B.intensity_g.GetValue(0) == Approx(0.5) );
|
||||
CHECK( B.intensity_b.GetValue(0) == Approx(0.6) );
|
||||
}
|
||||
|
||||
TEST_CASE("Clearing LUT path via JSON leaves LUT path empty", "[effect][colormap][json]")
|
||||
{
|
||||
ColorMap effect(
|
||||
lutPath(),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0)
|
||||
);
|
||||
Json::Value clear;
|
||||
clear["lut_path"] = std::string("");
|
||||
effect.SetJsonValue(clear);
|
||||
|
||||
auto v = effect.JsonValue();
|
||||
CHECK(v["lut_path"].asString() == "");
|
||||
}
|
||||
|
||||
TEST_CASE("PropertiesJSON exposes all four intensity properties", "[effect][colormap][ui]")
|
||||
{
|
||||
ColorMap effect;
|
||||
std::string props = effect.PropertiesJSON(0);
|
||||
Json::CharReaderBuilder rb;
|
||||
Json::Value root;
|
||||
std::string errs;
|
||||
std::istringstream is(props);
|
||||
REQUIRE(Json::parseFromStream(rb, is, &root, &errs));
|
||||
|
||||
CHECK(root.isMember("lut_path"));
|
||||
CHECK(root.isMember("intensity"));
|
||||
CHECK(root.isMember("intensity_r"));
|
||||
CHECK(root.isMember("intensity_g"));
|
||||
CHECK(root.isMember("intensity_b"));
|
||||
}
|
||||
|
||||
TEST_CASE("Full-intensity LUT changes pixel values", "[effect][colormap][lut]")
|
||||
{
|
||||
ColorMap effect(
|
||||
lutPath(),
|
||||
Keyframe(1.0), // full overall
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0)
|
||||
);
|
||||
|
||||
auto in = makeTestFrame();
|
||||
QColor before = in->GetImage()->pixelColor(0,0);
|
||||
auto out = effect.GetFrame(in, 2);
|
||||
QColor after = out->GetImage()->pixelColor(0,0);
|
||||
|
||||
CHECK(after != before);
|
||||
}
|
||||
|
||||
TEST_CASE("Half-intensity LUT changes pixel values less than full-intensity", "[effect][colormap][lut]")
|
||||
{
|
||||
auto in = makeTestFrame();
|
||||
QColor before = in->GetImage()->pixelColor(0,0);
|
||||
|
||||
ColorMap half(
|
||||
lutPath(),
|
||||
Keyframe(0.5), // half overall
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0)
|
||||
);
|
||||
auto out_half = half.GetFrame(in, 3);
|
||||
QColor h = out_half->GetImage()->pixelColor(0,0);
|
||||
|
||||
ColorMap full(
|
||||
lutPath(),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0)
|
||||
);
|
||||
auto out_full = full.GetFrame(in, 3);
|
||||
QColor f = out_full->GetImage()->pixelColor(0,0);
|
||||
|
||||
int diff_half = std::abs(h.red() - before.red())
|
||||
+ std::abs(h.green() - before.green())
|
||||
+ std::abs(h.blue() - before.blue());
|
||||
int diff_full = std::abs(f.red() - before.red())
|
||||
+ std::abs(f.green() - before.green())
|
||||
+ std::abs(f.blue() - before.blue());
|
||||
|
||||
CHECK(diff_half < diff_full);
|
||||
}
|
||||
|
||||
TEST_CASE("Disabling red channel produces different result than full-intensity", "[effect][colormap][lut]")
|
||||
{
|
||||
auto in = makeTestFrame();
|
||||
QColor before = in->GetImage()->pixelColor(0,0);
|
||||
|
||||
ColorMap full(
|
||||
lutPath(),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0)
|
||||
);
|
||||
auto out_full = full.GetFrame(in, 4);
|
||||
QColor f = out_full->GetImage()->pixelColor(0,0);
|
||||
|
||||
ColorMap red_off(
|
||||
lutPath(),
|
||||
Keyframe(1.0),
|
||||
Keyframe(0.0), // red off
|
||||
Keyframe(1.0),
|
||||
Keyframe(1.0)
|
||||
);
|
||||
auto out_off = red_off.GetFrame(in, 4);
|
||||
QColor r = out_off->GetImage()->pixelColor(0,0);
|
||||
|
||||
CHECK(r != f);
|
||||
}
|
||||
Reference in New Issue
Block a user