Camera Raw filter's model and pixel kernels (12.8a)

This commit is contained in:
雷电芽衣 committed 2026-09-27 19:55:59 -04:00
1 parent 4291f4249a
commit b544e12b1d
33 files changed
+4438 -27

No files matched your search

+173
View File
@@ -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"
+86
View File
@@ -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());
}
}
+189
View File
@@ -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"
+228
View File
@@ -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"
+236
View File
@@ -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"
+310
View File
@@ -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"
+285
View File
@@ -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"
+326
View File
@@ -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"
+3 -2
View File
@@ -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);
+17
View File
@@ -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;
+9
View File
@@ -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)