14.27: Camera Raw's Color Mixer, Calibration, curve, vignettes and Color Grading match Photoshop
This commit is contained in:
26 files changed
+375
-113
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user