Camera Raw filter's model and pixel kernels (12.8a)
This commit is contained in:
1 parent
4291f4249a
commit
b544e12b1d
33 files changed
+4438
-27
No files matched your search
@@ -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};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 ®ion(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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user