Camera Raw filter's model and pixel kernels (12.8a)

This commit is contained in:
雷电芽衣 committed 2026-09-27 19:55:59 -04:00
1 parent 4291f4249a
commit b544e12b1d
33 files changed
+4438 -27

No files matched your search

+306
View File
@@ -0,0 +1,306 @@
#include "Document/BrushStroke.h"
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include <QFutureWatcher>
#include <QPainter>
#include <QtConcurrent>
#include <cmath>
// Swift's CameraRaw.swift session extension: samples, drags, guides.
namespace {
// One pixel's premultiplied bytes, or none past the image.
std::optional<std::array<int, 4>> pixelAt(const QImage &image, QPointF pixel)
{
if (!(pixel.x() >= 0 && pixel.y() >= 0 && pixel.x() < image.width() && pixel.y() < image.height()))
return std::nullopt;
QImage one = BrushRaster::context(1, 1, false);
{
QPainter painter(&one);
BrushRaster::draw(image, QRectF(0, 0, 1, 1), painter, QRectF(std::floor(pixel.x()), std::floor(pixel.y()), 1, 1));
}
const uchar *bytes = one.constBits();
return std::array<int, 4>{bytes[0], bytes[1], bytes[2], bytes[3]};
}
std::array<double, 3> straight(const std::array<int, 4> &bytes)
{
return {std::min(1.0, bytes[0] / double(bytes[3])), std::min(1.0, bytes[1] / double(bytes[3])), std::min(1.0, bytes[2] / double(bytes[3]))};
}
double hueDegrees(double red, double green, double blue)
{
const double high = std::max({red, green, blue}), low = std::min({red, green, blue}), chroma = high - low;
if (!(chroma > 1e-6))
return 0;
const double hue = high == red ? (green - blue) / chroma : high == green ? 2 + (blue - red) / chroma : 4 + (red - green) / chroma;
const double degrees = hue * 60;
return degrees < 0 ? degrees + 360 : degrees;
}
}
void EditorSession::setCameraRawPanel(const CameraRawPanel &panel)
{
if (!m_filterEdit)
return;
m_filterEdit->rawPanel = panel;
notify();
}
void EditorSession::sampleCameraRawWhiteBalance(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw || !m_filterEdit->rawPanel.samplesWhiteBalance || m_filterEdit->committing
|| !m_document || !(point.x() >= 0 && point.y() >= 0 && point.x() < m_document->size().width() && point.y() < m_document->size().height()))
return;
const FilterEdit &edit = *m_filterEdit;
try {
const std::optional<std::array<int, 4>> bytes = pixelAt(edit.original.image(), edit.mapping.inverted().map(point));
if (!bytes || (*bytes)[3] == 0)
return;
const std::array<double, 3> rgb = straight(*bytes);
const std::optional<CameraRawSettings::Balance> solved = CameraRawSettings::neutralizeStraight(rgb[0], rgb[1], rgb[2]);
if (!solved)
return;
FilterSettings settings = edit.settings;
settings.cameraRaw.temperature = solved->temperature;
settings.cameraRaw.tint = solved->tint;
settings.cameraRaw.whiteBalance = CameraRawWhiteBalance::custom;
updateFilter(settings, edit.preview);
} catch (const ExportError &error) {
setBrushError(QString::fromUtf8(error.what()));
}
}
void EditorSession::applyCameraRawAutoWhiteBalance(std::function<void()> done)
{
const auto finish = [this, done] {
if (done)
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
};
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw || m_filterEdit->committing) {
finish();
return;
}
FilterSettings settings = m_filterEdit->settings;
settings.cameraRaw.whiteBalance = CameraRawWhiteBalance::automatic;
updateFilter(settings, m_filterEdit->preview);
// A worker takes the average; it lands if Auto stands.
auto *watcher = new QFutureWatcher<std::optional<CameraRawSettings::Balance>>(this);
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, id = m_filterEdit->id, finish] {
watcher->deleteLater();
const std::optional<CameraRawSettings::Balance> solved = watcher->result();
if (solved && m_filterEdit && m_filterEdit->id == id && !m_filterEdit->committing
&& m_filterEdit->settings.cameraRaw.whiteBalance == CameraRawWhiteBalance::automatic) {
FilterSettings value = m_filterEdit->settings;
value.cameraRaw.temperature = solved->temperature;
value.cameraRaw.tint = solved->tint;
updateFilter(value, m_filterEdit->preview);
}
finish();
});
// A painted layer flattens here; a failure scans nothing.
watcher->setFuture(QtConcurrent::run([original = m_filterEdit->original]() -> std::optional<CameraRawSettings::Balance> {
try {
return CameraRawSettings::autoBalance(original.image());
} catch (const ExportError &error) {
qCWarning(lcApp) << "Camera Raw's Auto found no memory:" << error.what();
return std::nullopt;
}
}));
}
void EditorSession::sampleCameraRawDefringe(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw || !m_filterEdit->rawPanel.samplesDefringe || m_filterEdit->committing
|| !m_document || !(point.x() >= 0 && point.y() >= 0 && point.x() < m_document->size().width() && point.y() < m_document->size().height()))
return;
const FilterEdit &edit = *m_filterEdit;
try {
const std::optional<std::array<int, 4>> bytes = pixelAt(edit.original.image(), edit.mapping.inverted().map(point));
if (!bytes || (*bytes)[3] == 0)
return;
const std::array<double, 3> rgb = straight(*bytes);
const double hue = hueDegrees(rgb[0], rgb[1], rgb[2]);
FilterSettings settings = edit.settings;
CameraRawOpticsSettings &optics = settings.cameraRaw.optics;
const double purpleCenter = 290, greenCenter = 90, span = 25;
if (std::abs(hue - purpleCenter) < std::abs(hue - greenCenter)) {
optics.purpleHueLow = hue - span;
optics.purpleHueHigh = hue + span;
if (optics.purpleAmount == 0)
optics.purpleAmount = 50;
} else {
optics.greenHueLow = hue - span;
optics.greenHueHigh = hue + span;
if (optics.greenAmount == 0)
optics.greenAmount = 50;
}
updateFilter(settings, edit.preview);
} catch (const ExportError &error) {
setBrushError(QString::fromUtf8(error.what()));
}
}
void EditorSession::updateCameraRawReadout(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw)
return;
FilterEdit &edit = *m_filterEdit;
const QImage &image = edit.preparedPreview ? *edit.preparedPreview : edit.previewSource;
std::optional<std::array<int, 3>> readout;
try {
const std::optional<std::array<int, 4>> bytes = pixelAt(image, edit.previewMapping.inverted().map(point));
if (bytes && (*bytes)[3] > 0) {
const int alpha = (*bytes)[3];
readout = std::array<int, 3>{};
for (size_t channel = 0; channel < 3; ++channel)
(*readout)[channel] = std::min(255, ((*bytes)[channel] * 255 + alpha / 2) / alpha);
}
} catch (const ExportError &error) {
// Swift's `try?`: no memory keeps the last readout.
qCWarning(lcApp) << "Camera Raw's readout found no memory:" << error.what();
return;
}
if (edit.rawPanel.readout == readout)
return;
edit.rawPanel.readout = readout;
notify();
}
void EditorSession::beginCameraRawDrag(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw)
return;
const std::optional<CameraRawSample> sample = cameraRawSample(point);
if (!sample)
return;
m_filterEdit->rawPanel.drag = CameraRawDrag{point.y(), m_filterEdit->settings.cameraRaw, sample->tone, sample->hue};
notify();
}
void EditorSession::dragCameraRaw(QPointF point)
{
if (!m_filterEdit || !m_filterEdit->rawPanel.drag)
return;
const FilterEdit &edit = *m_filterEdit;
const CameraRawDrag &drag = *edit.rawPanel.drag;
const double delta = (drag.startY - point.y()) * 0.35;
FilterSettings settings = edit.settings;
settings.cameraRaw = drag.settings;
const CameraRawPanel &panel = edit.rawPanel;
if (panel.targetsCurve) {
if (panel.curvePage == CameraRawCurvePage::parametric) {
CameraRawCurveSettings before = drag.settings.curve;
settings.cameraRaw.curve.region(drag.tone) = std::min(100.0, std::max(-100.0, before.region(drag.tone) + delta));
} else {
settings.cameraRaw.curve = drag.settings.curve.nudged(panel.pointChannel, drag.tone, delta / 100);
}
} else if (panel.targetsMixer) {
const std::array<double, 8> weights = CameraRawMixerSettings::weights(drag.hue);
std::array<double, 8> &target = panel.mixerTab == CameraRawMixerTab::hue ? settings.cameraRaw.mixer.hue
: panel.mixerTab == CameraRawMixerTab::saturation ? settings.cameraRaw.mixer.saturation
: settings.cameraRaw.mixer.luminance;
const std::array<double, 8> &from = panel.mixerTab == CameraRawMixerTab::hue ? drag.settings.mixer.hue
: panel.mixerTab == CameraRawMixerTab::saturation ? drag.settings.mixer.saturation
: drag.settings.mixer.luminance;
for (size_t index = 0; index < 8; ++index)
if (weights[index] > 0)
target[index] = std::min(100.0, std::max(-100.0, from[index] + delta * weights[index]));
}
updateFilter(settings, edit.preview);
}
void EditorSession::sampleCameraRawPointColor(QPointF point)
{
if (!m_filterEdit || !m_filterEdit->rawPanel.samplesPointColor)
return;
const std::optional<CameraRawSample> sample = cameraRawSample(point);
if (!sample)
return;
FilterEdit &edit = *m_filterEdit;
FilterSettings settings = edit.settings;
std::vector<CameraRawPointColor> &points = settings.cameraRaw.mixer.points;
CameraRawPointColor color{.hue = sample->hue, .saturation = sample->saturation, .luminance = sample->luminance};
const int index = edit.rawPanel.pointIndex;
if (index >= 0 && size_t(index) < points.size()) {
color.hueShift = points[size_t(index)].hueShift;
color.saturationShift = points[size_t(index)].saturationShift;
color.luminanceShift = points[size_t(index)].luminanceShift;
points[size_t(index)] = color;
} else if (points.size() < 8) {
points.push_back(color);
edit.rawPanel.pointIndex = int(points.size()) - 1;
}
updateFilter(settings, edit.preview);
}
void EditorSession::beginCameraRawGeometryGuide(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw || !m_filterEdit->rawPanel.drawingGeometryGuide)
return;
const std::optional<QPointF> normalized = cameraRawNormalizedPoint(point);
if (!normalized)
return;
m_filterEdit->rawPanel.guideDraft = std::pair(*normalized, *normalized);
notify();
}
void EditorSession::continueCameraRawGeometryGuide(QPointF point)
{
if (!m_filterEdit || !m_filterEdit->rawPanel.drawingGeometryGuide || !m_filterEdit->rawPanel.guideDraft)
return;
const std::optional<QPointF> normalized = cameraRawNormalizedPoint(point);
if (!normalized)
return;
m_filterEdit->rawPanel.guideDraft->second = *normalized;
notify();
}
void EditorSession::commitCameraRawGeometryGuide()
{
if (!m_filterEdit || !m_filterEdit->rawPanel.guideDraft)
return;
FilterEdit &edit = *m_filterEdit;
const auto [start, end] = *std::exchange(edit.rawPanel.guideDraft, std::nullopt);
FilterSettings settings = edit.settings;
settings.cameraRaw.geometry.guides.push_back({start.x(), start.y(), end.x(), end.y()});
settings.cameraRaw.geometry.upright = CameraRawUprightMode::guided;
updateFilter(settings, edit.preview);
}
std::optional<QPointF> EditorSession::cameraRawNormalizedPoint(QPointF point) const
{
if (!m_filterEdit)
return std::nullopt;
const QImage &image = m_filterEdit->previewSource;
const QPointF pixel = m_filterEdit->previewMapping.inverted().map(point);
if (!(pixel.x() >= 0 && pixel.y() >= 0 && pixel.x() < image.width() && pixel.y() < image.height()))
return std::nullopt;
return QPointF(pixel.x() / image.width(), pixel.y() / image.height());
}
std::optional<EditorSession::CameraRawSample> EditorSession::cameraRawSample(QPointF point) const
{
if (!m_filterEdit)
return std::nullopt;
const QImage &image = m_filterEdit->preparedPreview ? *m_filterEdit->preparedPreview : m_filterEdit->previewSource;
std::optional<std::array<int, 4>> bytes;
try {
bytes = pixelAt(image, m_filterEdit->previewMapping.inverted().map(point));
} catch (const ExportError &error) {
// Swift's `try?`: no memory, no sample.
qCWarning(lcApp) << "Camera Raw's sample found no memory:" << error.what();
return std::nullopt;
}
if (!bytes || (*bytes)[3] == 0)
return std::nullopt;
const auto [red, green, blue] = straight(*bytes);
const double high = std::max({red, green, blue}), low = std::min({red, green, blue}), chroma = high - low;
double hue = 0;
if (chroma > 1e-6) {
hue = (high == red ? (green - blue) / chroma : high == green ? 2 + (blue - red) / chroma : 4 + (red - green) / chroma) / 6;
if (hue < 0)
hue += 1;
}
return CameraRawSample{0.2126 * red + 0.7152 * green + 0.0722 * blue, hue * 360, high == 0 ? 0 : chroma / high, (high + low) / 2};
}
+28
View File
@@ -0,0 +1,28 @@
// Swift's CameraRaw.swift session extension; in EditorSession's body.
public:
// The panel's state, which Swift's panel writes on the edit.
void setCameraRawPanel(const CameraRawPanel &panel);
// The white-balance eyedropper: the clicked pixel turns neutral.
void sampleCameraRawWhiteBalance(QPointF point);
// White Balance Auto: the mode now, the sliders later.
void applyCameraRawAutoWhiteBalance(std::function<void()> done = {});
// The defringe eyedropper centres a range on the fringe.
void sampleCameraRawDefringe(QPointF point);
// The adjusted preview's RGB under the pointer.
void updateCameraRawReadout(QPointF point);
void beginCameraRawDrag(QPointF point);
void dragCameraRaw(QPointF point);
void sampleCameraRawPointColor(QPointF point);
void beginCameraRawGeometryGuide(QPointF point);
void continueCameraRawGeometryGuide(QPointF point);
void commitCameraRawGeometryGuide();
private:
std::optional<QPointF> cameraRawNormalizedPoint(QPointF point) const;
struct CameraRawSample {
double tone;
double hue;
double saturation;
double luminance;
};
std::optional<CameraRawSample> cameraRawSample(QPointF point) const;
+276
View File
@@ -0,0 +1,276 @@
#include "Document/CameraRaw.h"
#include "Document/BrushStroke.h"
#include "Document/Filters.h"
#include "Document/Levels.h"
#include <QPainter>
#include <algorithm>
#include <cmath>
#include <numbers>
extern "C" {
#include "AdjustPixels.h"
#include "LevelsPixels.h"
}
using ImageAdjustmentPixels::clamp;
namespace {
double decode(double encoded)
{
return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4);
}
// The image drawn into the kernels' premultiplied context.
QImage drawn(const QImage &image)
{
QImage context = BrushRaster::context(image.width(), image.height(), false);
QPainter painter(&context);
BrushRaster::draw(image, QRectF(0, 0, image.width(), image.height()), painter);
return context;
}
}
bool CameraRawSettings::adjustsLight() const
{
return exposure != 0 || contrast != 0 || highlights != 0 || shadows != 0 || whites != 0 || blacks != 0;
}
bool CameraRawSettings::adjustsColor() const
{
return temperature != 0 || tint != 0 || vibrance != 0 || saturation != 0;
}
bool CameraRawSettings::adjustsEffects() const
{
return texture != 0 || clarity != 0 || dehaze != 0 || glow != 0 || vignetteAmount != 0 || grainAmount != 0;
}
bool CameraRawSettings::isIdentity() const
{
return !adjustsLight() && !adjustsColor() && !adjustsEffects() && !curve.adjusts() && !mixer.adjusts() && !grading.adjusts()
&& !detail.adjusts() && !optics.adjusts() && !geometry.adjusts() && !calibration.adjusts();
}
CameraRawSettings CameraRawSettings::normalized() const
{
CameraRawSettings result = *this;
result.exposure = clamp(exposure, exposureLow, exposureHigh, 0);
for (double CameraRawSettings::*tone : {&CameraRawSettings::contrast, &CameraRawSettings::highlights, &CameraRawSettings::shadows,
&CameraRawSettings::whites, &CameraRawSettings::blacks, &CameraRawSettings::temperature,
&CameraRawSettings::tint, &CameraRawSettings::vibrance, &CameraRawSettings::saturation,
&CameraRawSettings::texture, &CameraRawSettings::clarity, &CameraRawSettings::dehaze,
&CameraRawSettings::glowRange, &CameraRawSettings::glowSpread, &CameraRawSettings::glowWarmth,
&CameraRawSettings::vignetteAmount, &CameraRawSettings::vignetteRoundness})
result.*tone = clamp(this->*tone, -100, 100, 0);
result.glow = clamp(glow, 0, 100, 0);
result.vignetteMidpoint = clamp(vignetteMidpoint, 0, 100, 50);
result.vignetteFeather = clamp(vignetteFeather, 0, 100, 50);
result.vignetteHighlights = clamp(vignetteHighlights, 0, 100, 0);
result.grainAmount = clamp(grainAmount, 0, 100, 0);
result.grainSize = clamp(grainSize, 0, 100, 25);
result.grainRoughness = clamp(grainRoughness, 0, 100, 50);
result.curve = curve.normalized();
result.mixer = mixer.normalized();
result.grading = grading.normalized();
result.detail = detail.normalized();
result.optics = optics.normalized();
result.geometry = geometry.normalized();
result.calibration = calibration.normalized();
return result;
}
CameraRawSettings CameraRawSettings::applying(const CameraRawGroups &shown) const
{
CameraRawSettings result = *this;
if (!shown.light)
result.exposure = result.contrast = result.highlights = result.shadows = result.whites = result.blacks = 0;
if (!shown.color)
result.temperature = result.tint = result.vibrance = result.saturation = 0;
if (!shown.effects)
result.texture = result.clarity = result.dehaze = result.glow = result.vignetteAmount = result.grainAmount = 0;
if (!shown.curve)
result.curve = CameraRawCurveSettings();
if (!shown.mixer)
result.mixer = CameraRawMixerSettings();
if (!shown.grading)
result.grading = CameraRawGradingSettings();
if (!shown.detail)
result.detail = CameraRawDetailSettings();
if (!shown.optics)
result.optics = CameraRawOpticsSettings();
if (!shown.geometry)
result.geometry = CameraRawGeometrySettings();
if (!shown.calibration)
result.calibration = CameraRawCalibrationSettings();
return result;
}
double CameraRawSettings::grainKernelSize() const
{
return 0.5 + (grainSize / 100) * 19.5;
}
CameraRawSettings::Gains CameraRawSettings::gains() const
{
const double warm = temperature / 100, magenta = tint / 100;
return {1 + temperatureGain * warm + tintRedBlue * magenta, 1 - tintGreen * magenta, 1 - temperatureGain * warm + tintRedBlue * magenta};
}
QImage CameraRawSettings::apply(const QImage &image, std::optional<CameraRawClipping> clipping, double scale, quint32 seed, int visualizePointColor,
bool sharpenMask) const
{
const CameraRawSettings settings = normalized();
if (settings.isIdentity() && !clipping && visualizePointColor < 0 && !sharpenMask)
return image;
const Gains gain = settings.gains();
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);
const bool paintEffects = !clipping && !sharpenMask && settings.adjustsEffects();
const bool paintDetailOptics = !clipping && (settings.detail.adjusts() || settings.optics.adjusts() || sharpenMask);
QImage source = image;
if (!clipping && !sharpenMask && visualizePointColor < 0 && settings.geometry.adjusts())
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);
if (!clipping && !sharpenMask && settings.calibration.adjusts())
settings.applyCalibration(pixels, width, height, stride);
if (settings.adjustsLight() || settings.adjustsColor() || clipping)
adjust_camera_raw(pixels, w, h, s, gain.red, gain.green, gain.blue, settings.exposure, settings.contrast, settings.highlights,
settings.shadows, settings.whites, settings.blacks, settings.vibrance, settings.saturation, mode);
if (paintColor)
settings.applyCurveColor(pixels, width, height, stride, visualizePointColor);
if (paintEffects) {
if (settings.texture != 0 || settings.clarity != 0 || settings.dehaze != 0 || settings.glow != 0 || settings.vignetteAmount != 0)
adjust_camera_raw_effects(pixels, w, h, s, settings.texture, settings.clarity, settings.dehaze, settings.glow, int(settings.glowStyle),
settings.glowRange, settings.glowSpread, settings.glowWarmth, settings.vignetteAmount,
settings.vignetteMidpoint, settings.vignetteRoundness, settings.vignetteFeather,
settings.vignetteHighlights, int(settings.vignetteStyle), pixelScale);
if (settings.grainAmount > 0)
adjust_grain(pixels, w, h, s, settings.grainAmount, settings.grainKernelSize(), settings.grainRoughness, seed, 0, 0, 1 / pixelScale);
}
if (paintDetailOptics)
settings.applyDetailOptics(pixels, width, height, stride, pixelScale, PixelFilter::lensStrength, sharpenMask);
});
}
std::optional<CameraRawSettings::Balance> CameraRawSettings::neutralizeLinear(double red, double green, double blue)
{
if (!(red > 1e-4 && green > 1e-4 && blue > 1e-4))
return std::nullopt;
const double a1 = temperatureGain * red, b1 = tintRedBlue * red + tintGreen * green, c1 = green - red;
const double a2 = -temperatureGain * blue, b2 = tintRedBlue * blue + tintGreen * green, c2 = green - blue;
const double determinant = a1 * b2 - a2 * b1;
if (!(std::abs(determinant) > 1e-8))
return std::nullopt;
const double warm = (c1 * b2 - c2 * b1) / determinant, magenta = (a1 * c2 - a2 * c1) / determinant;
if (!std::isfinite(warm) || !std::isfinite(magenta))
return std::nullopt;
return Balance{warm * 100, magenta * 100};
}
std::optional<CameraRawSettings::Balance> CameraRawSettings::neutralizeStraight(double red, double green, double blue)
{
return neutralizeLinear(decode(red), decode(green), decode(blue));
}
std::optional<CameraRawSettings::Balance> CameraRawSettings::autoBalance(const QImage &image)
{
if (image.isNull())
return std::nullopt;
const QImage pixels = drawn(image);
double red = 0, green = 0, blue = 0, count = 0;
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;
red += decode(std::min(1.0, pixel[0] / alpha));
green += decode(std::min(1.0, pixel[1] / alpha));
blue += decode(std::min(1.0, pixel[2] / alpha));
count += 1;
}
}
if (count == 0)
return std::nullopt;
return neutralizeLinear(red / count, green / count, blue / count);
}
double CameraRawScope::peak() const
{
return std::max({LevelsHistogramDisplay::scale(red), LevelsHistogramDisplay::scale(green), LevelsHistogramDisplay::scale(blue)});
}
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());
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;
}
}
return scope;
}
QImage CameraRawScope::overlay(const QImage &image, bool shadows, bool highlights)
{
if (!shadows && !highlights)
return image;
return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
adjust_camera_raw_clip_overlay(pixels, size_t(width), size_t(height), size_t(stride), shadows ? 1 : 0, highlights ? 1 : 0);
});
}
std::pair<QImage, CameraRawScope> CameraRawScope::preview(const FilterJob &job)
{
FilterJob grade = job;
grade.cameraRawClipping = std::nullopt;
grade.showsShadowClipping = false;
grade.showsHighlightClipping = false;
grade.visualizesPointColor = -1;
grade.showsSharpenMask = false;
const QImage graded = PixelFilter::run(grade);
const CameraRawScope scope = make(graded).value_or(CameraRawScope());
// Swift leaves the point colour's view out; shown here.
if (job.cameraRawClipping || job.showsSharpenMask || job.visualizesPointColor >= 0)
return {PixelFilter::run(job), scope};
if (job.showsShadowClipping || job.showsHighlightClipping)
return {overlay(graded, job.showsShadowClipping, job.showsHighlightClipping), scope};
return {graded, scope};
}
int CameraRawPanel::pointColorVisualizeIndex(const CameraRawSettings &settings) const
{
const std::vector<CameraRawPointColor> &points = settings.mixer.points;
if (pointIndex < 0 || size_t(pointIndex) >= points.size() || !points[size_t(pointIndex)].visualize)
return -1;
return pointIndex;
}
+179
View File
@@ -0,0 +1,179 @@
#pragma once
#include "Document/CameraRawColor.h"
#include "Document/CameraRawDetailOptics.h"
#include "Document/CameraRawGeometryCalibration.h"
#include <QImage>
#include <array>
#include <optional>
#include <utility>
struct FilterJob;
// Relative offsets, not kelvin; `auto` is a keyword here.
enum class CameraRawWhiteBalance { custom, automatic };
enum class CameraRawGlowStyle { diffusion, bloom, halation };
// Highlight Priority's Highlights slider protects bright pixels.
enum class CameraRawVignetteStyle { highlightPriority, colorPriority, paintOverlay };
// Option on a Light slider shows clipping; never written in.
enum class CameraRawClipping { highlights = 1, shadows = 2 };
enum class CameraRawScopeMode { histogram, vectorscope };
// Swift's panel eyes: a hidden group leaves the grade.
struct CameraRawGroups {
bool light = true;
bool color = true;
bool effects = true;
bool curve = true;
bool mixer = true;
bool grading = true;
bool detail = true;
bool optics = true;
bool geometry = true;
bool calibration = true;
friend bool operator==(const CameraRawGroups &, const CameraRawGroups &) = default;
};
// Defaults leave the image unchanged.
struct CameraRawSettings {
static constexpr double exposureLow = -5, exposureHigh = 5;
// A full warm or cool swing on red and blue.
static constexpr double temperatureGain = 0.35;
static constexpr double tintRedBlue = 0.15;
static constexpr double tintGreen = 0.30;
CameraRawWhiteBalance whiteBalance = CameraRawWhiteBalance::custom;
// Cool to warm, −100 to 100; tint green to magenta.
double temperature = 0;
double tint = 0;
// Stops of linear light, −5 to 5.
double exposure = 0;
double contrast = 0;
double highlights = 0;
double shadows = 0;
double whites = 0;
double blacks = 0;
double vibrance = 0;
double saturation = 0;
// Local contrast: Texture the finer band, Clarity the broader.
double texture = 0;
double clarity = 0;
double dehaze = 0;
// 0 to 100; range, spread and warmth rest at zero.
double glow = 0;
CameraRawGlowStyle glowStyle = CameraRawGlowStyle::diffusion;
double glowRange = 0;
double glowSpread = 0;
double glowWarmth = 0;
// Negative darkens the edges, positive lightens; the centre stays.
double vignetteAmount = 0;
CameraRawVignetteStyle vignetteStyle = CameraRawVignetteStyle::highlightPriority;
double vignetteMidpoint = 50;
double vignetteRoundness = 0;
double vignetteFeather = 50;
double vignetteHighlights = 0;
double grainAmount = 0;
double grainSize = 25;
double grainRoughness = 50;
CameraRawCurveSettings curve;
CameraRawMixerSettings mixer;
CameraRawGradingSettings grading;
CameraRawDetailSettings detail;
CameraRawOpticsSettings optics;
CameraRawGeometrySettings geometry;
CameraRawCalibrationSettings calibration;
bool adjustsLight() const;
bool adjustsColor() const;
bool adjustsEffects() const;
bool isIdentity() const;
CameraRawSettings normalized() const;
// The grade with some panel eyes off: those groups zeroed.
CameraRawSettings applying(const CameraRawGroups &shown) const;
// Camera Raw's 0 to 100 size in `adjust_grain`'s pixels.
double grainKernelSize() const;
struct Gains {
double red;
double green;
double blue;
};
Gains gains() const;
// `clipping`: the Option view; `scale`: preview pixels a pixel.
QImage apply(const QImage &image, std::optional<CameraRawClipping> clipping = std::nullopt, double scale = 1, quint32 seed = 0,
int visualizePointColor = -1, bool sharpenMask = false) const;
struct Balance {
double temperature;
double tint;
};
// The temperature and tint making a linear pixel neutral.
static std::optional<Balance> neutralizeLinear(double red, double green, double blue);
static std::optional<Balance> neutralizeStraight(double red, double green, double blue);
// Gray-world balance of the opaque pixels.
static std::optional<Balance> autoBalance(const QImage &image);
void applyCurveColor(uchar *pixels, int width, int height, qsizetype stride, int visualize) const;
void applyDetailOptics(uchar *pixels, int width, int height, qsizetype stride, double scale, double profileStrength, bool sharpenMask) const;
void applyCalibration(uchar *pixels, int width, int height, qsizetype stride) const;
friend bool operator==(const CameraRawSettings &, const CameraRawSettings &) = default;
};
// An RGB histogram and a hue and saturation vectorscope.
struct CameraRawScope {
static constexpr int binCount = 256;
static constexpr int scopeSide = 64;
std::array<double, binCount> red{};
std::array<double, binCount> green{};
std::array<double, binCount> blue{};
// Density from the centre out, row by row.
std::vector<double> vectorscope = std::vector<double>(scopeSide * scopeSide);
// One vertical scale, so the ribbons stay comparable.
double peak() const;
// Counts the graded image, clear pixels skipped.
static std::optional<CameraRawScope> make(const QImage &image);
// Clipped shadows blue, clipped highlights red, over the grade.
static QImage overlay(const QImage &image, bool shadows, bool highlights);
// The preview and the scope of the grade itself.
static std::pair<QImage, CameraRawScope> preview(const FilterJob &job);
friend bool operator==(const CameraRawScope &, const CameraRawScope &) = default;
};
// Swift's CameraRawDrag: a targeted adjustment under way.
struct CameraRawDrag {
double startY;
CameraRawSettings settings;
double tone;
double hue;
};
// Swift's FilterEdit fields for the Camera Raw panel.
struct CameraRawPanel {
// Eyes off drop a group from preview and OK.
CameraRawGroups shows;
CameraRawCurvePage curvePage = CameraRawCurvePage::parametric;
CameraRawPointChannel pointChannel = CameraRawPointChannel::rgb;
CameraRawMixerPage mixerPage = CameraRawMixerPage::hsl;
CameraRawMixerTab mixerTab = CameraRawMixerTab::hue;
int mixerSwatch = 0;
int pointIndex = 0;
CameraRawGradePage gradePage = CameraRawGradePage::threeWay;
bool targetsCurve = false;
bool targetsMixer = false;
bool samplesPointColor = false;
std::optional<CameraRawDrag> drag;
// The white-balance and defringe eyedroppers.
bool samplesWhiteBalance = false;
bool samplesDefringe = false;
// Guided Upright: lines dragged on the preview.
bool drawingGeometryGuide = false;
std::optional<std::pair<QPointF, QPointF>> guideDraft;
// Option on a Light slider, or on Sharpening's Masking.
std::optional<CameraRawClipping> clipping;
bool sharpenMask = false;
// The histogram's clipping marks, never baked in on OK.
bool showsShadowClipping = false;
bool showsHighlightClipping = false;
CameraRawScopeMode scopeMode = CameraRawScopeMode::histogram;
std::optional<CameraRawScope> scope;
// The adjusted preview's RGB under the pointer.
std::optional<std::array<int, 3>> readout;
// The point colour being shown in isolation, or −1.
int pointColorVisualizeIndex(const CameraRawSettings &settings) const;
};
+271
View File
@@ -0,0 +1,271 @@
#include "Document/CameraRawColor.h"
#include "Document/CameraRaw.h"
#include "Document/ImageAdjustments.h"
#include <algorithm>
#include <cmath>
extern "C" {
#include "AdjustPixels.h"
}
namespace {
using ImageAdjustmentPixels::clamp;
double point(double x, const std::vector<CurvePoint> &points)
{
if (points.size() < 2)
return x;
CurvesSettings curve;
curve.channels[0].clear();
for (const CurvePoint &each : points)
curve.channels[0].push_back({each.x * 255, each.y * 255});
return curve.value(x * 255, 0) / 255;
}
// Swift's repair: sorted, ends pinned, inner points a hundredth apart.
std::vector<CurvePoint> repair(const std::vector<CurvePoint> &points)
{
std::vector<CurvePoint> sorted;
std::copy_if(points.begin(), points.end(), std::back_inserter(sorted), [](const CurvePoint &each) { return std::isfinite(each.x) && std::isfinite(each.y); });
std::stable_sort(sorted.begin(), sorted.end(), [](const CurvePoint &left, const CurvePoint &right) { return left.x < right.x; });
if (sorted.size() < 2)
return CameraRawCurveSettings::linear();
const auto unit = [](double value) { return std::min(1.0, std::max(0.0, value)); };
std::vector<CurvePoint> kept{{0, unit(sorted.front().y)}};
for (size_t index = 1; index + 1 < sorted.size(); ++index) {
const double x = std::min(0.99, std::max(0.01, sorted[index].x));
if (!(x > kept.back().x + 0.01))
continue;
kept.push_back({x, unit(sorted[index].y)});
}
kept.push_back({1, unit(sorted.back().y)});
return kept;
}
std::vector<CurvePoint> &pointsOf(CameraRawCurveSettings &curve, CameraRawPointChannel channel)
{
switch (channel) {
case CameraRawPointChannel::rgb: return curve.rgb;
case CameraRawPointChannel::red: return curve.red;
case CameraRawPointChannel::green: return curve.green;
case CameraRawPointChannel::blue: return curve.blue;
}
throw std::logic_error("unknown point channel");
}
}
std::vector<CurvePoint> CameraRawCurveSettings::linear()
{
return {{0, 0}, {1, 1}};
}
std::vector<CurvePoint> CameraRawCurveSettings::mediumContrast()
{
return {{0, 0}, {0.25, 0.18}, {0.75, 0.82}, {1, 1}};
}
std::vector<CurvePoint> CameraRawCurveSettings::strongContrast()
{
return {{0, 0}, {0.25, 0.10}, {0.75, 0.90}, {1, 1}};
}
bool CameraRawCurveSettings::isLinear(const std::vector<CurvePoint> &points)
{
return points == linear();
}
bool CameraRawCurveSettings::adjusts() const
{
return shadows != 0 || darks != 0 || lights != 0 || highlights != 0 || refineSaturation != 0 || !isLinear(rgb) || !isLinear(red)
|| !isLinear(green) || !isLinear(blue);
}
double CameraRawCurveSettings::parametric(double tone) const
{
const double shadow = shadowSplit / 100, dark = darkSplit / 100, light = lightSplit / 100;
double amount, lo, hi;
if (tone < shadow)
amount = shadows, lo = 0, hi = shadow;
else if (tone < dark)
amount = darks, lo = shadow, hi = dark;
else if (tone < light)
amount = lights, lo = dark, hi = light;
else
amount = highlights, lo = light, hi = 1;
const double span = std::max(0.02, hi - lo);
const double weight = 1 - std::abs(tone - (lo + hi) / 2) / (span / 2);
// A tone in its region weighs 0 to 1, unfloored.
return std::min(1.0, std::max(0.0, tone + (amount / 100) * weight * 0.22));
}
std::vector<float> CameraRawCurveSettings::lumaTable() const
{
std::vector<float> table(256);
for (int index = 0; index < 256; ++index)
table[size_t(index)] = float(point(parametric(index / 255.0), rgb));
return table;
}
std::vector<float> CameraRawCurveSettings::channelTable(const std::vector<CurvePoint> &points) const
{
std::vector<float> table(256);
for (int index = 0; index < 256; ++index)
table[size_t(index)] = float(point(index / 255.0, points));
return table;
}
CameraRawCurveSettings CameraRawCurveSettings::nudged(CameraRawPointChannel channel, double tone, double delta) const
{
CameraRawCurveSettings result = *this;
std::vector<CurvePoint> &points = pointsOf(result, channel);
// Swift's `min(by:)`: the first nearest point.
size_t nearest = 0;
for (size_t index = 1; index < points.size(); ++index)
if (std::abs(points[index].x - tone) < std::abs(points[nearest].x - tone))
nearest = index;
points.at(nearest).y = std::min(1.0, std::max(0.0, points[nearest].y + delta));
return result;
}
double &CameraRawCurveSettings::region(double tone)
{
if (tone < shadowSplit / 100)
return shadows;
if (tone < darkSplit / 100)
return darks;
if (tone < lightSplit / 100)
return lights;
return highlights;
}
CameraRawCurveSettings CameraRawCurveSettings::normalized() const
{
CameraRawCurveSettings result = *this;
result.shadows = clamp(shadows, -100, 100, 0);
result.darks = clamp(darks, -100, 100, 0);
result.lights = clamp(lights, -100, 100, 0);
result.highlights = clamp(highlights, -100, 100, 0);
result.refineSaturation = clamp(refineSaturation, -100, 100, 0);
result.shadowSplit = clamp(shadowSplit, 5, 90, 25);
result.darkSplit = clamp(darkSplit, result.shadowSplit + 2, 95, 50);
result.lightSplit = clamp(lightSplit, result.darkSplit + 2, 98, 75);
result.rgb = repair(rgb);
result.red = repair(red);
result.green = repair(green);
result.blue = repair(blue);
return result;
}
const std::array<QString, 8> CameraRawMixerSettings::names{QStringLiteral("Reds"), QStringLiteral("Oranges"), QStringLiteral("Yellows"),
QStringLiteral("Greens"), QStringLiteral("Aquas"), QStringLiteral("Blues"),
QStringLiteral("Purples"), QStringLiteral("Magentas")};
bool CameraRawMixerSettings::adjusts() const
{
const auto moved = [](const std::array<double, 8> &values) { return std::any_of(values.begin(), values.end(), [](double value) { return value != 0; }); };
return moved(hue) || moved(saturation) || moved(luminance)
|| std::any_of(points.begin(), points.end(), [](const CameraRawPointColor &each) {
return each.hueShift != 0 || each.saturationShift != 0 || each.luminanceShift != 0;
});
}
std::array<double, 8> CameraRawMixerSettings::weights(double degrees)
{
std::array<double, 8> result{};
for (size_t index = 0; index < 8; ++index) {
double distance = std::abs(degrees - centers[index]);
if (distance > 180)
distance = 360 - distance;
result[index] = std::max(0.0, 1 - distance / 40);
}
return result;
}
std::vector<float> CameraRawMixerSettings::mixerFloats() const
{
std::vector<float> result;
for (const std::array<double, 8> *values : {&hue, &saturation, &luminance})
for (const double value : *values)
result.push_back(float(value / 100));
return result;
}
std::vector<float> CameraRawMixerSettings::pointFloats() const
{
std::vector<float> result;
for (const CameraRawPointColor &each : points)
for (const double value : {each.hue / 360, each.saturation, each.luminance, each.hueShift / 100, each.saturationShift / 100,
each.luminanceShift / 100, each.hueRange / 360, each.saturationRange, each.luminanceRange})
result.push_back(float(value));
return result;
}
CameraRawMixerSettings CameraRawMixerSettings::normalized() const
{
CameraRawMixerSettings result = *this;
for (std::array<double, 8> *values : {&result.hue, &result.saturation, &result.luminance})
for (double &value : *values)
value = clamp(value, -100, 100, 0);
result.points.clear();
for (size_t index = 0; index < std::min<size_t>(points.size(), 8); ++index)
result.points.push_back(points[index].normalized());
return result;
}
CameraRawPointColor CameraRawPointColor::normalized() const
{
CameraRawPointColor result = *this;
result.hue = clamp(hue, 0, 360, 0);
result.saturation = clamp(saturation, 0, 1, 0);
result.luminance = clamp(luminance, 0, 1, 0);
result.hueShift = clamp(hueShift, -100, 100, 0);
result.saturationShift = clamp(saturationShift, -100, 100, 0);
result.luminanceShift = clamp(luminanceShift, -100, 100, 0);
result.hueRange = clamp(hueRange, 5, 180, 30);
result.saturationRange = clamp(saturationRange, 0.05, 1, 0.4);
result.luminanceRange = clamp(luminanceRange, 0.05, 1, 0.4);
return result;
}
CameraRawGradeWheel CameraRawGradeWheel::normalized() const
{
return {clamp(hue, 0, 360, 0), clamp(saturation, 0, 100, 0), clamp(luminance, -100, 100, 0)};
}
std::array<CameraRawGradeWheel, 4> CameraRawGradingSettings::wheels() const
{
return {shadows, midtones, highlights, global};
}
bool CameraRawGradingSettings::adjusts() const
{
const std::array<CameraRawGradeWheel, 4> all = wheels();
return std::any_of(all.begin(), all.end(), [](const CameraRawGradeWheel &wheel) { return wheel.saturation != 0 || wheel.luminance != 0; });
}
std::vector<float> CameraRawGradingSettings::gradeFloats() const
{
std::vector<float> result;
for (const CameraRawGradeWheel &wheel : wheels())
for (const double value : {wheel.hue / 360, wheel.saturation / 100, wheel.luminance / 100})
result.push_back(float(value));
return result;
}
CameraRawGradingSettings CameraRawGradingSettings::normalized() const
{
return {shadows.normalized(), midtones.normalized(), highlights.normalized(), global.normalized(), clamp(blending, 0, 100, 50),
clamp(balance, -100, 100, 0)};
}
void CameraRawSettings::applyCurveColor(uchar *pixels, int width, int height, qsizetype stride, int visualize) const
{
const CameraRawCurveSettings shape = curve.normalized();
const CameraRawMixerSettings mixing = mixer.normalized();
const CameraRawGradingSettings grade = grading.normalized();
const std::vector<float> luma = shape.lumaTable(), redTable = shape.channelTable(shape.red), greenTable = shape.channelTable(shape.green),
blueTable = shape.channelTable(shape.blue), mixerFloats = mixing.mixerFloats(), pointFloats = mixing.pointFloats(),
gradeFloats = grade.gradeFloats();
adjust_camera_raw_curve_color(pixels, size_t(width), size_t(height), size_t(stride), luma.data(), redTable.data(), greenTable.data(),
blueTable.data(), shape.refineSaturation / 100, mixerFloats.data(), int(mixing.points.size()), pointFloats.data(),
gradeFloats.data(), grade.blending / 100, grade.balance / 100, visualize);
}
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include "Document/Curves.h"
#include <QString>
#include <array>
#include <vector>
// Swift's CameraRawColor.swift: Curve, Color Mixer, Color Grading.
enum class CameraRawCurvePage { parametric, point };
enum class CameraRawPointChannel { rgb, red, green, blue };
enum class CameraRawMixerPage { hsl, color, point };
enum class CameraRawMixerTab { hue, saturation, luminance };
enum class CameraRawGradePage { threeWay, shadows, midtones, highlights, global };
// Parametric regions and point curves, points 0 to 1.
struct CameraRawCurveSettings {
double shadows = 0;
double darks = 0;
double lights = 0;
double highlights = 0;
// Dividers, 0 to 100: where each region hands off.
double shadowSplit = 25;
double darkSplit = 50;
double lightSplit = 75;
std::vector<CurvePoint> rgb = linear();
std::vector<CurvePoint> red = linear();
std::vector<CurvePoint> green = linear();
std::vector<CurvePoint> blue = linear();
// How far the composite curve moves saturation too.
double refineSaturation = 0;
static std::vector<CurvePoint> linear();
static std::vector<CurvePoint> mediumContrast();
static std::vector<CurvePoint> strongContrast();
static bool isLinear(const std::vector<CurvePoint> &points);
bool adjusts() const;
// Lifts or lowers the region a tone belongs to.
double parametric(double tone) const;
std::vector<float> lumaTable() const;
std::vector<float> channelTable(const std::vector<CurvePoint> &points) const;
CameraRawCurveSettings nudged(CameraRawPointChannel channel, double tone, double delta) const;
// Swift's region(for:): the parametric amount a tone moves.
double &region(double tone);
CameraRawCurveSettings normalized() const;
friend bool operator==(const CameraRawCurveSettings &, const CameraRawCurveSettings &) = default;
};
// One picked colour and how far its adjustment reaches.
struct CameraRawPointColor {
double hue = 0;
double saturation = 0;
double luminance = 0;
double hueShift = 0;
double saturationShift = 0;
double luminanceShift = 0;
double hueRange = 30;
double saturationRange = 0.4;
double luminanceRange = 0.4;
bool visualize = false;
CameraRawPointColor normalized() const;
friend bool operator==(const CameraRawPointColor &, const CameraRawPointColor &) = default;
};
// Eight colour families, each hue, saturation, luminance −100 to 100.
struct CameraRawMixerSettings {
static const std::array<QString, 8> names;
static constexpr std::array<double, 8> centers{0, 30, 60, 120, 180, 240, 270, 300};
std::array<double, 8> hue{};
std::array<double, 8> saturation{};
std::array<double, 8> luminance{};
std::vector<CameraRawPointColor> points{};
bool adjusts() const;
// How much each family shares a hue; neighbours overlap.
static std::array<double, 8> weights(double degrees);
std::vector<float> mixerFloats() const;
std::vector<float> pointFloats() const;
CameraRawMixerSettings normalized() const;
friend bool operator==(const CameraRawMixerSettings &, const CameraRawMixerSettings &) = default;
};
struct CameraRawGradeWheel {
double hue = 0;
double saturation = 0;
double luminance = 0;
CameraRawGradeWheel normalized() const;
friend bool operator==(const CameraRawGradeWheel &, const CameraRawGradeWheel &) = default;
};
// Four wheels, their overlap and which end they favour.
struct CameraRawGradingSettings {
CameraRawGradeWheel shadows;
CameraRawGradeWheel midtones;
CameraRawGradeWheel highlights;
CameraRawGradeWheel global;
// 0 to 100: more lets the tonal wheels overlap.
double blending = 50;
// −100 to 100: negative favours shadows, positive highlights.
double balance = 0;
std::array<CameraRawGradeWheel, 4> wheels() const;
bool adjusts() const;
std::vector<float> gradeFloats() const;
CameraRawGradingSettings normalized() const;
friend bool operator==(const CameraRawGradingSettings &, const CameraRawGradingSettings &) = default;
};
+86
View File
@@ -0,0 +1,86 @@
#include "Document/CameraRawDetailOptics.h"
#include "Document/CameraRaw.h"
#include "Document/ImageAdjustments.h"
#include <utility>
extern "C" {
#include "AdjustPixels.h"
}
using ImageAdjustmentPixels::clamp;
bool CameraRawDetailSettings::adjustsSharpening() const
{
return sharpenAmount != 0;
}
bool CameraRawDetailSettings::adjustsNoise() const
{
return noiseLuminance != 0 || noiseColor != 0;
}
bool CameraRawDetailSettings::adjusts() const
{
return adjustsSharpening() || adjustsNoise();
}
CameraRawDetailSettings CameraRawDetailSettings::normalized() const
{
return {clamp(sharpenAmount, 0, 150, 0), clamp(sharpenRadius, 0, 100, 10), clamp(sharpenDetail, 0, 100, 25),
clamp(sharpenMasking, 0, 100, 0), clamp(noiseLuminance, 0, 100, 0), clamp(noiseLuminanceDetail, 0, 100, 50),
clamp(noiseLuminanceContrast, 0, 100, 0), clamp(noiseColor, 0, 100, 0), clamp(noiseColorDetail, 0, 100, 50),
clamp(noiseColorSmoothness, 0, 100, 50)};
}
bool CameraRawOpticsSettings::adjusts() const
{
return removeChromaticAberration || enableLensProfile || distortion != 0 || purpleAmount != 0 || greenAmount != 0 || vignetteAmount != 0;
}
CameraRawOpticsSettings CameraRawOpticsSettings::normalized() const
{
CameraRawOpticsSettings result = *this;
result.profileDistortion = clamp(profileDistortion, 0, 100, 100);
result.profileVignetting = clamp(profileVignetting, 0, 100, 100);
result.distortion = clamp(distortion, -100, 100, 0);
result.purpleAmount = clamp(purpleAmount, 0, 100, 0);
result.greenAmount = clamp(greenAmount, 0, 100, 0);
result.vignetteAmount = clamp(vignetteAmount, -100, 100, 0);
result.vignetteMidpoint = clamp(vignetteMidpoint, 0, 100, 50);
result.purpleHueLow = clamp(purpleHueLow, 0, 360, 270);
result.purpleHueHigh = clamp(purpleHueHigh, 0, 360, 310);
result.greenHueLow = clamp(greenHueLow, 0, 360, 60);
result.greenHueHigh = clamp(greenHueHigh, 0, 360, 120);
if (result.purpleHueLow > result.purpleHueHigh)
std::swap(result.purpleHueLow, result.purpleHueHigh);
if (result.greenHueLow > result.greenHueHigh)
std::swap(result.greenHueLow, result.greenHueHigh);
return result;
}
double CameraRawOpticsSettings::distortionK(double profileStrength) const
{
const double manual = distortion / 100 * profileStrength;
const double profile = enableLensProfile ? profileDistortion / 100 * profileStrength : 0;
return manual + profile;
}
void CameraRawSettings::applyDetailOptics(uchar *pixels, int width, int height, qsizetype stride, double scale, double profileStrength,
bool sharpenMask) const
{
const CameraRawDetailSettings sharpening = detail.normalized();
const CameraRawOpticsSettings lens = optics.normalized();
const size_t w = size_t(width), h = size_t(height), s = size_t(stride);
if (sharpenMask) {
adjust_camera_raw_sharpen_mask_overlay(pixels, w, h, s, sharpening.sharpenRadius, sharpening.sharpenDetail, sharpening.sharpenMasking, scale);
return;
}
if (lens.adjusts())
adjust_camera_raw_optics(pixels, w, h, s, lens.removeChromaticAberration ? 1 : 0, lens.enableLensProfile ? 1 : 0, lens.profileDistortion,
lens.profileVignetting, lens.distortionK(profileStrength), lens.purpleAmount, lens.purpleHueLow, lens.purpleHueHigh,
lens.greenAmount, lens.greenHueLow, lens.greenHueHigh, lens.vignetteAmount, lens.vignetteMidpoint, scale);
if (sharpening.adjusts())
adjust_camera_raw_detail(pixels, w, h, s, sharpening.sharpenAmount, sharpening.sharpenRadius, sharpening.sharpenDetail,
sharpening.sharpenMasking, sharpening.noiseLuminance, sharpening.noiseLuminanceDetail,
sharpening.noiseLuminanceContrast, sharpening.noiseColor, sharpening.noiseColorDetail,
sharpening.noiseColorSmoothness, scale);
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
// Sharpening and noise reduction; Amount runs 0 to 150.
struct CameraRawDetailSettings {
double sharpenAmount = 0;
double sharpenRadius = 10;
double sharpenDetail = 25;
double sharpenMasking = 0;
double noiseLuminance = 0;
double noiseLuminanceDetail = 50;
double noiseLuminanceContrast = 0;
double noiseColor = 0;
double noiseColorDetail = 50;
double noiseColorSmoothness = 50;
bool adjustsSharpening() const;
bool adjustsNoise() const;
bool adjusts() const;
CameraRawDetailSettings normalized() const;
friend bool operator==(const CameraRawDetailSettings &, const CameraRawDetailSettings &) = default;
};
// Generic lens corrections: a rendered layer carries no profile.
struct CameraRawOpticsSettings {
bool removeChromaticAberration = false;
bool enableLensProfile = false;
double profileDistortion = 100;
double profileVignetting = 100;
// −100 to 100, the Lens Correction filter's sign.
double distortion = 0;
double purpleAmount = 0;
// Degrees, 0 to 360; the low handle stays lower.
double purpleHueLow = 270;
double purpleHueHigh = 310;
double greenAmount = 0;
double greenHueLow = 60;
double greenHueHigh = 120;
// Brightens the corners against lens falloff.
double vignetteAmount = 0;
double vignetteMidpoint = 50;
bool adjusts() const;
CameraRawOpticsSettings normalized() const;
// The radial distortion handed to `lens_distort`.
double distortionK(double profileStrength) const;
friend bool operator==(const CameraRawOpticsSettings &, const CameraRawOpticsSettings &) = default;
};
@@ -0,0 +1,200 @@
#include "Document/CameraRawGeometryCalibration.h"
#include "Document/BrushStroke.h"
#include "Document/CameraRaw.h"
#include "Document/ImageAdjustments.h"
#include "IO/ImageExporter.h"
#include <QPainter>
#include <QPolygonF>
#include <QTransform>
#include <cmath>
#include <numbers>
extern "C" {
#include "AdjustPixels.h"
#include "BrushPixels.h"
}
using ImageAdjustmentPixels::clamp;
namespace {
bool readable(const CameraRawGeometryGuide &guide)
{
return std::hypot(guide.endX - guide.startX, guide.endY - guide.startY) > 0.01;
}
// Swift's guidedCorrections: the first line levels, the second tilts.
std::array<double, 3> guided(const std::vector<CameraRawGeometryGuide> &guides)
{
if (guides.empty())
return {0, 0, 0};
const CameraRawGeometryGuide &first = guides.front();
const double dx = first.endX - first.startX, dy = first.endY - first.startY;
if (std::hypot(dx, dy) <= 1e-4)
return {0, 0, 0};
double rotate = -std::atan2(dy, dx) * 180 / std::numbers::pi;
if (rotate > 45)
rotate -= 90;
else if (rotate < -45)
rotate += 90;
double vertical = 0, horizontal = 0;
if (guides.size() > 1) {
const CameraRawGeometryGuide &second = guides[1];
const double sx = second.endX - second.startX, sy = second.endY - second.startY;
if (std::hypot(sx, sy) > 1e-4) {
const double angle = std::atan2(sy, sx) * 180 / std::numbers::pi;
vertical = std::abs(angle) > 45 ? (angle > 0 ? 25 : -25) : 0;
horizontal = std::abs(angle) <= 45 ? (angle > 0 ? 25 : -25) : 0;
}
}
return {vertical, horizontal, rotate};
}
}
bool CameraRawGeometrySettings::usesGuides() const
{
return upright == CameraRawUprightMode::guided && std::any_of(guides.begin(), guides.end(), readable);
}
bool CameraRawGeometrySettings::adjusts() const
{
return usesGuides() || vertical != 0 || horizontal != 0 || rotate != 0 || aspect != 0 || scale != 0 || offsetX != 0 || offsetY != 0;
}
CameraRawGeometrySettings CameraRawGeometrySettings::normalized() const
{
CameraRawGeometrySettings result = *this;
result.vertical = clamp(vertical, -100, 100, 0);
result.horizontal = clamp(horizontal, -100, 100, 0);
result.rotate = clamp(rotate, -45, 45, 0);
result.aspect = clamp(aspect, -100, 100, 0);
result.scale = clamp(scale, -100, 100, 0);
result.offsetX = clamp(offsetX, -100, 100, 0);
result.offsetY = clamp(offsetY, -100, 100, 0);
result.guides.clear();
std::copy_if(guides.begin(), guides.end(), std::back_inserter(result.guides), readable);
return result;
}
std::array<double, 3> CameraRawGeometrySettings::effectiveCorrections() const
{
if (upright == CameraRawUprightMode::off)
return {vertical, horizontal, rotate};
const std::array<double, 3> extra = guided(guides);
return {vertical + extra[0], horizontal + extra[1], rotate + extra[2]};
}
// Core Image's corners, y measured up from the bottom.
std::array<QPointF, 4> CameraRawGeometrySettings::outputCorners(int width, int height, double vertical, double horizontal, double rotation) const
{
const double w = width, h = height;
const double strength = projection == CameraRawProjection::perspective ? 1.0 : 0.55;
const double v = vertical / 100 * w * 0.18 * strength, hz = horizontal / 100 * h * 0.18 * strength;
const double aspectScale = 1 + aspect / 200, zoom = 1 + scale / 100;
const double shiftX = offsetX / 100 * w * 0.15, shiftY = offsetY / 100 * h * 0.15;
std::array<QPointF, 4> corners{QPointF(-v + shiftX, h + shiftY), QPointF(w + v + shiftX, h + shiftY), QPointF(w + hz + shiftX, -shiftY),
QPointF(-hz + shiftX, -shiftY)};
const QPointF center(w / 2 + shiftX, h / 2 + shiftY);
const double radians = rotation * std::numbers::pi / 180, cosine = std::cos(radians), sine = std::sin(radians);
for (QPointF &corner : corners) {
const double dx = corner.x() - center.x(), dy = corner.y() - center.y();
corner = QPointF(center.x() + dx * cosine - dy * sine, center.y() + dx * sine + dy * cosine);
}
if (aspectScale != 1)
for (QPointF &corner : corners)
corner = QPointF(center.x() + (corner.x() - center.x()) * aspectScale, center.y() + (corner.y() - center.y()) / aspectScale);
if (zoom != 1)
for (QPointF &corner : corners)
corner = center + (corner - center) * zoom;
return corners;
}
QImage CameraRawGeometrySettings::apply(const QImage &image) const
{
const CameraRawGeometrySettings settings = normalized();
const int width = image.width(), height = image.height();
if (!settings.adjusts() || width <= 0 || height <= 0)
return image;
const auto [vertical, horizontal, rotate] = settings.effectiveCorrections();
const std::array<QPointF, 4> corners = settings.outputCorners(width, height, vertical, horizontal, rotate);
// Core Image counts y upward: top-down, a corner's y flips.
QPolygonF target;
for (const QPointF &corner : corners)
target << QPointF(corner.x(), height - corner.y());
QTransform warp;
// Normalized settings keep the quad whole; else a bug.
if (!QTransform::quadToQuad(QPolygonF(QRectF(0, 0, width, height)).mid(0, 4), target, warp))
throw std::logic_error("the geometry's corners fold");
QImage result = BrushRaster::context(width, height, false);
{
QPainter painter(&result);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
painter.setTransform(warp);
painter.drawImage(QRectF(0, 0, width, height), image);
}
if (!settings.constrainCrop)
return result;
std::array<size_t, 4> edges{};
brush_alpha_bounds(result.constBits(), size_t(width), size_t(height), size_t(result.bytesPerLine()), edges.data());
const QRect crop(int(edges[0]), int(edges[1]), int(edges[2] - edges[0]), int(edges[3] - edges[1]));
if (crop.width() < 1 || crop.height() < 1 || (crop.width() >= width && crop.height() >= height))
return result;
const QImage cropped = result.copy(crop);
if (cropped.isNull())
throw ExportError(ExportError::Kind::render);
QImage fitted = BrushRaster::context(width, height, false);
const double fit = std::min(double(width) / crop.width(), double(height) / crop.height());
QPainter painter(&fitted);
BrushRaster::draw(cropped, QRectF((width - crop.width() * fit) / 2, (height - crop.height() * fit) / 2, crop.width() * fit, crop.height() * fit),
painter);
return fitted;
}
QString rawValue(CameraRawProcessVersion version)
{
return QStringLiteral("Version %1").arg(int(version) + 1);
}
QString summary(CameraRawProcessVersion version)
{
switch (version) {
case CameraRawProcessVersion::version1:
return QStringLiteral("Earliest response. Hue, saturation, and shadow tint move about half as far as Version 6.");
case CameraRawProcessVersion::version2:
return QStringLiteral("A little stronger than Version 1. The sliders below still fall well short of the current look.");
case CameraRawProcessVersion::version3:
return QStringLiteral("Firmer color than Version 2. Primary shifts stay gentler than the current process.");
case CameraRawProcessVersion::version4:
return QStringLiteral("The 2012 response. Calibration reaches most of the strength used by Version 6.");
case CameraRawProcessVersion::version5:
return QStringLiteral("Close to the current process, with slightly softer primary and shadow shifts.");
case CameraRawProcessVersion::version6:
return QStringLiteral("Current default. The calibration sliders below apply at full strength.");
}
throw std::logic_error("unknown process version");
}
bool CameraRawCalibrationSettings::adjusts() const
{
return shadowTint != 0 || redHue != 0 || redSaturation != 0 || greenHue != 0 || greenSaturation != 0 || blueHue != 0 || blueSaturation != 0;
}
CameraRawCalibrationSettings CameraRawCalibrationSettings::normalized() const
{
return {process,
clamp(shadowTint, -100, 100, 0),
clamp(redHue, -100, 100, 0),
clamp(redSaturation, -100, 100, 0),
clamp(greenHue, -100, 100, 0),
clamp(greenSaturation, -100, 100, 0),
clamp(blueHue, -100, 100, 0),
clamp(blueSaturation, -100, 100, 0)};
}
void CameraRawSettings::applyCalibration(uchar *pixels, int width, int height, qsizetype stride) const
{
const CameraRawCalibrationSettings primaries = calibration.normalized();
if (!primaries.adjusts())
return;
adjust_camera_raw_calibration(pixels, size_t(width), size_t(height), size_t(stride), primaries.shadowTint, primaries.redHue,
primaries.redSaturation, primaries.greenHue, primaries.greenSaturation, primaries.blueHue,
primaries.blueSaturation, int(primaries.process) + 1);
}
@@ -0,0 +1,62 @@
#pragma once
#include <QImage>
#include <QPointF>
#include <QString>
#include <array>
#include <vector>
// Swift's CameraRawGeometryCalibration.swift: Geometry, Calibration.
enum class CameraRawUprightMode { off, guided };
enum class CameraRawProjection { perspective, rectilinear };
// A guide line, 0 to 1 from the lower left.
struct CameraRawGeometryGuide {
double startX = 0;
double startY = 0;
double endX = 0;
double endY = 0;
friend bool operator==(const CameraRawGeometryGuide &, const CameraRawGeometryGuide &) = default;
};
struct CameraRawGeometrySettings {
CameraRawUprightMode upright = CameraRawUprightMode::off;
CameraRawProjection projection = CameraRawProjection::perspective;
double vertical = 0;
double horizontal = 0;
double rotate = 0;
double aspect = 0;
double scale = 0;
double offsetX = 0;
double offsetY = 0;
bool constrainCrop = false;
std::vector<CameraRawGeometryGuide> guides{};
bool adjusts() const;
CameraRawGeometrySettings normalized() const;
// Perspective and turn; Constrain Crop trims empty edges.
QImage apply(const QImage &image) const;
friend bool operator==(const CameraRawGeometrySettings &, const CameraRawGeometrySettings &) = default;
private:
bool usesGuides() const;
std::array<double, 3> effectiveCorrections() const;
std::array<QPointF, 4> outputCorners(int width, int height, double vertical, double horizontal, double rotation) const;
};
enum class CameraRawProcessVersion { version1, version2, version3, version4, version5, version6 };
QString rawValue(CameraRawProcessVersion version);
// What this process does to the calibration sliders.
QString summary(CameraRawProcessVersion version);
struct CameraRawCalibrationSettings {
CameraRawProcessVersion process = CameraRawProcessVersion::version6;
double shadowTint = 0;
double redHue = 0;
double redSaturation = 0;
double greenHue = 0;
double greenSaturation = 0;
double blueHue = 0;
double blueSaturation = 0;
bool adjusts() const;
CameraRawCalibrationSettings normalized() const;
friend bool operator==(const CameraRawCalibrationSettings &, const CameraRawCalibrationSettings &) = default;
};
+2
View File
@@ -100,6 +100,8 @@ struct SessionJobs {
std::optional<QString> failure;
// The job's settings, which the preview was made with.
FilterSettings settings;
// Camera Raw's scope of the grade itself.
std::optional<CameraRawScope> scope = std::nullopt;
};
struct FilterMade {
std::optional<ImportedImage> asset;
+1
View File
@@ -315,6 +315,7 @@ public:
#include "Document/LayerAdjustment+Session.h"
#include "Document/AdjustmentEditing+Session.h"
#include "Document/LayerEffects+Session.h"
#include "Document/CameraRaw+Session.h"
#include "Document/Guides+Session.h"
#include "Document/ObjectSelection+Session.h"
signals:
+15 -3
View File
@@ -114,6 +114,10 @@ void EditorSession::renderFilterPreview()
m_filterRendering = true;
m_filterPreview.setFuture(QtConcurrent::run([job]() -> Filtered {
try {
if (job.kind == FilterKind::cameraRaw) {
auto [image, scope] = CameraRawScope::preview(job);
return Filtered{std::move(image), std::nullopt, job.settings, std::move(scope)};
}
return Filtered{PixelFilter::run(job), std::nullopt, job.settings};
} catch (const ContentFillError &error) {
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
@@ -139,6 +143,8 @@ void EditorSession::finishFilterPreview()
FilterEdit &edit = *m_filterEdit;
edit.preparing = false;
edit.previewError = result.failure;
if (result.scope)
edit.rawPanel.scope = result.scope;
if (edit.preview || isAutomatic(edit.kind)) {
edit.preparedPreview = result.image;
edit.preparedSettings = result.settings;
@@ -191,10 +197,15 @@ void EditorSession::commitFilter(std::function<void()> done)
finish();
return;
}
// A hidden Camera Raw group stays out next time too.
const FilterSettings rendered = edit.renderSettings();
if (edit.kind == FilterKind::cameraRaw)
m_filterSettings = rendered;
// Nothing to change closes as Cancel does, with no step.
if ((edit.kind == FilterKind::lensCorrection && edit.settings.distortion == 0)
|| (edit.kind == FilterKind::exposure && edit.settings.exposure == ExposureSettings())
|| (edit.kind == FilterKind::grain && edit.settings.grain.amount == 0)) {
|| (edit.kind == FilterKind::grain && edit.settings.grain.amount == 0)
|| (edit.kind == FilterKind::cameraRaw && rendered.cameraRaw.isIdentity())) {
cancelFilter();
finish();
return;
@@ -203,7 +214,8 @@ void EditorSession::commitFilter(std::function<void()> done)
// Swift cancels the running preview: its result and queue drop.
edit.pending.reset();
m_filterPreviewFor = QUuid();
m_filterSettings = edit.settings;
if (edit.kind != FilterKind::cameraRaw)
m_filterSettings = edit.settings;
setIsProjectBusy(true);
m_committingFilter = Committing{edit.id, std::move(done)};
// A mask hides the background, which can come back.
@@ -214,7 +226,7 @@ void EditorSession::commitFilter(std::function<void()> done)
const std::optional<QImage> cached = isAutomatic(edit.kind) && edit.preparedSettings == edit.settings ? edit.preparedPreview : std::nullopt;
const bool spreads = edit.kind == FilterKind::gaussianBlur || edit.kind == FilterKind::motionBlur;
// A painted layer flattens in the worker, which catches failures.
m_filterCommit.setFuture(QtConcurrent::run([kind = edit.kind, original = edit.original, grownImage = edit.grownImage, settings = edit.settings,
m_filterCommit.setFuture(QtConcurrent::run([kind = edit.kind, original = edit.original, grownImage = edit.grownImage, settings = rendered,
selection = edit.selection, mapping = edit.mapping, seed = edit.seed, cached,
grown = edit.grownTransform, spreads]() -> FilterMade {
try {
+25 -1
View File
@@ -24,6 +24,7 @@ QString rawValue(FilterKind kind)
case FilterKind::motionBlur: return QStringLiteral("Motion Blur");
case FilterKind::addNoise: return QStringLiteral("Add Noise");
case FilterKind::lensCorrection: return QStringLiteral("Lens Correction");
case FilterKind::cameraRaw: return QStringLiteral("Camera Raw Filter");
case FilterKind::removeBackground: return QStringLiteral("Remove Background");
case FilterKind::contentAwareFill: return QStringLiteral("Content-Aware Fill");
case FilterKind::curves: return QStringLiteral("Curves");
@@ -67,6 +68,7 @@ FilterSettings FilterSettings::normalized() const
result.exposure = exposure.normalized();
result.gradientMap = gradientMap.normalized();
result.grain = grain.normalized();
result.cameraRaw = cameraRaw.normalized();
return result;
}
@@ -118,6 +120,9 @@ QImage PixelFilter::run(const FilterJob &job)
case FilterKind::gradientMap: image = settings.gradientMap.apply(job.image); break;
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);
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;
case FilterKind::removeBackground: image = SubjectRemoval::run(job.image, settings); break;
@@ -243,5 +248,24 @@ std::optional<QImage> FilterEdit::previewImage(QUuid id) const
FilterJob FilterEdit::previewJob() const
{
return FilterJob{kind, previewSource, settings, previewScale, selection, previewMapping, seed};
return FilterJob{kind,
previewSource,
renderSettings(),
previewScale,
selection,
previewMapping,
seed,
rawPanel.clipping,
rawPanel.showsShadowClipping,
rawPanel.showsHighlightClipping,
rawPanel.pointColorVisualizeIndex(settings.cameraRaw),
rawPanel.sharpenMask};
}
FilterSettings FilterEdit::renderSettings() const
{
FilterSettings value = settings;
if (kind == FilterKind::cameraRaw)
value.cameraRaw = value.cameraRaw.applying(rawPanel.shows);
return value;
}
+18 -2
View File
@@ -1,4 +1,5 @@
#pragma once
#include "Document/CameraRaw.h"
#include "Document/Curves.h"
#include "Document/ImageAdjustments.h"
#include "Document/LayerTransform.h"
@@ -14,7 +15,10 @@
struct ImageLayer;
// The Filter and Image menus' filters.
enum class FilterKind { gaussianBlur, motionBlur, addNoise, lensCorrection, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain, blackWhite, colorBalance };
enum class FilterKind {
gaussianBlur, motionBlur, addNoise, lensCorrection, cameraRaw, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain, blackWhite,
colorBalance
};
// The menu's title, Swift's rawValue.
QString rawValue(FilterKind kind);
// Made without settings: its preview is its result.
@@ -23,7 +27,8 @@ bool isAutomatic(FilterKind kind);
bool isImageAdjustment(FilterKind kind);
// Swift's allCases, the menus' order.
inline constexpr std::array allFilterKinds{FilterKind::gaussianBlur, FilterKind::motionBlur, FilterKind::addNoise,
FilterKind::lensCorrection, FilterKind::removeBackground, FilterKind::contentAwareFill,
FilterKind::lensCorrection, FilterKind::cameraRaw, FilterKind::removeBackground,
FilterKind::contentAwareFill,
FilterKind::curves, FilterKind::exposure, FilterKind::gradientMap,
FilterKind::grain, FilterKind::blackWhite, FilterKind::colorBalance};
@@ -52,6 +57,7 @@ struct FilterSettings {
GrainSettings grain{};
BlackWhiteSettings blackWhite{};
ColorBalanceSettings colorBalance{};
CameraRawSettings cameraRaw{};
BackgroundQuality backgroundQuality = BackgroundQuality::basic;
// How far, in pixels, the mask is pulled onto edges.
double refineEdges = 12;
@@ -74,6 +80,13 @@ struct FilterJob {
QTransform mapping;
// Add Noise's pattern: the same seed, the same grain.
quint32 seed = 0;
// Camera Raw's preview-only views; a commit leaves them off.
std::optional<CameraRawClipping> cameraRawClipping = std::nullopt;
bool showsShadowClipping = false;
bool showsHighlightClipping = false;
// The point colour shown alone, or −1 for the grade.
int visualizesPointColor = -1;
bool showsSharpenMask = false;
};
// Swift's PixelFilter.
@@ -106,6 +119,8 @@ public:
// Pads the grid for the blur; it only ever grows.
void growForBlur();
std::optional<QImage> previewImage(QUuid id) const;
// The settings as rendered: a hidden Camera Raw group drops.
FilterSettings renderSettings() const;
FilterJob previewJob() const;
// Swift's object identity: a preview lands on its own edit.
@@ -135,6 +150,7 @@ public:
// The settings the prepared preview was made with.
std::optional<FilterSettings> preparedSettings;
std::optional<FilterJob> pending;
CameraRawPanel rawPanel;
private:
void grow(const QRectF &extent);
+3 -2
View File
@@ -215,7 +215,8 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
QMenu *filter = bar.addMenu(QStringLiteral("Fil&ter"));
for (const FilterKind kind : allFilterKinds) {
if (kind != FilterKind::contentAwareFill && !isImageAdjustment(kind))
// Camera Raw's entry arrives with its panel (12.8b).
if (kind != FilterKind::contentAwareFill && kind != FilterKind::cameraRaw && !isImageAdjustment(kind))
add(filter, filterName(kind), rawValue(kind) + QStringLiteral("…"), QKeySequence(), [this, kind] { session().beginFilter(kind); });
}
@@ -357,7 +358,7 @@ void CompositorMenus::synchronize()
action(QString::fromLatin1(name))->setEnabled(s.canModifySelection());
action(QStringLiteral("levels"))->setEnabled(s.canAdjustColors() && !s.hueSaturation());
for (const FilterKind kind : allFilterKinds) {
if (kind != FilterKind::contentAwareFill)
if (kind != FilterKind::contentAwareFill && kind != FilterKind::cameraRaw)
action(filterName(kind))->setEnabled(s.canAdjustColors() && !s.hueSaturation());
}
action(QStringLiteral("hueSaturation"))->setEnabled(s.canAdjustColors());
+2
View File
@@ -182,6 +182,8 @@ FilterSheet::FilterSheet(EditorSession &session, QWidget *parent)
break;
case FilterKind::removeBackground: background(); break;
case FilterKind::blackWhite: blackWhite(); break;
// CameraRawControls arrive with the panel (12.8b).
case FilterKind::cameraRaw: break;
case FilterKind::colorBalance: colorBalance(); break;
case FilterKind::contentAwareFill:
m_column->addWidget(words(QStringLiteral("Fill the selection using surrounding pixels from this layer."), false, this));