14.25: Camera Raw's Light and Color sliders match Photoshop's
This commit is contained in:
17 files changed
+513
-136
No files matched your search
+14
-27
@@ -1,5 +1,6 @@
|
||||
#include "Document/CameraRaw.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/CameraRawTables.h"
|
||||
#include "Document/Filters.h"
|
||||
#include "Document/Levels.h"
|
||||
#include <QPainter>
|
||||
@@ -19,6 +20,11 @@ double decode(double encoded)
|
||||
return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
double encode(double linear)
|
||||
{
|
||||
return linear <= 0.0031308 ? linear * 12.92 : 1.055 * std::pow(linear, 1 / 2.4) - 0.055;
|
||||
}
|
||||
|
||||
// The image drawn into the kernels' premultiplied context.
|
||||
QImage drawn(const QImage &image)
|
||||
{
|
||||
@@ -109,19 +115,12 @@ 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
|
||||
@@ -135,9 +134,12 @@ QImage CameraRawSettings::apply(const QImage &image, std::optional<CameraRawClip
|
||||
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 (settings.adjustsLight() || settings.adjustsColor() || clipping) {
|
||||
const CameraRawTables::Stages stages = CameraRawTables::stages(settings);
|
||||
const std::vector<float> before = CameraRawTables::compose(stages.before), after = CameraRawTables::compose(stages.after);
|
||||
adjust_camera_raw(pixels, w, h, s, before.empty() ? nullptr : before.data(), after.empty() ? nullptr : after.data(), CameraRawTables::grid,
|
||||
settings.highlights, settings.shadows, mode);
|
||||
}
|
||||
if (paintColor)
|
||||
settings.applyCurveColor(pixels, width, height, stride, visualizePointColor);
|
||||
if (paintEffects) {
|
||||
@@ -154,24 +156,9 @@ QImage CameraRawSettings::apply(const QImage &image, std::optional<CameraRawClip
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
return CameraRawTables::neutralize(red, green, blue);
|
||||
}
|
||||
|
||||
std::optional<CameraRawSettings::Balance> CameraRawSettings::autoBalance(const QImage &image)
|
||||
@@ -195,7 +182,7 @@ std::optional<CameraRawSettings::Balance> CameraRawSettings::autoBalance(const Q
|
||||
}
|
||||
if (count == 0)
|
||||
return std::nullopt;
|
||||
return neutralizeLinear(red / count, green / count, blue / count);
|
||||
return neutralizeStraight(encode(red / count), encode(green / count), encode(blue / count));
|
||||
}
|
||||
|
||||
double CameraRawScope::peak() const
|
||||
|
||||
@@ -36,10 +36,6 @@ struct CameraRawGroups {
|
||||
// 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.
|
||||
@@ -91,12 +87,6 @@ struct CameraRawSettings {
|
||||
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;
|
||||
@@ -104,8 +94,7 @@ struct CameraRawSettings {
|
||||
double temperature;
|
||||
double tint;
|
||||
};
|
||||
// The temperature and tint making a linear pixel neutral.
|
||||
static std::optional<Balance> neutralizeLinear(double red, double green, double blue);
|
||||
// The temperature and tint turning a straight sRGB colour gray.
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "Document/CameraRawTables.h"
|
||||
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <zlib.h>
|
||||
|
||||
namespace CameraRawTables {
|
||||
namespace {
|
||||
constexpr size_t entries = size_t(size) * size * size * 3;
|
||||
// White balance 9 × 9, then the six other sliders.
|
||||
constexpr int exposureStart = 81, contrastStart = 102, whitesStart = 111, blacksStart = 120;
|
||||
constexpr int saturationStart = 129, vibranceStart = 138, count = 147;
|
||||
|
||||
// Raw DEFLATE, as Foundation's `.zlib` writes it.
|
||||
std::vector<uint8_t> inflated(const QByteArray &packed, size_t expected)
|
||||
{
|
||||
std::vector<uint8_t> out(expected);
|
||||
z_stream stream{};
|
||||
if (inflateInit2(&stream, -MAX_WBITS) != Z_OK)
|
||||
throw std::logic_error("Camera Raw's tables: inflate would not start");
|
||||
stream.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(packed.constData()));
|
||||
stream.avail_in = uInt(packed.size());
|
||||
stream.next_out = out.data();
|
||||
stream.avail_out = uInt(out.size());
|
||||
const int status = inflate(&stream, Z_FINISH);
|
||||
const size_t made = stream.total_out;
|
||||
inflateEnd(&stream);
|
||||
if (status != Z_STREAM_END || made != expected)
|
||||
throw std::logic_error("Camera Raw's tables do not inflate to their size");
|
||||
return out;
|
||||
}
|
||||
|
||||
// Each entry is a running difference from no change.
|
||||
std::vector<uint8_t> load()
|
||||
{
|
||||
QFile file(QStringLiteral(":/CameraRawTables.bin"));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
throw std::logic_error("Camera Raw's tables are missing from the build");
|
||||
const QByteArray bytes = file.readAll();
|
||||
const auto byte = [&](int at) { return int(uchar(bytes[at])); };
|
||||
if (bytes.size() <= 8 || !bytes.startsWith("CRT2") || (byte(4) | byte(5) << 8) != size || (byte(6) | byte(7) << 8) != count)
|
||||
throw std::logic_error("Camera Raw's tables have the wrong header");
|
||||
const std::vector<uint8_t> planes = inflated(bytes.mid(8), count * entries);
|
||||
std::vector<uint8_t> out(count * entries);
|
||||
const size_t cells = size_t(size) * size * size;
|
||||
uint8_t level[size];
|
||||
for (int i = 0; i < size; ++i)
|
||||
level[i] = uint8_t(std::lround(i * 255.0 / (size - 1)));
|
||||
for (size_t table = 0; table < count; ++table) {
|
||||
for (size_t channel = 0; channel < 3; ++channel) {
|
||||
const size_t base = (table * 3 + channel) * cells;
|
||||
uint8_t running = 0;
|
||||
for (size_t cell = 0; cell < cells; ++cell) {
|
||||
running = uint8_t(running + planes[base + cell]);
|
||||
const size_t axis = channel == 0 ? cell / (size * size) : channel == 1 ? (cell / size) % size : cell % size;
|
||||
out[table * entries + cell * 3 + channel] = uint8_t(running + level[axis]);
|
||||
}
|
||||
}
|
||||
}
|
||||
qCInfo(lcRendering) << "Camera Raw's tables loaded:" << count << "tables";
|
||||
return out;
|
||||
}
|
||||
|
||||
const uint8_t *table(int index)
|
||||
{
|
||||
return tables().data() + size_t(index) * entries;
|
||||
}
|
||||
|
||||
struct Between {
|
||||
int index;
|
||||
double fraction;
|
||||
};
|
||||
|
||||
// The two tables beside `value`, and how far between.
|
||||
Between between(double value, double low, double step, int tables)
|
||||
{
|
||||
const double position = std::min(double(tables - 1), std::max(0.0, (value - low) / step));
|
||||
const int index = std::min(tables - 2, int(position));
|
||||
return {index, position - index};
|
||||
}
|
||||
|
||||
CameraRawStage stage(int start, double value, double low, double step, int tables)
|
||||
{
|
||||
const Between at = between(value, low, step, tables);
|
||||
return CameraRawStage{{table(start + at.index), table(start + at.index + 1), nullptr, nullptr},
|
||||
{float(1 - at.fraction), float(at.fraction), 0, 0}};
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<uint8_t> &tables()
|
||||
{
|
||||
static const std::vector<uint8_t> loaded = load();
|
||||
return loaded;
|
||||
}
|
||||
|
||||
CameraRawStage whiteBalance(double temperature, double tint)
|
||||
{
|
||||
const Between row = between(temperature, -100, 25, 9), column = between(tint, -100, 25, 9);
|
||||
const double down = row.fraction, across = column.fraction;
|
||||
return CameraRawStage{{table(row.index * 9 + column.index), table(row.index * 9 + column.index + 1), table((row.index + 1) * 9 + column.index),
|
||||
table((row.index + 1) * 9 + column.index + 1)},
|
||||
{float((1 - down) * (1 - across)), float((1 - down) * across), float(down * (1 - across)), float(down * across)}};
|
||||
}
|
||||
|
||||
Stages stages(const CameraRawSettings &settings)
|
||||
{
|
||||
Stages out;
|
||||
if (settings.exposure != 0)
|
||||
out.before.push_back(stage(exposureStart, settings.exposure, -5, 0.5, 21));
|
||||
if (settings.temperature != 0 || settings.tint != 0)
|
||||
out.before.push_back(whiteBalance(settings.temperature, settings.tint));
|
||||
if (settings.contrast != 0)
|
||||
out.before.push_back(stage(contrastStart, settings.contrast, -100, 25, 9));
|
||||
if (settings.whites != 0)
|
||||
out.after.push_back(stage(whitesStart, settings.whites, -100, 25, 9));
|
||||
if (settings.blacks != 0)
|
||||
out.after.push_back(stage(blacksStart, settings.blacks, -100, 25, 9));
|
||||
if (settings.saturation != 0)
|
||||
out.after.push_back(stage(saturationStart, settings.saturation, -100, 25, 9));
|
||||
if (settings.vibrance != 0)
|
||||
out.after.push_back(stage(vibranceStart, settings.vibrance, -100, 25, 9));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<float> compose(const std::vector<CameraRawStage> &stages)
|
||||
{
|
||||
if (stages.empty())
|
||||
return {};
|
||||
std::vector<float> out;
|
||||
try {
|
||||
out.resize(size_t(grid) * grid * grid * 3);
|
||||
} catch (const std::bad_alloc &) {
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
}
|
||||
camera_raw_compose(out.data(), grid, stages.data(), int(stages.size()), size);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<CameraRawSettings::Balance> neutralize(double red, double green, double blue)
|
||||
{
|
||||
if (!(std::min({red, green, blue}) > 1e-4))
|
||||
return std::nullopt;
|
||||
const auto cast = [&](double temperature, double tint) {
|
||||
const CameraRawStage stage = whiteBalance(temperature, tint);
|
||||
double color[3] = {red, green, blue};
|
||||
camera_raw_stage_color(&stage, size, color);
|
||||
return std::max({color[0], color[1], color[2]}) - std::min({color[0], color[1], color[2]});
|
||||
};
|
||||
CameraRawSettings::Balance best{0, 0};
|
||||
double bestCast = cast(0, 0);
|
||||
for (const auto &[step, reach] : {std::pair{10.0, 100.0}, {2.0, 10.0}, {0.5, 2.0}}) {
|
||||
const CameraRawSettings::Balance center = best;
|
||||
const int steps = int(std::lround(2 * reach / step));
|
||||
for (int i = 0; i <= steps; ++i) {
|
||||
const double temperature = center.temperature - reach + step * i;
|
||||
for (int j = 0; j <= steps; ++j) {
|
||||
const double tint = center.tint - reach + step * j;
|
||||
if (std::abs(temperature) > 100 || std::abs(tint) > 100)
|
||||
continue;
|
||||
const double found = cast(temperature, tint);
|
||||
if (found < bestCast) {
|
||||
best = {temperature, tint};
|
||||
bestCast = found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "Document/CameraRaw.h"
|
||||
extern "C" {
|
||||
#include "AdjustPixels.h"
|
||||
}
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// Photoshop's Light and Color sliders, measured as 17³ colour tables.
|
||||
namespace CameraRawTables {
|
||||
constexpr int size = 17;
|
||||
// The grid the stages compose onto, finer than the tables.
|
||||
constexpr int grid = 33;
|
||||
|
||||
// Every table, red slowest, 3 bytes a colour; loaded once.
|
||||
const std::vector<uint8_t> &tables();
|
||||
// Temperature and Tint: the four grid tables around them.
|
||||
CameraRawStage whiteBalance(double temperature, double tint);
|
||||
struct Stages {
|
||||
std::vector<CameraRawStage> before;
|
||||
std::vector<CameraRawStage> after;
|
||||
};
|
||||
// Before Highlights and Shadows, and after; zero sliders left out.
|
||||
Stages stages(const CameraRawSettings &settings);
|
||||
// The stages over the composing grid; empty without stages.
|
||||
std::vector<float> compose(const std::vector<CameraRawStage> &stages);
|
||||
// Temperature and Tint turning a straight sRGB colour gray.
|
||||
std::optional<CameraRawSettings::Balance> neutralize(double red, double green, double blue);
|
||||
}
|
||||
Binary file not shown.
Reference in new issue
Block a user