14.28: Camera Raw's preview 5 to 20 times faster

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 15:29:31 -04:00
1 parent 817b32b4aa
commit 6dc0ffbfa3
15 files changed
+807 -222

No files matched your search

+21 -41
View File
@@ -6,10 +6,8 @@
#include <QPainter>
#include <algorithm>
#include <cmath>
#include <numbers>
extern "C" {
#include "AdjustPixels.h"
#include "LevelsPixels.h"
}
using ImageAdjustmentPixels::clamp;
@@ -116,15 +114,17 @@ double CameraRawSettings::grainKernelSize() const
}
QImage CameraRawSettings::apply(const QImage &image, std::optional<CameraRawClipping> clipping, double scale, quint32 seed, int visualizePointColor,
bool sharpenMask) const
bool sharpenMask, std::optional<CameraRawTables::Brightness> known) const
{
const CameraRawSettings settings = normalized();
if (settings.isIdentity() && !clipping && visualizePointColor < 0 && !sharpenMask)
return image;
const int mode = clipping ? int(*clipping) : 0;
const double pixelScale = scale > 0 ? scale : 1;
const bool paintColor = !clipping && !sharpenMask
&& (settings.curve.adjusts() || settings.mixer.adjusts() || settings.grading.adjusts() || visualizePointColor >= 0);
// Curve, Mixer and Grading join the table unless curve-passed.
const bool foldsColor = !clipping && !sharpenMask && visualizePointColor < 0 && settings.mixer.points.empty() && settings.curve.refineSaturation == 0;
const bool colorAdjusts = settings.curve.adjusts() || settings.mixer.adjusts() || settings.grading.adjusts();
const bool paintColor = !foldsColor && !clipping && !sharpenMask && (colorAdjusts || visualizePointColor >= 0);
const bool paintEffects = !clipping && !sharpenMask && settings.adjustsEffects();
const bool paintDetailOptics = !clipping && (settings.detail.adjusts() || settings.optics.adjusts() || sharpenMask);
QImage source = image;
@@ -132,16 +132,17 @@ QImage CameraRawSettings::apply(const QImage &image, std::optional<CameraRawClip
source = settings.geometry.apply(source);
return ImageAdjustmentPixels::run(source, [&](uchar *pixels, int width, int height, qsizetype stride) {
const size_t w = size_t(width), h = size_t(height), s = size_t(stride);
const bool adaptive = settings.contrast != 0 || settings.highlights != 0 || settings.shadows != 0;
const CameraRawTables::Brightness brightness = !adaptive ? CameraRawTables::Brightness()
: known ? known.value()
: CameraRawTables::brightness(pixels, width, height, stride);
if (!clipping && !sharpenMask && settings.calibration.adjusts())
settings.applyCalibration(pixels, width, height, stride);
if (settings.adjustsLight() || settings.adjustsColor() || settings.calibration.adjusts()
|| (settings.curve.hasMeasuredDividers() && settings.curve.adjusts()) || clipping) {
const CameraRawTables::Brightness brightness = settings.contrast != 0 || settings.highlights != 0 || settings.shadows != 0
? CameraRawTables::brightness(pixels, width, height, stride)
: CameraRawTables::Brightness();
const std::vector<float> table = CameraRawTables::compose(CameraRawTables::stages(settings, brightness));
std::vector<float> table = CameraRawTables::compose(CameraRawTables::stages(settings, brightness));
if (foldsColor && colorAdjusts)
table = settings.foldingColor(std::move(table));
if (!table.empty() || clipping)
adjust_camera_raw(pixels, w, h, s, table.empty() ? nullptr : table.data(), CameraRawTables::grid, mode);
}
if (paintColor)
settings.applyCurveColor(pixels, width, height, stride, visualizePointColor);
if (paintEffects) {
@@ -196,36 +197,15 @@ std::optional<CameraRawScope> CameraRawScope::make(const QImage &image)
{
if (image.isNull())
return std::nullopt;
const QImage pixels = drawn(image);
std::array<double, binCount * 4> bins{};
// Row by row: Qt may pad rows; the kernel counts.
for (int y = 0; y < pixels.height(); ++y)
levels_histogram(pixels.constScanLine(y), nullptr, size_t(pixels.width()), bins.data());
// Every other pixel each way, from premultiplied bytes directly.
const QImage pixels = image.format() == QImage::Format_RGBA8888_Premultiplied ? image : drawn(image);
std::array<double, binCount * 3> bins{};
CameraRawScope scope;
std::copy_n(bins.begin() + binCount, binCount, scope.red.begin());
std::copy_n(bins.begin() + 2 * binCount, binCount, scope.green.begin());
std::copy_n(bins.begin() + 3 * binCount, binCount, scope.blue.begin());
for (int y = 0; y < pixels.height(); ++y) {
const uchar *row = pixels.constScanLine(y);
for (int x = 0; x < pixels.width(); ++x) {
const uchar *pixel = row + x * 4;
const double alpha = pixel[3];
if (alpha == 0)
continue;
const double r = std::min(1.0, pixel[0] / alpha), g = std::min(1.0, pixel[1] / alpha), b = std::min(1.0, pixel[2] / alpha);
const double high = std::max({r, g, b}), low = std::min({r, g, b}), chroma = high - low;
if (!(chroma > 1e-4 && high > 1e-4))
continue;
double hue = high == r ? (g - b) / chroma : high == g ? 2 + (b - r) / chroma : 4 + (r - g) / chroma;
// A negative hue needs no wrap: the angle turns round.
hue /= 6;
const double angle = hue * 2 * std::numbers::pi, reach = chroma / high;
const double plotX = 0.5 + std::cos(angle) * reach * 0.48, plotY = 0.5 + std::sin(angle) * reach * 0.48;
const int column = std::min(scopeSide - 1, std::max(0, int(plotX * scopeSide)));
const int line = std::min(scopeSide - 1, std::max(0, int(plotY * scopeSide)));
scope.vectorscope[size_t(line * scopeSide + column)] += alpha / 255;
}
}
camera_raw_scope(pixels.constBits(), size_t(pixels.width()), size_t(pixels.height()), size_t(pixels.bytesPerLine()), 2, bins.data(),
scope.vectorscope.data(), scopeSide);
std::copy_n(bins.begin(), binCount, scope.red.begin());
std::copy_n(bins.begin() + binCount, binCount, scope.green.begin());
std::copy_n(bins.begin() + 2 * binCount, binCount, scope.blue.begin());
return scope;
}
+13 -2
View File
@@ -33,6 +33,15 @@ struct CameraRawGroups {
friend bool operator==(const CameraRawGroups &, const CameraRawGroups &) = default;
};
namespace CameraRawTables {
// What adaptive sliders read first, in sRGB levels.
struct Brightness {
double brightest = 128;
double luminance = 128;
double logBrightest = 128;
};
}
// Defaults leave the image unchanged.
struct CameraRawSettings {
static constexpr double exposureLow = -5, exposureHigh = 5;
@@ -87,9 +96,11 @@ struct CameraRawSettings {
CameraRawSettings applying(const CameraRawGroups &shown) const;
// Camera Raw's 0 to 100 size in `adjust_grain`'s pixels.
double grainKernelSize() const;
// `clipping`: the Option view; `scale`: preview pixels a pixel.
// Option view, preview scale, and the source's brightness once known.
QImage apply(const QImage &image, std::optional<CameraRawClipping> clipping = std::nullopt, double scale = 1, quint32 seed = 0,
int visualizePointColor = -1, bool sharpenMask = false) const;
int visualizePointColor = -1, bool sharpenMask = false, std::optional<CameraRawTables::Brightness> brightness = std::nullopt) const;
// Curve, Mixer and Grading run over a composed table.
std::vector<float> foldingColor(std::vector<float> composed) const;
struct Balance {
double temperature;
double tint;
+16
View File
@@ -255,6 +255,22 @@ CameraRawGradingSettings CameraRawGradingSettings::normalized() const
clamp(balance, -100, 100, 0)};
}
std::vector<float> CameraRawSettings::foldingColor(std::vector<float> composed) const
{
std::vector<float> table = composed.empty() ? CameraRawTables::identity() : std::move(composed);
const CameraRawCurveSettings shape = curve.normalized();
if (shape.adjusts()) {
const std::vector<float> tone = shape.toneTable(), red = shape.channelTable(shape.red), green = shape.channelTable(shape.green),
blue = shape.channelTable(shape.blue);
camera_raw_table_curves(table.data(), CameraRawTables::grid, tone.data(), red.data(), green.data(), blue.data());
}
std::vector<CameraRawStage> stages = CameraRawTables::mixerStages(mixer.normalized());
const std::vector<CameraRawStage> graded = CameraRawTables::gradingStages(grading.normalized());
stages.insert(stages.end(), graded.begin(), graded.end());
camera_raw_compose_onto(table.data(), CameraRawTables::grid, stages.data(), int(stages.size()), CameraRawTables::size);
return table;
}
void CameraRawSettings::applyCurveColor(uchar *pixels, int width, int height, qsizetype stride, int visualize) const
{
const CameraRawCurveSettings shape = curve.normalized();
+29 -3
View File
@@ -1,9 +1,11 @@
#include "Document/CameraRawTables.h"
#include "Document/BrushStroke.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include <QFile>
#include <QPainter>
#include <array>
#include <cmath>
#include <span>
@@ -160,6 +162,20 @@ Brightness brightness(const uchar *pixels, int width, int height, qsizetype stri
return {out[0] * 255, out[1] * 255, out[2] * 255};
}
Brightness brightness(const QImage &image)
{
try {
QImage drawn = BrushRaster::context(image.width(), image.height(), false);
QPainter painter(&drawn);
BrushRaster::draw(image, QRectF(0, 0, image.width(), image.height()), painter);
painter.end();
return brightness(drawn.constBits(), drawn.width(), drawn.height(), drawn.bytesPerLine());
} catch (const ExportError &error) {
qCWarning(lcRendering) << "Camera Raw could not measure the picture's brightness:" << error.what();
return {};
}
}
std::vector<CameraRawStage> stages(const CameraRawSettings &settings, const Brightness &brightness)
{
std::vector<CameraRawStage> out;
@@ -252,10 +268,9 @@ std::vector<CameraRawStage> mixerStages(const CameraRawMixerSettings &mixer)
return out;
}
std::vector<float> compose(const std::vector<CameraRawStage> &stages)
namespace {
std::vector<float> composed(const std::vector<CameraRawStage> &stages)
{
if (stages.empty())
return {};
std::vector<float> out;
try {
out.resize(size_t(grid) * grid * grid * 3);
@@ -265,6 +280,17 @@ std::vector<float> compose(const std::vector<CameraRawStage> &stages)
camera_raw_compose(out.data(), grid, stages.data(), int(stages.size()), size);
return out;
}
}
std::vector<float> identity()
{
return composed({});
}
std::vector<float> compose(const std::vector<CameraRawStage> &stages)
{
return stages.empty() ? std::vector<float>() : composed(stages);
}
std::optional<CameraRawSettings::Balance> neutralize(double red, double green, double blue)
{
+3 -6
View File
@@ -20,19 +20,16 @@ const std::vector<uint8_t> &tables();
CameraRawStage whiteBalance(double temperature, double tint);
// Exposure's 21 tables in turn, for the lens vignette.
const uint8_t *exposureTables();
// What adaptive sliders read first, in sRGB levels.
struct Brightness {
double brightest = 128;
double luminance = 128;
double logBrightest = 128;
};
Brightness brightness(const uchar *pixels, int width, int height, qsizetype stride);
Brightness brightness(const QImage &image);
// Camera Raw's order, sliders at zero left out.
std::vector<CameraRawStage> stages(const CameraRawSettings &settings, const Brightness &brightness);
// Color Grading's stages: luminance, global, midtones, highlights, shadows.
std::vector<CameraRawStage> gradingStages(const CameraRawGradingSettings &grading);
// The Mixer's stages: luminance, then saturation, then hue.
std::vector<CameraRawStage> mixerStages(const CameraRawMixerSettings &mixer);
// No change, on the composing grid.
std::vector<float> identity();
// The stages over the composing grid; empty without stages.
std::vector<float> compose(const std::vector<CameraRawStage> &stages);
// Temperature and Tint turning a straight sRGB colour gray.
+14 -2
View File
@@ -1,6 +1,7 @@
#include "Document/Filters.h"
#include "Document/DocumentLimits.h"
#include "Document/BrushStroke.h"
#include "Document/CameraRawTables.h"
#include "Document/ContentFill.h"
#include "Document/EditorSession+Model.h"
#include "Document/PixelAdjust.h"
@@ -161,7 +162,8 @@ QImage PixelFilter::run(const FilterJob &job)
case FilterKind::blackWhite: image = settings.blackWhite.apply(job.image); break;
case FilterKind::colorBalance: image = settings.colorBalance.apply(job.image); break;
case FilterKind::cameraRaw:
image = settings.cameraRaw.apply(job.image, job.cameraRawClipping, job.scale, job.seed, job.visualizesPointColor, job.showsSharpenMask);
image = settings.cameraRaw.apply(job.image, job.cameraRawClipping, job.scale, job.seed, job.visualizesPointColor, job.showsSharpenMask,
job.cameraRawBrightness);
break;
// Grain sits in layer pixels; the job's seed patterns it.
case FilterKind::grain: image = settings.grain.apply(job.image, QPointF(), 1 / job.scale, job.seed); break;
@@ -325,7 +327,17 @@ FilterJob FilterEdit::previewJob() const
rawPanel.pointColorVisualizeIndex(settings.cameraRaw),
rawPanel.sharpenMask,
{},
canvas};
canvas,
kind == FilterKind::cameraRaw ? std::optional(previewBrightness()) : std::nullopt};
}
CameraRawTables::Brightness FilterEdit::previewBrightness() const
{
if (m_cameraRawBrightness && m_cameraRawBrightness->first == previewSource.cacheKey())
return m_cameraRawBrightness->second;
const CameraRawTables::Brightness value = CameraRawTables::brightness(previewSource);
m_cameraRawBrightness = std::pair{previewSource.cacheKey(), value};
return value;
}
FilterSettings FilterEdit::renderSettings() const
+6
View File
@@ -114,6 +114,8 @@ struct FilterJob {
QPointF noiseOrigin{};
// Vignette on an empty layer: the canvas it fills.
std::optional<QRectF> canvas = std::nullopt;
// Camera Raw's adaptive sliders' reading of `image`, once known.
std::optional<CameraRawTables::Brightness> cameraRawBrightness = std::nullopt;
};
// Swift's PixelFilter.
@@ -149,6 +151,8 @@ public:
// The settings as rendered: a hidden Camera Raw group drops.
FilterSettings renderSettings() const;
FilterJob previewJob() const;
// Measured once from the preview's source, again when it changes.
CameraRawTables::Brightness previewBrightness() const;
// Swift's object identity: a preview lands on its own edit.
const QUuid id = QUuid::createUuid();
@@ -193,4 +197,6 @@ private:
void grow(const QRectF &extent);
// Swift's prepared and prepare: mapping and preview copy.
void prepare(const ImportedImage &source, const LayerTransform &placed);
// The source's identity and what it read.
mutable std::optional<std::pair<qint64, CameraRawTables::Brightness>> m_cameraRawBrightness;
};
+10
View File
@@ -1,15 +1,25 @@
#include "Document/ImageAdjustments.h"
#include "Document/BrushStroke.h"
#include "IO/ProjectStore.h"
#include "Rendering/PoolMap.h"
#include <QPainter>
#include <algorithm>
#include <array>
#include <cmath>
#include <numeric>
extern "C" {
#include "AdjustPixels.h"
#include "LevelsPixels.h"
}
// Swift's GCD bands for AdjustPixels.c, on QtConcurrent.
extern "C" void adjust_bands(size_t bands, void (*band)(void *context, size_t index), void *context)
{
std::vector<size_t> indices(bands);
std::iota(indices.begin(), indices.end(), size_t(0));
PoolMap::blocking(indices, [band, context](size_t index) { band(context, index); });
}
QImage ImageAdjustmentPixels::run(const QImage &image, const std::function<void(uchar *, int, int, qsizetype)> &body)
{
QImage context = BrushRaster::context(image.width(), image.height(), false);