14.27: Camera Raw's Color Mixer, Calibration, curve, vignettes and Color Grading match Photoshop

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 14:05:24 -04:00
1 parent 8b7a8ffa0e
commit 9c0a23ea7e
26 files changed
+375 -113

No files matched your search

+2 -1
View File
@@ -134,7 +134,8 @@ 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) {
if (settings.adjustsLight() || settings.adjustsColor() || settings.calibration.adjusts()
|| (settings.curve.hasMeasuredDividers() && settings.curve.adjusts()) || clipping) {
const CameraRawTables::Brightness brightness = settings.contrast != 0 || settings.highlights != 0 || settings.shadows != 0
? CameraRawTables::brightness(pixels, width, height, stride)
: CameraRawTables::Brightness();
+14 -4
View File
@@ -1,5 +1,6 @@
#include "Document/CameraRawColor.h"
#include "Document/CameraRaw.h"
#include "Document/CameraRawTables.h"
#include "Document/ImageAdjustments.h"
#include <algorithm>
#include <cmath>
@@ -106,11 +107,17 @@ double CameraRawCurveSettings::parametric(double tone) const
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(parametric(index / 255.0), rgb));
table[size_t(index)] = float(point(measured ? index / 255.0 : parametric(index / 255.0), rgb));
return table;
}
@@ -271,10 +278,13 @@ 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();
// 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 = mixing.mixerFloats(), pointFloats = mixing.pointFloats(),
gradeFloats = grade.gradeFloats();
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);
gradeFloats.data(), grade.blending / 100, grade.balance / 100, visualize, mixerTable.empty() ? nullptr : mixerTable.data(),
gradeTable.empty() ? nullptr : gradeTable.data(), CameraRawTables::grid);
}
+2
View File
@@ -35,6 +35,8 @@ struct CameraRawCurveSettings {
bool adjusts() const;
// Photoshop's parametric curve: gamma bends, smoothed as Curves.
double parametric(double tone) const;
// Photoshop's tables draw the parametric curve: dividers as measured.
bool hasMeasuredDividers() const;
// Applied to red, green and blue alike.
std::vector<float> toneTable() const;
std::vector<float> channelTable(const std::vector<CurvePoint> &points) const;
+3 -1
View File
@@ -1,5 +1,6 @@
#include "Document/CameraRawDetailOptics.h"
#include "Document/CameraRaw.h"
#include "Document/CameraRawTables.h"
#include "Document/ImageAdjustments.h"
#include <utility>
extern "C" {
@@ -77,7 +78,8 @@ void CameraRawSettings::applyDetailOptics(uchar *pixels, int width, int height,
if (lens.adjusts())
adjust_camera_raw_optics(pixels, w, h, s, lens.removeChromaticAberration ? 1 : 0, lens.enableLensProfile ? 1 : 0, lens.profileDistortion,
lens.profileVignetting, lens.distortionK(profileStrength), lens.purpleAmount, lens.purpleHueLow, lens.purpleHueHigh,
lens.greenAmount, lens.greenHueLow, lens.greenHueHigh, lens.vignetteAmount, lens.vignetteMidpoint, scale);
lens.greenAmount, lens.greenHueLow, lens.greenHueHigh, lens.vignetteAmount, lens.vignetteMidpoint, scale,
CameraRawTables::exposureTables(), CameraRawTables::size);
if (sharpening.adjusts())
adjust_camera_raw_detail(pixels, w, h, s, sharpening.sharpenAmount, sharpening.sharpenRadius, sharpening.sharpenDetail,
sharpening.sharpenMasking, sharpening.noiseLuminance, sharpening.noiseLuminanceDetail,
@@ -192,9 +192,9 @@ CameraRawCalibrationSettings CameraRawCalibrationSettings::normalized() const
void CameraRawSettings::applyCalibration(uchar *pixels, int width, int height, qsizetype stride) const
{
const CameraRawCalibrationSettings primaries = calibration.normalized();
if (!primaries.adjusts())
// The primaries come from Photoshop's tables; this draws Shadows Tint.
if (primaries.shadowTint == 0)
return;
adjust_camera_raw_calibration(pixels, size_t(width), size_t(height), size_t(stride), primaries.shadowTint, primaries.redHue,
primaries.redSaturation, primaries.greenHue, primaries.greenSaturation, primaries.blueHue,
primaries.blueSaturation, int(primaries.process) + 1);
adjust_camera_raw_calibration(pixels, size_t(width), size_t(height), size_t(stride), primaries.shadowTint, 0, 0, 0, 0, 0, 0,
int(primaries.process) + 1);
}
+124 -32
View File
@@ -5,54 +5,53 @@
#include <QFile>
#include <array>
#include <cmath>
#include <span>
#include <algorithm>
#include <stdexcept>
#include <zlib.h>
#include <lzma.h>
namespace CameraRawTables {
namespace {
constexpr size_t entries = size_t(size) * size * size * 3;
// White balance, five sliders, then three adaptive ones.
constexpr int exposureStart = 81, whitesStart = 102, blacksStart = 111, saturationStart = 120, vibranceStart = 129;
constexpr int contrastStart = 138, highlightsStart = 187, shadowsStart = 236, count = 285;
constexpr int contrastStart = 138, highlightsStart = 187, shadowsStart = 236;
// Then the Mixer, Calibration, the parametric curve, Color Grading.
constexpr int mixerStart = 285, calibrationStart = 405, parametricStart = 435, gradingStart = 455, gradingLuminanceStart = 743, count = 763;
constexpr std::array<double, 7> brightnesses{53, 80, 104, 130, 160, 190, 224};
constexpr std::array<double, 7> adaptiveValues{-100, -50, -25, 0, 25, 50, 100};
// Raw DEFLATE, as Foundation's `.zlib` writes it.
std::vector<uint8_t> inflated(const QByteArray &packed, size_t expected)
// An xz stream, as Foundation's `.lzma` writes it.
std::vector<uint8_t> unpacked(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");
uint64_t memory = UINT64_MAX;
size_t read = 0, made = 0;
const lzma_ret status = lzma_stream_buffer_decode(&memory, 0, nullptr, reinterpret_cast<const uint8_t *>(packed.constData()), &read,
size_t(packed.size()), out.data(), &made, out.size());
if (status != LZMA_OK || made != expected || read != size_t(packed.size()))
throw std::logic_error("Camera Raw's tables do not unpack to their size");
return out;
}
// Each entry is a running difference from no change.
// Each entry: a running difference from no change, stepped.
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)
const auto field = [&](int at) { return int(uchar(bytes[at])) | int(uchar(bytes[at + 1])) << 8; };
if (bytes.size() <= 10 || !bytes.startsWith("CRT3") || field(4) != size || field(6) != count || field(8) <= 0)
throw std::logic_error("Camera Raw's tables have the wrong header");
const std::vector<uint8_t> planes = inflated(bytes.mid(8), count * entries);
const int step = field(8);
const std::vector<uint8_t> planes = unpacked(bytes.mid(10), count * entries);
std::vector<uint8_t> out(count * entries);
const size_t cells = size_t(size) * size * size;
uint8_t level[size];
int level[size];
for (int i = 0; i < size; ++i)
level[i] = uint8_t(std::lround(i * 255.0 / (size - 1)));
level[i] = int(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;
@@ -60,7 +59,7 @@ std::vector<uint8_t> load()
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]);
out[table * entries + cell * 3 + channel] = uint8_t(std::clamp(level[axis] + int(int8_t(running)) * step, 0, 255));
}
}
}
@@ -78,6 +77,22 @@ struct Between {
double fraction;
};
struct Part {
const uint8_t *table;
double weight;
};
// Up to eight tables a stage; null means no change.
CameraRawStage blend(const std::vector<Part> &parts)
{
CameraRawStage stage{};
for (size_t index = 0; index < std::min<size_t>(parts.size(), 8); ++index) {
stage.table[index] = parts[index].table;
stage.weight[index] = float(parts[index].weight);
}
return stage;
}
// The two tables beside `value`, and how far between.
Between between(double value, double low, double step, int tables)
{
@@ -86,8 +101,8 @@ Between between(double value, double low, double step, int tables)
return {index, position - index};
}
// An adaptive slider: four tables round value and brightness.
Between between(double value, const std::array<double, 7> &points)
// The two of `points` (rising) beside `value`, and how far.
Between between(double value, std::span<const double> points)
{
const double held = std::min(points.back(), std::max(points.front(), value));
int index = 0;
@@ -102,15 +117,19 @@ CameraRawStage adaptive(int start, double value, double brightness)
const Between row = between(brightness, brightnesses), column = between(value, adaptiveValues);
const double down = row.fraction, across = column.fraction;
const int width = int(adaptiveValues.size()), base = start + row.index * width + column.index;
return CameraRawStage{{table(base), table(base + 1), table(base + width), table(base + width + 1)},
{float((1 - down) * (1 - across)), float((1 - down) * across), float(down * (1 - across)), float(down * across)}};
return blend({{table(base), (1 - down) * (1 - across)}, {table(base + 1), (1 - down) * across}, {table(base + width), down * (1 - across)},
{table(base + width + 1), down * across}});
}
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}};
return blend({{table(start + at.index), 1 - at.fraction}, {table(start + at.index + 1), at.fraction}});
}
CameraRawStage fifty(int start, double value)
{
return stage(start, value, -100, 50, 5);
}
}
@@ -124,9 +143,14 @@ 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)}};
const int base = row.index * 9 + column.index;
return blend({{table(base), (1 - down) * (1 - across)}, {table(base + 1), (1 - down) * across}, {table(base + 9), down * (1 - across)},
{table(base + 10), down * across}});
}
const uint8_t *exposureTables()
{
return table(exposureStart);
}
Brightness brightness(const uchar *pixels, int width, int height, qsizetype stride)
@@ -139,6 +163,12 @@ Brightness brightness(const uchar *pixels, int width, int height, qsizetype stri
std::vector<CameraRawStage> stages(const CameraRawSettings &settings, const Brightness &brightness)
{
std::vector<CameraRawStage> out;
const CameraRawCalibrationSettings &calibration = settings.calibration;
const std::array<double, 6> primaries{calibration.redHue, calibration.redSaturation, calibration.greenHue,
calibration.greenSaturation, calibration.blueHue, calibration.blueSaturation};
for (int index = 0; index < 6; ++index)
if (primaries[size_t(index)] != 0)
out.push_back(fifty(calibrationStart + index * 5, primaries[size_t(index)]));
if (settings.exposure != 0)
out.push_back(stage(exposureStart, settings.exposure, -5, 0.5, 21));
if (settings.temperature != 0 || settings.tint != 0)
@@ -157,6 +187,68 @@ std::vector<CameraRawStage> stages(const CameraRawSettings &settings, const Brig
out.push_back(stage(saturationStart, settings.saturation, -100, 25, 9));
if (settings.vibrance != 0)
out.push_back(stage(vibranceStart, settings.vibrance, -100, 25, 9));
const CameraRawCurveSettings &curve = settings.curve;
if (curve.hasMeasuredDividers()) {
const std::array<double, 4> regions{curve.shadows, curve.darks, curve.lights, curve.highlights};
for (int index = 0; index < 4; ++index)
if (regions[size_t(index)] != 0)
out.push_back(fifty(parametricStart + index * 5, regions[size_t(index)]));
}
return out;
}
std::vector<CameraRawStage> gradingStages(const CameraRawGradingSettings &grading)
{
std::vector<CameraRawStage> out;
const std::array<CameraRawGradeWheel, 4> lumas{grading.shadows, grading.midtones, grading.highlights, grading.global};
for (int index = 0; index < 4; ++index)
if (lumas[size_t(index)].luminance != 0)
out.push_back(fifty(gradingLuminanceStart + index * 5, lumas[size_t(index)].luminance));
const double balance = grading.balance / 100;
const double highlightShare = std::max(0.0, balance < 0 ? 1 + 1.4 * balance : 1 + 2 * balance);
const double shadowShare = std::max(0.0, balance < 0 ? 1 - 1.2 * balance : 1 - 1.4 * balance);
constexpr std::array<double, 3> blendings{0, 50, 100};
constexpr std::array<double, 4> saturations{0, 25, 50, 100};
const Between blended = between(grading.blending, blendings);
// Blending 0 holds two wheels, 50 all four, 100 two.
const auto section = [](int blendIndex, int wheel) { return gradingStart + (blendIndex == 0 ? 0 : blendIndex == 1 ? 72 : 216) + wheel * 36; };
struct Wheel {
int index;
CameraRawGradeWheel color;
double share;
bool blends;
};
for (const Wheel &wheel : {Wheel{3, grading.global, 1, false}, Wheel{2, grading.midtones, 1, false}, Wheel{1, grading.highlights, highlightShare, true},
Wheel{0, grading.shadows, shadowShare, true}}) {
if (!(wheel.color.saturation * wheel.share > 0))
continue;
const double position = std::fmod(wheel.color.hue, 360) / 30;
const int first = int(position) % 12, second = (first + 1) % 12;
const double around = position - int(position);
const Between level = between(wheel.color.saturation * wheel.share, saturations);
std::vector<Part> parts;
const std::vector<std::pair<int, double>> blending = wheel.blends
? std::vector<std::pair<int, double>>{{blended.index, 1 - blended.fraction}, {blended.index + 1, blended.fraction}}
: std::vector<std::pair<int, double>>{{1, 1}};
for (const auto &[blendIndex, blendWeight] : blending) {
const int base = section(blendIndex, wheel.index);
for (const auto &[hue, hueWeight] : {std::pair{first, 1 - around}, std::pair{second, around}})
for (const auto &[saturation, saturationWeight] : {std::pair{level.index, 1 - level.fraction}, std::pair{level.index + 1, level.fraction}})
parts.push_back({saturation == 0 ? nullptr : table(base + hue * 3 + saturation - 1), blendWeight * hueWeight * saturationWeight});
}
out.push_back(blend(parts));
}
return out;
}
std::vector<CameraRawStage> mixerStages(const CameraRawMixerSettings &mixer)
{
std::vector<CameraRawStage> out;
const std::array<const std::array<double, 8> *, 3> kinds{&mixer.luminance, &mixer.saturation, &mixer.hue};
for (int kind = 0; kind < 3; ++kind)
for (int color = 0; color < 8; ++color)
if ((*kinds[size_t(kind)])[size_t(color)] != 0)
out.push_back(fifty(mixerStart + (kind * 8 + color) * 5, (*kinds[size_t(kind)])[size_t(color)]));
return out;
}
+6
View File
@@ -18,6 +18,8 @@ constexpr int grid = 33;
const std::vector<uint8_t> &tables();
// Temperature and Tint: the four grid tables around them.
CameraRawStage whiteBalance(double temperature, double tint);
// Exposure's 21 tables in turn, for the lens vignette.
const uint8_t *exposureTables();
// What adaptive sliders read first, in sRGB levels.
struct Brightness {
double brightest = 128;
@@ -27,6 +29,10 @@ struct Brightness {
Brightness brightness(const uchar *pixels, int width, int height, qsizetype stride);
// Camera Raw's order, sliders at zero left out.
std::vector<CameraRawStage> stages(const CameraRawSettings &settings, const Brightness &brightness);
// Color Grading's stages: luminance, global, midtones, highlights, shadows.
std::vector<CameraRawStage> gradingStages(const CameraRawGradingSettings &grading);
// The Mixer's stages: luminance, then saturation, then hue.
std::vector<CameraRawStage> mixerStages(const CameraRawMixerSettings &mixer);
// 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.