Files
OmaPhoto/src/Document/CameraRawColor.cpp
T

291 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
}