#include "CameraRawFixtures.h" #include #include #include // Swift 1.4.6's Color noise reduction: color smoothed, brightness kept. using namespace CameraRawFixtures; namespace { struct Measured { double color; double brightness; }; // Swift's measure: mean spread and brightness inside a margin. Measured measure(const QImage &image, int margin) { const std::vector> read = pixels(image); double color = 0, brightness = 0; int counted = 0; for (int y = margin; y < image.height() - margin; ++y) for (int x = margin; x < image.width() - margin; ++x) { const std::array &p = read[size_t(y * image.width() + x)]; color += chroma(p); brightness += 0.2126 * p[0] + 0.7152 * p[1] + 0.0722 * p[2]; ++counted; } return {color / counted, brightness / counted}; } CameraRawSettings colorNoise(double amount, double detail = 50, double smoothness = 50) { CameraRawSettings settings; settings.detail.noiseColor = amount; settings.detail.noiseColorDetail = detail; settings.detail.noiseColorSmoothness = smoothness; return settings; } // Red, then blue of some alpha from x 15. QImage split(int blueAlpha) { QImage picture = BrushRaster::context(31, 21, false); QPainter painter(&picture); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(QRect(0, 0, 15, 21), QColor(230, 51, 51)); painter.fillRect(QRect(15, 0, 16, 21), QColor(51, 51, 230, blueAlpha)); return picture; } } class CameraRawColorNoiseTests : public QObject { Q_OBJECT private slots: void colorNoiseReductionRemovesColorGrainNotBrightness(); void colorNoiseReductionIgnoresWhatClearPixelsHeld(); void clearPixelsPullNoColorTowardGray(); void colorDetailKeepsARealColorEdge(); void aSpeckTakesTheColorAroundItAndKeepsItsBrightness(); void translucentPixelsKeepTheirColor(); void coverageWeighsNeighboursAndSmoothnessReaches(); void blueAloneMakesAnEdgeAndTheReachFollowsAmountAndScale(); }; void CameraRawColorNoiseTests::colorNoiseReductionRemovesColorGrainNotBrightness() { QImage grain = BrushRaster::context(120, 120, false); quint32 seed = 12345; for (int y = 0; y < 120; ++y) { uchar *row = grain.scanLine(y); for (int x = 0; x < 120; ++x) { for (int c = 0; c < 3; ++c) { seed = seed * 1664525u + 1013904223u; row[x * 4 + c] = uchar(64 + int(seed >> 25)); } row[x * 4 + 3] = 255; } } const Measured before = measure(CameraRawSettings().apply(grain), 10); const Measured after = measure(colorNoise(100).apply(grain), 10); QVERIFY2(after.color < before.color * 0.25, qPrintable(QStringLiteral("%1 → %2").arg(before.color).arg(after.color))); QVERIFY2(std::abs(after.brightness - before.brightness) < 1, qPrintable(QStringLiteral("%1 → %2").arg(before.brightness).arg(after.brightness))); // Half the amount takes about half the speckle. const Measured half = measure(colorNoise(50).apply(grain), 10); QVERIFY(half.color < before.color * 0.75 && half.color > after.color * 1.5); } void CameraRawColorNoiseTests::colorNoiseReductionIgnoresWhatClearPixelsHeld() { const CameraRawSettings settings = colorNoise(60); QImage picture = BrushRaster::context(97, 61, false); for (int y = 0; y < 61; ++y) { uchar *row = picture.scanLine(y); for (int x = 0; x < 97; ++x) { const int alpha = x < 30 ? 0 : 255; uchar *p = row + x * 4; p[0] = uchar((x * 7 + y * 3) % 256 * alpha / 255); p[1] = uchar((x * x + y * 5) % 256 * alpha / 255); p[2] = uchar((x * 2 + y * y) % 256 * alpha / 255); p[3] = uchar(alpha); } } // Planes on the heap; handed-out memory holds bytes. mallopt(M_MMAP_THRESHOLD, 64 << 20); const std::vector> first = pixels(settings.apply(picture)); mallopt(M_PERTURB, 0x5a); const QImage saturated = image(97, 61, 1, 0, 0.2); for (int run = 0; run < 4; ++run) settings.apply(saturated); const std::vector> again = pixels(settings.apply(picture)); mallopt(M_PERTURB, 0); QVERIFY(again == first); } void CameraRawColorNoiseTests::clearPixelsPullNoColorTowardGray() { // Red beside clear pixels stays red: they weigh nothing. QImage picture = image(40, 20, 0.9, 0.2, 0.2); for (int y = 0; y < 20; ++y) for (int x = 0; x < 20; ++x) picture.setPixelColor(x, y, Qt::transparent); const std::vector> smoothed = pixels(colorNoise(100).apply(picture)); const std::array beside = smoothed[size_t(10 * 40 + 20)]; QVERIFY2(std::abs(beside[0] - 230) <= 1 && std::abs(beside[1] - 51) <= 1 && std::abs(beside[2] - 51) <= 1, qPrintable(QStringLiteral("%1 %2 %3").arg(beside[0]).arg(beside[1]).arg(beside[2]))); QCOMPARE(smoothed[size_t(10 * 40 + 5)][3], 0); // A lone pixel among clear ones keeps its colour too. QImage lone = BrushRaster::context(31, 31, false); lone.setPixelColor(15, 15, QColor(230, 51, 51)); QCOMPARE(pixels(colorNoise(100).apply(lone))[size_t(15 * 31 + 15)], (std::array{230, 51, 51, 255})); } void CameraRawColorNoiseTests::colorDetailKeepsARealColorEdge() { // Red meets blue at one brightness; Detail keeps the edge. QImage picture = image(60, 20, 0.8, 0.1, 0.1); QPainter(&picture).fillRect(QRect(30, 0, 30, 20), QColor::fromRgbF(0.1f, 0.1f, 0.8f)); const auto redness = [](const QImage &smoothed) { return pixels(smoothed)[size_t(10 * 60 + 29)][0]; }; const int sharp = redness(colorNoise(100, 100).apply(picture)), soft = redness(colorNoise(100, 0).apply(picture)); QVERIFY2(sharp > soft + 20, qPrintable(QStringLiteral("%1 %2").arg(sharp).arg(soft))); // Far from the edge both leave the red alone. QCOMPARE(pixels(colorNoise(100, 0).apply(picture))[size_t(10 * 60 + 2)][0], 204); // The same edge across, read down the picture. const QImage across = picture.transformed(QTransform().rotate(90)); const auto acrossRedness = [](const QImage &smoothed) { return pixels(smoothed)[size_t(29 * 20 + 10)][0]; }; const int acrossSharp = acrossRedness(colorNoise(100, 100).apply(across)), acrossSoft = acrossRedness(colorNoise(100, 0).apply(across)); QVERIFY2(acrossSharp > acrossSoft + 20, qPrintable(QStringLiteral("%1 %2").arg(acrossSharp).arg(acrossSoft))); } void CameraRawColorNoiseTests::aSpeckTakesTheColorAroundItAndKeepsItsBrightness() { QImage picture = image(21, 21, 0.6, 0.3, 0.3); picture.setPixelColor(10, 10, QColor::fromRgbF(0.3f, 0.3f, 0.6f)); const std::array before = pixels(picture)[size_t(10 * 21 + 10)]; const std::array after = pixels(colorNoise(100, 0).apply(picture))[size_t(10 * 21 + 10)]; const auto luma = [](const std::array &p) { return 0.2126 * p[0] + 0.7152 * p[1] + 0.0722 * p[2]; }; QVERIFY2(after[0] > after[2] + 40, qPrintable(QStringLiteral("%1 %2 %3").arg(after[0]).arg(after[1]).arg(after[2]))); QVERIFY2(std::abs(luma(after) - luma(before)) < 1, qPrintable(QStringLiteral("%1 → %2").arg(luma(before)).arg(luma(after)))); } void CameraRawColorNoiseTests::translucentPixelsKeepTheirColor() { // Coverage weighs the color in and divides it out again. const QImage picture = image(30, 30, 0.8, 0.2, 0.2, 0.5); const std::array before = pixels(picture)[size_t(15 * 30 + 15)]; const std::array after = pixels(colorNoise(100).apply(picture))[size_t(15 * 30 + 15)]; for (size_t channel = 0; channel < 4; ++channel) QVERIFY2(std::abs(after[channel] - before[channel]) <= 2, qPrintable(QStringLiteral("%1: %2 → %3").arg(channel).arg(before[channel]).arg(after[channel]))); } void CameraRawColorNoiseTests::coverageWeighsNeighboursAndSmoothnessReaches() { // A faint blue pulls the red only by its coverage. using Pixel = std::array; QCOMPARE(pixels(colorNoise(100, 0).apply(split(32)))[size_t(10 * 31 + 14)], (Pixel{215, 53, 71, 255})); // Smoothness widens the reach: none at 0, some at 100. QCOMPARE(pixels(colorNoise(100, 0, 0).apply(split(255)))[size_t(10 * 31 + 9)], (Pixel{230, 51, 51, 255})); QCOMPARE(pixels(colorNoise(100, 0, 100).apply(split(255)))[size_t(10 * 31 + 9)], (Pixel{203, 55, 87, 255})); } void CameraRawColorNoiseTests::blueAloneMakesAnEdgeAndTheReachFollowsAmountAndScale() { using Pixel = std::array; // Two opaque halves, 60 by 20, meeting at x 30. const auto halves = [](QColor left, QColor right) { QImage picture = BrushRaster::context(60, 20, false); QPainter painter(&picture); painter.fillRect(QRect(0, 0, 30, 20), left); painter.fillRect(QRect(30, 0, 30, 20), right); return picture; }; // An edge mostly in the blue difference: Detail keeps it. const QImage blue = halves(QColor(102, 115, 26), QColor(102, 100, 179)); QCOMPARE(pixels(colorNoise(100, 100).apply(blue))[size_t(10 * 60 + 29)], (Pixel{102, 115, 26, 255})); const QImage blueAcross = blue.transformed(QTransform().rotate(90)); QCOMPARE(pixels(colorNoise(100, 100).apply(blueAcross))[size_t(29 * 20 + 10)], (Pixel{102, 115, 26, 255})); // One in the red difference alone, blue kept equal. const QImage red = halves(QColor(26, 115, 100), QColor(179, 100, 124)); QCOMPARE(pixels(colorNoise(100, 100).apply(red))[size_t(10 * 60 + 29)], (Pixel{26, 115, 100, 255})); QVERIFY(pixels(colorNoise(100, 0).apply(red))[size_t(10 * 60 + 29)] != (Pixel{26, 115, 100, 255})); const QImage redAcross = red.transformed(QTransform().rotate(90)); QCOMPARE(pixels(colorNoise(100, 100).apply(redAcross))[size_t(29 * 20 + 10)], (Pixel{26, 115, 100, 255})); // The reach shrinks with the amount and the scale. const QImage redBlue = halves(QColor(230, 51, 51), QColor(51, 51, 230)); QCOMPARE(pixels(colorNoise(50, 0).apply(redBlue))[size_t(10 * 60 + 24)], (Pixel{230, 51, 51, 255})); QCOMPARE(pixels(colorNoise(100, 0).apply(redBlue, std::nullopt, 0.25))[size_t(10 * 60 + 27)], (Pixel{230, 51, 51, 255})); } QTEST_MAIN(CameraRawColorNoiseTests) #include "CameraRawColorNoiseTests.moc"