14.28: Camera Raw's preview 5 to 20 times faster
This commit is contained in:
15 files changed
+807
-222
No files matched your search
@@ -0,0 +1,198 @@
|
||||
#include "CameraRawFixtures.h"
|
||||
#include "Document/CameraRawTables.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/Filters.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's a8630e4: one table, the brightness once, a sparser scope.
|
||||
using namespace CameraRawFixtures;
|
||||
|
||||
class CameraRawFoldTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void curveMixerAndGradingFoldIntoTheTable();
|
||||
void aPointColourOrRefineSaturationKeepsTheCurvePass();
|
||||
void aKnownBrightnessIsReadBeforeCalibration();
|
||||
void aSessionMeasuresItsPreviewSourceOnce();
|
||||
void everyBandIsMeasuredAndCounted();
|
||||
void calibrationsBlueHueTurnsAndItsSaturationSaturates();
|
||||
};
|
||||
|
||||
namespace {
|
||||
QImage noise(int side = 32)
|
||||
{
|
||||
QImage picture = BrushRaster::context(side, side, false);
|
||||
quint32 state = 11;
|
||||
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::vector<float> &table)
|
||||
{
|
||||
QImage copy = picture.copy();
|
||||
adjust_camera_raw(copy.bits(), size_t(copy.width()), size_t(copy.height()), size_t(copy.bytesPerLine()), table.data(), CameraRawTables::grid, 0);
|
||||
return copy;
|
||||
}
|
||||
|
||||
CameraRawSettings coloured()
|
||||
{
|
||||
CameraRawSettings settings;
|
||||
settings.exposure = 0.5;
|
||||
settings.curve.rgb = CameraRawCurveSettings::mediumContrast();
|
||||
settings.curve.red = {{0, 0}, {0.4, 0.5}, {1, 1}};
|
||||
settings.mixer.saturation[1] = -40;
|
||||
settings.grading.highlights = {30, 40, 10};
|
||||
return settings;
|
||||
}
|
||||
|
||||
int farthest(const QImage &left, const QImage &right)
|
||||
{
|
||||
int most = 0;
|
||||
const auto a = pixels(left), b = pixels(right);
|
||||
for (size_t i = 0; i < a.size(); ++i)
|
||||
for (int c = 0; c < 4; ++c)
|
||||
most = std::max(most, std::abs(a[i][size_t(c)] - b[i][size_t(c)]));
|
||||
return most;
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawFoldTests::curveMixerAndGradingFoldIntoTheTable()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
const CameraRawSettings settings = coloured().normalized();
|
||||
std::vector<float> table = CameraRawTables::compose(CameraRawTables::stages(settings, {}));
|
||||
// The curve, then the Mixer and Grading, over Light's table.
|
||||
const CameraRawCurveSettings &curve = settings.curve;
|
||||
const std::vector<float> tone = curve.toneTable(), red = curve.channelTable(curve.red), green = curve.channelTable(curve.green),
|
||||
blue = curve.channelTable(curve.blue);
|
||||
camera_raw_table_curves(table.data(), CameraRawTables::grid, tone.data(), red.data(), green.data(), blue.data());
|
||||
std::vector<CameraRawStage> stages = CameraRawTables::mixerStages(settings.mixer);
|
||||
const std::vector<CameraRawStage> graded = CameraRawTables::gradingStages(settings.grading);
|
||||
stages.insert(stages.end(), graded.begin(), graded.end());
|
||||
camera_raw_compose_onto(table.data(), CameraRawTables::grid, stages.data(), int(stages.size()), CameraRawTables::size);
|
||||
QVERIFY(pixels(settings.apply(picture)) == pixels(kernel(picture, table)));
|
||||
// Without Light's table the fold starts from no change.
|
||||
CameraRawSettings alone;
|
||||
alone.curve.red = settings.curve.red;
|
||||
const std::vector<float> identity = CameraRawTables::identity();
|
||||
QCOMPARE(identity.size(), size_t(33) * 33 * 33 * 3);
|
||||
QVERIFY(identity[3 * (16 * 33 * 33 + 4 * 33 + 28)] == 0.5f && identity.back() == 1.0f);
|
||||
std::vector<float> curved = identity;
|
||||
const std::vector<float> straight = alone.curve.toneTable(), redOnly = alone.curve.channelTable(alone.curve.red);
|
||||
camera_raw_table_curves(curved.data(), CameraRawTables::grid, straight.data(), redOnly.data(), straight.data(), straight.data());
|
||||
QVERIFY(alone.foldingColor({}) == curved);
|
||||
QVERIFY(pixels(alone.apply(picture)) == pixels(kernel(picture, curved)));
|
||||
}
|
||||
|
||||
void CameraRawFoldTests::aPointColourOrRefineSaturationKeepsTheCurvePass()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
// A point colour that weighs nothing changes no pixel.
|
||||
CameraRawSettings pointed = coloured();
|
||||
pointed.mixer.points = {CameraRawPointColor{.hue = 200, .saturation = 0.01, .luminance = 0.99}};
|
||||
const QImage folded = coloured().apply(picture), passed = pointed.apply(picture);
|
||||
QVERIFY(farthest(folded, passed) <= 3 && pixels(folded) != pixels(passed));
|
||||
// Refine Saturation is the curve pass's alone.
|
||||
CameraRawSettings refined = coloured();
|
||||
refined.curve.refineSaturation = 30;
|
||||
const CameraRawSettings light = refined.normalized();
|
||||
QImage direct = kernel(picture, CameraRawTables::compose(CameraRawTables::stages(light, {})));
|
||||
light.applyCurveColor(direct.bits(), direct.width(), direct.height(), direct.bytesPerLine(), -1);
|
||||
QVERIFY(pixels(refined.apply(picture)) == pixels(direct));
|
||||
// A point-colour view keeps the pass, with no points too.
|
||||
const CameraRawSettings plain = coloured().normalized();
|
||||
QImage shown = kernel(picture, CameraRawTables::compose(CameraRawTables::stages(plain, {})));
|
||||
plain.applyCurveColor(shown.bits(), shown.width(), shown.height(), shown.bytesPerLine(), 0);
|
||||
QVERIFY(pixels(coloured().apply(picture, std::nullopt, 1, 0, 0)) == pixels(shown));
|
||||
}
|
||||
|
||||
void CameraRawFoldTests::aKnownBrightnessIsReadBeforeCalibration()
|
||||
{
|
||||
// Darker noise, where Shadows Tint turns many hues.
|
||||
QImage picture = noise();
|
||||
for (int y = 0; y < 32; ++y)
|
||||
for (int x = 0; x < 32; ++x) {
|
||||
const QColor colour = picture.pixelColor(x, y);
|
||||
picture.setPixelColor(x, y, QColor(colour.red() / 2, colour.green() / 2, colour.blue() / 2));
|
||||
}
|
||||
CameraRawSettings settings;
|
||||
settings.shadows = 100;
|
||||
settings.calibration.shadowTint = 100;
|
||||
// Measured on the pixels before Shadows Tint changes them.
|
||||
const CameraRawTables::Brightness before = CameraRawTables::brightness(picture);
|
||||
QImage direct = picture.copy();
|
||||
settings.normalized().applyCalibration(direct.bits(), direct.width(), direct.height(), direct.bytesPerLine());
|
||||
const CameraRawTables::Brightness after = CameraRawTables::brightness(direct);
|
||||
const QImage expected = kernel(direct, CameraRawTables::compose(CameraRawTables::stages(settings.normalized(), before)));
|
||||
const QImage late = kernel(direct, CameraRawTables::compose(CameraRawTables::stages(settings.normalized(), after)));
|
||||
QVERIFY(pixels(settings.apply(picture)) == pixels(expected));
|
||||
QVERIFY(pixels(late) != pixels(expected));
|
||||
// A brightness handed in is used as it is.
|
||||
const CameraRawTables::Brightness dim{60, 60, 60};
|
||||
const QImage handed = kernel(direct, CameraRawTables::compose(CameraRawTables::stages(settings.normalized(), dim)));
|
||||
QVERIFY(pixels(settings.apply(picture, std::nullopt, 1, 0, -1, false, dim)) == pixels(handed) && pixels(handed) != pixels(expected));
|
||||
}
|
||||
|
||||
void CameraRawFoldTests::aSessionMeasuresItsPreviewSourceOnce()
|
||||
{
|
||||
const QImage picture = noise();
|
||||
EditorSession session;
|
||||
session.createDocument(32, 32);
|
||||
session.insert(ImportedImage(picture, picture, QStringLiteral("Picture")));
|
||||
session.beginFilter(FilterKind::cameraRaw);
|
||||
const FilterEdit &edit = session.filterEdit().value();
|
||||
const CameraRawTables::Brightness measured = CameraRawTables::brightness(edit.previewSource);
|
||||
const CameraRawTables::Brightness carried = edit.previewJob().cameraRawBrightness.value();
|
||||
QVERIFY(carried.brightest == measured.brightest && carried.luminance == measured.luminance && carried.logBrightest == measured.logBrightest);
|
||||
QCOMPARE(edit.previewBrightness().brightest, measured.brightest);
|
||||
session.cancelFilter();
|
||||
session.beginFilter(FilterKind::gaussianBlur);
|
||||
QVERIFY(!session.filterEdit().value().previewJob().cameraRawBrightness);
|
||||
}
|
||||
|
||||
// Forty rows of gray: every band's rows reach the sums.
|
||||
void CameraRawFoldTests::everyBandIsMeasuredAndCounted()
|
||||
{
|
||||
QImage ramp = BrushRaster::context(1, 40, false);
|
||||
for (int y = 0; y < 40; ++y)
|
||||
ramp.setPixelColor(0, y, QColor(y * 6, y * 6, y * 6));
|
||||
// Python's reading of all forty rows.
|
||||
const CameraRawTables::Brightness read = CameraRawTables::brightness(ramp);
|
||||
QVERIFY(std::abs(read.brightest - 139.556468482) < 1e-6 && std::abs(read.logBrightest - 91.779144447) < 1e-6);
|
||||
// Sixteen bands of three rows, every other row within each.
|
||||
const CameraRawScope scope = CameraRawScope::make(ramp).value();
|
||||
std::array<double, 256> expected{};
|
||||
for (int band = 0; band < 16; ++band)
|
||||
for (int y = band * 3; y < std::min(40, band * 3 + 3); y += 2)
|
||||
expected[size_t(y * 6)] += 1;
|
||||
for (size_t level = 0; level < 256; ++level)
|
||||
QVERIFY2(scope.red[level] == expected[level] && scope.blue[level] == expected[level], qPrintable(QString::number(level)));
|
||||
}
|
||||
|
||||
// The blue primary: hue turns the hue, saturation saturates.
|
||||
void CameraRawFoldTests::calibrationsBlueHueTurnsAndItsSaturationSaturates()
|
||||
{
|
||||
const auto hsl = [](const QImage &picture) {
|
||||
const QColor colour = picture.pixelColor(0, 0).toHsl();
|
||||
return std::array<int, 3>{colour.hslHue(), colour.hslSaturation(), colour.lightness()};
|
||||
};
|
||||
QImage blue = BrushRaster::context(1, 1, false);
|
||||
blue.setPixelColor(0, 0, QColor(60, 80, 200));
|
||||
const std::array<int, 3> before = hsl(blue);
|
||||
QImage turned = blue.copy(), saturated = blue.copy();
|
||||
adjust_camera_raw_calibration(turned.bits(), 1, 1, size_t(turned.bytesPerLine()), 0, 0, 0, 0, 0, 80, 0, 6);
|
||||
adjust_camera_raw_calibration(saturated.bits(), 1, 1, size_t(saturated.bytesPerLine()), 0, 0, 0, 0, 0, 0, -80, 6);
|
||||
QVERIFY(std::abs(hsl(turned)[0] - before[0]) > 5 && std::abs(hsl(turned)[1] - before[1]) <= 3);
|
||||
QVERIFY(std::abs(hsl(saturated)[0] - before[0]) <= 2 && hsl(saturated)[1] < before[1] - 40);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CameraRawFoldTests)
|
||||
#include "CameraRawFoldTests.moc"
|
||||
Reference in new issue
Block a user