Camera Raw curves as Photoshop's: the fitted parametric curve, the tone curve on each channel, one drag in the graph (13.18)
This commit is contained in:
1 parent
5bfbc6cd5d
commit
a77cce4598
17 files changed
+862
-143
No files matched your search
@@ -79,25 +79,34 @@ bool CameraRawCurveSettings::adjusts() const
|
||||
|| !isLinear(green) || !isLinear(blue);
|
||||
}
|
||||
|
||||
double CameraRawCurveSettings::parametric(double tone) const
|
||||
// Bends tones below `lower` by `low`, above `upper` by `high`.
|
||||
double CameraRawCurveSettings::bend(double tone, double lower, double low, double upper, double high)
|
||||
{
|
||||
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));
|
||||
// Fitted to Photoshop: Darks −51 dips 0.1 there.
|
||||
const double strength = 1.66;
|
||||
if (tone < lower)
|
||||
return lower * std::pow(tone / lower, std::pow(2, -low / 100 * strength));
|
||||
if (tone > upper) {
|
||||
const double rest = 1 - upper;
|
||||
return 1 - rest * std::pow((1 - tone) / rest, std::pow(2, high / 100 * strength));
|
||||
}
|
||||
return tone;
|
||||
}
|
||||
|
||||
std::vector<float> CameraRawCurveSettings::lumaTable() const
|
||||
double CameraRawCurveSettings::parametric(double tone) const
|
||||
{
|
||||
if (shadows == 0 && darks == 0 && lights == 0 && highlights == 0)
|
||||
return tone;
|
||||
// Gamma bends, smoothed as Image › Curves smooths.
|
||||
std::vector<CurvePoint> anchors;
|
||||
for (int index = 0; index <= 32; ++index) {
|
||||
const double x = index / 32.0;
|
||||
anchors.push_back({x, bend(bend(x, shadowSplit / 100, shadows, lightSplit / 100, highlights), darkSplit / 100, darks, darkSplit / 100, lights)});
|
||||
}
|
||||
return point(tone, anchors);
|
||||
}
|
||||
|
||||
std::vector<float> CameraRawCurveSettings::toneTable() const
|
||||
{
|
||||
std::vector<float> table(256);
|
||||
for (int index = 0; index < 256; ++index)
|
||||
@@ -262,10 +271,10 @@ void CameraRawSettings::applyCurveColor(uchar *pixels, int width, int height, qs
|
||||
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),
|
||||
const std::vector<float> tone = shape.toneTable(), 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(),
|
||||
adjust_camera_raw_curve_color(pixels, size_t(width), size_t(height), size_t(stride), tone.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);
|
||||
}
|
||||
@@ -33,15 +33,19 @@ struct CameraRawCurveSettings {
|
||||
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.
|
||||
// Photoshop's parametric curve: gamma bends, smoothed as Curves.
|
||||
double parametric(double tone) const;
|
||||
std::vector<float> lumaTable() const;
|
||||
// Applied to red, green and blue alike.
|
||||
std::vector<float> toneTable() 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;
|
||||
|
||||
private:
|
||||
static double bend(double tone, double lower, double low, double upper, double high);
|
||||
};
|
||||
|
||||
// One picked colour and how far its adjustment reaches.
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "UI/CameraRawColorControls.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
// Swift's curve graph drags, picked once at the press.
|
||||
std::optional<CameraRawCurveControls::Drag> CameraRawCurveControls::parametricDrag(QPointF at, QSizeF size) const
|
||||
{
|
||||
const CameraRawCurveSettings curve = raw().curve;
|
||||
const double tone = at.x() / std::max(size.width(), 1.0) * 100;
|
||||
const std::array<double, 3> splits{curve.shadowSplit, curve.darkSplit, curve.lightSplit};
|
||||
// Along the bottom, the nearest divider, the first of equals.
|
||||
if (at.y() > size.height() - 18) {
|
||||
size_t nearest = 0;
|
||||
for (size_t index = 1; index < splits.size(); ++index)
|
||||
if (std::abs(splits[index] - tone) < std::abs(splits[nearest] - tone))
|
||||
nearest = index;
|
||||
return Drag{.kind = Drag::Kind::divider, .index = nearest, .from = at};
|
||||
}
|
||||
double CameraRawCurveSettings::*key = tone < splits[0] ? &CameraRawCurveSettings::shadows
|
||||
: tone < splits[1] ? &CameraRawCurveSettings::darks
|
||||
: tone < splits[2] ? &CameraRawCurveSettings::lights
|
||||
: &CameraRawCurveSettings::highlights;
|
||||
return Drag{.kind = Drag::Kind::region, .key = key, .start = curve.*key, .from = at};
|
||||
}
|
||||
|
||||
std::optional<CameraRawCurveControls::Drag> CameraRawCurveControls::pointDrag(QPointF at, QSizeF size)
|
||||
{
|
||||
const double x = at.x() / std::max(size.width(), 1.0), y = 1 - at.y() / std::max(size.height(), 1.0);
|
||||
std::vector<CurvePoint> points = currentPoints();
|
||||
// On a point, that point; the first of equals.
|
||||
std::optional<size_t> near;
|
||||
for (size_t index = 0; index < points.size(); ++index)
|
||||
if (!near || std::hypot(points[index].x - x, points[index].y - y) < std::hypot(points[*near].x - x, points[*near].y - y))
|
||||
near = index;
|
||||
if (near && std::hypot(points[*near].x - x, points[*near].y - y) < 0.055) {
|
||||
m_selectedPoint = near;
|
||||
return Drag{.kind = Drag::Kind::point, .index = *near, .from = at};
|
||||
}
|
||||
// Else a new point; the ends bound its x.
|
||||
if (points.size() >= 16 || !std::all_of(points.begin(), points.end(), [x](const CurvePoint &each) { return std::abs(each.x - x) > 0.01; }))
|
||||
return std::nullopt;
|
||||
const CurvePoint point{x, y};
|
||||
points.push_back(point);
|
||||
std::stable_sort(points.begin(), points.end(), [](const CurvePoint &left, const CurvePoint &right) { return left.x < right.x; });
|
||||
store(points);
|
||||
const size_t index = size_t(std::find(points.begin(), points.end(), point) - points.begin());
|
||||
m_selectedPoint = index;
|
||||
return Drag{.kind = Drag::Kind::point, .index = index, .from = at};
|
||||
}
|
||||
|
||||
void CameraRawCurveControls::beginDrag(QPointF at, QSizeF size)
|
||||
{
|
||||
// Qt may lose a release: every press picks afresh.
|
||||
const bool parametric = !m_session.filterEdit() || m_session.filterEdit()->rawPanel.curvePage == CameraRawCurvePage::parametric;
|
||||
m_drag = parametric ? parametricDrag(at, size) : pointDrag(at, size);
|
||||
continueDrag(at, size);
|
||||
}
|
||||
|
||||
void CameraRawCurveControls::continueDrag(QPointF at, QSizeF size)
|
||||
{
|
||||
if (!m_drag)
|
||||
return;
|
||||
const double x = at.x() / std::max(size.width(), 1.0), y = 1 - at.y() / std::max(size.height(), 1.0);
|
||||
const Drag drag = *m_drag;
|
||||
switch (drag.kind) {
|
||||
case Drag::Kind::point: {
|
||||
std::vector<CurvePoint> points = currentPoints();
|
||||
if (drag.index >= points.size())
|
||||
return;
|
||||
points[drag.index].y = y;
|
||||
// Ends stay ends; the rest keep their order.
|
||||
if (drag.index > 0 && drag.index < points.size() - 1)
|
||||
points[drag.index].x = std::min(points[drag.index + 1].x - 0.01, std::max(points[drag.index - 1].x + 0.01, x));
|
||||
store(points);
|
||||
return;
|
||||
}
|
||||
case Drag::Kind::divider: {
|
||||
const double value = x * 100;
|
||||
update([&drag, value](CameraRawSettings &settings) {
|
||||
// Below the next divider; normalizing does the rest.
|
||||
CameraRawCurveSettings &curve = settings.curve;
|
||||
if (drag.index == 0)
|
||||
curve.shadowSplit = std::min(value, curve.darkSplit - 2);
|
||||
else if (drag.index == 1)
|
||||
curve.darkSplit = std::min(curve.lightSplit - 2, value);
|
||||
else
|
||||
curve.lightSplit = value;
|
||||
});
|
||||
return;
|
||||
}
|
||||
case Drag::Kind::region: {
|
||||
const double amount = std::min(100.0, std::max(-100.0, drag.start - (at.y() - drag.from.y()) / std::max(size.height(), 1.0) * 200));
|
||||
update([&drag, amount](CameraRawSettings &settings) { settings.curve.*drag.key = std::round(amount); });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawCurveControls::removePoint(QPointF at, QSizeF size)
|
||||
{
|
||||
std::vector<CurvePoint> points = currentPoints();
|
||||
const double x = at.x() / size.width();
|
||||
// The nearest inner point by x, within 0.04; ends stay.
|
||||
std::optional<size_t> nearest;
|
||||
for (size_t index = 1; index + 1 < points.size(); ++index)
|
||||
if (!nearest || std::abs(points[index].x - x) < std::abs(points[*nearest].x - x))
|
||||
nearest = index;
|
||||
if (!nearest || !(std::abs(points[*nearest].x - x) < 0.04))
|
||||
return;
|
||||
points.erase(points.begin() + std::ptrdiff_t(*nearest));
|
||||
m_selectedPoint.reset();
|
||||
store(points);
|
||||
}
|
||||
@@ -87,103 +87,45 @@ protected:
|
||||
return;
|
||||
}
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(Qt::white);
|
||||
for (const CurvePoint &point : m_owner.currentPoints())
|
||||
painter.drawEllipse(QRectF(point.x * w - 4, (1 - point.y) * h - 4, 8, 8));
|
||||
const std::vector<CurvePoint> points = m_owner.currentPoints();
|
||||
for (size_t index = 0; index < points.size(); ++index) {
|
||||
painter.setBrush(m_owner.m_selectedPoint == index ? palette().color(QPalette::Highlight) : QColor(Qt::white));
|
||||
painter.drawEllipse(QRectF(points[index].x * w - 4, (1 - points[index].y) * h - 4, 8, 8));
|
||||
}
|
||||
}
|
||||
|
||||
// Swift's two drags: still, a press adds; moved, it drags.
|
||||
// Swift's one drag: picked at the press, kept until release.
|
||||
void mousePressEvent(QMouseEvent *event) override
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
m_press = event->position();
|
||||
m_owner.beginDrag(event->position(), size());
|
||||
}
|
||||
|
||||
void mouseMoveEvent(QMouseEvent *event) override
|
||||
{
|
||||
if (!m_press || !event->buttons().testFlag(Qt::LeftButton))
|
||||
return;
|
||||
if (!m_moved && QLineF(*m_press, event->position()).length() < 2)
|
||||
return;
|
||||
m_moved = true;
|
||||
if (parametric())
|
||||
moveDivider(event->position().x());
|
||||
else
|
||||
movePoint(event->position());
|
||||
if (event->buttons().testFlag(Qt::LeftButton))
|
||||
m_owner.continueDrag(event->position(), size());
|
||||
}
|
||||
|
||||
void mouseReleaseEvent(QMouseEvent *event) override
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && m_press && !m_moved && event->position() == *m_press && !parametric())
|
||||
addPoint(event->position());
|
||||
m_press.reset();
|
||||
m_moved = false;
|
||||
if (event->button() == Qt::LeftButton)
|
||||
m_owner.m_drag.reset();
|
||||
}
|
||||
|
||||
// The double click's own release must add nothing back.
|
||||
// The second press drags too; then the point goes.
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && !parametric())
|
||||
removePoint(event->position());
|
||||
if (event->button() != Qt::LeftButton)
|
||||
return;
|
||||
m_owner.beginDrag(event->position(), size());
|
||||
if (!parametric())
|
||||
m_owner.removePoint(event->position(), size());
|
||||
m_owner.m_drag.reset();
|
||||
}
|
||||
|
||||
private:
|
||||
void moveDivider(double x)
|
||||
{
|
||||
const double value = std::min(98.0, std::max(2.0, x / std::max(double(width()), 1.0) * 100));
|
||||
const CameraRawCurveSettings curve = m_owner.raw().curve;
|
||||
const std::array<double, 3> splits{curve.shadowSplit, curve.darkSplit, curve.lightSplit};
|
||||
size_t nearest = 0;
|
||||
for (size_t index = 1; index < 3; ++index)
|
||||
if (std::abs(splits[index] - value) < std::abs(splits[nearest] - value))
|
||||
nearest = index;
|
||||
m_owner.update([nearest, value](CameraRawSettings &settings) {
|
||||
(nearest == 0 ? settings.curve.shadowSplit : nearest == 1 ? settings.curve.darkSplit : settings.curve.lightSplit) = value;
|
||||
});
|
||||
}
|
||||
|
||||
void addPoint(QPointF at)
|
||||
{
|
||||
std::vector<CurvePoint> points = m_owner.currentPoints();
|
||||
points.push_back({std::min(0.99, std::max(0.01, at.x() / width())), std::min(1.0, std::max(0.0, 1 - at.y() / height()))});
|
||||
m_owner.store(points);
|
||||
}
|
||||
|
||||
// The nearest inner point by x; ends stay.
|
||||
std::optional<size_t> nearestInner(const std::vector<CurvePoint> &points, double x) const
|
||||
{
|
||||
std::optional<size_t> nearest;
|
||||
for (size_t index = 1; index + 1 < points.size(); ++index)
|
||||
if (!nearest || std::abs(points[index].x - x) < std::abs(points[*nearest].x - x))
|
||||
nearest = index;
|
||||
return nearest;
|
||||
}
|
||||
|
||||
void movePoint(QPointF at)
|
||||
{
|
||||
std::vector<CurvePoint> points = m_owner.currentPoints();
|
||||
const double x = at.x() / width();
|
||||
const std::optional<size_t> index = nearestInner(points, x);
|
||||
if (!index)
|
||||
return;
|
||||
points[*index] = {std::min(0.98, std::max(0.02, x)), std::min(1.0, std::max(0.0, 1 - at.y() / height()))};
|
||||
m_owner.store(points);
|
||||
}
|
||||
|
||||
void removePoint(QPointF at)
|
||||
{
|
||||
std::vector<CurvePoint> points = m_owner.currentPoints();
|
||||
const double x = at.x() / width();
|
||||
const std::optional<size_t> index = nearestInner(points, x);
|
||||
if (!index || !(std::abs(points[*index].x - x) < 0.04))
|
||||
return;
|
||||
points.erase(points.begin() + std::ptrdiff_t(*index));
|
||||
m_owner.store(points);
|
||||
}
|
||||
|
||||
CameraRawCurveControls &m_owner;
|
||||
std::optional<QPointF> m_press;
|
||||
bool m_moved = false;
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -250,6 +192,9 @@ CameraRawCurveControls::CameraRawCurveControls(EditorSession &session, QWidget *
|
||||
presetRow->addStretch(1);
|
||||
points->addLayout(presetRow);
|
||||
connect(m_preset, &QComboBox::activated, this, [this](int preset) {
|
||||
// A fix beyond Swift: a preset's points drop the choice.
|
||||
if (preset != 0)
|
||||
m_selectedPoint.reset();
|
||||
if (preset == 1)
|
||||
store(CameraRawCurveSettings::linear());
|
||||
else if (preset == 2)
|
||||
@@ -258,7 +203,7 @@ CameraRawCurveControls::CameraRawCurveControls(EditorSession &session, QWidget *
|
||||
store(CameraRawCurveSettings::strongContrast());
|
||||
});
|
||||
m_refine = new CameraRawRow({.name = QStringLiteral("refineSaturation"), .title = QStringLiteral("Refine Saturation"),
|
||||
.help = QStringLiteral("How much the RGB curve also changes color strength. Zero keeps it to brightness."), .titleWidth = 88,
|
||||
.help = QStringLiteral("How much the curve also changes color strength. Zero matches Photoshop; lower keeps it to brightness, higher adds more color."), .titleWidth = 88,
|
||||
.fixedTitle = true, .fieldWidth = 48, .scrub = 1},
|
||||
[this] { return raw().curve.refineSaturation; },
|
||||
[this](double value) { update([value](CameraRawSettings &settings) { settings.curve.refineSaturation = value; }); },
|
||||
@@ -334,15 +279,23 @@ void CameraRawCurveControls::synchronize()
|
||||
m_channelBox->setVisible(point);
|
||||
const CameraRawPointChannel channel = edit ? edit->rawPanel.pointChannel : CameraRawPointChannel::rgb;
|
||||
m_channel->button(int(channel))->setChecked(true);
|
||||
m_graph->setToolTip(point ? QStringLiteral("Drag a point. Click the curve to add one. Double-click a point to remove it.")
|
||||
: QStringLiteral("Drag a divider to change which tones the neighboring sliders affect."));
|
||||
// Swift's onChange: another channel drops the choice and drag.
|
||||
if (channel != m_shownChannel) {
|
||||
m_shownChannel = channel;
|
||||
m_selectedPoint.reset();
|
||||
m_drag.reset();
|
||||
}
|
||||
m_graph->setToolTip(point ? QStringLiteral("Drag a point. Click to add one. Double-click a point to remove it.")
|
||||
: QStringLiteral("Drag up or down to lift or lower those tones. Drag a divider along the bottom to change which tones each region covers."));
|
||||
m_graph->update();
|
||||
m_amounts->setVisible(!point);
|
||||
m_points->setVisible(point);
|
||||
const std::vector<CurvePoint> points = currentPoints();
|
||||
// Swift's selectedPoint: the last inner point, else the last.
|
||||
const CurvePoint selected = points.size() > 2 ? points[points.size() - 2] : points.back();
|
||||
m_selected->setText(QStringLiteral("In %1 Out %2").arg(std::lround(selected.x * 255)).arg(std::lround(selected.y * 255)));
|
||||
// Swift's selectedPoint: the chosen index, while it exists.
|
||||
const bool chosen = m_selectedPoint && *m_selectedPoint < points.size();
|
||||
m_selected->setVisible(chosen);
|
||||
if (chosen)
|
||||
m_selected->setText(QStringLiteral("In %1 Out %2").arg(std::lround(points[*m_selectedPoint].x * 255)).arg(std::lround(points[*m_selectedPoint].y * 255)));
|
||||
const QSignalBlocker quiet(m_preset);
|
||||
m_preset->setCurrentIndex(points == CameraRawCurveSettings::linear() ? 1
|
||||
: points == CameraRawCurveSettings::mediumContrast() ? 2
|
||||
|
||||
@@ -28,9 +28,25 @@ private:
|
||||
void update(const std::function<void(CameraRawSettings &)> &change);
|
||||
void store(const std::vector<CurvePoint> &points);
|
||||
void panel(const std::function<void(CameraRawPanel &)> &change);
|
||||
// Swift's Drag: a point, divider, or region.
|
||||
struct Drag {
|
||||
enum class Kind { point, divider, region } kind;
|
||||
size_t index = 0;
|
||||
double CameraRawCurveSettings::*key = nullptr;
|
||||
double start = 0;
|
||||
QPointF from;
|
||||
};
|
||||
std::optional<Drag> parametricDrag(QPointF at, QSizeF size) const;
|
||||
std::optional<Drag> pointDrag(QPointF at, QSizeF size);
|
||||
void beginDrag(QPointF at, QSizeF size);
|
||||
void continueDrag(QPointF at, QSizeF size);
|
||||
void removePoint(QPointF at, QSizeF size);
|
||||
friend class CameraRawCurveGraph;
|
||||
|
||||
EditorSession &m_session;
|
||||
std::optional<Drag> m_drag;
|
||||
std::optional<size_t> m_selectedPoint;
|
||||
CameraRawPointChannel m_shownChannel = CameraRawPointChannel::rgb;
|
||||
QButtonGroup *m_page;
|
||||
QWidget *m_channelBox;
|
||||
QButtonGroup *m_channel;
|
||||
|
||||
Reference in new issue
Block a user