Camera Raw filter's model and pixel kernels (12.8a)
This commit is contained in:
1 parent
4291f4249a
commit
b544e12b1d
33 files changed
+4438
-27
No files matched your search
@@ -0,0 +1,173 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include <QtTest>
|
||||
#include <cmath>
|
||||
|
||||
// Curve, Color Mixer and Color Grading, rule by rule.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
namespace {
|
||||
using Points = std::vector<CurvePoint>;
|
||||
|
||||
Points repaired(const Points &points)
|
||||
{
|
||||
CameraRawCurveSettings curve;
|
||||
curve.rgb = points;
|
||||
return curve.normalized().rgb;
|
||||
}
|
||||
|
||||
int shadowTint(double balance)
|
||||
{
|
||||
CameraRawSettings settings;
|
||||
settings.grading.shadows.saturation = 100;
|
||||
settings.grading.balance = balance;
|
||||
const std::array<int, 4> pixel = pixels(settings.apply(image(4, 4, 0.12, 0.12, 0.12)))[0];
|
||||
return pixel[0] - pixel[1];
|
||||
}
|
||||
}
|
||||
|
||||
class CameraRawColorTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void curvesAreRepairedIntoOrder();
|
||||
void theParametricCurveLiftsItsRegion();
|
||||
void aNudgeMovesTheFirstNearestPointWithinRange();
|
||||
void splitsAndAmountsStayInTheirRanges();
|
||||
void mixerFamiliesOverlapRoundTheWheel();
|
||||
void pointColorsStayInTheirRanges();
|
||||
void gradingWheelsAndBalanceReachTheKernel();
|
||||
void aPointColorReachesOnlyItsRange();
|
||||
void everyGroupSaysWhenItAdjusts();
|
||||
};
|
||||
|
||||
void CameraRawColorTests::curvesAreRepairedIntoOrder()
|
||||
{
|
||||
QCOMPARE(repaired({{1, 1}, {0.5, 0.2}, {0, 0}}), (Points{{0, 0}, {0.5, 0.2}, {1, 1}}));
|
||||
QCOMPARE(repaired({{0.3, 0.4}}), CameraRawCurveSettings::linear());
|
||||
QCOMPARE(repaired({{0, 0}, {std::nan(""), 0.5}, {1, 1}}), CameraRawCurveSettings::linear());
|
||||
// Ends pinned to 0 and 1, heights clamped.
|
||||
QCOMPARE(repaired({{0.1, -0.5}, {0.9, 1.5}}), CameraRawCurveSettings::linear());
|
||||
// Inner points keep a hundredth from ends and neighbours.
|
||||
QCOMPARE(repaired({{0, 0}, {0.995, 0.5}, {1, 1}}), (Points{{0, 0}, {0.99, 0.5}, {1, 1}}));
|
||||
QCOMPARE(repaired({{0, 0}, {0.5, 0.3}, {0.505, 0.6}, {1, 1}}), (Points{{0, 0}, {0.5, 0.3}, {1, 1}}));
|
||||
QCOMPARE(repaired({{0, 0}, {0.004, 0.3}, {1, 1}}), CameraRawCurveSettings::linear());
|
||||
// A curve of one point passes its input through.
|
||||
CameraRawCurveSettings curve;
|
||||
const std::vector<float> table = curve.channelTable({{0.5, 0.7}});
|
||||
QCOMPARE(table[100], float(100 / 255.0));
|
||||
}
|
||||
|
||||
void CameraRawColorTests::theParametricCurveLiftsItsRegion()
|
||||
{
|
||||
CameraRawCurveSettings curve;
|
||||
curve.shadows = 100;
|
||||
// The region's middle takes the whole 22%.
|
||||
QVERIFY(std::abs(curve.parametric(0.125) - 0.345) < 1e-12);
|
||||
QCOMPARE(curve.parametric(0.375), 0.375);
|
||||
curve.highlights = -100;
|
||||
curve.lightSplit = 100;
|
||||
// A region of no width still takes a fiftieth's span.
|
||||
QVERIFY(std::abs(curve.parametric(1) - 0.78) < 1e-12);
|
||||
// Region picks the amount a tone moves.
|
||||
CameraRawCurveSettings regions;
|
||||
regions.region(0.1) = 1;
|
||||
regions.region(0.3) = 2;
|
||||
regions.region(0.6) = 3;
|
||||
regions.region(0.9) = 4;
|
||||
QVERIFY(regions.shadows == 1 && regions.darks == 2 && regions.lights == 3 && regions.highlights == 4);
|
||||
}
|
||||
|
||||
void CameraRawColorTests::aNudgeMovesTheFirstNearestPointWithinRange()
|
||||
{
|
||||
const CameraRawCurveSettings curve;
|
||||
// Halfway between two points, the first is nearest.
|
||||
QCOMPARE(curve.nudged(CameraRawPointChannel::green, 0.5, 0.2).green, (Points{{0, 0.2}, {1, 1}}));
|
||||
QCOMPARE(curve.nudged(CameraRawPointChannel::blue, 0.9, 5).blue, (Points{{0, 0}, {1, 1}}));
|
||||
QCOMPARE(curve.nudged(CameraRawPointChannel::rgb, 0.1, -5).rgb, (Points{{0, 0}, {1, 1}}));
|
||||
QCOMPARE(curve.nudged(CameraRawPointChannel::rgb, 0.9, -0.25).rgb, (Points{{0, 0}, {1, 0.75}}));
|
||||
}
|
||||
|
||||
void CameraRawColorTests::splitsAndAmountsStayInTheirRanges()
|
||||
{
|
||||
CameraRawCurveSettings curve;
|
||||
curve.shadowSplit = 25;
|
||||
curve.darkSplit = 20;
|
||||
curve.lightSplit = 26;
|
||||
curve.refineSaturation = 150;
|
||||
const CameraRawCurveSettings normal = curve.normalized();
|
||||
QVERIFY(normal.darkSplit == 27 && normal.lightSplit == 29 && normal.refineSaturation == 100);
|
||||
curve.shadowSplit = 1;
|
||||
QCOMPARE(curve.normalized().shadowSplit, 5.0);
|
||||
}
|
||||
|
||||
void CameraRawColorTests::mixerFamiliesOverlapRoundTheWheel()
|
||||
{
|
||||
const std::array<double, 8> near = CameraRawMixerSettings::weights(350);
|
||||
QCOMPARE(near[0], 0.75);
|
||||
QCOMPARE(near[7], 0.0);
|
||||
const std::array<double, 8> orange = CameraRawMixerSettings::weights(30);
|
||||
QVERIFY(orange[0] == 0.25 && orange[1] == 1 && orange[2] == 0.25 && orange[3] == 0);
|
||||
CameraRawMixerSettings mixer;
|
||||
mixer.points = std::vector<CameraRawPointColor>(9);
|
||||
QCOMPARE(mixer.normalized().points.size(), size_t(8));
|
||||
}
|
||||
|
||||
void CameraRawColorTests::pointColorsStayInTheirRanges()
|
||||
{
|
||||
const CameraRawPointColor wild{.hue = 400, .saturation = 2, .luminance = -1, .hueShift = 300, .saturationShift = -300, .luminanceShift = 101,
|
||||
.hueRange = 200, .saturationRange = 0.01, .luminanceRange = 2};
|
||||
const CameraRawPointColor normal = wild.normalized();
|
||||
QVERIFY(normal.hue == 360 && normal.saturation == 1 && normal.luminance == 0 && normal.hueShift == 100 && normal.saturationShift == -100);
|
||||
QVERIFY(normal.luminanceShift == 100 && normal.hueRange == 180 && normal.saturationRange == 0.05 && normal.luminanceRange == 1);
|
||||
QCOMPARE(CameraRawPointColor{.hueRange = 2}.normalized().hueRange, 5.0);
|
||||
const CameraRawGradeWheel wheel = CameraRawGradeWheel{400, -20, -150}.normalized();
|
||||
QVERIFY(wheel.hue == 360 && wheel.saturation == 0 && wheel.luminance == -100);
|
||||
CameraRawGradingSettings grading;
|
||||
grading.blending = 150;
|
||||
grading.balance = -150;
|
||||
QVERIFY(grading.normalized().blending == 100 && grading.normalized().balance == -100);
|
||||
}
|
||||
|
||||
void CameraRawColorTests::gradingWheelsAndBalanceReachTheKernel()
|
||||
{
|
||||
// Balance runs in hundredths: half is between none and all.
|
||||
const int none = shadowTint(0), half = shadowTint(50), all = shadowTint(100);
|
||||
QVERIFY2(none > half && half > all, qPrintable(QStringLiteral("%1 %2 %3").arg(none).arg(half).arg(all)));
|
||||
// Luminance runs in hundredths too: measured once, then held.
|
||||
const auto lifted = [](double luminance) {
|
||||
CameraRawSettings settings;
|
||||
settings.grading.shadows.luminance = luminance;
|
||||
return pixels(settings.apply(image(4, 4, 0.12, 0.12, 0.12)))[0][0];
|
||||
};
|
||||
QVERIFY(lifted(0) == 31 && lifted(50) == 53 && lifted(100) == 76);
|
||||
}
|
||||
|
||||
void CameraRawColorTests::aPointColorReachesOnlyItsRange()
|
||||
{
|
||||
CameraRawSettings settings;
|
||||
settings.mixer.points = {CameraRawPointColor{.hue = 0, .saturation = 1, .luminance = 0.5, .luminanceShift = -100, .hueRange = 30}};
|
||||
const std::array<int, 4> red = pixels(settings.apply(image(4, 4, 1, 0, 0)))[0];
|
||||
QVERIFY(red[0] < 240);
|
||||
// Fifty degrees off lies outside thirty either way.
|
||||
const QImage orange = image(4, 4, 1, 50 / 60.0, 0);
|
||||
QVERIFY(pixels(settings.apply(orange)) == pixels(orange));
|
||||
}
|
||||
|
||||
void CameraRawColorTests::everyGroupSaysWhenItAdjusts()
|
||||
{
|
||||
CameraRawCurveSettings refined;
|
||||
refined.refineSaturation = 10;
|
||||
CameraRawCurveSettings blue;
|
||||
blue.blue = {{0, 0.1}, {1, 1}};
|
||||
QVERIFY(refined.adjusts() && blue.adjusts() && !CameraRawCurveSettings().adjusts());
|
||||
CameraRawMixerSettings lighter;
|
||||
lighter.points = {CameraRawPointColor{.luminanceShift = 10}};
|
||||
QVERIFY(lighter.adjusts() && !CameraRawMixerSettings{.points = {CameraRawPointColor{.hue = 40}}}.adjusts());
|
||||
CameraRawGradingSettings brighter;
|
||||
brighter.global.luminance = 10;
|
||||
CameraRawGradingSettings balanced;
|
||||
balanced.balance = 50;
|
||||
QVERIFY(brighter.adjusts() && !balanced.adjusts());
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawColorTests)
|
||||
#include "CameraRawColorTests.moc"
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/CameraRaw.h"
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
// Swift's CameraRawTests helpers: solid pictures and straight bytes.
|
||||
namespace CameraRawFixtures {
|
||||
inline QImage image(int width, int height, double red, double green, double blue, double alpha = 1)
|
||||
{
|
||||
QImage context = BrushRaster::context(width, height, false);
|
||||
QPainter painter(&context);
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
painter.fillRect(context.rect(), QColor::fromRgbF(float(red), float(green), float(blue), float(alpha)));
|
||||
return context;
|
||||
}
|
||||
|
||||
inline QImage gray(int width = 4, int height = 4, double alpha = 1)
|
||||
{
|
||||
return image(width, height, 128 / 255.0, 128 / 255.0, 128 / 255.0, alpha);
|
||||
}
|
||||
|
||||
// Straight RGBA, top row first, rounded as Swift's.
|
||||
inline std::vector<std::array<int, 4>> pixels(const QImage &source)
|
||||
{
|
||||
const QImage drawn = source.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
std::vector<std::array<int, 4>> result;
|
||||
for (int y = 0; y < drawn.height(); ++y) {
|
||||
const uchar *row = drawn.constScanLine(y);
|
||||
for (int x = 0; x < drawn.width(); ++x) {
|
||||
const uchar *p = row + x * 4;
|
||||
const int alpha = p[3];
|
||||
std::array<int, 4> pixel{0, 0, 0, alpha};
|
||||
for (int channel = 0; channel < 3; ++channel)
|
||||
pixel[size_t(channel)] = alpha == 0 ? 0 : std::min(255, (p[channel] * 255 + alpha / 2) / alpha);
|
||||
result.push_back(pixel);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline int chroma(const std::array<int, 4> &pixel)
|
||||
{
|
||||
return std::max({pixel[0], pixel[1], pixel[2]}) - std::min({pixel[0], pixel[1], pixel[2]});
|
||||
}
|
||||
|
||||
// Two tones side by side, one pixel each.
|
||||
inline QImage pair(int left, int right)
|
||||
{
|
||||
QImage context = BrushRaster::context(2, 1, false);
|
||||
context.setPixelColor(0, 0, QColor(left, left, left));
|
||||
context.setPixelColor(1, 0, QColor(right, right, right));
|
||||
return context;
|
||||
}
|
||||
|
||||
// A step, 40 to 200 at x 12.
|
||||
inline QImage step()
|
||||
{
|
||||
QImage context = BrushRaster::context(24, 4, false);
|
||||
for (int y = 0; y < 4; ++y)
|
||||
for (int x = 0; x < 24; ++x)
|
||||
context.setPixelColor(x, y, x < 12 ? QColor(40, 40, 40) : QColor(200, 200, 200));
|
||||
return context;
|
||||
}
|
||||
|
||||
// A checkerboard of 2-pixel squares: a warp shows on it.
|
||||
inline QImage checker(int width = 24, int height = 24)
|
||||
{
|
||||
QImage context = BrushRaster::context(width, height, false);
|
||||
for (int y = 0; y < height; ++y)
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const bool light = ((x / 2) + (y / 2)) % 2 == 0;
|
||||
const double level = light ? 0.9 : 0.1;
|
||||
context.setPixelColor(x, y, QColor::fromRgbF(float(level), float(level), float(level)));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
inline int peakIndex(const std::array<double, 256> &bins)
|
||||
{
|
||||
return int(std::max_element(bins.begin(), bins.end()) - bins.begin());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include <QtTest>
|
||||
#include <cmath>
|
||||
extern "C" {
|
||||
#include "AdjustPixels.h"
|
||||
}
|
||||
|
||||
// Detail, Optics, Geometry and Calibration, rule by rule.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
namespace {
|
||||
// Stripes two pixels wide, mirrored about the middle column.
|
||||
QImage stripes(int side = 24)
|
||||
{
|
||||
QImage picture = BrushRaster::context(side, side, false);
|
||||
for (int y = 0; y < side; ++y)
|
||||
for (int x = 0; x < side; ++x) {
|
||||
const int from = std::min(x, side - 1 - x);
|
||||
picture.setPixelColor(x, y, (from / 2) % 2 ? QColor(230, 230, 230) : QColor(20, 20, 20));
|
||||
}
|
||||
return picture;
|
||||
}
|
||||
|
||||
int transitions(const QImage &picture, int y)
|
||||
{
|
||||
int count = 0;
|
||||
for (int x = 1; x < picture.width(); ++x)
|
||||
count += (qRed(picture.pixel(x, y)) > 128) != (qRed(picture.pixel(x - 1, y)) > 128);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool mirrored(const QImage &picture)
|
||||
{
|
||||
for (int y = 0; y < picture.height(); ++y)
|
||||
for (int x = 0; x < picture.width() / 2; ++x)
|
||||
if (std::abs(qRed(picture.pixel(x, y)) - qRed(picture.pixel(picture.width() - 1 - x, y))) > 2)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
QImage warped(const std::function<void(CameraRawGeometrySettings &)> &change, const QImage &picture = stripes())
|
||||
{
|
||||
CameraRawGeometrySettings settings;
|
||||
change(settings);
|
||||
return settings.apply(picture);
|
||||
}
|
||||
}
|
||||
|
||||
class CameraRawLensTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void detailAndOpticsSayWhenTheyAdjust();
|
||||
void opticsStayInTheirRanges();
|
||||
void theProfileAddsDistortionOnlyWhenOn();
|
||||
void verticalWidensTheTopAndHorizontalTheBottom();
|
||||
void aGuidedLineTurnsAsTheSameRotationWould();
|
||||
void aSecondLineTiltsAQuarter();
|
||||
void projectionAspectAndScaleShapeTheWarp();
|
||||
void constrainCropFillsTheFrame();
|
||||
void calibrationRunsAtItsProcessVersion();
|
||||
};
|
||||
|
||||
void CameraRawLensTests::detailAndOpticsSayWhenTheyAdjust()
|
||||
{
|
||||
QVERIFY(CameraRawDetailSettings{.noiseColor = 10}.adjusts() && CameraRawDetailSettings{.noiseLuminance = 10}.adjusts());
|
||||
QVERIFY(CameraRawOpticsSettings{.enableLensProfile = true}.adjusts() && CameraRawOpticsSettings{.vignetteAmount = -20}.adjusts());
|
||||
QVERIFY(!CameraRawDetailSettings().adjusts() && !CameraRawOpticsSettings().adjusts());
|
||||
QVERIFY(CameraRawGeometrySettings{.aspect = 20}.adjusts() && !CameraRawGeometrySettings().adjusts());
|
||||
// A guide a two-hundredth long reads as no line.
|
||||
CameraRawGeometrySettings shortLine{.upright = CameraRawUprightMode::guided};
|
||||
shortLine.guides = {{0.5, 0.5, 0.505, 0.5}};
|
||||
QVERIFY(!shortLine.adjusts() && shortLine.normalized().guides.empty());
|
||||
shortLine.guides = {{0.5, 0.5, 0.52, 0.5}};
|
||||
QVERIFY(shortLine.adjusts());
|
||||
}
|
||||
|
||||
void CameraRawLensTests::opticsStayInTheirRanges()
|
||||
{
|
||||
QCOMPARE(CameraRawDetailSettings{.sharpenAmount = 140}.normalized().sharpenAmount, 140.0);
|
||||
QCOMPARE(CameraRawDetailSettings{.sharpenAmount = 160}.normalized().sharpenAmount, 150.0);
|
||||
const CameraRawOpticsSettings swapped = CameraRawOpticsSettings{.purpleHueLow = 320, .purpleHueHigh = 250, .greenHueLow = 130, .greenHueHigh = 40,
|
||||
.vignetteAmount = -50}
|
||||
.normalized();
|
||||
QVERIFY(swapped.purpleHueLow == 250 && swapped.purpleHueHigh == 320 && swapped.greenHueLow == 40 && swapped.greenHueHigh == 130);
|
||||
QCOMPARE(swapped.vignetteAmount, -50.0);
|
||||
QCOMPARE(CameraRawGeometrySettings{.rotate = 60}.normalized().rotate, 45.0);
|
||||
QCOMPARE(CameraRawCalibrationSettings{.process = CameraRawProcessVersion::version2}.normalized().process, CameraRawProcessVersion::version2);
|
||||
}
|
||||
|
||||
void CameraRawLensTests::theProfileAddsDistortionOnlyWhenOn()
|
||||
{
|
||||
CameraRawOpticsSettings optics{.profileDistortion = 100, .distortion = 50};
|
||||
QCOMPARE(optics.distortionK(0.35), 0.175);
|
||||
optics.enableLensProfile = true;
|
||||
QCOMPARE(optics.distortionK(0.35), 0.175 + 0.35);
|
||||
}
|
||||
|
||||
void CameraRawLensTests::verticalWidensTheTopAndHorizontalTheBottom()
|
||||
{
|
||||
const QImage picture = stripes();
|
||||
const QImage vertical = warped([](CameraRawGeometrySettings &g) { g.vertical = 60; });
|
||||
QVERIFY(transitions(vertical, 1) < transitions(vertical, 22));
|
||||
QVERIFY(mirrored(vertical));
|
||||
const QImage horizontal = warped([](CameraRawGeometrySettings &g) { g.horizontal = 60; });
|
||||
QVERIFY(transitions(horizontal, 22) < transitions(horizontal, 1));
|
||||
QVERIFY(mirrored(horizontal));
|
||||
QVERIFY(transitions(picture, 1) == transitions(picture, 22));
|
||||
}
|
||||
|
||||
void CameraRawLensTests::aGuidedLineTurnsAsTheSameRotationWould()
|
||||
{
|
||||
const QImage picture = checker(24, 24);
|
||||
for (const double direction : {1.0, -1.0}) {
|
||||
// Steep: 82.9° either way, levelled by a quarter turn back.
|
||||
const QImage guided = warped(
|
||||
[direction](CameraRawGeometrySettings &g) {
|
||||
g.upright = CameraRawUprightMode::guided;
|
||||
g.guides = {{0.5, 0.5, 0.6, 0.5 + 0.8 * direction}};
|
||||
},
|
||||
picture);
|
||||
const double angle = std::atan2(0.8 * direction, 0.1) * 180 / std::numbers::pi;
|
||||
const double turn = -angle + (direction > 0 ? 90 : -90);
|
||||
const QImage turned = warped([turn](CameraRawGeometrySettings &g) { g.rotate = turn; }, picture);
|
||||
QVERIFY(pixels(guided) == pixels(turned));
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawLensTests::aSecondLineTiltsAQuarter()
|
||||
{
|
||||
const QImage picture = checker(24, 24);
|
||||
const auto guided = [&](CameraRawGeometryGuide second) {
|
||||
return warped(
|
||||
[second](CameraRawGeometrySettings &g) {
|
||||
g.upright = CameraRawUprightMode::guided;
|
||||
g.guides = {{0.2, 0.5, 0.8, 0.5}, second};
|
||||
},
|
||||
picture);
|
||||
};
|
||||
const auto manual = [&](double vertical, double horizontal) {
|
||||
return warped([=](CameraRawGeometrySettings &g) { g.vertical = vertical, g.horizontal = horizontal; }, picture);
|
||||
};
|
||||
QVERIFY(pixels(guided({0.5, 0.1, 0.55, 0.9})) == pixels(manual(25, 0)));
|
||||
QVERIFY(pixels(guided({0.5, 0.9, 0.55, 0.1})) == pixels(manual(-25, 0)));
|
||||
QVERIFY(pixels(guided({0.1, 0.5, 0.9, 0.6})) == pixels(manual(0, 25)));
|
||||
QVERIFY(pixels(guided({0.1, 0.5, 0.9, 0.4})) == pixels(manual(0, -25)));
|
||||
}
|
||||
|
||||
void CameraRawLensTests::projectionAspectAndScaleShapeTheWarp()
|
||||
{
|
||||
const QImage picture = checker(24, 24);
|
||||
const QImage rectilinear = warped(
|
||||
[](CameraRawGeometrySettings &g) {
|
||||
g.projection = CameraRawProjection::rectilinear;
|
||||
g.vertical = 40;
|
||||
},
|
||||
picture);
|
||||
QVERIFY(pixels(rectilinear) == pixels(warped([](CameraRawGeometrySettings &g) { g.vertical = 40 * 0.55; }, picture)));
|
||||
QVERIFY(pixels(warped([](CameraRawGeometrySettings &g) { g.aspect = 50; }, picture)) != pixels(picture));
|
||||
QVERIFY(pixels(warped([](CameraRawGeometrySettings &g) { g.scale = 50; }, picture)) != pixels(picture));
|
||||
}
|
||||
|
||||
void CameraRawLensTests::constrainCropFillsTheFrame()
|
||||
{
|
||||
// Shrunk to its middle, the picture leaves every edge empty.
|
||||
const QImage open = warped([](CameraRawGeometrySettings &g) { g.scale = -50; });
|
||||
QCOMPARE(qAlpha(open.pixel(0, 0)), 0);
|
||||
const QImage cropped = warped([](CameraRawGeometrySettings &g) { g.scale = -50, g.constrainCrop = true; });
|
||||
QVERIFY(qAlpha(cropped.pixel(0, 0)) > 0 && qAlpha(cropped.pixel(23, 0)) > 0);
|
||||
}
|
||||
|
||||
void CameraRawLensTests::calibrationRunsAtItsProcessVersion()
|
||||
{
|
||||
// Each primary on top, so every field shows.
|
||||
QImage red = BrushRaster::context(3, 1, false);
|
||||
red.setPixelColor(0, 0, QColor(230, 120, 40));
|
||||
red.setPixelColor(1, 0, QColor(60, 210, 110));
|
||||
red.setPixelColor(2, 0, QColor(90, 50, 220));
|
||||
for (const CameraRawProcessVersion version : {CameraRawProcessVersion::version1, CameraRawProcessVersion::version4, CameraRawProcessVersion::version6}) {
|
||||
CameraRawSettings settings;
|
||||
settings.calibration = {version, 20, 80, 30, -40, 60, -70, 50};
|
||||
QImage direct = red.copy();
|
||||
adjust_camera_raw_calibration(direct.bits(), 3, 1, size_t(direct.bytesPerLine()), 20, 80, 30, -40, 60, -70, 50, int(version) + 1);
|
||||
QVERIFY(pixels(settings.apply(red)) == pixels(direct));
|
||||
}
|
||||
QCOMPARE(rawValue(CameraRawProcessVersion::version3), QString("Version 3"));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawLensTests)
|
||||
#include "CameraRawLensTests.moc"
|
||||
@@ -0,0 +1,228 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include "Document/Filters.h"
|
||||
#include <QtTest>
|
||||
#include <cmath>
|
||||
|
||||
// The grade's rules that Swift's tests leave to its code.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
namespace {
|
||||
// Every group but Light and Color moved, visibly.
|
||||
CameraRawSettings everything()
|
||||
{
|
||||
CameraRawSettings settings;
|
||||
settings.whites = 100;
|
||||
settings.curve.rgb = CameraRawCurveSettings::strongContrast();
|
||||
settings.grading.shadows.saturation = 100;
|
||||
settings.vignetteAmount = -100;
|
||||
settings.detail.sharpenAmount = 150;
|
||||
settings.optics.distortion = 100;
|
||||
settings.geometry.vertical = 60;
|
||||
settings.calibration.redHue = 80;
|
||||
return settings;
|
||||
}
|
||||
|
||||
QImage colourful()
|
||||
{
|
||||
QImage picture = checker(16, 16);
|
||||
for (int y = 0; y < 16; ++y)
|
||||
for (int x = 0; x < 8; ++x)
|
||||
picture.setPixelColor(x, y, QColor(200, 40 + 10 * y, 30));
|
||||
return picture;
|
||||
}
|
||||
}
|
||||
|
||||
class CameraRawRulesTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void boundsAndNormalizingCoverEveryField();
|
||||
void anUntouchedGradeHandsTheImageBack();
|
||||
void theClippingAndMaskViewsShowNothingElse();
|
||||
void theScaleDefaultsToOneAndReachesTheRadii();
|
||||
void eachEyeDropsItsOwnGroupAlone();
|
||||
void grainSizeMapsIntoTheKernelsScale();
|
||||
void autoBalanceSkipsClearPixels();
|
||||
void thePreviewShowsItsViewAndCountsTheGrade();
|
||||
void aPointColorShowsAloneOnlyWhenAsked();
|
||||
};
|
||||
|
||||
void CameraRawRulesTests::boundsAndNormalizingCoverEveryField()
|
||||
{
|
||||
// Every tone clamps to ±100, every unit to 0–100.
|
||||
CameraRawSettings past;
|
||||
const std::array tones{&CameraRawSettings::contrast, &CameraRawSettings::highlights, &CameraRawSettings::shadows, &CameraRawSettings::whites,
|
||||
&CameraRawSettings::blacks, &CameraRawSettings::temperature, &CameraRawSettings::tint, &CameraRawSettings::vibrance,
|
||||
&CameraRawSettings::saturation, &CameraRawSettings::texture, &CameraRawSettings::clarity, &CameraRawSettings::dehaze,
|
||||
&CameraRawSettings::glowRange, &CameraRawSettings::glowSpread, &CameraRawSettings::glowWarmth, &CameraRawSettings::vignetteAmount,
|
||||
&CameraRawSettings::vignetteRoundness};
|
||||
const std::array units{&CameraRawSettings::glow, &CameraRawSettings::vignetteMidpoint, &CameraRawSettings::vignetteFeather,
|
||||
&CameraRawSettings::vignetteHighlights, &CameraRawSettings::grainAmount, &CameraRawSettings::grainSize,
|
||||
&CameraRawSettings::grainRoughness};
|
||||
for (double CameraRawSettings::*field : tones)
|
||||
past.*field = 150;
|
||||
for (double CameraRawSettings::*field : units)
|
||||
past.*field = 150;
|
||||
past.exposure = 9;
|
||||
CameraRawSettings clamped = past.normalized();
|
||||
for (double CameraRawSettings::*field : tones)
|
||||
QCOMPARE(clamped.*field, 100.0);
|
||||
for (double CameraRawSettings::*field : units)
|
||||
QCOMPARE(clamped.*field, 100.0);
|
||||
QCOMPARE(clamped.exposure, 5.0);
|
||||
for (double CameraRawSettings::*field : tones)
|
||||
past.*field = -150;
|
||||
for (double CameraRawSettings::*field : units)
|
||||
past.*field = -150;
|
||||
past.exposure = -9;
|
||||
clamped = past.normalized();
|
||||
for (double CameraRawSettings::*field : tones)
|
||||
QCOMPARE(clamped.*field, -100.0);
|
||||
for (double CameraRawSettings::*field : units)
|
||||
QCOMPARE(clamped.*field, 0.0);
|
||||
QCOMPARE(clamped.exposure, -5.0);
|
||||
CameraRawSettings wild;
|
||||
wild.vignetteMidpoint = std::nan("");
|
||||
wild.grainSize = 400;
|
||||
wild.grainRoughness = -3;
|
||||
wild.vignetteFeather = 250;
|
||||
wild.glow = 101;
|
||||
const CameraRawSettings normal = wild.normalized();
|
||||
QVERIFY(normal.vignetteMidpoint == 50 && normal.grainSize == 100 && normal.grainRoughness == 0 && normal.vignetteFeather == 100 && normal.glow == 100);
|
||||
}
|
||||
|
||||
void CameraRawRulesTests::anUntouchedGradeHandsTheImageBack()
|
||||
{
|
||||
const QImage input = gray();
|
||||
QCOMPARE(CameraRawSettings().apply(input).cacheKey(), input.cacheKey());
|
||||
CameraRawSettings moved;
|
||||
moved.exposure = 1;
|
||||
QVERIFY(moved.apply(input).cacheKey() != input.cacheKey());
|
||||
}
|
||||
|
||||
void CameraRawRulesTests::theClippingAndMaskViewsShowNothingElse()
|
||||
{
|
||||
const QImage picture = colourful();
|
||||
CameraRawSettings light;
|
||||
light.whites = 100;
|
||||
const CameraRawSettings all = everything();
|
||||
QVERIFY(pixels(all.apply(picture, CameraRawClipping::highlights)) == pixels(light.apply(picture, CameraRawClipping::highlights)));
|
||||
QVERIFY(pixels(all.apply(picture, CameraRawClipping::shadows)) == pixels(light.apply(picture, CameraRawClipping::shadows)));
|
||||
// An edge off the centre, where the lens moves it.
|
||||
QImage edge = image(24, 24, 0.9, 0.9, 0.9);
|
||||
for (int y = 0; y < 24; ++y)
|
||||
for (int x = 0; x < 5; ++x)
|
||||
edge.setPixelColor(x, y, QColor(40, 40, 40));
|
||||
QVERIFY(pixels(all.apply(edge, CameraRawClipping::highlights)) == pixels(light.apply(edge, CameraRawClipping::highlights)));
|
||||
// The mask reads the Light grade, as Swift's does.
|
||||
CameraRawSettings sharpening = light;
|
||||
sharpening.detail = all.detail;
|
||||
sharpening.optics = all.optics;
|
||||
QVERIFY(pixels(all.apply(picture, std::nullopt, 1, 0, -1, true)) == pixels(sharpening.apply(picture, std::nullopt, 1, 0, -1, true)));
|
||||
// Showing a point colour leaves the geometry out.
|
||||
CameraRawSettings warped;
|
||||
warped.geometry.vertical = 60;
|
||||
warped.mixer.points = {CameraRawPointColor{.hue = 10, .saturation = 0.8, .luminance = 0.5, .visualize = true}};
|
||||
CameraRawSettings flat = warped;
|
||||
flat.geometry = CameraRawGeometrySettings();
|
||||
QVERIFY(pixels(warped.apply(picture, std::nullopt, 1, 0, 0)) == pixels(flat.apply(picture, std::nullopt, 1, 0, 0)));
|
||||
QVERIFY(pixels(warped.apply(picture)) != pixels(flat.apply(picture)));
|
||||
}
|
||||
|
||||
void CameraRawRulesTests::theScaleDefaultsToOneAndReachesTheRadii()
|
||||
{
|
||||
CameraRawSettings settings;
|
||||
settings.clarity = 100;
|
||||
const QImage edge = step();
|
||||
QVERIFY(pixels(settings.apply(edge, std::nullopt, 0)) == pixels(settings.apply(edge, std::nullopt, 1)));
|
||||
QVERIFY(pixels(settings.apply(edge, std::nullopt, -2)) == pixels(settings.apply(edge, std::nullopt, 1)));
|
||||
QVERIFY(pixels(settings.apply(edge, std::nullopt, 0.25)) != pixels(settings.apply(edge, std::nullopt, 1)));
|
||||
// Grain's units read the scale: a negative one is one.
|
||||
CameraRawSettings grainy;
|
||||
grainy.grainAmount = 60;
|
||||
QVERIFY(pixels(grainy.apply(gray(16, 16), std::nullopt, -2, 3)) == pixels(grainy.apply(gray(16, 16), std::nullopt, 1, 3)));
|
||||
}
|
||||
|
||||
void CameraRawRulesTests::eachEyeDropsItsOwnGroupAlone()
|
||||
{
|
||||
const CameraRawSettings all = [] {
|
||||
CameraRawSettings settings = everything();
|
||||
settings.exposure = 1;
|
||||
settings.temperature = 20;
|
||||
settings.texture = 30;
|
||||
settings.mixer.hue[0] = 50;
|
||||
return settings;
|
||||
}();
|
||||
const CameraRawSettings color = all.applying({.color = false});
|
||||
QVERIFY(color.temperature == 0 && color.saturation == 0 && color.exposure == 1);
|
||||
CameraRawSettings saturated = all;
|
||||
saturated.saturation = 40;
|
||||
QCOMPARE(saturated.applying({.color = false}).saturation, 0.0);
|
||||
const std::vector<std::pair<CameraRawGroups, std::function<bool(const CameraRawSettings &)>>> groups{
|
||||
{{.mixer = false}, [](const CameraRawSettings &s) { return s.mixer == CameraRawMixerSettings(); }},
|
||||
{{.grading = false}, [](const CameraRawSettings &s) { return s.grading == CameraRawGradingSettings(); }},
|
||||
{{.optics = false}, [](const CameraRawSettings &s) { return s.optics == CameraRawOpticsSettings(); }},
|
||||
{{.geometry = false}, [](const CameraRawSettings &s) { return s.geometry == CameraRawGeometrySettings(); }}};
|
||||
for (const auto &[shown, dropped] : groups) {
|
||||
const CameraRawSettings hidden = all.applying(shown);
|
||||
QVERIFY(dropped(hidden));
|
||||
QVERIFY(hidden.exposure == 1 && hidden.curve == all.curve && hidden.texture == 30);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawRulesTests::grainSizeMapsIntoTheKernelsScale()
|
||||
{
|
||||
CameraRawSettings settings;
|
||||
QCOMPARE(settings.grainKernelSize(), 0.5 + 0.25 * 19.5);
|
||||
settings.grainSize = 100;
|
||||
QCOMPARE(settings.grainKernelSize(), 20.0);
|
||||
settings.grainSize = 0;
|
||||
QCOMPARE(settings.grainKernelSize(), 0.5);
|
||||
}
|
||||
|
||||
void CameraRawRulesTests::autoBalanceSkipsClearPixels()
|
||||
{
|
||||
const double red = 160 / 255.0, green = 140 / 255.0, blue = 120 / 255.0;
|
||||
QImage half = image(8, 8, red, green, blue);
|
||||
for (int y = 0; y < 8; ++y)
|
||||
for (int x = 0; x < 4; ++x)
|
||||
half.setPixelColor(x, y, Qt::transparent);
|
||||
const CameraRawSettings::Balance solid = CameraRawSettings::autoBalance(image(8, 8, red, green, blue)).value();
|
||||
const CameraRawSettings::Balance cut = CameraRawSettings::autoBalance(half).value();
|
||||
QVERIFY(std::abs(cut.temperature - solid.temperature) < 1e-9 && std::abs(cut.tint - solid.tint) < 1e-9);
|
||||
QVERIFY(!CameraRawSettings::autoBalance(image(8, 8, red, green, blue, 0)));
|
||||
}
|
||||
|
||||
void CameraRawRulesTests::thePreviewShowsItsViewAndCountsTheGrade()
|
||||
{
|
||||
FilterSettings settings;
|
||||
settings.cameraRaw.whites = 100;
|
||||
const QImage picture = pair(230, 20);
|
||||
FilterJob job{FilterKind::cameraRaw, picture, settings, 1, std::nullopt, QTransform()};
|
||||
const auto [graded, gradedScope] = CameraRawScope::preview(job);
|
||||
QVERIFY(pixels(graded) == pixels(settings.cameraRaw.apply(picture)));
|
||||
QVERIFY(gradedScope == CameraRawScope::make(graded).value());
|
||||
job.cameraRawClipping = CameraRawClipping::highlights;
|
||||
const auto [view, viewScope] = CameraRawScope::preview(job);
|
||||
QVERIFY(pixels(view) == pixels(settings.cameraRaw.apply(picture, CameraRawClipping::highlights)));
|
||||
QVERIFY(viewScope == gradedScope);
|
||||
job.cameraRawClipping = std::nullopt;
|
||||
job.showsHighlightClipping = true;
|
||||
const auto [marked, markedScope] = CameraRawScope::preview(job);
|
||||
QVERIFY(pixels(marked) == pixels(CameraRawScope::overlay(graded, false, true)) && pixels(marked) != pixels(graded));
|
||||
QVERIFY(markedScope == gradedScope);
|
||||
}
|
||||
|
||||
void CameraRawRulesTests::aPointColorShowsAloneOnlyWhenAsked()
|
||||
{
|
||||
CameraRawSettings settings;
|
||||
settings.mixer.points = {CameraRawPointColor{.hue = 10}, CameraRawPointColor{.hue = 200, .visualize = true}};
|
||||
CameraRawPanel panel;
|
||||
QCOMPARE(panel.pointColorVisualizeIndex(settings), -1);
|
||||
panel.pointIndex = 1;
|
||||
QCOMPARE(panel.pointColorVisualizeIndex(settings), 1);
|
||||
panel.pointIndex = 2;
|
||||
QCOMPARE(panel.pointColorVisualizeIndex(settings), -1);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawRulesTests)
|
||||
#include "CameraRawRulesTests.moc"
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's CameraRawTests through the session, and geometry.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
namespace {
|
||||
bool awaited(const std::function<void(std::function<void()>)> &start)
|
||||
{
|
||||
bool done = false;
|
||||
start([&done] { done = true; });
|
||||
return QTest::qWaitFor([&done] { return done; }, 20000);
|
||||
}
|
||||
|
||||
std::unique_ptr<EditorSession> opened(const QImage &image)
|
||||
{
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(image.width(), image.height());
|
||||
session->insert(ImportedImage(image, image, QStringLiteral("Picture")));
|
||||
session->beginFilter(FilterKind::cameraRaw);
|
||||
return session;
|
||||
}
|
||||
|
||||
QImage layerImage(const EditorSession &session)
|
||||
{
|
||||
return session.activeLayer().value().asset.value().image();
|
||||
}
|
||||
|
||||
// Swift's `edit.samplesWhiteBalance = true`, through the panel.
|
||||
void arm(EditorSession &session, const std::function<void(CameraRawPanel &)> &change)
|
||||
{
|
||||
CameraRawPanel panel = session.filterEdit().value().rawPanel;
|
||||
change(panel);
|
||||
session.setCameraRawPanel(panel);
|
||||
}
|
||||
}
|
||||
|
||||
class CameraRawSessionTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void eyedropperAndAutoNeutralizeAWarmPixel();
|
||||
void autoWhiteBalanceNeutralizesAfterTheScan();
|
||||
void hiddenGroupIsLeftOutAndOkIsOneUndoOrNone();
|
||||
void hiddenGroupStaysOutOfTheNextOpen();
|
||||
void geometryWarpAndCalibrationPrimaries();
|
||||
void thePreviewCarriesThePanelsViewsAndItsScope();
|
||||
void guidedUprightFollowsADrawnLineAndLeavesAnUnguidedPicture();
|
||||
};
|
||||
|
||||
void CameraRawSessionTests::eyedropperAndAutoNeutralizeAWarmPixel()
|
||||
{
|
||||
const double red = 160 / 255.0, green = 140 / 255.0, blue = 120 / 255.0;
|
||||
const QImage warm = image(8, 8, red, green, blue);
|
||||
const std::array<int, 4> before = pixels(warm)[0];
|
||||
const CameraRawSettings::Balance solved = CameraRawSettings::neutralizeStraight(red, green, blue).value();
|
||||
CameraRawSettings settings;
|
||||
settings.temperature = solved.temperature;
|
||||
settings.tint = solved.tint;
|
||||
QVERIFY(chroma(pixels(settings.apply(warm))[0]) < chroma(before) / 2);
|
||||
const CameraRawSettings::Balance automatic = CameraRawSettings::autoBalance(warm).value();
|
||||
settings.temperature = automatic.temperature;
|
||||
settings.tint = automatic.tint;
|
||||
QVERIFY(chroma(pixels(settings.apply(warm))[0]) < chroma(before) / 2);
|
||||
const auto session = opened(warm);
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.samplesWhiteBalance = true; });
|
||||
session->sampleCameraRawWhiteBalance(QPointF(1, 1));
|
||||
const FilterEdit &edit = session->filterEdit().value();
|
||||
QVERIFY(edit.settings.cameraRaw.whiteBalance == CameraRawWhiteBalance::custom);
|
||||
QVERIFY(edit.settings.cameraRaw.temperature != 0 || edit.settings.cameraRaw.tint != 0);
|
||||
QVERIFY2(chroma(pixels(edit.renderSettings().cameraRaw.apply(warm))[0]) < chroma(before) / 2, "the canvas sample neutralizes");
|
||||
session->cancelFilter();
|
||||
// A transparent pixel is ignored.
|
||||
const auto empty = opened(image(8, 8, 1, 0, 0, 0));
|
||||
const FilterSettings untouched = empty->filterEdit().value().settings;
|
||||
arm(*empty, [](CameraRawPanel &panel) { panel.samplesWhiteBalance = true; });
|
||||
empty->sampleCameraRawWhiteBalance(QPointF(1, 1));
|
||||
QVERIFY(empty->filterEdit().value().settings == untouched);
|
||||
}
|
||||
|
||||
void CameraRawSessionTests::autoWhiteBalanceNeutralizesAfterTheScan()
|
||||
{
|
||||
const QImage warm = image(8, 8, 160 / 255.0, 140 / 255.0, 120 / 255.0);
|
||||
const std::array<int, 4> before = pixels(warm)[0];
|
||||
const auto session = opened(warm);
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session->applyCameraRawAutoWhiteBalance(std::move(done)); }));
|
||||
const CameraRawSettings raw = session->filterEdit().value().settings.cameraRaw;
|
||||
QVERIFY(raw.whiteBalance == CameraRawWhiteBalance::automatic);
|
||||
QVERIFY(chroma(pixels(raw.apply(warm))[0]) < chroma(before) / 2);
|
||||
session->cancelFilter();
|
||||
const auto empty = opened(image(8, 8, 1, 0, 0, 0));
|
||||
QVERIFY(awaited([&](std::function<void()> done) { empty->applyCameraRawAutoWhiteBalance(std::move(done)); }));
|
||||
const CameraRawSettings untouched = empty->filterEdit().value().settings.cameraRaw;
|
||||
QVERIFY(untouched.whiteBalance == CameraRawWhiteBalance::automatic);
|
||||
QVERIFY2(untouched.temperature == 0 && untouched.tint == 0, "a layer with no coverage leaves the sliders alone");
|
||||
}
|
||||
|
||||
void CameraRawSessionTests::hiddenGroupIsLeftOutAndOkIsOneUndoOrNone()
|
||||
{
|
||||
const QImage input = gray(8, 8);
|
||||
CameraRawSettings settings;
|
||||
settings.exposure = 1;
|
||||
settings.temperature = 40;
|
||||
const CameraRawSettings hiddenLight = settings.applying({.light = false});
|
||||
QVERIFY(pixels(hiddenLight.apply(input)) != pixels(input));
|
||||
QVERIFY(hiddenLight.exposure == 0 && hiddenLight.temperature == 40);
|
||||
const CameraRawSettings neither = settings.applying({.light = false, .color = false});
|
||||
QVERIFY(neither.isIdentity() && pixels(neither.apply(input)) == pixels(input));
|
||||
const auto session = opened(input);
|
||||
const auto original = pixels(input);
|
||||
int count = session->history.undoCount();
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session->commitFilter(std::move(done)); }));
|
||||
QVERIFY2(!session->filterEdit() && session->history.undoCount() == count, "an unchanged grade is not an edit");
|
||||
QVERIFY(pixels(layerImage(*session)) == original);
|
||||
// An eye turned off drops that group.
|
||||
const auto exposed = [&] {
|
||||
session->beginFilter(FilterKind::cameraRaw);
|
||||
FilterSettings edited = session->filterEdit().value().settings;
|
||||
edited.cameraRaw.exposure = 1;
|
||||
session->updateFilter(edited, true);
|
||||
};
|
||||
exposed();
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.shows.light = false; });
|
||||
count = session->history.undoCount();
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session->commitFilter(std::move(done)); }));
|
||||
QCOMPARE(session->history.undoCount(), count);
|
||||
QVERIFY(pixels(layerImage(*session)) == original);
|
||||
exposed();
|
||||
session->cancelFilter();
|
||||
QVERIFY(!session->filterEdit() && pixels(layerImage(*session)) == original);
|
||||
exposed();
|
||||
count = session->history.undoCount();
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session->commitFilter(std::move(done)); }));
|
||||
QVERIFY(session->history.undoCount() == count + 1 && session->history.undoName() == "Camera Raw Filter");
|
||||
QVERIFY2(std::abs(pixels(layerImage(*session))[0][0] - 176) <= 2, "OK bakes the grade");
|
||||
}
|
||||
|
||||
void CameraRawSessionTests::hiddenGroupStaysOutOfTheNextOpen()
|
||||
{
|
||||
const QImage input = gray(8, 8);
|
||||
const auto session = opened(input);
|
||||
FilterSettings edited = session->filterEdit().value().settings;
|
||||
edited.cameraRaw.exposure = 1;
|
||||
edited.cameraRaw.temperature = 40;
|
||||
session->updateFilter(edited, true);
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.shows.light = false; });
|
||||
QVERIFY(awaited([&](std::function<void()> done) { session->commitFilter(std::move(done)); }));
|
||||
// OK bakes the grade, the hidden Light left out.
|
||||
CameraRawSettings colour;
|
||||
colour.temperature = 40;
|
||||
QVERIFY(pixels(layerImage(*session)) == pixels(colour.apply(input)));
|
||||
session->beginFilter(FilterKind::cameraRaw);
|
||||
const CameraRawSettings restored = session->filterEdit().value().settings.cameraRaw;
|
||||
QVERIFY(restored.exposure == 0 && restored.temperature == 40);
|
||||
session->cancelFilter();
|
||||
}
|
||||
|
||||
void CameraRawSessionTests::thePreviewCarriesThePanelsViewsAndItsScope()
|
||||
{
|
||||
QImage picture = step();
|
||||
for (int y = 0; y < 4; ++y)
|
||||
for (int x = 0; x < 24; ++x)
|
||||
picture.setPixelColor(x, y, x < 12 ? QColor(5, 5, 5) : QColor(250, 200, 240));
|
||||
const auto session = opened(picture);
|
||||
FilterSettings settings = session->filterEdit().value().settings;
|
||||
settings.cameraRaw.whites = 100;
|
||||
settings.cameraRaw.blacks = -100;
|
||||
session->updateFilter(settings, true);
|
||||
QTRY_VERIFY(!session->filterEdit().value().preparing);
|
||||
const QImage graded = session->filterEdit().value().preparedPreview.value();
|
||||
QVERIFY(pixels(graded) == pixels(settings.cameraRaw.apply(picture)));
|
||||
QVERIFY(session->filterEdit().value().rawPanel.scope == CameraRawScope::make(graded));
|
||||
// The panel's views reach the preview, never the scope.
|
||||
const std::optional<CameraRawScope> scope = CameraRawScope::make(graded);
|
||||
const auto shown = [&](const std::function<void(CameraRawPanel &)> &change) {
|
||||
arm(*session, change);
|
||||
session->updateFilter(session->filterEdit().value().settings, true);
|
||||
[[maybe_unused]] const bool landed = QTest::qWaitFor([&] { return !session->filterEdit().value().preparing; }, 5000);
|
||||
return std::pair(pixels(session->filterEdit().value().preparedPreview.value()), session->filterEdit().value().rawPanel.scope == scope);
|
||||
};
|
||||
const std::vector<std::pair<std::function<void(CameraRawPanel &)>, std::vector<std::array<int, 4>>>> views{
|
||||
{[](CameraRawPanel &p) { p.clipping = CameraRawClipping::highlights; }, pixels(settings.cameraRaw.apply(picture, CameraRawClipping::highlights))},
|
||||
{[](CameraRawPanel &p) { p.clipping.reset(); p.showsHighlightClipping = true; }, pixels(CameraRawScope::overlay(graded, false, true))},
|
||||
{[](CameraRawPanel &p) { p.showsHighlightClipping = false; p.showsShadowClipping = true; }, pixels(CameraRawScope::overlay(graded, true, false))},
|
||||
{[](CameraRawPanel &p) { p.showsShadowClipping = false; p.sharpenMask = true; },
|
||||
pixels(settings.cameraRaw.apply(picture, std::nullopt, 1, 0, -1, true))},
|
||||
};
|
||||
for (size_t index = 0; index < views.size(); ++index) {
|
||||
const auto [preview, scoped] = shown(views[index].first);
|
||||
QVERIFY(preview == views[index].second && scoped);
|
||||
QVERIFY2(preview != pixels(graded), qPrintable(QString::number(index)));
|
||||
}
|
||||
// A chosen point colour shows alone while it asks to.
|
||||
FilterSettings pointed = settings;
|
||||
pointed.cameraRaw.mixer.points = {CameraRawPointColor{.hue = 10, .saturation = 1, .luminance = 0.5, .visualize = true}};
|
||||
session->updateFilter(pointed, true);
|
||||
const auto alone = shown([](CameraRawPanel &p) { p.sharpenMask = false; }).first;
|
||||
QVERIFY(alone == pixels(pointed.cameraRaw.apply(picture, std::nullopt, 1, 0, 0)) && alone != pixels(graded));
|
||||
QVERIFY(session->filterEdit().value().rawPanel.scope == CameraRawScope::make(pointed.cameraRaw.apply(picture)));
|
||||
// Settings arrive normalized: exposure stops at five.
|
||||
pointed.cameraRaw.exposure = 9;
|
||||
session->updateFilter(pointed, true);
|
||||
QCOMPARE(session->filterEdit().value().settings.cameraRaw.exposure, 5.0);
|
||||
session->updateFilter(settings, true);
|
||||
// An eye turned off keeps its group out of view.
|
||||
QVERIFY(shown([](CameraRawPanel &p) { p.sharpenMask = false; p.shows.light = false; }).first == pixels(picture));
|
||||
}
|
||||
|
||||
void CameraRawSessionTests::geometryWarpAndCalibrationPrimaries()
|
||||
{
|
||||
const QImage grid = checker(12, 12);
|
||||
CameraRawSettings settings;
|
||||
settings.geometry.vertical = 40;
|
||||
QVERIFY(pixels(settings.geometry.apply(grid)) != pixels(grid));
|
||||
settings = CameraRawSettings();
|
||||
settings.calibration.redHue = 80;
|
||||
const QImage red = image(4, 4, 1, 0, 0);
|
||||
const std::array<int, 4> before = pixels(red)[0];
|
||||
QVERIFY2(pixels(settings.apply(red))[0] != before, "calibration shifts a pure red");
|
||||
QVERIFY(pixels(settings.applying({.calibration = false}).apply(red))[0] == before);
|
||||
}
|
||||
|
||||
void CameraRawSessionTests::guidedUprightFollowsADrawnLineAndLeavesAnUnguidedPicture()
|
||||
{
|
||||
const QImage cool = image(16, 16, 0.2, 0.45, 0.8), warm = image(16, 16, 0.85, 0.25, 0.15);
|
||||
CameraRawSettings settings;
|
||||
settings.geometry.upright = CameraRawUprightMode::guided;
|
||||
QVERIFY(pixels(settings.apply(cool)) == pixels(cool));
|
||||
QVERIFY(pixels(settings.apply(warm)) == pixels(warm));
|
||||
settings.geometry.guides = {CameraRawGeometryGuide{0.1, 0.15, 0.9, 0.8}};
|
||||
const auto coolGuided = pixels(settings.apply(cool)), warmGuided = pixels(settings.apply(warm));
|
||||
QVERIFY(coolGuided != pixels(cool) && warmGuided != pixels(warm) && coolGuided != warmGuided);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawSessionTests)
|
||||
#include "CameraRawSessionTests.moc"
|
||||
@@ -0,0 +1,310 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include "Document/Filters.h"
|
||||
#include <QtTest>
|
||||
#include <cmath>
|
||||
|
||||
// Swift's CameraRawTests: the grade on pixels, group by group.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
class CameraRawTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void defaultsLeavePixelsAndAlphaAlone();
|
||||
void exposureAddsOneStopAndContrastPivotsAroundMidGray();
|
||||
void tonalSlidersMoveTheEndTheyName();
|
||||
void temperatureWarmsAndTintMovesTowardMagenta();
|
||||
void vibranceFavorsDullColorsAndProtectsSkinWhileSaturationDoesNot();
|
||||
void textureAndClaritySharpenAnEdgeAndLeaveAFlatField();
|
||||
void dehazeDeepensOrLiftsAndKeepsAlpha();
|
||||
void glowIsIdleAtZeroAndHalationFringeIsRedderThanDiffusion();
|
||||
void vignetteDarkensCornersAndHighlightsOnlyWhileDarkening();
|
||||
void grainIsStableAndTheEffectsEyeDropsTheWholeGroup();
|
||||
void histogramFollowsTheGradeAndClippingPaintStaysOffTheResult();
|
||||
void curveMixerAndGradingChangeOnlyTheirOwnTones();
|
||||
void detailSharpeningNoiseAndMaskingPreview();
|
||||
void opticsDistortionDefringeAndDetailEye();
|
||||
};
|
||||
|
||||
void CameraRawTests::defaultsLeavePixelsAndAlphaAlone()
|
||||
{
|
||||
const QImage input = gray(4, 4, 0.5);
|
||||
QVERIFY(pixels(CameraRawSettings().apply(input)) == pixels(input));
|
||||
CameraRawSettings broken;
|
||||
broken.exposure = std::nan("");
|
||||
broken.temperature = 400;
|
||||
QVERIFY(broken.normalized().exposure == 0 && broken.normalized().temperature == 100);
|
||||
QVERIFY(!isImageAdjustment(FilterKind::cameraRaw));
|
||||
}
|
||||
|
||||
void CameraRawTests::exposureAddsOneStopAndContrastPivotsAroundMidGray()
|
||||
{
|
||||
const QImage input = gray();
|
||||
CameraRawSettings settings;
|
||||
settings.exposure = 1;
|
||||
const std::array<int, 4> brighter = pixels(settings.apply(input))[0];
|
||||
QVERIFY2(std::abs(brighter[0] - 176) <= 2, qPrintable(QString::number(brighter[0])));
|
||||
QVERIFY(brighter[0] == brighter[1] && brighter[1] == brighter[2]);
|
||||
const QImage translucent = gray(4, 4, 0.5);
|
||||
QCOMPARE(pixels(settings.apply(translucent))[0][3], pixels(translucent)[0][3]);
|
||||
settings = CameraRawSettings();
|
||||
settings.contrast = 100;
|
||||
const auto pushed = pixels(settings.apply(pair(64, 192)));
|
||||
QVERIFY2(pushed[0][0] < 10 && pushed[1][0] > 250, "contrast +100 drives the pair apart");
|
||||
settings.contrast = -100;
|
||||
const auto flat = pixels(settings.apply(pair(64, 192)));
|
||||
QVERIFY2(std::abs(flat[0][0] - 128) <= 2 && std::abs(flat[1][0] - 128) <= 2, "contrast −100 meets at mid gray");
|
||||
}
|
||||
|
||||
void CameraRawTests::tonalSlidersMoveTheEndTheyName()
|
||||
{
|
||||
const QImage brightAndMid = pair(230, 128);
|
||||
CameraRawSettings settings;
|
||||
settings.highlights = -100;
|
||||
const auto recovered = pixels(settings.apply(brightAndMid));
|
||||
QVERIFY(recovered[0][0] < 200 && std::abs(recovered[1][0] - 128) <= 2);
|
||||
settings = CameraRawSettings();
|
||||
settings.whites = 100;
|
||||
const auto clipped = pixels(settings.apply(brightAndMid));
|
||||
QVERIFY(clipped[0][0] == 255 && std::abs(clipped[1][0] - 128) <= 2);
|
||||
const auto view = pixels(settings.apply(brightAndMid, CameraRawClipping::highlights));
|
||||
QVERIFY2((view[0] == std::array{255, 255, 255, 255}) && (view[1] == std::array{0, 0, 0, 255}), "highlight clipping is not the grade");
|
||||
const QImage darkAndMid = pair(20, 128);
|
||||
settings = CameraRawSettings();
|
||||
settings.shadows = 100;
|
||||
const auto opened = pixels(settings.apply(darkAndMid));
|
||||
QVERIFY(opened[0][0] > 50 && std::abs(opened[1][0] - 128) <= 2);
|
||||
settings = CameraRawSettings();
|
||||
settings.blacks = -100;
|
||||
const auto crushed = pixels(settings.apply(darkAndMid));
|
||||
QVERIFY(crushed[0][0] < 20 && std::abs(crushed[1][0] - 128) <= 2);
|
||||
const auto shadowView = pixels(settings.apply(darkAndMid, CameraRawClipping::shadows));
|
||||
QVERIFY2((shadowView[0] == std::array{0, 0, 0, 255}) && (shadowView[1] == std::array{255, 255, 255, 255}), "shadow clipping is not the grade");
|
||||
}
|
||||
|
||||
void CameraRawTests::temperatureWarmsAndTintMovesTowardMagenta()
|
||||
{
|
||||
const QImage input = gray();
|
||||
CameraRawSettings settings;
|
||||
settings.temperature = 100;
|
||||
const std::array<int, 4> warm = pixels(settings.apply(input))[0];
|
||||
QVERIFY(warm[0] > 128 && warm[2] < 128 && warm[0] > warm[2]);
|
||||
settings = CameraRawSettings();
|
||||
settings.tint = 100;
|
||||
const std::array<int, 4> magenta = pixels(settings.apply(input))[0];
|
||||
QVERIFY(magenta[1] < 128 && magenta[1] < magenta[0] && magenta[1] < magenta[2]);
|
||||
}
|
||||
|
||||
void CameraRawTests::vibranceFavorsDullColorsAndProtectsSkinWhileSaturationDoesNot()
|
||||
{
|
||||
const QImage dullGreen = image(4, 4, 77 / 255.0, 153 / 255.0, 77 / 255.0), saturatedGreen = image(4, 4, 20 / 255.0, 200 / 255.0, 20 / 255.0);
|
||||
const QImage skin = image(4, 4, 153 / 255.0, 115 / 255.0, 77 / 255.0);
|
||||
CameraRawSettings settings;
|
||||
settings.vibrance = 100;
|
||||
const int dullBefore = chroma(pixels(dullGreen)[0]), saturatedBefore = chroma(pixels(saturatedGreen)[0]), skinBefore = chroma(pixels(skin)[0]);
|
||||
const int dullDelta = chroma(pixels(settings.apply(dullGreen))[0]) - dullBefore;
|
||||
const int saturatedDelta = chroma(pixels(settings.apply(saturatedGreen))[0]) - saturatedBefore;
|
||||
const int skinDelta = chroma(pixels(settings.apply(skin))[0]) - skinBefore;
|
||||
QVERIFY(dullDelta > saturatedDelta + 10);
|
||||
QCOMPARE(dullBefore, skinBefore);
|
||||
QVERIFY(dullDelta > skinDelta + 10);
|
||||
const QImage red = image(4, 4, 160 / 255.0, 120 / 255.0, 120 / 255.0), blue = image(4, 4, 100 / 255.0, 100 / 255.0, 140 / 255.0);
|
||||
settings = CameraRawSettings();
|
||||
settings.saturation = 100;
|
||||
const double redRatio = double(chroma(pixels(settings.apply(red))[0])) / chroma(pixels(red)[0]);
|
||||
const double blueRatio = double(chroma(pixels(settings.apply(blue))[0])) / chroma(pixels(blue)[0]);
|
||||
QVERIFY2(std::abs(redRatio - 2) < 0.15 && std::abs(blueRatio - 2) < 0.15, "saturation doubles both");
|
||||
}
|
||||
|
||||
void CameraRawTests::textureAndClaritySharpenAnEdgeAndLeaveAFlatField()
|
||||
{
|
||||
const QImage flat = gray();
|
||||
CameraRawSettings settings;
|
||||
settings.texture = 100;
|
||||
settings.clarity = 100;
|
||||
QVERIFY2(pixels(settings.apply(flat)) == pixels(flat), "a flat field has no local contrast");
|
||||
const QImage edge = step();
|
||||
const auto original = pixels(edge);
|
||||
settings = CameraRawSettings();
|
||||
settings.texture = 100;
|
||||
const auto textured = pixels(settings.apply(edge));
|
||||
QCOMPARE(textured[0][0], original[0][0]);
|
||||
QVERIFY2(textured[8][0] == original[8][0], "texture's fine radius does not reach this far");
|
||||
QVERIFY(textured[11][0] != original[11][0]);
|
||||
settings = CameraRawSettings();
|
||||
settings.clarity = 100;
|
||||
const auto clarified = pixels(settings.apply(edge));
|
||||
QCOMPARE(clarified[0][0], original[0][0]);
|
||||
QVERIFY2(clarified[8][0] != original[8][0], "clarity's wider radius reaches further in");
|
||||
settings.clarity = -100;
|
||||
const auto softened = pixels(settings.apply(edge));
|
||||
QVERIFY(std::abs(softened[12][0] - softened[11][0]) < std::abs(original[12][0] - original[11][0]));
|
||||
}
|
||||
|
||||
void CameraRawTests::dehazeDeepensOrLiftsAndKeepsAlpha()
|
||||
{
|
||||
const QImage dark = image(4, 4, 30 / 255.0, 30 / 255.0, 30 / 255.0), pale = image(4, 4, 180 / 255.0, 150 / 255.0, 150 / 255.0);
|
||||
CameraRawSettings settings;
|
||||
settings.dehaze = 100;
|
||||
QVERIFY(pixels(settings.apply(dark))[0][0] < 30);
|
||||
const std::array<int, 4> paleBefore = pixels(pale)[0];
|
||||
QVERIFY(chroma(pixels(settings.apply(pale))[0]) > chroma(paleBefore));
|
||||
settings.dehaze = -100;
|
||||
QVERIFY(pixels(settings.apply(dark))[0][0] > 30);
|
||||
QVERIFY(chroma(pixels(settings.apply(pale))[0]) < chroma(paleBefore));
|
||||
const QImage translucent = image(4, 4, 30 / 255.0, 30 / 255.0, 30 / 255.0, 0.5);
|
||||
settings.dehaze = 100;
|
||||
QCOMPARE(pixels(settings.apply(translucent))[0][3], pixels(translucent)[0][3]);
|
||||
}
|
||||
|
||||
void CameraRawTests::glowIsIdleAtZeroAndHalationFringeIsRedderThanDiffusion()
|
||||
{
|
||||
QImage spot = image(21, 21, 0, 0, 0);
|
||||
for (int y = 8; y < 13; ++y)
|
||||
for (int x = 8; x < 13; ++x)
|
||||
spot.setPixelColor(x, y, Qt::white);
|
||||
CameraRawSettings settings;
|
||||
settings.glowWarmth = 100;
|
||||
settings.glowRange = 100;
|
||||
QVERIFY2(pixels(settings.apply(spot)) == pixels(spot), "range and warmth do nothing until Glow is raised");
|
||||
settings.glow = 100;
|
||||
settings.glowStyle = CameraRawGlowStyle::diffusion;
|
||||
const auto diffusion = pixels(settings.apply(spot));
|
||||
settings.glowStyle = CameraRawGlowStyle::halation;
|
||||
const auto halation = pixels(settings.apply(spot));
|
||||
const size_t fringe = 10 * 21 + 15;
|
||||
QVERIFY(diffusion[fringe][0] > diffusion[0][0]);
|
||||
QVERIFY((halation[0] == std::array{0, 0, 0, 255}));
|
||||
QVERIFY(halation[fringe][0] - halation[fringe][1] > diffusion[fringe][0] - diffusion[fringe][1]);
|
||||
}
|
||||
|
||||
void CameraRawTests::vignetteDarkensCornersAndHighlightsOnlyWhileDarkening()
|
||||
{
|
||||
const QImage field = gray(9, 9);
|
||||
CameraRawSettings settings;
|
||||
settings.vignetteAmount = -100;
|
||||
const auto darkened = pixels(settings.apply(field));
|
||||
QVERIFY(std::abs(darkened[4 * 9 + 4][0] - 128) <= 2 && darkened[0][0] < darkened[4 * 9 + 4][0] - 40);
|
||||
const QImage white = image(9, 9, 1, 1, 1);
|
||||
settings.vignetteHighlights = 100;
|
||||
settings.vignetteStyle = CameraRawVignetteStyle::highlightPriority;
|
||||
const int kept = pixels(settings.apply(white))[0][0];
|
||||
settings.vignetteHighlights = 0;
|
||||
const int exposed = pixels(settings.apply(white))[0][0];
|
||||
QVERIFY(kept > exposed + 40);
|
||||
settings.vignetteHighlights = 100;
|
||||
settings.vignetteStyle = CameraRawVignetteStyle::paintOverlay;
|
||||
QVERIFY2(pixels(settings.apply(white))[0][0] < kept, "paint overlay does not use Highlights");
|
||||
settings = CameraRawSettings();
|
||||
settings.vignetteAmount = 100;
|
||||
const auto plain = pixels(settings.apply(field));
|
||||
settings.vignetteHighlights = 100;
|
||||
QVERIFY2(plain == pixels(settings.apply(field)), "highlights is idle while the vignette lightens");
|
||||
}
|
||||
|
||||
void CameraRawTests::grainIsStableAndTheEffectsEyeDropsTheWholeGroup()
|
||||
{
|
||||
const QImage field = gray(16, 16);
|
||||
CameraRawSettings settings;
|
||||
settings.grainSize = 40;
|
||||
settings.grainRoughness = 80;
|
||||
QVERIFY(pixels(settings.apply(field, std::nullopt, 1, 4)) == pixels(field));
|
||||
settings.grainAmount = 70;
|
||||
const auto first = pixels(settings.apply(field, std::nullopt, 1, 4));
|
||||
QVERIFY(first == pixels(settings.apply(field, std::nullopt, 1, 4)));
|
||||
QVERIFY(first != pixels(field));
|
||||
QVERIFY(first[0][0] == first[0][1] && first[0][1] == first[0][2]);
|
||||
QCOMPARE(pixels(settings.apply(image(4, 4, 0.5, 0.5, 0.5, 0), std::nullopt, 1, 4))[0][3], 0);
|
||||
const QImage edge = step();
|
||||
settings = CameraRawSettings();
|
||||
settings.texture = 100;
|
||||
settings.grainAmount = 50;
|
||||
const CameraRawSettings hidden = settings.applying({.effects = false});
|
||||
QVERIFY(hidden.isIdentity());
|
||||
QVERIFY(pixels(hidden.apply(edge, std::nullopt, 1, 2)) == pixels(edge));
|
||||
QVERIFY(pixels(settings.apply(edge, std::nullopt, 1, 2)) != pixels(edge));
|
||||
}
|
||||
|
||||
void CameraRawTests::histogramFollowsTheGradeAndClippingPaintStaysOffTheResult()
|
||||
{
|
||||
const QImage black = image(4, 4, 0, 0, 0), white = image(4, 4, 1, 1, 1);
|
||||
QCOMPARE(peakIndex(CameraRawScope::make(black).value().red), 0);
|
||||
QCOMPARE(peakIndex(CameraRawScope::make(white).value().red), 255);
|
||||
CameraRawSettings settings;
|
||||
settings.exposure = 1;
|
||||
QVERIFY(peakIndex(CameraRawScope::make(settings.apply(gray())).value().red) > 128);
|
||||
const std::array<int, 4> shadowed = pixels(CameraRawScope::overlay(black, true, false))[0];
|
||||
QVERIFY(shadowed[2] > shadowed[0]);
|
||||
const std::array<int, 4> highlighted = pixels(CameraRawScope::overlay(white, false, true))[0];
|
||||
QVERIFY(highlighted[0] > highlighted[2]);
|
||||
QVERIFY(pixels(CameraRawScope::overlay(black, false, false)) == pixels(black));
|
||||
const CameraRawScope scope = CameraRawScope::make(image(4, 4, 1, 0, 0)).value();
|
||||
const auto hottest = std::max_element(scope.vectorscope.begin(), scope.vectorscope.end()) - scope.vectorscope.begin();
|
||||
QVERIFY2(hottest % CameraRawScope::scopeSide > CameraRawScope::scopeSide / 2, "red sits on the right of the vectorscope");
|
||||
}
|
||||
|
||||
void CameraRawTests::curveMixerAndGradingChangeOnlyTheirOwnTones()
|
||||
{
|
||||
const QImage dark = image(4, 4, 0.12, 0.12, 0.12), light = image(4, 4, 0.62, 0.62, 0.62);
|
||||
CameraRawSettings settings;
|
||||
settings.curve.shadows = 100;
|
||||
const int darkGain = pixels(settings.apply(dark))[0][0] - int(std::lround(0.12 * 255));
|
||||
const int lightGain = pixels(settings.apply(light))[0][0] - int(std::lround(0.62 * 255));
|
||||
QVERIFY(darkGain > lightGain + 8);
|
||||
settings = CameraRawSettings();
|
||||
settings.curve.rgb = CameraRawCurveSettings::strongContrast();
|
||||
QVERIFY(pixels(settings.apply(image(4, 4, 0.25, 0.25, 0.25)))[0][0] < 55);
|
||||
settings = CameraRawSettings();
|
||||
settings.mixer.hue[0] = 100;
|
||||
const std::array<int, 4> shifted = pixels(settings.apply(image(4, 4, 1, 0, 0)))[0];
|
||||
QVERIFY(shifted[1] > shifted[2]);
|
||||
settings = CameraRawSettings();
|
||||
settings.grading.shadows.saturation = 100;
|
||||
const std::array<int, 4> gradedDark = pixels(settings.apply(dark))[0];
|
||||
const std::array<int, 4> gradedLight = pixels(settings.apply(image(4, 4, 1, 1, 1)))[0];
|
||||
QVERIFY(gradedDark[0] > gradedDark[1] + 5 && std::abs(gradedLight[0] - gradedLight[1]) <= 2);
|
||||
settings.grading.balance = 100;
|
||||
const std::array<int, 4> balanced = pixels(settings.apply(dark))[0];
|
||||
QVERIFY(balanced[0] - balanced[1] < gradedDark[0] - gradedDark[1]);
|
||||
settings = CameraRawSettings();
|
||||
settings.curve.shadows = 100;
|
||||
QVERIFY(pixels(settings.applying({.curve = false}).apply(dark)) == pixels(dark));
|
||||
}
|
||||
|
||||
void CameraRawTests::detailSharpeningNoiseAndMaskingPreview()
|
||||
{
|
||||
const QImage edge = step();
|
||||
CameraRawSettings settings;
|
||||
settings.detail.sharpenAmount = 150;
|
||||
settings.detail.sharpenRadius = 50;
|
||||
QVERIFY(pixels(settings.apply(edge)) != pixels(edge));
|
||||
settings = CameraRawSettings();
|
||||
settings.detail.noiseLuminance = 80;
|
||||
QVERIFY2(pixels(settings.apply(gray(8, 8))) == pixels(gray(8, 8)), "luminance NR leaves a flat field alone");
|
||||
settings.detail.sharpenMasking = 50;
|
||||
const auto mask = pixels(settings.apply(edge, std::nullopt, 1, 0, -1, true));
|
||||
QVERIFY(std::all_of(mask.begin(), mask.end(), [](const std::array<int, 4> &p) { return p[0] == p[1] && p[1] == p[2]; }));
|
||||
QVERIFY(settings.detail.adjusts());
|
||||
}
|
||||
|
||||
void CameraRawTests::opticsDistortionDefringeAndDetailEye()
|
||||
{
|
||||
const QImage stepped = checker();
|
||||
CameraRawSettings settings;
|
||||
settings.optics.distortion = 100;
|
||||
QVERIFY2(pixels(settings.apply(stepped)) != pixels(stepped), "distortion resamples pixels");
|
||||
const QImage purple = image(4, 4, 0.8, 0.2, 0.9);
|
||||
settings = CameraRawSettings();
|
||||
settings.optics.purpleAmount = 100;
|
||||
settings.optics.purpleHueLow = 250;
|
||||
settings.optics.purpleHueHigh = 320;
|
||||
QVERIFY(chroma(pixels(settings.apply(purple))[0]) < chroma(pixels(purple)[0]));
|
||||
settings = CameraRawSettings();
|
||||
settings.optics.removeChromaticAberration = true;
|
||||
QVERIFY(settings.optics.adjusts());
|
||||
settings = CameraRawSettings();
|
||||
settings.detail.sharpenAmount = 40;
|
||||
QVERIFY(pixels(settings.applying({.detail = false}).apply(step())) == pixels(step()));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawTests)
|
||||
#include "CameraRawTests.moc"
|
||||
@@ -0,0 +1,285 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include <QSignalSpy>
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's Camera Raw session tools: eyedroppers, drags, guides.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
namespace {
|
||||
std::unique_ptr<EditorSession> opened(const QImage &image)
|
||||
{
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(image.width(), image.height());
|
||||
session->insert(ImportedImage(image, image, QStringLiteral("Picture")));
|
||||
session->beginFilter(FilterKind::cameraRaw);
|
||||
return session;
|
||||
}
|
||||
|
||||
void arm(EditorSession &session, const std::function<void(CameraRawPanel &)> &change)
|
||||
{
|
||||
CameraRawPanel panel = session.filterEdit().value().rawPanel;
|
||||
change(panel);
|
||||
session.setCameraRawPanel(panel);
|
||||
}
|
||||
|
||||
const CameraRawSettings &raw(const EditorSession &session)
|
||||
{
|
||||
return session.filterEdit().value().settings.cameraRaw;
|
||||
}
|
||||
}
|
||||
|
||||
class CameraRawToolsTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theDefringeEyedropperCentresTheNearerRange();
|
||||
void theEyedroppersNeedArmingAPixelAndAnOpenEdit();
|
||||
void theReadoutFollowsTheAdjustedPreview();
|
||||
void aTargetedDragMovesTheCurveOrTheMixer();
|
||||
void pointColorsArePickedAndReplaced();
|
||||
void aDrawnGuideTurnsUprightGuided();
|
||||
void theWhiteBalanceToolsKeepTheirMode();
|
||||
void samplesReadEveryHueAndStayWithTheirKind();
|
||||
};
|
||||
|
||||
void CameraRawToolsTests::theDefringeEyedropperCentresTheNearerRange()
|
||||
{
|
||||
const auto purple = opened(image(8, 8, 0.8, 0.2, 0.9));
|
||||
arm(*purple, [](CameraRawPanel &panel) { panel.samplesDefringe = true; });
|
||||
purple->sampleCameraRawDefringe(QPointF(2, 2));
|
||||
const double hue = (4 + 0.6 / 0.7) * 60;
|
||||
QVERIFY(std::abs(raw(*purple).optics.purpleHueLow - (hue - 25)) < 0.5 && std::abs(raw(*purple).optics.purpleHueHigh - (hue + 25)) < 0.5);
|
||||
QCOMPARE(raw(*purple).optics.purpleAmount, 50.0);
|
||||
QCOMPARE(raw(*purple).optics.greenAmount, 0.0);
|
||||
// A set amount stays; green fringe moves the green range.
|
||||
FilterSettings settings = purple->filterEdit().value().settings;
|
||||
settings.cameraRaw.optics.purpleAmount = 80;
|
||||
purple->updateFilter(settings, true);
|
||||
purple->sampleCameraRawDefringe(QPointF(2, 2));
|
||||
QCOMPARE(raw(*purple).optics.purpleAmount, 80.0);
|
||||
const auto green = opened(image(8, 8, 0.4, 0.8, 0.2));
|
||||
arm(*green, [](CameraRawPanel &panel) { panel.samplesDefringe = true; });
|
||||
green->sampleCameraRawDefringe(QPointF(2, 2));
|
||||
const double greenHue = (2 + (0.2 - 0.4) / 0.6) * 60;
|
||||
QVERIFY(std::abs(raw(*green).optics.greenHueLow - (greenHue - 25)) < 0.5 && std::abs(raw(*green).optics.greenHueHigh - (greenHue + 25)) < 0.5);
|
||||
QCOMPARE(raw(*green).optics.greenAmount, 50.0);
|
||||
QVERIFY(raw(*green).optics.purpleHueLow == 270 && raw(*green).optics.purpleAmount == 0);
|
||||
// Past the wheel's end the range is held within 360.
|
||||
const auto rose = opened(image(8, 8, 1, 0, 64 / 255.0));
|
||||
arm(*rose, [](CameraRawPanel &panel) { panel.samplesDefringe = true; });
|
||||
rose->sampleCameraRawDefringe(QPointF(2, 2));
|
||||
QCOMPARE(raw(*rose).optics.purpleHueHigh, 360.0);
|
||||
}
|
||||
|
||||
void CameraRawToolsTests::theEyedroppersNeedArmingAPixelAndAnOpenEdit()
|
||||
{
|
||||
const auto session = opened(image(8, 8, 0.8, 0.2, 0.9));
|
||||
const FilterSettings untouched = session->filterEdit().value().settings;
|
||||
// Unarmed, outside the document or on a clear pixel: nothing.
|
||||
session->sampleCameraRawDefringe(QPointF(2, 2));
|
||||
session->sampleCameraRawWhiteBalance(QPointF(2, 2));
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.samplesDefringe = panel.samplesWhiteBalance = true; });
|
||||
for (const QPointF outside : {QPointF(-0.5, 2), QPointF(8, 2), QPointF(2, 8)}) {
|
||||
session->sampleCameraRawDefringe(outside);
|
||||
session->sampleCameraRawWhiteBalance(outside);
|
||||
}
|
||||
QVERIFY(session->filterEdit().value().settings == untouched);
|
||||
const auto clear = opened(image(8, 8, 0.8, 0.2, 0.9, 0));
|
||||
arm(*clear, [](CameraRawPanel &panel) { panel.samplesDefringe = true; });
|
||||
clear->sampleCameraRawDefringe(QPointF(2, 2));
|
||||
QVERIFY(clear->filterEdit().value().settings.cameraRaw == CameraRawSettings());
|
||||
// The panel's setter announces; with no edit it is silent.
|
||||
QSignalSpy changes(session.get(), &EditorSession::changed);
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.mixerSwatch = 3; });
|
||||
QCOMPARE(changes.count(), 1);
|
||||
QCOMPARE(session->filterEdit().value().rawPanel.mixerSwatch, 3);
|
||||
session->cancelFilter();
|
||||
changes.clear();
|
||||
session->setCameraRawPanel(CameraRawPanel());
|
||||
QCOMPARE(changes.count(), 0);
|
||||
// A clear pixel reads nothing.
|
||||
clear->updateCameraRawReadout(QPointF(2, 2));
|
||||
QVERIFY(!clear->filterEdit().value().rawPanel.readout);
|
||||
}
|
||||
|
||||
void CameraRawToolsTests::theReadoutFollowsTheAdjustedPreview()
|
||||
{
|
||||
const auto session = opened(image(8, 8, 160 / 255.0, 140 / 255.0, 120 / 255.0));
|
||||
QSignalSpy changes(session.get(), &EditorSession::changed);
|
||||
session->updateCameraRawReadout(QPointF(3, 3));
|
||||
QCOMPARE(session->filterEdit().value().rawPanel.readout, std::optional(std::array{160, 140, 120}));
|
||||
QCOMPARE(changes.count(), 1);
|
||||
// The same pixel again says nothing; past the layer, none.
|
||||
session->updateCameraRawReadout(QPointF(4, 4));
|
||||
QCOMPARE(changes.count(), 1);
|
||||
session->updateCameraRawReadout(QPointF(9, 3));
|
||||
QVERIFY(!session->filterEdit().value().rawPanel.readout);
|
||||
// Once a preview lands it reads the adjusted pixels.
|
||||
FilterSettings settings = session->filterEdit().value().settings;
|
||||
settings.cameraRaw.exposure = 1;
|
||||
session->updateFilter(settings, true);
|
||||
QTRY_VERIFY(!session->filterEdit().value().preparing);
|
||||
session->updateCameraRawReadout(QPointF(3, 3));
|
||||
const std::array<int, 3> read = session->filterEdit().value().rawPanel.readout.value();
|
||||
const std::array<int, 4> expected = pixels(session->filterEdit().value().preparedPreview.value())[0];
|
||||
QVERIFY(read[0] == expected[0] && read[1] == expected[1] && read[2] == expected[2] && read[0] > 160);
|
||||
}
|
||||
|
||||
void CameraRawToolsTests::aTargetedDragMovesTheCurveOrTheMixer()
|
||||
{
|
||||
const auto session = opened(gray(8, 8));
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.targetsCurve = true; });
|
||||
// Mid gray is a light; a hundred points up, 35.
|
||||
session->beginCameraRawDrag(QPointF(2, 5));
|
||||
session->dragCameraRaw(QPointF(2, -95));
|
||||
QCOMPARE(raw(*session).curve.lights, 35.0);
|
||||
QCOMPARE(raw(*session).curve.darks, 0.0);
|
||||
// Each step measures from the press, not the last step.
|
||||
session->dragCameraRaw(QPointF(2, -1000));
|
||||
QCOMPARE(raw(*session).curve.lights, 100.0);
|
||||
session->dragCameraRaw(QPointF(2, 105));
|
||||
QCOMPARE(raw(*session).curve.lights, -35.0);
|
||||
// A drag starts over from the settings at its press.
|
||||
FilterSettings moved = session->filterEdit().value().settings;
|
||||
moved.cameraRaw.exposure = 1;
|
||||
session->updateFilter(moved, true);
|
||||
session->dragCameraRaw(QPointF(2, 5));
|
||||
QVERIFY(raw(*session).exposure == 0 && raw(*session).curve.lights == 0);
|
||||
// The point page nudges the nearest point, a hundredth.
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.curvePage = CameraRawCurvePage::point; panel.pointChannel = CameraRawPointChannel::red; });
|
||||
session->beginCameraRawDrag(QPointF(2, 5));
|
||||
session->dragCameraRaw(QPointF(2, 105));
|
||||
QCOMPARE(raw(*session).curve.red, (std::vector<CurvePoint>{{0, 0}, {1, 0.65}}));
|
||||
QVERIFY(raw(*session).curve.rgb == CameraRawCurveSettings::linear());
|
||||
// The mixer's tab shares the drag by each family's weight.
|
||||
const auto red = opened(image(8, 8, 1, 0, 0));
|
||||
arm(*red, [](CameraRawPanel &panel) { panel.targetsMixer = true; panel.mixerTab = CameraRawMixerTab::saturation; });
|
||||
red->beginCameraRawDrag(QPointF(2, 5));
|
||||
red->dragCameraRaw(QPointF(2, -35));
|
||||
QCOMPARE(raw(*red).mixer.saturation[0], 14.0);
|
||||
QCOMPARE(raw(*red).mixer.saturation[1], 3.5);
|
||||
QVERIFY(raw(*red).mixer.hue[0] == 0 && raw(*red).mixer.saturation[2] == 0);
|
||||
// Without a drag begun, a drag does nothing.
|
||||
const auto idle = opened(gray(8, 8));
|
||||
arm(*idle, [](CameraRawPanel &panel) { panel.targetsCurve = true; });
|
||||
idle->dragCameraRaw(QPointF(2, -95));
|
||||
QVERIFY(raw(*idle) == CameraRawSettings());
|
||||
}
|
||||
|
||||
void CameraRawToolsTests::pointColorsArePickedAndReplaced()
|
||||
{
|
||||
const auto session = opened(image(8, 8, 1, 0.5, 0));
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.samplesPointColor = true; panel.pointIndex = -1; });
|
||||
session->sampleCameraRawPointColor(QPointF(1, 1));
|
||||
QCOMPARE(raw(*session).mixer.points.size(), size_t(1));
|
||||
const CameraRawPointColor picked = raw(*session).mixer.points[0];
|
||||
QVERIFY(std::abs(picked.hue - 30) < 0.5 && std::abs(picked.saturation - 1) < 0.01 && std::abs(picked.luminance - 0.5) < 0.01);
|
||||
QCOMPARE(session->filterEdit().value().rawPanel.pointIndex, 0);
|
||||
// Picked again, the chosen point takes it, keeping shifts.
|
||||
FilterSettings settings = session->filterEdit().value().settings;
|
||||
settings.cameraRaw.mixer.points[0].hueShift = 20;
|
||||
settings.cameraRaw.mixer.points[0].saturationShift = -30;
|
||||
settings.cameraRaw.mixer.points[0].luminanceShift = 40;
|
||||
session->updateFilter(settings, true);
|
||||
session->sampleCameraRawPointColor(QPointF(1, 1));
|
||||
QCOMPARE(raw(*session).mixer.points.size(), size_t(1));
|
||||
const CameraRawPointColor kept = raw(*session).mixer.points[0];
|
||||
QVERIFY(kept.hueShift == 20 && kept.saturationShift == -30 && kept.luminanceShift == 40);
|
||||
// A pick chooses its addition; with none chosen, eight.
|
||||
for (int pick = 0; pick < 9; ++pick) {
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.pointIndex = -1; });
|
||||
session->sampleCameraRawPointColor(QPointF(1, 1));
|
||||
}
|
||||
QCOMPARE(raw(*session).mixer.points.size(), size_t(8));
|
||||
QCOMPARE(session->filterEdit().value().rawPanel.pointIndex, -1);
|
||||
// A hue past red's end reads round the wheel.
|
||||
const auto rose = opened(image(8, 8, 1, 0, 64 / 255.0));
|
||||
arm(*rose, [](CameraRawPanel &panel) { panel.samplesPointColor = true; });
|
||||
rose->sampleCameraRawPointColor(QPointF(1, 1));
|
||||
QVERIFY(std::abs(raw(*rose).mixer.points.at(0).hue - (360 - 60 * 64 / 255.0)) < 0.01);
|
||||
}
|
||||
|
||||
void CameraRawToolsTests::aDrawnGuideTurnsUprightGuided()
|
||||
{
|
||||
const auto session = opened(gray(8, 8));
|
||||
session->beginCameraRawGeometryGuide(QPointF(2, 2));
|
||||
QVERIFY(!session->filterEdit().value().rawPanel.guideDraft);
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.drawingGeometryGuide = true; });
|
||||
session->beginCameraRawGeometryGuide(QPointF(-1, 2));
|
||||
QVERIFY(!session->filterEdit().value().rawPanel.guideDraft);
|
||||
session->beginCameraRawGeometryGuide(QPointF(2, 2));
|
||||
session->continueCameraRawGeometryGuide(QPointF(6, 4));
|
||||
// A point past the preview leaves the draft alone.
|
||||
session->continueCameraRawGeometryGuide(QPointF(9, 4));
|
||||
QCOMPARE(session->filterEdit().value().rawPanel.guideDraft, std::optional(std::pair(QPointF(0.25, 0.25), QPointF(0.75, 0.5))));
|
||||
session->commitCameraRawGeometryGuide();
|
||||
QVERIFY(!session->filterEdit().value().rawPanel.guideDraft);
|
||||
QVERIFY(raw(*session).geometry.upright == CameraRawUprightMode::guided);
|
||||
QCOMPARE(raw(*session).geometry.guides, (std::vector<CameraRawGeometryGuide>{{0.25, 0.25, 0.75, 0.5}}));
|
||||
}
|
||||
|
||||
void CameraRawToolsTests::theWhiteBalanceToolsKeepTheirMode()
|
||||
{
|
||||
const QImage warm = image(8, 8, 160 / 255.0, 140 / 255.0, 120 / 255.0);
|
||||
// The eyedropper sets Custom over Auto.
|
||||
const auto sampled = opened(warm);
|
||||
FilterSettings automatic = sampled->filterEdit().value().settings;
|
||||
automatic.cameraRaw.whiteBalance = CameraRawWhiteBalance::automatic;
|
||||
sampled->updateFilter(automatic, true);
|
||||
arm(*sampled, [](CameraRawPanel &panel) { panel.samplesWhiteBalance = true; });
|
||||
sampled->sampleCameraRawWhiteBalance(QPointF(1, 1));
|
||||
QVERIFY(raw(*sampled).whiteBalance == CameraRawWhiteBalance::custom);
|
||||
// Custom chosen before the scan lands keeps the sliders.
|
||||
const auto changed = opened(warm);
|
||||
bool done = false;
|
||||
changed->applyCameraRawAutoWhiteBalance([&done] { done = true; });
|
||||
FilterSettings custom = changed->filterEdit().value().settings;
|
||||
custom.cameraRaw.whiteBalance = CameraRawWhiteBalance::custom;
|
||||
changed->updateFilter(custom, true);
|
||||
QTRY_VERIFY(done);
|
||||
QVERIFY(raw(*changed).temperature == 0 && raw(*changed).tint == 0);
|
||||
}
|
||||
|
||||
void CameraRawToolsTests::samplesReadEveryHueAndStayWithTheirKind()
|
||||
{
|
||||
// Green and blue on top take their own hue branches.
|
||||
const auto hueOf = [](double red, double green, double blue) {
|
||||
const auto session = opened(image(8, 8, red, green, blue));
|
||||
arm(*session, [](CameraRawPanel &panel) { panel.samplesPointColor = true; });
|
||||
session->sampleCameraRawPointColor(QPointF(1, 1));
|
||||
return raw(*session).mixer.points.at(0).hue;
|
||||
};
|
||||
QVERIFY(std::abs(hueOf(0.2, 0.8, 0.4) - (2 + 0.2 / 0.6) * 60) < 0.5);
|
||||
QVERIFY(std::abs(hueOf(0.3, 0.2, 0.9) - (4 + 0.1 / 0.7) * 60) < 0.5);
|
||||
// The Luminance tab moves luminance alone.
|
||||
const auto red = opened(image(8, 8, 1, 0, 0));
|
||||
arm(*red, [](CameraRawPanel &panel) { panel.targetsMixer = true; panel.mixerTab = CameraRawMixerTab::luminance; });
|
||||
red->beginCameraRawDrag(QPointF(2, 5));
|
||||
red->dragCameraRaw(QPointF(2, -35));
|
||||
QVERIFY(raw(*red).mixer.luminance[0] > 0 && raw(*red).mixer.hue[0] == 0 && raw(*red).mixer.saturation[0] == 0);
|
||||
// Rec. 709 tone: pure green is a light.
|
||||
const auto green = opened(image(8, 8, 0, 1, 0));
|
||||
arm(*green, [](CameraRawPanel &panel) { panel.targetsCurve = true; });
|
||||
green->beginCameraRawDrag(QPointF(2, 5));
|
||||
green->dragCameraRaw(QPointF(2, -95));
|
||||
QVERIFY(raw(*green).curve.lights == 35 && raw(*green).curve.darks == 0);
|
||||
// A soft pixel's readout rounds to nearest.
|
||||
QImage soft = BrushRaster::context(4, 4, false);
|
||||
soft.fill(Qt::transparent);
|
||||
for (int y = 0; y < 4; ++y)
|
||||
for (int x = 0; x < 4; ++x)
|
||||
std::fill_n(soft.scanLine(y) + x * 4, 3, uchar(51)), soft.scanLine(y)[x * 4 + 3] = 128;
|
||||
const auto readout = opened(soft);
|
||||
readout->updateCameraRawReadout(QPointF(1, 1));
|
||||
QCOMPARE(readout->filterEdit().value().rawPanel.readout, std::optional(std::array{102, 102, 102}));
|
||||
// Another filter's edit takes no white balance.
|
||||
const auto blur = opened(image(8, 8, 0.8, 0.5, 0.3));
|
||||
blur->cancelFilter();
|
||||
blur->beginFilter(FilterKind::gaussianBlur);
|
||||
arm(*blur, [](CameraRawPanel &panel) { panel.samplesWhiteBalance = true; });
|
||||
blur->sampleCameraRawWhiteBalance(QPointF(1, 1));
|
||||
QVERIFY(raw(*blur).temperature == 0 && raw(*blur).tint == 0);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawToolsTests)
|
||||
#include "CameraRawToolsTests.moc"
|
||||
@@ -0,0 +1,326 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include "Document/Filters.h"
|
||||
#include <QPainter>
|
||||
#include <QtTest>
|
||||
#include <cmath>
|
||||
extern "C" {
|
||||
#include "AdjustPixels.h"
|
||||
}
|
||||
|
||||
// Each setting reaches its kernel argument, each eye its fields.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
namespace {
|
||||
// Colours that differ everywhere, so every kernel argument shows.
|
||||
QImage noise(int side = 32)
|
||||
{
|
||||
QImage picture = BrushRaster::context(side, side, false);
|
||||
quint32 state = 7;
|
||||
for (int y = 0; y < side; ++y)
|
||||
for (int x = 0; x < side; ++x) {
|
||||
std::array<int, 3> channel{};
|
||||
for (int &value : channel) {
|
||||
state = state * 1664525 + 1013904223;
|
||||
value = int(state >> 24);
|
||||
}
|
||||
picture.setPixelColor(x, y, QColor(channel[0], channel[1], channel[2]));
|
||||
}
|
||||
return picture;
|
||||
}
|
||||
|
||||
QImage kernel(const QImage &picture, const std::function<void(uchar *, size_t, size_t, size_t)> &run)
|
||||
{
|
||||
QImage copy = picture.copy();
|
||||
run(copy.bits(), size_t(copy.width()), size_t(copy.height()), size_t(copy.bytesPerLine()));
|
||||
return copy;
|
||||
}
|
||||
|
||||
// The picture drawn under a transform, as the warp does.
|
||||
QImage placed(const QImage &picture, const QTransform &transform)
|
||||
{
|
||||
QImage result = BrushRaster::context(picture.width(), picture.height(), false);
|
||||
QPainter painter(&result);
|
||||
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
|
||||
painter.setTransform(transform);
|
||||
painter.drawImage(QRectF(0, 0, picture.width(), picture.height()), picture);
|
||||
return result;
|
||||
}
|
||||
|
||||
int farthest(const std::vector<std::array<int, 4>> &left, const std::vector<std::array<int, 4>> &right)
|
||||
{
|
||||
int most = 0;
|
||||
for (size_t index = 0; index < left.size(); ++index)
|
||||
for (size_t channel = 0; channel < 4; ++channel)
|
||||
most = std::max(most, std::abs(left[index][channel] - right[index][channel]));
|
||||
return most;
|
||||
}
|
||||
|
||||
double decode(double encoded)
|
||||
{
|
||||
return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
}
|
||||
|
||||
class CameraRawWiringTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void aClipViewShowsOnAnUntouchedGrade();
|
||||
void autoAveragesLinearLight();
|
||||
void eachEyeClearsEveryFieldOfItsGroup();
|
||||
void effectsReachTheKernelInOrder();
|
||||
void opticsAndDetailReachTheKernelsInOrder();
|
||||
void offsetAndAspectPlaceThePicture();
|
||||
void constrainCropFitsTheWholeFrame();
|
||||
void theScopeCountsEachChannelAndHue();
|
||||
void curveMixerAndGradingReachTheKernelInOrder();
|
||||
void theSharpenMaskAndEachLensFlagReachTheirKernels();
|
||||
void grainScalesByThePreview();
|
||||
void turnVerticalAndScalePlaceThePicture();
|
||||
};
|
||||
|
||||
void CameraRawWiringTests::aClipViewShowsOnAnUntouchedGrade()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
const QImage clipped = CameraRawSettings().apply(picture, CameraRawClipping::highlights);
|
||||
const QImage direct = kernel(picture, [](uchar *bytes, size_t w, size_t h, size_t s) {
|
||||
adjust_camera_raw(bytes, w, h, s, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, int(CameraRawClipping::highlights));
|
||||
});
|
||||
QVERIFY(pixels(clipped) == pixels(direct) && pixels(clipped) != pixels(picture));
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::autoAveragesLinearLight()
|
||||
{
|
||||
QImage picture = BrushRaster::context(2, 1, false);
|
||||
picture.setPixelColor(0, 0, QColor(200, 100, 50));
|
||||
picture.setPixelColor(1, 0, QColor(20, 60, 120));
|
||||
const auto mean = [](const std::function<double(double)> &read, int left, int right) { return (read(left / 255.0) + read(right / 255.0)) / 2; };
|
||||
const auto same = [](double value) { return value; };
|
||||
const CameraRawSettings::Balance linear =
|
||||
CameraRawSettings::neutralizeLinear(mean(decode, 200, 20), mean(decode, 100, 60), mean(decode, 50, 120)).value();
|
||||
const CameraRawSettings::Balance encoded =
|
||||
CameraRawSettings::neutralizeLinear(mean(same, 200, 20), mean(same, 100, 60), mean(same, 50, 120)).value();
|
||||
const CameraRawSettings::Balance automatic = CameraRawSettings::autoBalance(picture).value();
|
||||
QVERIFY(std::abs(automatic.temperature - linear.temperature) < 1e-9 && std::abs(automatic.tint - linear.tint) < 1e-9);
|
||||
QVERIFY(std::abs(automatic.temperature - encoded.temperature) > 1);
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::eachEyeClearsEveryFieldOfItsGroup()
|
||||
{
|
||||
CameraRawSettings all;
|
||||
double next = 1;
|
||||
for (double CameraRawSettings::*field :
|
||||
{&CameraRawSettings::exposure, &CameraRawSettings::contrast, &CameraRawSettings::highlights, &CameraRawSettings::shadows,
|
||||
&CameraRawSettings::whites, &CameraRawSettings::blacks, &CameraRawSettings::temperature, &CameraRawSettings::tint,
|
||||
&CameraRawSettings::vibrance, &CameraRawSettings::saturation, &CameraRawSettings::texture, &CameraRawSettings::clarity,
|
||||
&CameraRawSettings::dehaze, &CameraRawSettings::glow, &CameraRawSettings::vignetteAmount, &CameraRawSettings::grainAmount})
|
||||
all.*field = next++;
|
||||
const auto cleared = [&](std::initializer_list<double CameraRawSettings::*> fields) {
|
||||
CameraRawSettings expected = all;
|
||||
for (double CameraRawSettings::*field : fields)
|
||||
expected.*field = 0;
|
||||
return expected;
|
||||
};
|
||||
QVERIFY(all.applying({.light = false})
|
||||
== cleared({&CameraRawSettings::exposure, &CameraRawSettings::contrast, &CameraRawSettings::highlights, &CameraRawSettings::shadows,
|
||||
&CameraRawSettings::whites, &CameraRawSettings::blacks}));
|
||||
QVERIFY(all.applying({.color = false})
|
||||
== cleared({&CameraRawSettings::temperature, &CameraRawSettings::tint, &CameraRawSettings::vibrance, &CameraRawSettings::saturation}));
|
||||
QVERIFY(all.applying({.effects = false})
|
||||
== cleared({&CameraRawSettings::texture, &CameraRawSettings::clarity, &CameraRawSettings::dehaze, &CameraRawSettings::glow,
|
||||
&CameraRawSettings::vignetteAmount, &CameraRawSettings::grainAmount}));
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::effectsReachTheKernelInOrder()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
CameraRawSettings settings;
|
||||
settings.texture = 30, settings.clarity = -20, settings.dehaze = 15, settings.glow = 40, settings.glowStyle = CameraRawGlowStyle::bloom;
|
||||
settings.glowRange = 10, settings.glowSpread = -30, settings.glowWarmth = 25, settings.vignetteAmount = -60;
|
||||
settings.vignetteStyle = CameraRawVignetteStyle::colorPriority, settings.vignetteMidpoint = 30, settings.vignetteRoundness = 20;
|
||||
settings.vignetteFeather = 70, settings.vignetteHighlights = 40;
|
||||
const QImage direct = kernel(picture, [](uchar *bytes, size_t w, size_t h, size_t s) {
|
||||
adjust_camera_raw_effects(bytes, w, h, s, 30, -20, 15, 40, int(CameraRawGlowStyle::bloom), 10, -30, 25, -60, 30, 20, 70, 40,
|
||||
int(CameraRawVignetteStyle::colorPriority), 1);
|
||||
});
|
||||
QVERIFY(pixels(settings.apply(picture)) == pixels(direct));
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::opticsAndDetailReachTheKernelsInOrder()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
CameraRawSettings settings;
|
||||
settings.optics = {.removeChromaticAberration = true, .enableLensProfile = true, .profileDistortion = 60, .profileVignetting = 40,
|
||||
.distortion = 20, .purpleAmount = 30, .purpleHueLow = 260, .purpleHueHigh = 320, .greenAmount = 40, .greenHueLow = 50,
|
||||
.greenHueHigh = 130, .vignetteAmount = -45, .vignetteMidpoint = 30};
|
||||
settings.detail = {.sharpenAmount = 80, .sharpenRadius = 30, .sharpenDetail = 60, .sharpenMasking = 20, .noiseLuminance = 40,
|
||||
.noiseLuminanceDetail = 70, .noiseLuminanceContrast = 30, .noiseColor = 50, .noiseColorDetail = 20,
|
||||
.noiseColorSmoothness = 80};
|
||||
const double k = settings.optics.distortionK(PixelFilter::lensStrength);
|
||||
const QImage direct = kernel(picture, [k](uchar *bytes, size_t w, size_t h, size_t s) {
|
||||
adjust_camera_raw_optics(bytes, w, h, s, 1, 1, 60, 40, k, 30, 260, 320, 40, 50, 130, -45, 30, 1);
|
||||
adjust_camera_raw_detail(bytes, w, h, s, 80, 30, 60, 20, 40, 70, 30, 50, 20, 80, 1);
|
||||
});
|
||||
QVERIFY(pixels(settings.apply(picture)) == pixels(direct));
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::offsetAndAspectPlaceThePicture()
|
||||
{
|
||||
const QImage picture = checker(24, 24);
|
||||
const auto warped = [&](const std::function<void(CameraRawGeometrySettings &)> &change) {
|
||||
CameraRawGeometrySettings geometry;
|
||||
change(geometry);
|
||||
return pixels(geometry.apply(picture));
|
||||
};
|
||||
// Offset X slides; Offset Y stretches from the middle.
|
||||
QVERIFY(farthest(warped([](CameraRawGeometrySettings &g) { g.offsetX = 40; }), pixels(placed(picture, QTransform::fromTranslate(24 * 0.06, 0))))
|
||||
<= 1);
|
||||
const double stretch = (24 + 2 * 24 * 0.06) / 24;
|
||||
QVERIFY(farthest(warped([](CameraRawGeometrySettings &g) { g.offsetY = 40; }),
|
||||
pixels(placed(picture, QTransform::fromTranslate(12, 12).scale(1, stretch).translate(-12, -12))))
|
||||
<= 1);
|
||||
// Aspect widens by its factor and flattens by the same.
|
||||
QVERIFY(farthest(warped([](CameraRawGeometrySettings &g) { g.aspect = 50; }),
|
||||
pixels(placed(picture, QTransform::fromTranslate(12, 12).scale(1.25, 1 / 1.25).translate(-12, -12))))
|
||||
<= 1);
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::constrainCropFitsTheWholeFrame()
|
||||
{
|
||||
// A wide box fills the width, the top clear.
|
||||
CameraRawGeometrySettings wide{.aspect = 50, .scale = -50, .constrainCrop = true};
|
||||
const QImage filled = wide.apply(checker(24, 24));
|
||||
QVERIFY(qAlpha(filled.pixel(0, 12)) > 0 && qAlpha(filled.pixel(23, 12)) > 0);
|
||||
QCOMPARE(qAlpha(filled.pixel(12, 0)), 0);
|
||||
// A box filling one side alone is still centred.
|
||||
CameraRawGeometrySettings tall{.aspect = -50, .offsetX = 50};
|
||||
const QImage loose = tall.apply(checker(24, 24));
|
||||
tall.constrainCrop = true;
|
||||
const QImage centred = tall.apply(checker(24, 24));
|
||||
QCOMPARE(qAlpha(loose.pixel(3, 12)), 0);
|
||||
QVERIFY(qAlpha(centred.pixel(3, 12)) > 0);
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::theScopeCountsEachChannelAndHue()
|
||||
{
|
||||
QImage picture = BrushRaster::context(3, 1, false);
|
||||
picture.setPixelColor(0, 0, QColor(0, 255, 0, 128));
|
||||
picture.setPixelColor(1, 0, QColor(0, 0, 255));
|
||||
picture.setPixelColor(2, 0, QColor(255, 0, 255));
|
||||
const CameraRawScope scope = CameraRawScope::make(picture).value();
|
||||
// Counts weigh by alpha: the green pixel is half.
|
||||
const double half = 128 / 255.0;
|
||||
const auto near = [](double left, double right) { return std::abs(left - right) < 1e-9; };
|
||||
QVERIFY(near(scope.red[0], 1 + half) && near(scope.red[255], 1));
|
||||
QVERIFY(near(scope.green[255], half) && near(scope.green[0], 2));
|
||||
QVERIFY(near(scope.blue[255], 2) && near(scope.blue[0], half));
|
||||
const auto cell = [&](int column, int line) { return scope.vectorscope[size_t(line * CameraRawScope::scopeSide + column)]; };
|
||||
// Green at 120°, blue at 240°, magenta at 300°.
|
||||
QVERIFY(std::abs(cell(16, 58) - 128 / 255.0) < 1e-9);
|
||||
QCOMPARE(cell(16, 5), 1.0);
|
||||
QCOMPARE(cell(47, 5), 1.0);
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::curveMixerAndGradingReachTheKernelInOrder()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
CameraRawSettings settings;
|
||||
CameraRawCurveSettings &curve = settings.curve;
|
||||
curve.lights = 30, curve.shadows = -20, curve.refineSaturation = 40;
|
||||
curve.rgb = {{0, 0.05}, {0.5, 0.6}, {1, 0.95}};
|
||||
curve.red = {{0, 0}, {0.3, 0.45}, {1, 1}};
|
||||
curve.green = {{0, 0.1}, {1, 0.8}};
|
||||
curve.blue = {{0, 0}, {0.7, 0.5}, {1, 1}};
|
||||
for (size_t index = 0; index < 8; ++index) {
|
||||
settings.mixer.hue[index] = 5 + double(index);
|
||||
settings.mixer.saturation[index] = -30 + 7 * double(index);
|
||||
settings.mixer.luminance[index] = 20 - 4 * double(index);
|
||||
}
|
||||
settings.mixer.points = {{200, 0.6, 0.4, 25, -35, 15, 40, 0.5, 0.3, false}, {40, 0.8, 0.6, -10, 20, -45, 60, 0.3, 0.6, false}};
|
||||
settings.grading = {{210, 40, -20}, {100, 25, 10}, {30, 55, 15}, {300, 10, -5}, 70, -30};
|
||||
const CameraRawCurveSettings shape = curve.normalized();
|
||||
const std::vector<float> luma = shape.lumaTable(), red = shape.channelTable(shape.red), green = shape.channelTable(shape.green),
|
||||
blue = shape.channelTable(shape.blue);
|
||||
std::vector<float> mixer;
|
||||
for (size_t index = 0; index < 8; ++index)
|
||||
mixer.push_back(float((5 + double(index)) / 100));
|
||||
for (size_t index = 0; index < 8; ++index)
|
||||
mixer.push_back(float((-30 + 7 * double(index)) / 100));
|
||||
for (size_t index = 0; index < 8; ++index)
|
||||
mixer.push_back(float((20 - 4 * double(index)) / 100));
|
||||
const std::vector<float> points{float(200 / 360.0), 0.6f, 0.4f, 0.25f, -0.35f, 0.15f, float(40 / 360.0), 0.5f, 0.3f,
|
||||
float(40 / 360.0), 0.8f, 0.6f, -0.1f, 0.2f, -0.45f, float(60 / 360.0), 0.3f, 0.6f};
|
||||
const std::vector<float> grade{float(210 / 360.0), 0.4f, -0.2f, float(100 / 360.0), 0.25f, 0.1f,
|
||||
float(30 / 360.0), 0.55f, 0.15f, float(300 / 360.0), 0.1f, -0.05f};
|
||||
for (const int visualize : {-1, 1}) {
|
||||
const QImage direct = kernel(picture, [&](uchar *bytes, size_t w, size_t h, size_t stride) {
|
||||
adjust_camera_raw_curve_color(bytes, w, h, stride, luma.data(), red.data(), green.data(), blue.data(), 0.4, mixer.data(), 2, points.data(),
|
||||
grade.data(), 0.7, -0.3, visualize);
|
||||
});
|
||||
QVERIFY(pixels(settings.apply(picture, std::nullopt, 1, 0, visualize)) == pixels(direct));
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::theSharpenMaskAndEachLensFlagReachTheirKernels()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
CameraRawSettings masked;
|
||||
masked.detail = {.sharpenAmount = 50, .sharpenRadius = 30, .sharpenDetail = 60, .sharpenMasking = 20};
|
||||
const QImage mask = kernel(picture, [](uchar *bytes, size_t w, size_t h, size_t s) { adjust_camera_raw_sharpen_mask_overlay(bytes, w, h, s, 30, 60, 20, 1); });
|
||||
QVERIFY(pixels(masked.apply(picture, std::nullopt, 1, 0, -1, true)) == pixels(mask));
|
||||
const QImage half = kernel(picture, [](uchar *bytes, size_t w, size_t h, size_t s) { adjust_camera_raw_sharpen_mask_overlay(bytes, w, h, s, 80, 60, 20, 0.25); });
|
||||
masked.detail.sharpenRadius = 80;
|
||||
QVERIFY(pixels(masked.apply(picture, std::nullopt, 0.25, 0, -1, true)) == pixels(half));
|
||||
for (const bool chromatic : {true, false}) {
|
||||
CameraRawSettings lens;
|
||||
lens.optics.removeChromaticAberration = chromatic;
|
||||
lens.optics.enableLensProfile = !chromatic;
|
||||
const double k = lens.optics.distortionK(PixelFilter::lensStrength);
|
||||
const QImage direct = kernel(picture, [&](uchar *bytes, size_t w, size_t h, size_t s) {
|
||||
adjust_camera_raw_optics(bytes, w, h, s, chromatic ? 1 : 0, chromatic ? 0 : 1, 100, 100, k, 0, 270, 310, 0, 60, 120, 0, 50, 1);
|
||||
});
|
||||
QVERIFY(pixels(lens.apply(picture)) == pixels(direct));
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::grainScalesByThePreview()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
CameraRawSettings settings;
|
||||
settings.grainAmount = 40;
|
||||
const QImage direct = kernel(picture, [](uchar *bytes, size_t w, size_t h, size_t s) { adjust_grain(bytes, w, h, s, 40, 5.375, 50, 3, 0, 0, 2); });
|
||||
QVERIFY(pixels(settings.apply(picture, std::nullopt, 0.5, 3)) == pixels(direct));
|
||||
// A dull colour plots nearer the middle: chroma over value.
|
||||
QImage dull = BrushRaster::context(1, 1, false);
|
||||
dull.setPixelColor(0, 0, QColor(128, 64, 64));
|
||||
QCOMPARE(CameraRawScope::make(dull).value().vectorscope[size_t(32 * CameraRawScope::scopeSide + 47)], 1.0);
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::turnVerticalAndScalePlaceThePicture()
|
||||
{
|
||||
const QImage picture = checker(24, 24);
|
||||
const auto warped = [&](const std::function<void(CameraRawGeometrySettings &)> &change) {
|
||||
CameraRawGeometrySettings geometry;
|
||||
change(geometry);
|
||||
return pixels(geometry.apply(picture));
|
||||
};
|
||||
// A positive turn goes counterclockwise on screen, as Core Image's.
|
||||
QVERIFY(farthest(warped([](CameraRawGeometrySettings &g) { g.rotate = 10; }),
|
||||
pixels(placed(picture, QTransform::fromTranslate(12, 12).rotate(-10).translate(-12, -12))))
|
||||
<= 1);
|
||||
QVERIFY(farthest(warped([](CameraRawGeometrySettings &g) { g.scale = 50; }),
|
||||
pixels(placed(picture, QTransform::fromTranslate(12, 12).scale(1.5, 1.5).translate(-12, -12))))
|
||||
<= 1);
|
||||
// Vertical 40 widens the top by 0.072 of the width.
|
||||
const double v = 0.4 * 24 * 0.18;
|
||||
QTransform trapezoid;
|
||||
QVERIFY(QTransform::quadToQuad(QPolygonF(QRectF(0, 0, 24, 24)).mid(0, 4),
|
||||
QPolygonF({QPointF(-v, 0), QPointF(24 + v, 0), QPointF(24, 24), QPointF(0, 24)}), trapezoid));
|
||||
QVERIFY(farthest(warped([](CameraRawGeometrySettings &g) { g.vertical = 40; }), pixels(placed(picture, trapezoid))) <= 1);
|
||||
QTransform wideBottom;
|
||||
QVERIFY(QTransform::quadToQuad(QPolygonF(QRectF(0, 0, 24, 24)).mid(0, 4),
|
||||
QPolygonF({QPointF(0, 0), QPointF(24, 0), QPointF(24 + v, 24), QPointF(-v, 24)}), wideBottom));
|
||||
QVERIFY(farthest(warped([](CameraRawGeometrySettings &g) { g.horizontal = 40; }), pixels(placed(picture, wideBottom))) <= 1);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawWiringTests)
|
||||
#include "CameraRawWiringTests.moc"
|
||||
@@ -66,7 +66,8 @@ void FilterModelTests::kindsKeepSwiftsNames()
|
||||
{
|
||||
const std::pair<FilterKind, const char *> kinds[] = {
|
||||
{FilterKind::gaussianBlur, "Gaussian Blur"}, {FilterKind::motionBlur, "Motion Blur"}, {FilterKind::addNoise, "Add Noise"},
|
||||
{FilterKind::lensCorrection, "Lens Correction"}, {FilterKind::removeBackground, "Remove Background"},
|
||||
{FilterKind::lensCorrection, "Lens Correction"}, {FilterKind::cameraRaw, "Camera Raw Filter"},
|
||||
{FilterKind::removeBackground, "Remove Background"},
|
||||
{FilterKind::contentAwareFill, "Content-Aware Fill"}, {FilterKind::curves, "Curves"}, {FilterKind::exposure, "Exposure"},
|
||||
{FilterKind::gradientMap, "Gradient Map"}, {FilterKind::grain, "Grain"}, {FilterKind::blackWhite, "Black & White"},
|
||||
{FilterKind::colorBalance, "Color Balance"}};
|
||||
@@ -77,7 +78,7 @@ void FilterModelTests::kindsKeepSwiftsNames()
|
||||
QVERIFY(allFilterKinds[index] == kind);
|
||||
QCOMPARE(rawValue(kind), QString(name));
|
||||
QCOMPARE(isAutomatic(kind), kind == FilterKind::contentAwareFill || kind == FilterKind::removeBackground);
|
||||
QCOMPARE(isImageAdjustment(kind), index >= 6);
|
||||
QCOMPARE(isImageAdjustment(kind), index >= 7);
|
||||
}
|
||||
// Remove Background's qualities, Swift's words and order.
|
||||
QVERIFY(allBackgroundQualities.size() == 2 && allBackgroundQualities[0] == BackgroundQuality::basic);
|
||||
|
||||
@@ -54,6 +54,7 @@ private slots:
|
||||
void exposureWorksInLinearLightWithOffsetAndGamma();
|
||||
void gradientMapColorsByBrightnessAndReverses();
|
||||
void grainIsFixedInDocumentSpaceAndLeavesTransparencyAlone();
|
||||
void grainSizeControlsParticleScaleEvenWithRoughness();
|
||||
void imageMenuExposureChangesTheLayerInOneStep();
|
||||
void gradientMapColorsUseTheAppColorPicker();
|
||||
void settingsSaveAndOlderAdjustmentsStillOpen();
|
||||
@@ -118,6 +119,22 @@ void ImageAdjustmentTests::grainIsFixedInDocumentSpaceAndLeavesTransparencyAlone
|
||||
QVERIFY2(std::all_of(cleared.begin(), cleared.end(), [](const std::array<int, 4> &pixel) { return pixel[3] == 0; }), "clear pixels stay clear");
|
||||
}
|
||||
|
||||
// Swift's 1.2.3 test: bigger grain, larger and smoother particles.
|
||||
void ImageAdjustmentTests::grainSizeControlsParticleScaleEvenWithRoughness()
|
||||
{
|
||||
const QImage source = gray(64, 64);
|
||||
const std::vector<std::array<int, 4>> small = pixels(GrainSettings{70, 1, 70, 17}.apply(source));
|
||||
const std::vector<std::array<int, 4>> large = pixels(GrainSettings{70, 12, 70, 17}.apply(source));
|
||||
const auto neighbouring = [](const std::vector<std::array<int, 4>> &values) {
|
||||
double total = 0, count = 0;
|
||||
for (int y = 0; y < 64; ++y)
|
||||
for (int x = 1; x < 64; ++x, ++count)
|
||||
total += std::abs(values[size_t(y * 64 + x)][0] - values[size_t(y * 64 + x - 1)][0]);
|
||||
return total / count;
|
||||
};
|
||||
QVERIFY(neighbouring(large) < neighbouring(small) * 0.7);
|
||||
}
|
||||
|
||||
void ImageAdjustmentTests::imageMenuExposureChangesTheLayerInOneStep()
|
||||
{
|
||||
EditorSession session;
|
||||
|
||||
@@ -206,6 +206,15 @@ compositor_test(RawImporterTests)
|
||||
compositor_test(RawDevelopTests)
|
||||
compositor_test(ToolDefaultsTests)
|
||||
compositor_test(ColorAdjustmentTests)
|
||||
compositor_test(CameraRawTests)
|
||||
compositor_test(CameraRawSessionTests)
|
||||
compositor_test(CameraRawToolsTests)
|
||||
compositor_test(CameraRawRulesTests)
|
||||
compositor_test(CameraRawColorTests)
|
||||
compositor_test(CameraRawLensTests)
|
||||
target_include_directories(CameraRawLensTests PRIVATE ${OMAPHOTO_KERNELS})
|
||||
compositor_test(CameraRawWiringTests)
|
||||
target_include_directories(CameraRawWiringTests PRIVATE ${OMAPHOTO_KERNELS})
|
||||
compositor_test(LayerEffectsRendererTests)
|
||||
compositor_test(LayerFlipTests)
|
||||
compositor_test(LayerMergeTests)
|
||||
|
||||
Reference in new issue
Block a user