14.25: Camera Raw's Light and Color sliders match Photoshop's

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 13:03:24 -04:00
1 parent 15bb440bc9
commit efadd6f7c6
17 files changed
+513 -136

No files matched your search

+14 -27
View File
@@ -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
+1 -12
View File
@@ -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);
+174
View File
@@ -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;
}
}
+31
View File
@@ -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.