291 lines
12 KiB
C++
291 lines
12 KiB
C++
#include "Document/CameraRawColor.h"
|
||
#include "Document/CameraRaw.h"
|
||
#include "Document/CameraRawTables.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);
|
||
}
|
||
|
||
// Bends tones below `lower` by `low`, above `upper` by `high`.
|
||
double CameraRawCurveSettings::bend(double tone, double lower, double low, double upper, double high)
|
||
{
|
||
// 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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
bool CameraRawCurveSettings::hasMeasuredDividers() const
|
||
{
|
||
return shadowSplit == 25 && darkSplit == 50 && lightSplit == 75 && refineSaturation == 0;
|
||
}
|
||
|
||
std::vector<float> CameraRawCurveSettings::toneTable() const
|
||
{
|
||
const bool measured = hasMeasuredDividers();
|
||
std::vector<float> table(256);
|
||
for (int index = 0; index < 256; ++index)
|
||
table[size_t(index)] = float(point(measured ? index / 255.0 : 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();
|
||
// The eight colours and Color Grading come from Photoshop's tables.
|
||
const std::vector<float> tone = shape.toneTable(), redTable = shape.channelTable(shape.red), greenTable = shape.channelTable(shape.green),
|
||
blueTable = shape.channelTable(shape.blue), mixerFloats(24, 0), pointFloats = mixing.pointFloats(), gradeFloats(12, 0);
|
||
const std::vector<float> mixerTable = CameraRawTables::compose(CameraRawTables::mixerStages(mixing));
|
||
const std::vector<float> gradeTable = CameraRawTables::compose(CameraRawTables::gradingStages(grade));
|
||
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, mixerTable.empty() ? nullptr : mixerTable.data(),
|
||
gradeTable.empty() ? nullptr : gradeTable.data(), CameraRawTables::grid);
|
||
}
|