#include "Document/CameraRawColor.h" #include "Document/CameraRaw.h" #include "Document/CameraRawTables.h" #include "Document/ImageAdjustments.h" #include #include extern "C" { #include "AdjustPixels.h" } namespace { using ImageAdjustmentPixels::clamp; double point(double x, const std::vector &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 repair(const std::vector &points) { std::vector 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 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 &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 CameraRawCurveSettings::linear() { return {{0, 0}, {1, 1}}; } std::vector CameraRawCurveSettings::mediumContrast() { return {{0, 0}, {0.25, 0.18}, {0.75, 0.82}, {1, 1}}; } std::vector CameraRawCurveSettings::strongContrast() { return {{0, 0}, {0.25, 0.10}, {0.75, 0.90}, {1, 1}}; } bool CameraRawCurveSettings::isLinear(const std::vector &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 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 CameraRawCurveSettings::toneTable() const { const bool measured = hasMeasuredDividers(); std::vector 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 CameraRawCurveSettings::channelTable(const std::vector &points) const { std::vector 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 &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 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 &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 CameraRawMixerSettings::weights(double degrees) { std::array 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 CameraRawMixerSettings::mixerFloats() const { std::vector result; for (const std::array *values : {&hue, &saturation, &luminance}) for (const double value : *values) result.push_back(float(value / 100)); return result; } std::vector CameraRawMixerSettings::pointFloats() const { std::vector 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 *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(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 CameraRawGradingSettings::wheels() const { return {shadows, midtones, highlights, global}; } bool CameraRawGradingSettings::adjusts() const { const std::array all = wheels(); return std::any_of(all.begin(), all.end(), [](const CameraRawGradeWheel &wheel) { return wheel.saturation != 0 || wheel.luminance != 0; }); } std::vector CameraRawGradingSettings::gradeFloats() const { std::vector 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 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 mixerTable = CameraRawTables::compose(CameraRawTables::mixerStages(mixing)); const std::vector 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); }