14.25: Camera Raw's Light and Color sliders match Photoshop's
This commit is contained in:
17 files changed
+513
-136
No files matched your search
@@ -132,7 +132,7 @@ void CameraRawSessionTests::hiddenGroupIsLeftOutAndOkIsOneUndoOrNone()
|
||||
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");
|
||||
QVERIFY2(std::abs(pixels(layerImage(*session))[0][0] - 181) <= 2, "OK bakes the grade");
|
||||
}
|
||||
|
||||
void CameraRawSessionTests::hiddenGroupStaysOutOfTheNextOpen()
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include "Document/CameraRawTables.h"
|
||||
#include <QtTest>
|
||||
|
||||
// The measured tables as Swift reads them, checked against Python.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
class CameraRawTablesTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theTablesDecodeAsWritten();
|
||||
void aStageBlendsTheTablesAroundItsValue();
|
||||
void eachSliderPicksItsOwnTables();
|
||||
void neutralizeFindsTheGridsGrayest();
|
||||
void identitySlidersComposeNothing();
|
||||
};
|
||||
|
||||
namespace {
|
||||
const uint8_t *entry(int table, int cell)
|
||||
{
|
||||
constexpr size_t entries = size_t(CameraRawTables::size) * CameraRawTables::size * CameraRawTables::size * 3;
|
||||
return CameraRawTables::tables().data() + table * entries + size_t(cell) * 3;
|
||||
}
|
||||
|
||||
int indexOf(const uint8_t *table)
|
||||
{
|
||||
constexpr size_t entries = size_t(CameraRawTables::size) * CameraRawTables::size * CameraRawTables::size * 3;
|
||||
return table ? int((table - CameraRawTables::tables().data()) / qsizetype(entries)) : -1;
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawTablesTests::theTablesDecodeAsWritten()
|
||||
{
|
||||
const std::vector<uint8_t> &tables = CameraRawTables::tables();
|
||||
QCOMPARE(tables.size(), size_t(147) * 17 * 17 * 17 * 3);
|
||||
quint64 hash = 0xcbf29ce484222325ULL;
|
||||
for (const uint8_t byte : tables)
|
||||
hash = (hash ^ byte) * 0x100000001b3ULL;
|
||||
QCOMPARE(hash, quint64(0x6a9f829e703f2df6ULL));
|
||||
QCOMPARE(std::vector<int>(entry(40, 2456), entry(40, 2456) + 3), (std::vector<int>{128, 128, 128}));
|
||||
QCOMPARE(std::vector<int>(entry(81, 100), entry(81, 100) + 3), (std::vector<int>{0, 4, 32}));
|
||||
QCOMPARE(std::vector<int>(entry(146, 4912), entry(146, 4912) + 3), (std::vector<int>{255, 255, 255}));
|
||||
}
|
||||
|
||||
void CameraRawTablesTests::aStageBlendsTheTablesAroundItsValue()
|
||||
{
|
||||
const CameraRawStage balance = CameraRawTables::whiteBalance(30, -60);
|
||||
QCOMPARE((std::vector<int>{indexOf(balance.table[0]), indexOf(balance.table[1]), indexOf(balance.table[2]), indexOf(balance.table[3])}),
|
||||
(std::vector<int>{46, 47, 55, 56}));
|
||||
QCOMPARE((std::vector<float>{balance.weight[0], balance.weight[1], balance.weight[2], balance.weight[3]}),
|
||||
(std::vector<float>{0.32f, 0.48f, 0.08f, 0.12f}));
|
||||
// At the far corner the last pair takes it all.
|
||||
const CameraRawStage corner = CameraRawTables::whiteBalance(100, 100);
|
||||
QVERIFY(indexOf(corner.table[0]) == 70 && indexOf(corner.table[3]) == 80 && corner.weight[3] == 1 && corner.weight[0] == 0);
|
||||
// Past the range it holds at the end, never beyond.
|
||||
const CameraRawStage past = CameraRawTables::whiteBalance(150, 175);
|
||||
QVERIFY(indexOf(past.table[3]) == 80 && past.weight[3] == 1 && past.weight[0] == 0);
|
||||
const CameraRawStage low = CameraRawTables::whiteBalance(-100, -100);
|
||||
QVERIFY(indexOf(low.table[0]) == 0 && low.weight[0] == 1 && low.weight[1] == 0);
|
||||
}
|
||||
|
||||
void CameraRawTablesTests::eachSliderPicksItsOwnTables()
|
||||
{
|
||||
CameraRawSettings settings;
|
||||
settings.exposure = 1.25;
|
||||
settings.temperature = 30;
|
||||
settings.contrast = -100;
|
||||
settings.whites = 100;
|
||||
settings.blacks = 12.5;
|
||||
settings.saturation = -62.5;
|
||||
settings.vibrance = 80;
|
||||
const CameraRawTables::Stages stages = CameraRawTables::stages(settings);
|
||||
QCOMPARE(stages.before.size(), size_t(3));
|
||||
QCOMPARE(stages.after.size(), size_t(4));
|
||||
const auto pair = [](const CameraRawStage &stage) {
|
||||
return std::vector<double>{double(indexOf(stage.table[0])), double(indexOf(stage.table[1])), stage.weight[0], stage.weight[1]};
|
||||
};
|
||||
QCOMPARE(pair(stages.before[0]), (std::vector<double>{93, 94, 0.5, 0.5}));
|
||||
QCOMPARE(indexOf(stages.before[1].table[0]), 5 * 9 + 4);
|
||||
QCOMPARE(pair(stages.before[2]), (std::vector<double>{102, 103, 1, 0}));
|
||||
QCOMPARE(pair(stages.after[0]), (std::vector<double>{118, 119, 0, 1}));
|
||||
QCOMPARE(pair(stages.after[1]), (std::vector<double>{124, 125, 0.5, 0.5}));
|
||||
QCOMPARE(pair(stages.after[2]), (std::vector<double>{130, 131, 0.5, 0.5}));
|
||||
QVERIFY(indexOf(stages.after[3].table[0]) == 145 && std::abs(stages.after[3].weight[1] - 0.2f) < 1e-6);
|
||||
// Tint alone still makes the white balance stage.
|
||||
CameraRawSettings tint;
|
||||
tint.tint = -25;
|
||||
const CameraRawTables::Stages tinted = CameraRawTables::stages(tint);
|
||||
QVERIFY(tinted.before.size() == 1 && tinted.after.empty() && indexOf(tinted.before[0].table[0]) == 4 * 9 + 3);
|
||||
}
|
||||
|
||||
void CameraRawTablesTests::neutralizeFindsTheGridsGrayest()
|
||||
{
|
||||
// Python's own search over the same tables found these.
|
||||
const CameraRawSettings::Balance warm = CameraRawTables::neutralize(160 / 255.0, 140 / 255.0, 120 / 255.0).value();
|
||||
QVERIFY(warm.temperature == -19 && warm.tint == -7);
|
||||
const CameraRawSettings::Balance blue = CameraRawTables::neutralize(0.2, 0.45, 0.8).value();
|
||||
QVERIFY(blue.temperature == 100 && blue.tint == 25.5);
|
||||
const CameraRawSettings::Balance green = CameraRawTables::neutralize(0.3, 0.5, 0.25).value();
|
||||
QVERIFY(green.temperature == 5 && green.tint == 90.5);
|
||||
// White stays white anywhere: the first of the ties wins.
|
||||
const CameraRawSettings::Balance white = CameraRawTables::neutralize(1, 1, 1).value();
|
||||
QVERIFY(white.temperature == 0 && white.tint == 0);
|
||||
QVERIFY(!CameraRawTables::neutralize(0.5, 0.0001, 0.5));
|
||||
QVERIFY(CameraRawTables::neutralize(0.5, 0.00011, 0.5));
|
||||
}
|
||||
|
||||
void CameraRawTablesTests::identitySlidersComposeNothing()
|
||||
{
|
||||
const CameraRawTables::Stages none = CameraRawTables::stages(CameraRawSettings());
|
||||
QVERIFY(none.before.empty() && none.after.empty() && CameraRawTables::compose(none.before).empty());
|
||||
CameraRawSettings exposure;
|
||||
exposure.exposure = 0.5;
|
||||
const std::vector<float> composed = CameraRawTables::compose(CameraRawTables::stages(exposure).before);
|
||||
QCOMPARE(composed.size(), size_t(33) * 33 * 33 * 3);
|
||||
// The corners are its table's own black and white.
|
||||
QVERIFY(composed[0] == entry(92, 0)[0] / 255.0f && composed.back() == entry(92, 4912)[2] / 255.0f);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawTablesTests)
|
||||
#include "CameraRawTablesTests.moc"
|
||||
@@ -10,9 +10,10 @@ class CameraRawTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void defaultsLeavePixelsAndAlphaAlone();
|
||||
void exposureAddsOneStopAndContrastPivotsAroundMidGray();
|
||||
void exposureAndContrastMatchPhotoshop();
|
||||
void tonalSlidersMoveTheEndTheyName();
|
||||
void temperatureWarmsAndTintMovesTowardMagenta();
|
||||
void whiteBalanceAndVibranceMatchPhotoshop();
|
||||
void vibranceFavorsDullColorsAndProtectsSkinWhileSaturationDoesNot();
|
||||
void textureAndClaritySharpenAnEdgeAndLeaveAFlatField();
|
||||
void dehazeDeepensOrLiftsAndKeepsAlpha();
|
||||
@@ -36,23 +37,24 @@ void CameraRawTests::defaultsLeavePixelsAndAlphaAlone()
|
||||
QVERIFY(!isImageAdjustment(FilterKind::cameraRaw));
|
||||
}
|
||||
|
||||
void CameraRawTests::exposureAddsOneStopAndContrastPivotsAroundMidGray()
|
||||
// Photoshop's Camera Raw Filter's own numbers, measured.
|
||||
void CameraRawTests::exposureAndContrastMatchPhotoshop()
|
||||
{
|
||||
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])));
|
||||
QVERIFY2(std::abs(brighter[0] - 181) <= 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");
|
||||
QVERIFY2(std::abs(pushed[0][0] - 36) <= 3 && std::abs(pushed[1][0] - 213) <= 3, "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");
|
||||
QVERIFY2(std::abs(flat[0][0] - 88) <= 3 && std::abs(flat[1][0] - 174) <= 3, "contrast −100 draws them together");
|
||||
}
|
||||
|
||||
void CameraRawTests::tonalSlidersMoveTheEndTheyName()
|
||||
@@ -65,7 +67,7 @@ void CameraRawTests::tonalSlidersMoveTheEndTheyName()
|
||||
settings = CameraRawSettings();
|
||||
settings.whites = 100;
|
||||
const auto clipped = pixels(settings.apply(brightAndMid));
|
||||
QVERIFY(clipped[0][0] == 255 && std::abs(clipped[1][0] - 128) <= 2);
|
||||
QVERIFY2(clipped[0][0] == 255 && clipped[1][0] > 128, "whites lift the midtone too, as in Photoshop");
|
||||
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);
|
||||
@@ -76,7 +78,7 @@ void CameraRawTests::tonalSlidersMoveTheEndTheyName()
|
||||
settings = CameraRawSettings();
|
||||
settings.blacks = -100;
|
||||
const auto crushed = pixels(settings.apply(darkAndMid));
|
||||
QVERIFY(crushed[0][0] < 20 && std::abs(crushed[1][0] - 128) <= 2);
|
||||
QVERIFY2(crushed[0][0] == 0 && std::abs(crushed[1][0] - 91) <= 3, "blacks darken the midtone too");
|
||||
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");
|
||||
}
|
||||
@@ -94,6 +96,33 @@ void CameraRawTests::temperatureWarmsAndTintMovesTowardMagenta()
|
||||
QVERIFY(magenta[1] < 128 && magenta[1] < magenta[0] && magenta[1] < magenta[2]);
|
||||
}
|
||||
|
||||
// Once a fraction of Photoshop's strength (Swift's issue #252).
|
||||
void CameraRawTests::whiteBalanceAndVibranceMatchPhotoshop()
|
||||
{
|
||||
struct Case {
|
||||
const char *name;
|
||||
std::array<int, 3> input;
|
||||
std::function<void(CameraRawSettings &)> adjust;
|
||||
std::array<int, 3> expected;
|
||||
};
|
||||
const std::vector<Case> cases{
|
||||
{"temperature +100", {128, 128, 128}, [](CameraRawSettings &s) { s.temperature = 100; }, {235, 189, 117}},
|
||||
{"temperature −50", {128, 128, 128}, [](CameraRawSettings &s) { s.temperature = -50; }, {102, 146, 210}},
|
||||
{"tint +100", {128, 128, 128}, [](CameraRawSettings &s) { s.tint = 100; }, {159, 125, 215}},
|
||||
{"tint −100", {128, 128, 128}, [](CameraRawSettings &s) { s.tint = -100; }, {98, 198, 120}},
|
||||
{"vibrance +100 spares skin", {153, 107, 107}, [](CameraRawSettings &s) { s.vibrance = 100; }, {167, 98, 98}},
|
||||
{"vibrance +100 fills a dull cyan", {107, 153, 153}, [](CameraRawSettings &s) { s.vibrance = 100; }, {0, 160, 160}},
|
||||
{"vibrance −100", {92, 92, 230}, [](CameraRawSettings &s) { s.vibrance = -100; }, {164, 164, 210}},
|
||||
};
|
||||
for (const Case &test : cases) {
|
||||
CameraRawSettings settings;
|
||||
test.adjust(settings);
|
||||
const std::array<int, 4> got = pixels(settings.apply(image(4, 4, test.input[0] / 255.0, test.input[1] / 255.0, test.input[2] / 255.0)))[0];
|
||||
for (int c = 0; c < 3; ++c)
|
||||
QVERIFY2(std::abs(got[c] - test.expected[c]) <= 4, test.name);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -112,7 +141,8 @@ void CameraRawTests::vibranceFavorsDullColorsAndProtectsSkinWhileSaturationDoesN
|
||||
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");
|
||||
// Photoshop doubles chroma in linear light: less in sRGB.
|
||||
QVERIFY2(std::abs(redRatio - 1.85) < 0.15 && std::abs(blueRatio - 1.73) < 0.15, "saturation raises both");
|
||||
}
|
||||
|
||||
void CameraRawTests::textureAndClaritySharpenAnEdgeAndLeaveAFlatField()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include "Document/CameraRawTables.h"
|
||||
#include "Document/Filters.h"
|
||||
#include <QPainter>
|
||||
#include <QtTest>
|
||||
@@ -65,6 +66,7 @@ class CameraRawWiringTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void aClipViewShowsOnAnUntouchedGrade();
|
||||
void theTablesRunAroundHighlightsAndShadows();
|
||||
void autoAveragesLinearLight();
|
||||
void eachEyeClearsEveryFieldOfItsGroup();
|
||||
void effectsReachTheKernelInOrder();
|
||||
@@ -83,11 +85,33 @@ 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));
|
||||
adjust_camera_raw(bytes, w, h, s, nullptr, nullptr, CameraRawTables::grid, 0, 0, int(CameraRawClipping::highlights));
|
||||
});
|
||||
QVERIFY(pixels(clipped) == pixels(direct) && pixels(clipped) != pixels(picture));
|
||||
}
|
||||
|
||||
// Exposure, balance and Contrast first; Whites to Vibrance after.
|
||||
void CameraRawWiringTests::theTablesRunAroundHighlightsAndShadows()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
CameraRawSettings settings;
|
||||
settings.exposure = 1;
|
||||
settings.temperature = 40;
|
||||
settings.highlights = -80;
|
||||
settings.shadows = 60;
|
||||
settings.whites = -50;
|
||||
settings.saturation = 30;
|
||||
const CameraRawTables::Stages stages = CameraRawTables::stages(settings);
|
||||
const std::vector<float> before = CameraRawTables::compose(stages.before), after = CameraRawTables::compose(stages.after);
|
||||
const QImage direct = kernel(picture, [&](uchar *bytes, size_t w, size_t h, size_t s) {
|
||||
adjust_camera_raw(bytes, w, h, s, before.data(), after.data(), CameraRawTables::grid, -80, 60, 0);
|
||||
});
|
||||
const QImage swapped = kernel(picture, [&](uchar *bytes, size_t w, size_t h, size_t s) {
|
||||
adjust_camera_raw(bytes, w, h, s, after.data(), before.data(), CameraRawTables::grid, -80, 60, 0);
|
||||
});
|
||||
QVERIFY(pixels(settings.apply(picture)) == pixels(direct) && pixels(direct) != pixels(swapped));
|
||||
}
|
||||
|
||||
void CameraRawWiringTests::autoAveragesLinearLight()
|
||||
{
|
||||
QImage picture = BrushRaster::context(2, 1, false);
|
||||
@@ -95,10 +119,12 @@ void CameraRawWiringTests::autoAveragesLinearLight()
|
||||
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();
|
||||
// The linear mean encoded again, as Swift's Auto does.
|
||||
const auto encode = [](double linear) { return linear <= 0.0031308 ? linear * 12.92 : 1.055 * std::pow(linear, 1 / 2.4) - 0.055; };
|
||||
const CameraRawSettings::Balance linear = CameraRawSettings::neutralizeStraight(encode(mean(decode, 200, 20)), encode(mean(decode, 100, 60)),
|
||||
encode(mean(decode, 50, 120))).value();
|
||||
const CameraRawSettings::Balance encoded =
|
||||
CameraRawSettings::neutralizeLinear(mean(same, 200, 20), mean(same, 100, 60), mean(same, 50, 120)).value();
|
||||
CameraRawSettings::neutralizeStraight(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);
|
||||
|
||||
+2
-1
@@ -315,6 +315,8 @@ target_include_directories(RawDitherTests PRIVATE ${OMAPHOTO_KERNELS})
|
||||
compositor_test(ToolDefaultsTests)
|
||||
compositor_test(ColorAdjustmentTests)
|
||||
compositor_test(CameraRawTests)
|
||||
compositor_test(CameraRawTablesTests)
|
||||
target_include_directories(CameraRawTablesTests PRIVATE ${OMAPHOTO_KERNELS})
|
||||
compositor_test(CameraRawColorNoiseTests)
|
||||
compositor_test(CameraRawToneOrderTests)
|
||||
compositor_test(LastFilterTests)
|
||||
@@ -389,5 +391,4 @@ compositor_test(SliderSnapTests)
|
||||
compositor_test(ProjectResizeFailureTests)
|
||||
|
||||
# The `.svgz` fixture is gzip, written through zlib.
|
||||
find_package(ZLIB REQUIRED)
|
||||
target_link_libraries(SVGImportTests PRIVATE ZLIB::ZLIB)
|
||||
Reference in new issue
Block a user