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
+21
-41
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user