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

+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"