Black & White, Color Balance and Invert adjustments (12.7b)
This commit is contained in:
1 parent
7d47cc367a
commit
4291f4249a
33 files changed
+819
-52
No files matched your search
@@ -75,15 +75,21 @@ void EditorSession::openAdjustmentEditor(QUuid id, const LayerAdjustment &origin
|
||||
m_hueSaturation.emplace(layer.id, made.asset.value(), std::nullopt, layer.transform);
|
||||
m_hueSaturation->settings = original.resolvedHSV();
|
||||
break;
|
||||
case AdjustmentKind::invert:
|
||||
throw std::logic_error("Invert has no editor to open");
|
||||
case AdjustmentKind::curves:
|
||||
case AdjustmentKind::exposure:
|
||||
case AdjustmentKind::gradientMap:
|
||||
case AdjustmentKind::grain: {
|
||||
case AdjustmentKind::grain:
|
||||
case AdjustmentKind::blackWhite:
|
||||
case AdjustmentKind::colorBalance: {
|
||||
FilterSettings settings;
|
||||
settings.curves = original.curves;
|
||||
settings.exposure = original.exposure();
|
||||
settings.gradientMap = original.gradientMap();
|
||||
settings.grain = original.grain();
|
||||
settings.blackWhite = original.blackWhite();
|
||||
settings.colorBalance = original.colorBalance();
|
||||
m_filterEdit.emplace(filterKind(original.kind).value(), layer, std::nullopt, settings, std::nullopt);
|
||||
break;
|
||||
}
|
||||
@@ -113,6 +119,9 @@ LayerAdjustment EditorSession::editedAdjustment() const
|
||||
case AdjustmentKind::exposure: value.setExposure(m_filterEdit.value().settings.exposure); break;
|
||||
case AdjustmentKind::gradientMap: value.setGradientMap(m_filterEdit.value().settings.gradientMap); break;
|
||||
case AdjustmentKind::grain: value.setGrain(m_filterEdit.value().settings.grain); break;
|
||||
case AdjustmentKind::blackWhite: value.setBlackWhite(m_filterEdit.value().settings.blackWhite); break;
|
||||
case AdjustmentKind::colorBalance: value.setColorBalance(m_filterEdit.value().settings.colorBalance); break;
|
||||
case AdjustmentKind::invert: throw std::logic_error("Invert has no editor to read");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ void EditorSession::renderFilterPreview()
|
||||
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
|
||||
} catch (const ExportError &error) {
|
||||
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
|
||||
} catch (const ProjectError &error) {
|
||||
// Settings past their bounds: Black & White, Color Balance.
|
||||
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -228,6 +231,8 @@ void EditorSession::commitFilter(std::function<void()> done)
|
||||
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
|
||||
} catch (const ExportError &error) {
|
||||
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
|
||||
} catch (const ProjectError &error) {
|
||||
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ QString rawValue(FilterKind kind)
|
||||
case FilterKind::exposure: return QStringLiteral("Exposure");
|
||||
case FilterKind::gradientMap: return QStringLiteral("Gradient Map");
|
||||
case FilterKind::grain: return QStringLiteral("Grain");
|
||||
case FilterKind::blackWhite: return QStringLiteral("Black & White");
|
||||
case FilterKind::colorBalance: return QStringLiteral("Color Balance");
|
||||
}
|
||||
throw std::logic_error("unknown filter kind");
|
||||
}
|
||||
@@ -46,7 +48,8 @@ QString rawValue(BackgroundQuality quality)
|
||||
|
||||
bool isImageAdjustment(FilterKind kind)
|
||||
{
|
||||
return kind == FilterKind::curves || kind == FilterKind::exposure || kind == FilterKind::gradientMap || kind == FilterKind::grain;
|
||||
return kind == FilterKind::curves || kind == FilterKind::exposure || kind == FilterKind::gradientMap || kind == FilterKind::grain
|
||||
|| kind == FilterKind::blackWhite || kind == FilterKind::colorBalance;
|
||||
}
|
||||
|
||||
FilterSettings FilterSettings::normalized() const
|
||||
@@ -113,6 +116,8 @@ QImage PixelFilter::run(const FilterJob &job)
|
||||
case FilterKind::curves: image = settings.curves.apply(job.image); break;
|
||||
case FilterKind::exposure: image = settings.exposure.apply(job.image); break;
|
||||
case FilterKind::gradientMap: image = settings.gradientMap.apply(job.image); break;
|
||||
case FilterKind::blackWhite: image = settings.blackWhite.apply(job.image); break;
|
||||
case FilterKind::colorBalance: image = settings.colorBalance.apply(job.image); break;
|
||||
// Grain sits in layer pixels; the job's seed patterns it.
|
||||
case FilterKind::grain: image = settings.grain.apply(job.image, QPointF(), 1 / job.scale, job.seed); break;
|
||||
case FilterKind::removeBackground: image = SubjectRemoval::run(job.image, settings); break;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
struct ImageLayer;
|
||||
|
||||
// The Filter and Image menus' filters.
|
||||
enum class FilterKind { gaussianBlur, motionBlur, addNoise, lensCorrection, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain };
|
||||
enum class FilterKind { gaussianBlur, motionBlur, addNoise, lensCorrection, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain, blackWhite, colorBalance };
|
||||
// The menu's title, Swift's rawValue.
|
||||
QString rawValue(FilterKind kind);
|
||||
// Made without settings: its preview is its result.
|
||||
@@ -25,7 +25,7 @@ bool isImageAdjustment(FilterKind kind);
|
||||
inline constexpr std::array allFilterKinds{FilterKind::gaussianBlur, FilterKind::motionBlur, FilterKind::addNoise,
|
||||
FilterKind::lensCorrection, FilterKind::removeBackground, FilterKind::contentAwareFill,
|
||||
FilterKind::curves, FilterKind::exposure, FilterKind::gradientMap,
|
||||
FilterKind::grain};
|
||||
FilterKind::grain, FilterKind::blackWhite, FilterKind::colorBalance};
|
||||
|
||||
// Remove Background's mask: the model's alone, or refined.
|
||||
enum class BackgroundQuality { basic, advanced };
|
||||
@@ -50,6 +50,8 @@ struct FilterSettings {
|
||||
ExposureSettings exposure{};
|
||||
GradientMapSettings gradientMap{};
|
||||
GrainSettings grain{};
|
||||
BlackWhiteSettings blackWhite{};
|
||||
ColorBalanceSettings colorBalance{};
|
||||
BackgroundQuality backgroundQuality = BackgroundQuality::basic;
|
||||
// How far, in pixels, the mask is pulled onto edges.
|
||||
double refineEdges = 12;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "IO/ProjectStore.h"
|
||||
#include <QPainter>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
extern "C" {
|
||||
@@ -131,3 +132,57 @@ QImage GrainSettings::apply(const QImage &image, QPointF origin, double unitsPer
|
||||
adjust_grain(pixels, size_t(width), size_t(height), size_t(stride), amount, size, roughness, pattern, origin.x(), origin.y(), unitsPerPixel);
|
||||
});
|
||||
}
|
||||
|
||||
bool BlackWhiteSettings::isValid() const
|
||||
{
|
||||
const std::array weights{reds, yellows, greens, cyans, blues, magentas};
|
||||
return std::all_of(weights.begin(), weights.end(), [](double weight) { return weight >= low && weight <= high; })
|
||||
&& tintHue >= 0 && tintHue <= 360 && tintSaturation >= 0 && tintSaturation <= 100;
|
||||
}
|
||||
|
||||
QImage BlackWhiteSettings::apply(const QImage &image) const
|
||||
{
|
||||
if (!isValid())
|
||||
throw ProjectError(ProjectError::Kind::invalid);
|
||||
// The kernel's order: red, yellow, green, cyan, blue, magenta.
|
||||
const std::array<float, 6> weights{float(reds / 100), float(yellows / 100), float(greens / 100),
|
||||
float(cyans / 100), float(blues / 100), float(magentas / 100)};
|
||||
return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
|
||||
adjust_black_white(pixels, size_t(width), size_t(height), size_t(stride), weights.data(), tint ? 1 : 0, tintHue, tintSaturation / 100);
|
||||
});
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::array<double, 9> amounts(const ColorBalanceSettings &settings)
|
||||
{
|
||||
return {settings.shadowCyanRed, settings.shadowMagentaGreen, settings.shadowYellowBlue, settings.midCyanRed, settings.midMagentaGreen,
|
||||
settings.midYellowBlue, settings.highlightCyanRed, settings.highlightMagentaGreen, settings.highlightYellowBlue};
|
||||
}
|
||||
}
|
||||
|
||||
bool ColorBalanceSettings::isValid() const
|
||||
{
|
||||
const std::array<double, 9> all = amounts(*this);
|
||||
return std::all_of(all.begin(), all.end(), [](double amount) { return amount >= low && amount <= high; });
|
||||
}
|
||||
|
||||
bool ColorBalanceSettings::isIdentity() const
|
||||
{
|
||||
const std::array<double, 9> all = amounts(*this);
|
||||
return std::all_of(all.begin(), all.end(), [](double amount) { return amount == 0; });
|
||||
}
|
||||
|
||||
QImage ColorBalanceSettings::apply(const QImage &image) const
|
||||
{
|
||||
if (!isValid())
|
||||
throw ProjectError(ProjectError::Kind::invalid);
|
||||
if (isIdentity())
|
||||
return image;
|
||||
std::array<float, 9> ranges;
|
||||
const std::array<double, 9> all = amounts(*this);
|
||||
std::transform(all.begin(), all.end(), ranges.begin(), [](double amount) { return float(amount / 100); });
|
||||
return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
|
||||
adjust_color_balance(pixels, size_t(width), size_t(height), size_t(stride), ranges.data(), ranges.data() + 3, ranges.data() + 6,
|
||||
preserveLuminosity ? 1 : 0);
|
||||
});
|
||||
}
|
||||
@@ -74,3 +74,42 @@ struct GrainSettings {
|
||||
QImage apply(const QImage &image, QPointF origin = QPointF(), double unitsPerPixel = 1, std::optional<quint32> seed = std::nullopt) const;
|
||||
friend bool operator==(const GrainSettings &, const GrainSettings &) = default;
|
||||
};
|
||||
|
||||
// Photoshop's Black & White: how bright each colour family turns.
|
||||
struct BlackWhiteSettings {
|
||||
static constexpr double low = -200, high = 300;
|
||||
// Photoshop's defaults, a percent each.
|
||||
double reds = 40;
|
||||
double yellows = 60;
|
||||
double greens = 40;
|
||||
double cyans = 60;
|
||||
double blues = 20;
|
||||
double magentas = 80;
|
||||
// Colours the result, keeping its tones: sepia, cyanotype.
|
||||
bool tint = false;
|
||||
double tintHue = 40;
|
||||
double tintSaturation = 20;
|
||||
bool isValid() const;
|
||||
QImage apply(const QImage &image) const;
|
||||
friend bool operator==(const BlackWhiteSettings &, const BlackWhiteSettings &) = default;
|
||||
};
|
||||
|
||||
// Color Balance: each pair shifted in shadows, midtones and highlights.
|
||||
struct ColorBalanceSettings {
|
||||
static constexpr double low = -100, high = 100;
|
||||
double shadowCyanRed = 0;
|
||||
double shadowMagentaGreen = 0;
|
||||
double shadowYellowBlue = 0;
|
||||
double midCyanRed = 0;
|
||||
double midMagentaGreen = 0;
|
||||
double midYellowBlue = 0;
|
||||
double highlightCyanRed = 0;
|
||||
double highlightMagentaGreen = 0;
|
||||
double highlightYellowBlue = 0;
|
||||
// Puts each pixel's brightness back, so only colour moves.
|
||||
bool preserveLuminosity = true;
|
||||
bool isValid() const;
|
||||
bool isIdentity() const;
|
||||
QImage apply(const QImage &image) const;
|
||||
friend bool operator==(const ColorBalanceSettings &, const ColorBalanceSettings &) = default;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Document/LayerAdjustment.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/PixelInvert.h"
|
||||
#include <QRandomGenerator>
|
||||
#include <cmath>
|
||||
|
||||
@@ -12,6 +13,9 @@ QString rawValue(AdjustmentKind kind)
|
||||
case AdjustmentKind::exposure: return QStringLiteral("Exposure");
|
||||
case AdjustmentKind::gradientMap: return QStringLiteral("Gradient Map");
|
||||
case AdjustmentKind::grain: return QStringLiteral("Grain");
|
||||
case AdjustmentKind::invert: return QStringLiteral("Invert");
|
||||
case AdjustmentKind::blackWhite: return QStringLiteral("Black & White");
|
||||
case AdjustmentKind::colorBalance: return QStringLiteral("Color Balance");
|
||||
}
|
||||
throw std::logic_error("unknown adjustment kind");
|
||||
}
|
||||
@@ -32,12 +36,21 @@ std::optional<FilterKind> filterKind(AdjustmentKind kind)
|
||||
case AdjustmentKind::exposure: return FilterKind::exposure;
|
||||
case AdjustmentKind::gradientMap: return FilterKind::gradientMap;
|
||||
case AdjustmentKind::grain: return FilterKind::grain;
|
||||
case AdjustmentKind::blackWhite: return FilterKind::blackWhite;
|
||||
case AdjustmentKind::colorBalance: return FilterKind::colorBalance;
|
||||
// Panels of their own; Invert has nothing to set.
|
||||
case AdjustmentKind::hsv:
|
||||
case AdjustmentKind::levels: return std::nullopt;
|
||||
case AdjustmentKind::levels:
|
||||
case AdjustmentKind::invert: return std::nullopt;
|
||||
}
|
||||
throw std::logic_error("unknown adjustment kind");
|
||||
}
|
||||
|
||||
bool isEditable(AdjustmentKind kind)
|
||||
{
|
||||
return kind != AdjustmentKind::invert;
|
||||
}
|
||||
|
||||
HueSaturationSettings LayerAdjustment::resolvedHSV() const
|
||||
{
|
||||
return hsvSettings.value_or(HueSaturationSettings(hue, saturation, lightness, colorize));
|
||||
@@ -73,6 +86,26 @@ void LayerAdjustment::setGrain(const GrainSettings &value)
|
||||
grainSettings = value;
|
||||
}
|
||||
|
||||
BlackWhiteSettings LayerAdjustment::blackWhite() const
|
||||
{
|
||||
return blackWhiteSettings.value_or(BlackWhiteSettings());
|
||||
}
|
||||
|
||||
void LayerAdjustment::setBlackWhite(const BlackWhiteSettings &value)
|
||||
{
|
||||
blackWhiteSettings = value;
|
||||
}
|
||||
|
||||
ColorBalanceSettings LayerAdjustment::colorBalance() const
|
||||
{
|
||||
return colorBalanceSettings.value_or(ColorBalanceSettings());
|
||||
}
|
||||
|
||||
void LayerAdjustment::setColorBalance(const ColorBalanceSettings &value)
|
||||
{
|
||||
colorBalanceSettings = value;
|
||||
}
|
||||
|
||||
// Bounds refuse what is no number: no isFinite terms.
|
||||
bool LayerAdjustment::isValid() const
|
||||
{
|
||||
@@ -96,7 +129,8 @@ bool LayerAdjustment::isValid() const
|
||||
if (!(range == range.normalized()))
|
||||
return false;
|
||||
}
|
||||
return curves.isValid() && exposure().isValid() && gradientMap().isValid() && grain().isValid();
|
||||
return curves.isValid() && exposure().isValid() && gradientMap().isValid() && grain().isValid() && blackWhite().isValid()
|
||||
&& colorBalance().isValid();
|
||||
}
|
||||
|
||||
QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region) const
|
||||
@@ -108,6 +142,9 @@ QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region)
|
||||
case AdjustmentKind::curves: return curves.apply(image);
|
||||
case AdjustmentKind::exposure: return exposure().apply(image);
|
||||
case AdjustmentKind::gradientMap: return gradientMap().apply(image);
|
||||
case AdjustmentKind::blackWhite: return blackWhite().apply(image);
|
||||
case AdjustmentKind::colorBalance: return colorBalance().apply(image);
|
||||
case AdjustmentKind::invert: return PixelInvert::run(PixelInvert::Job{image, false, QTransform(), std::nullopt});
|
||||
case AdjustmentKind::grain: {
|
||||
// Grain sits in the document, however the image is cut.
|
||||
const QRectF area = region.value_or(QRectF(0, 0, image.width(), image.height()));
|
||||
@@ -143,7 +180,9 @@ void EditorSession::addAdjustment(AdjustmentKind kind)
|
||||
m_collapsedGroupIDs.remove(*layer.parentID);
|
||||
setActiveLayerID(layer.id);
|
||||
endEdit();
|
||||
setAdjustmentEditingID(layer.id);
|
||||
// Invert has nothing to set: it applies, no editor opens.
|
||||
if (isEditable(kind))
|
||||
setAdjustmentEditingID(layer.id);
|
||||
}
|
||||
|
||||
void EditorSession::updateAdjustment(QUuid id, const LayerAdjustment &value)
|
||||
|
||||
@@ -10,15 +10,18 @@
|
||||
#include <optional>
|
||||
|
||||
// An adjustment layer's kind, in the Layer menu's order.
|
||||
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain };
|
||||
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain, invert, blackWhite, colorBalance };
|
||||
inline constexpr std::array allAdjustmentKinds{AdjustmentKind::hsv, AdjustmentKind::levels, AdjustmentKind::curves,
|
||||
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain};
|
||||
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain,
|
||||
AdjustmentKind::invert, AdjustmentKind::blackWhite, AdjustmentKind::colorBalance};
|
||||
// Swift's rawValue: the new layer's name and the manifest's word.
|
||||
QString rawValue(AdjustmentKind kind);
|
||||
// The kind a manifest names, or none.
|
||||
std::optional<AdjustmentKind> adjustmentKind(const QString &text);
|
||||
// The filter panel that edits it; Levels and Hue/Saturation: none.
|
||||
// The filter panel that edits it; Levels, Hue/Saturation, Invert: none.
|
||||
std::optional<FilterKind> filterKind(AdjustmentKind kind);
|
||||
// Every kind but Invert opens an editor.
|
||||
bool isEditable(AdjustmentKind kind);
|
||||
|
||||
// An adjustment layer's settings; its kind reads only its own.
|
||||
struct LayerAdjustment {
|
||||
@@ -35,6 +38,8 @@ struct LayerAdjustment {
|
||||
std::optional<ExposureSettings> exposureSettings = std::nullopt;
|
||||
std::optional<GradientMapSettings> gradientMapSettings = std::nullopt;
|
||||
std::optional<GrainSettings> grainSettings = std::nullopt;
|
||||
std::optional<BlackWhiteSettings> blackWhiteSettings = std::nullopt;
|
||||
std::optional<ColorBalanceSettings> colorBalanceSettings = std::nullopt;
|
||||
HueSaturationSettings resolvedHSV() const;
|
||||
// Swift's computed properties over the optional settings.
|
||||
ExposureSettings exposure() const;
|
||||
@@ -43,6 +48,10 @@ struct LayerAdjustment {
|
||||
void setGradientMap(const GradientMapSettings &value);
|
||||
GrainSettings grain() const;
|
||||
void setGrain(const GrainSettings &value);
|
||||
BlackWhiteSettings blackWhite() const;
|
||||
void setBlackWhite(const BlackWhiteSettings &value);
|
||||
ColorBalanceSettings colorBalance() const;
|
||||
void setColorBalance(const ColorBalanceSettings &value);
|
||||
bool isValid() const;
|
||||
// `region`: the document `image` covers, whole pixels by default.
|
||||
QImage apply(const QImage &image, std::optional<QRectF> region = std::nullopt) const;
|
||||
|
||||
Reference in new issue
Block a user