Magic tool Object mode (Compositor 1.1.8)
This commit is contained in:
1 parent
78bfa2445d
commit
3d56688ce4
39 files changed
+1193
-134
No files matched your search
@@ -46,11 +46,12 @@ struct SessionJobs {
|
||||
std::optional<QImage> image;
|
||||
std::optional<QString> failure;
|
||||
};
|
||||
// What a wand click waits for and applies.
|
||||
// The Magic tool's job, either mode, and its step's name.
|
||||
struct Wanding {
|
||||
SelectionMode mode;
|
||||
QUuid documentID;
|
||||
std::function<void()> done;
|
||||
QString name;
|
||||
};
|
||||
struct Wanded {
|
||||
std::optional<QPainterPath> path;
|
||||
|
||||
@@ -96,7 +96,7 @@ QString label(NavigationTool tool)
|
||||
case NavigationTool::move: return QStringLiteral("Move / Transform (V)");
|
||||
case NavigationTool::marquee: return QStringLiteral("Marquee (M)");
|
||||
case NavigationTool::lasso: return QStringLiteral("Lasso (L)");
|
||||
case NavigationTool::wand: return QStringLiteral("Magic Wand (W)");
|
||||
case NavigationTool::wand: return QStringLiteral("Magic (W) · Tab switches Wand and Object");
|
||||
case NavigationTool::crop: return QStringLiteral("Crop (C)");
|
||||
case NavigationTool::brush: return QStringLiteral("Brush (B) · Eraser (E)");
|
||||
case NavigationTool::spotHealing: return QStringLiteral("Spot Healing Brush (J)");
|
||||
|
||||
@@ -165,6 +165,8 @@ void EditorSession::cycleToolMode()
|
||||
return;
|
||||
if (m_tool == NavigationTool::marquee)
|
||||
toggleMarqueeKind();
|
||||
else if (m_tool == NavigationTool::wand)
|
||||
setWandMode(allWandModes[(size_t(m_wandMode) + 1) % allWandModes.size()]);
|
||||
else if (m_tool == NavigationTool::lasso)
|
||||
toggleLassoKind();
|
||||
else if (m_tool == NavigationTool::shape)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Document/LevelsAutomatic.h"
|
||||
#include "Document/FloatingSelection.h"
|
||||
#include "Document/MagicWand.h"
|
||||
#include "Document/ObjectSelection.h"
|
||||
#include "Document/SelectionClipboard.h"
|
||||
#include "Document/SelectionEdits.h"
|
||||
#include "Document/SmudgeLiquify.h"
|
||||
@@ -298,6 +299,7 @@ public:
|
||||
#include "Document/AdjustmentEditing+Session.h"
|
||||
#include "Document/LayerEffects+Session.h"
|
||||
#include "Document/Guides+Session.h"
|
||||
#include "Document/ObjectSelection+Session.h"
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
@@ -376,6 +378,8 @@ private:
|
||||
std::optional<Inverting> m_inverting;
|
||||
QFutureWatcher<Inverted> m_inverter;
|
||||
WandSettings m_wandSettings;
|
||||
WandMode m_wandMode = WandMode::wand;
|
||||
ObjectSelectionSettings m_objectSelectionSettings;
|
||||
std::optional<Wanding> m_wanding;
|
||||
QFutureWatcher<Wanded> m_wand;
|
||||
std::optional<Subjecting> m_subjecting;
|
||||
|
||||
@@ -3,6 +3,8 @@ public:
|
||||
// Off the UI thread; `done` follows the step or refusal.
|
||||
void magicWand(QPointF point, SelectionMode mode, std::function<void()> done = {});
|
||||
|
||||
// What selection-from-image tools read, at document size.
|
||||
std::optional<QImage> selectionSample(const CanvasDocument &document, bool sampleAllLayers) const;
|
||||
|
||||
private:
|
||||
void finishWand();
|
||||
std::optional<QImage> wandSample(const CanvasDocument &document) const;
|
||||
@@ -97,13 +97,13 @@ void EditorSession::setWandSettings(const WandSettings &settings)
|
||||
}
|
||||
|
||||
// What the wand reads: composite, or the active layer's pixels.
|
||||
std::optional<QImage> EditorSession::wandSample(const CanvasDocument &document) const
|
||||
std::optional<QImage> EditorSession::selectionSample(const CanvasDocument &document, bool sampleAllLayers) const
|
||||
{
|
||||
try {
|
||||
QImage context = BrushRaster::context(document.width, document.height, false);
|
||||
QPainter painter(&context);
|
||||
// A painted asset flattens here; the catch covers it too.
|
||||
if (m_wandSettings.sampleAllLayers) {
|
||||
if (sampleAllLayers) {
|
||||
drawLiveComposite(document, painter);
|
||||
} else if (const std::optional<ImageLayer> layer = activeLayer(); layer && !layer->isGroup && layer->asset) {
|
||||
const LayerTransform transform = displayedTransform(*layer);
|
||||
@@ -112,7 +112,7 @@ std::optional<QImage> EditorSession::wandSample(const CanvasDocument &document)
|
||||
painter.end();
|
||||
return context;
|
||||
} catch (const ExportError &error) {
|
||||
qCWarning(lcApp) << "the wand cannot sample the canvas:" << error.what();
|
||||
qCWarning(lcApp) << "a selection tool cannot sample the canvas:" << error.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
@@ -131,13 +131,13 @@ void EditorSession::magicWand(QPointF point, SelectionMode mode, std::function<v
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const std::optional<QImage> sample = wandSample(*document);
|
||||
const std::optional<QImage> sample = selectionSample(*document, m_wandSettings.sampleAllLayers);
|
||||
if (!sample) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setIsProjectBusy(true);
|
||||
m_wanding = Wanding{mode, document->id, std::move(done)};
|
||||
m_wanding = Wanding{mode, document->id, std::move(done), QStringLiteral("Magic Wand")};
|
||||
m_wand.setFuture(QtConcurrent::run([sample = *sample, point, settings = m_wandSettings]() -> Wanded {
|
||||
try {
|
||||
return Wanded{MagicWand::select(sample, point, settings), std::nullopt};
|
||||
@@ -155,7 +155,7 @@ void EditorSession::finishWand()
|
||||
const Wanding pending = std::exchange(m_wanding, std::nullopt).value();
|
||||
setIsProjectBusy(false);
|
||||
if (result.failure) {
|
||||
qCWarning(lcApp).noquote() << "the wand could not select:" << *result.failure;
|
||||
qCWarning(lcApp).noquote() << pending.name << "could not select:" << *result.failure;
|
||||
setBrushError(result.failure);
|
||||
} else if (m_document && m_document->id == pending.documentID) {
|
||||
if (!result.path) {
|
||||
@@ -164,9 +164,9 @@ void EditorSession::finishWand()
|
||||
deselect();
|
||||
} else if (pending.mode == SelectionMode::replace) {
|
||||
// A traced outline lies on the canvas: no costly clip.
|
||||
setSelection(DocumentSelection{*result.path, m_selectionAntialiased}, QStringLiteral("Magic Wand"));
|
||||
setSelection(DocumentSelection{*result.path, m_selectionAntialiased}, pending.name);
|
||||
} else {
|
||||
applySelection(*result.path, pending.mode, QStringLiteral("Magic Wand"));
|
||||
applySelection(*result.path, pending.mode, pending.name);
|
||||
}
|
||||
}
|
||||
if (pending.done)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Swift's ObjectSelection extension, and the Magic tool's mode.
|
||||
public:
|
||||
WandMode wandMode() const { return m_wandMode; }
|
||||
void setWandMode(WandMode mode);
|
||||
const ObjectSelectionSettings &objectSelectionSettings() const { return m_objectSelectionSettings; }
|
||||
void setObjectSelectionSettings(const ObjectSelectionSettings &settings);
|
||||
// Off the UI thread; `done` follows the step or refusal.
|
||||
void selectObject(QPointF point, SelectionMode mode, std::function<void()> done = {});
|
||||
@@ -0,0 +1,272 @@
|
||||
#include "Document/ObjectSelection.h"
|
||||
#include "Document/GuidedMatte.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/MagicWand.h"
|
||||
#include "Document/SubjectRemoval.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <QtConcurrent>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
double perpendicularDistance(QPointF point, QPointF a, QPointF b)
|
||||
{
|
||||
const double dx = b.x() - a.x(), dy = b.y() - a.y();
|
||||
const double length = std::hypot(dx, dy);
|
||||
if (!(length > 0))
|
||||
return std::hypot(point.x() - a.x(), point.y() - a.y());
|
||||
return std::abs(dy * point.x() - dx * point.y() + b.x() * a.y() - b.y() * a.x()) / length;
|
||||
}
|
||||
|
||||
// Swift's Douglas–Peucker on an open polyline.
|
||||
std::vector<QPointF> simplifyOpen(const std::vector<QPointF> &points, size_t first, size_t last, double tolerance)
|
||||
{
|
||||
if (last <= first + 1)
|
||||
return {points[first], points[last]};
|
||||
size_t farthest = first + 1;
|
||||
double greatest = 0;
|
||||
for (size_t index = first + 1; index < last; ++index) {
|
||||
const double distance = perpendicularDistance(points[index], points[first], points[last]);
|
||||
if (distance > greatest) {
|
||||
greatest = distance;
|
||||
farthest = index;
|
||||
}
|
||||
}
|
||||
if (!(greatest > tolerance))
|
||||
return {points[first], points[last]};
|
||||
std::vector<QPointF> left = simplifyOpen(points, first, farthest, tolerance);
|
||||
const std::vector<QPointF> right = simplifyOpen(points, farthest, last, tolerance);
|
||||
left.pop_back();
|
||||
left.insert(left.end(), right.begin(), right.end());
|
||||
return left;
|
||||
}
|
||||
|
||||
std::vector<QPointF> simplifyClosed(const std::vector<QPointF> &points, double tolerance)
|
||||
{
|
||||
if (points.size() < 4)
|
||||
return points;
|
||||
// Break at a stable extreme so the whole contour survives.
|
||||
const size_t start = size_t(std::min_element(points.begin(), points.end(), [](QPointF lhs, QPointF rhs) {
|
||||
return lhs.x() == rhs.x() ? lhs.y() < rhs.y() : lhs.x() < rhs.x();
|
||||
}) - points.begin());
|
||||
std::vector<QPointF> open(points.begin() + std::ptrdiff_t(start), points.end());
|
||||
open.insert(open.end(), points.begin(), points.begin() + std::ptrdiff_t(start));
|
||||
open.push_back(open.front());
|
||||
open = simplifyOpen(open, 0, open.size() - 1, tolerance);
|
||||
// Its ends are the same point.
|
||||
open.pop_back();
|
||||
return open.size() >= 3 ? open : points;
|
||||
}
|
||||
|
||||
std::vector<QPointF> chaikin(std::vector<QPointF> points, int iterations)
|
||||
{
|
||||
for (int round = 0; round < iterations; ++round) {
|
||||
std::vector<QPointF> next;
|
||||
next.reserve(points.size() * 2);
|
||||
for (size_t index = 0; index < points.size(); ++index) {
|
||||
const QPointF a = points[index], b = points[(index + 1) % points.size()];
|
||||
next.push_back(a * 0.75 + b * 0.25);
|
||||
next.push_back(a * 0.25 + b * 0.75);
|
||||
}
|
||||
points = std::move(next);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
bool any(const std::vector<uchar> &mask, int width, int height, int x, int y, bool set)
|
||||
{
|
||||
for (int ny = std::max(0, y - 1); ny <= std::min(height - 1, y + 1); ++ny) {
|
||||
for (int nx = std::max(0, x - 1); nx <= std::min(width - 1, x + 1); ++nx) {
|
||||
if ((mask[size_t(ny) * size_t(width) + size_t(nx)] != 0) == set)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<QPainterPath> traced(const QImage &image, QPointF point, int edgeOffset, bool smoothEdges)
|
||||
{
|
||||
const int width = image.width(), height = image.height();
|
||||
const int x = int(std::floor(point.x())), y = int(std::floor(point.y()));
|
||||
if (!std::isfinite(point.x()) || !std::isfinite(point.y()) || x < 0 || x >= width || y < 0 || y >= height)
|
||||
return std::nullopt;
|
||||
QImage foreground;
|
||||
try {
|
||||
foreground = SubjectRemoval::foreground(image);
|
||||
} catch (const SubjectRemovalError &error) {
|
||||
// No subject anywhere: Vision finds no instance.
|
||||
if (error.kind == SubjectRemovalError::Kind::noSubject)
|
||||
return std::nullopt;
|
||||
throw;
|
||||
}
|
||||
using ObjectSelection::grid;
|
||||
// Regions on the model's grid stand for Vision's instances.
|
||||
const QImage cells = foreground.scaled(grid, grid, Qt::IgnoreAspectRatio, Qt::SmoothTransformation).convertToFormat(QImage::Format_Grayscale8);
|
||||
if (cells.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
std::vector<uchar> binary(size_t(grid) * grid);
|
||||
for (int row = 0; row < grid; ++row) {
|
||||
for (int column = 0; column < grid; ++column)
|
||||
binary[size_t(row) * grid + size_t(column)] = cells.constScanLine(row)[column] >= 128;
|
||||
}
|
||||
const int gx = std::clamp(int(point.x() / width * grid), 0, grid - 1), gy = std::clamp(int(point.y() / height * grid), 0, grid - 1);
|
||||
const std::optional<std::vector<uchar>> coarse = ObjectSelection::region(binary, grid, grid, gx, gy);
|
||||
if (!coarse)
|
||||
return std::nullopt;
|
||||
QImage small(grid, grid, QImage::Format_Grayscale8);
|
||||
if (small.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
for (int row = 0; row < grid; ++row) {
|
||||
for (int column = 0; column < grid; ++column)
|
||||
small.scanLine(row)[column] = (*coarse)[size_t(row) * grid + size_t(column)] ? 255 : 0;
|
||||
}
|
||||
// Swift's edge-preserving upsampling: the image guides the edge.
|
||||
const QImage up = small.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
const QImage refined = GuidedMatte::refine(up, image, 5, std::numeric_limits<double>::max());
|
||||
std::vector<uchar> mask(size_t(width) * size_t(height));
|
||||
for (int row = 0; row < height; ++row) {
|
||||
const uchar *line = refined.constScanLine(row);
|
||||
for (int column = 0; column < width; ++column)
|
||||
mask[size_t(row) * size_t(width) + size_t(column)] = line[column] >= 128 ? 255 : 0;
|
||||
}
|
||||
mask = ObjectSelection::adjusted(std::move(mask), width, height, edgeOffset);
|
||||
const std::optional<QPainterPath> outline = MagicWand::outline(mask, width, height);
|
||||
if (!outline)
|
||||
return std::nullopt;
|
||||
return smoothEdges ? ObjectSelection::smoothed(*outline) : *outline;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QPainterPath> ObjectSelection::select(const QImage &image, QPointF point, int edgeOffset, bool smoothEdges)
|
||||
{
|
||||
try {
|
||||
return traced(image, point, edgeOffset, smoothEdges);
|
||||
} catch (const std::bad_alloc &) {
|
||||
// Masks the size of the image found no memory.
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::vector<uchar>> ObjectSelection::region(const std::vector<uchar> &mask, int width, int height, int x, int y)
|
||||
{
|
||||
const auto at = [width](int column, int row) { return size_t(row) * size_t(width) + size_t(column); };
|
||||
if (!mask[at(x, y)])
|
||||
return std::nullopt;
|
||||
std::vector<uchar> result(mask.size());
|
||||
std::deque<std::pair<int, int>> pending{{x, y}};
|
||||
// Eight neighbours join a region, the cell itself included.
|
||||
while (!pending.empty()) {
|
||||
const auto [column, row] = pending.front();
|
||||
pending.pop_front();
|
||||
for (int ny = std::max(0, row - 1); ny <= std::min(height - 1, row + 1); ++ny) {
|
||||
for (int nx = std::max(0, column - 1); nx <= std::min(width - 1, column + 1); ++nx) {
|
||||
if (mask[at(nx, ny)] && !result[at(nx, ny)]) {
|
||||
result[at(nx, ny)] = 1;
|
||||
pending.emplace_back(nx, ny);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uchar> ObjectSelection::adjusted(std::vector<uchar> mask, int width, int height, int edgeOffset)
|
||||
{
|
||||
const int steps = std::min(10, std::abs(edgeOffset));
|
||||
for (int step = 0; step < steps; ++step) {
|
||||
std::vector<uchar> result = mask;
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const size_t index = size_t(y) * size_t(width) + size_t(x);
|
||||
if (edgeOffset > 0 && mask[index])
|
||||
result[index] = any(mask, width, height, x, y, false) ? 0 : 255;
|
||||
else if (edgeOffset < 0 && !mask[index] && any(mask, width, height, x, y, true))
|
||||
result[index] = 255;
|
||||
}
|
||||
}
|
||||
mask = std::move(result);
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
QPainterPath ObjectSelection::smoothed(const QPainterPath &path)
|
||||
{
|
||||
std::vector<std::vector<QPointF>> subpaths;
|
||||
std::vector<QPointF> current;
|
||||
const auto finish = [&] {
|
||||
// Qt's close repeats the start, where a CGPath's adds none.
|
||||
if (!current.empty() && current.front() == current.back())
|
||||
current.pop_back();
|
||||
if (current.size() >= 3)
|
||||
subpaths.push_back(current);
|
||||
};
|
||||
for (int index = 0; index < path.elementCount(); ++index) {
|
||||
const QPainterPath::Element element = path.elementAt(index);
|
||||
if (element.isMoveTo()) {
|
||||
finish();
|
||||
current = {element};
|
||||
} else if (element.isLineTo()) {
|
||||
current.push_back(element);
|
||||
} else if (element.type == QPainterPath::CurveToDataElement && (index + 1 == path.elementCount() || path.elementAt(index + 1).type != QPainterPath::CurveToDataElement)) {
|
||||
// A curve's end point, as Swift keeps it.
|
||||
current.push_back(element);
|
||||
}
|
||||
}
|
||||
finish();
|
||||
QPainterPath result;
|
||||
result.setFillRule(path.fillRule());
|
||||
for (const std::vector<QPointF> &subpath : subpaths) {
|
||||
const std::vector<QPointF> points = chaikin(simplifyClosed(subpath, 1.6), 3);
|
||||
result.moveTo(points.front());
|
||||
for (size_t index = 1; index < points.size(); ++index)
|
||||
result.lineTo(points[index]);
|
||||
result.closeSubpath();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EditorSession::setWandMode(WandMode mode)
|
||||
{
|
||||
m_wandMode = mode;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setObjectSelectionSettings(const ObjectSelectionSettings &settings)
|
||||
{
|
||||
m_objectSelectionSettings = settings;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::selectObject(QPointF point, SelectionMode mode, std::function<void()> done)
|
||||
{
|
||||
// The caller resumes from the event loop, as after await.
|
||||
const auto finish = [this, done] {
|
||||
if (done)
|
||||
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
|
||||
};
|
||||
const std::optional<CanvasDocument> &document = m_document;
|
||||
// Swift's positive bounds refuse a point that is no number.
|
||||
const bool inside = document && point.x() >= 0 && point.y() >= 0 && point.x() < document->width && point.y() < document->height;
|
||||
const std::optional<QImage> sample = canEditSelection() && !m_selectionMoveOrigin && inside
|
||||
? selectionSample(*document, m_objectSelectionSettings.sampleAllLayers)
|
||||
: std::nullopt;
|
||||
if (!sample) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setIsProjectBusy(true);
|
||||
m_wanding = Wanding{mode, document->id, std::move(done), QStringLiteral("Object Selection")};
|
||||
m_wand.setFuture(QtConcurrent::run([sample = *sample, point, offset = m_objectSelectionSettings.edgeOffset, smooth = m_selectionAntialiased]() -> Wanded {
|
||||
try {
|
||||
return Wanded{ObjectSelection::select(sample, point, offset, smooth), std::nullopt};
|
||||
} catch (const SubjectRemovalError &error) {
|
||||
return Wanded{std::nullopt, QString::fromUtf8(error.what())};
|
||||
} catch (const MagicWandError &error) {
|
||||
return Wanded{std::nullopt, QString::fromUtf8(error.what())};
|
||||
} catch (const ExportError &error) {
|
||||
return Wanded{std::nullopt, QString::fromUtf8(error.what())};
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <QImage>
|
||||
#include <QPainterPath>
|
||||
#include <optional>
|
||||
|
||||
struct ObjectSelectionSettings {
|
||||
// Read the visible composite rather than the active layer.
|
||||
bool sampleAllLayers = true;
|
||||
// Positive erodes the mask inward; negative grows it outward.
|
||||
int edgeOffset = 0;
|
||||
friend bool operator==(const ObjectSelectionSettings &, const ObjectSelectionSettings &) = default;
|
||||
};
|
||||
|
||||
// Swift's ObjectSelection; U²-Net regions stand for Vision's instances.
|
||||
namespace ObjectSelection {
|
||||
// The model's grid the regions are found on.
|
||||
inline constexpr int grid = 320;
|
||||
// The outline in pixels; none off the image or background.
|
||||
std::optional<QPainterPath> select(const QImage &image, QPointF point, int edgeOffset, bool smoothEdges);
|
||||
// The binary grid's region holding the cell, else none.
|
||||
std::optional<std::vector<uchar>> region(const std::vector<uchar> &mask, int width, int height, int x, int y);
|
||||
// Swift's steps, a pixel ring each: erode inward, dilate outward.
|
||||
std::vector<uchar> adjusted(std::vector<uchar> mask, int width, int height, int edgeOffset);
|
||||
// Stair steps rounded off; winding and subpath order kept.
|
||||
QPainterPath smoothed(const QPainterPath &path);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include <cmath>
|
||||
|
||||
// Select's Expand, Contract and Feather, and their amounts.
|
||||
void EditorSession::setSelectionExpandAmount(int amount)
|
||||
{
|
||||
m_selectionExpandAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionContractAmount(int amount)
|
||||
{
|
||||
m_selectionContractAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionFeatherAmount(int amount)
|
||||
{
|
||||
m_selectionFeatherAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionAmountOperation(std::optional<SelectionAmountOperation> operation)
|
||||
{
|
||||
m_selectionAmountOperation = operation;
|
||||
resumeFileRequests();
|
||||
notify();
|
||||
}
|
||||
|
||||
bool EditorSession::canModifySelection() const
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
return current && !current->isEmpty() && canEditSelection() && !m_lassoDraft;
|
||||
}
|
||||
|
||||
void EditorSession::expandSelection(int amount)
|
||||
{
|
||||
resizeSelection(amount, QStringLiteral("Expand Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::contractSelection(int amount)
|
||||
{
|
||||
resizeSelection(-amount, QStringLiteral("Contract Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::promptSelectionAmount(SelectionAmountOperation operation)
|
||||
{
|
||||
if (!canModifySelection())
|
||||
return;
|
||||
setSelectionAmountOperation(operation);
|
||||
}
|
||||
|
||||
void EditorSession::confirmSelectionAmount(int amount)
|
||||
{
|
||||
const std::optional<SelectionAmountOperation> operation = m_selectionAmountOperation;
|
||||
if (!operation || amount < 1 || amount > (*operation == SelectionAmountOperation::feather ? 250 : 500))
|
||||
return;
|
||||
setSelectionAmountOperation(std::nullopt);
|
||||
switch (*operation) {
|
||||
case SelectionAmountOperation::expand:
|
||||
setSelectionExpandAmount(amount);
|
||||
expandSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::contract:
|
||||
setSelectionContractAmount(amount);
|
||||
contractSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::feather:
|
||||
setSelectionFeatherAmount(amount);
|
||||
featherSelection(amount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorSession::featherSelection(int amount)
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (!canModifySelection() || !current || amount <= 0)
|
||||
return;
|
||||
// Two soft edges spread less than their sum.
|
||||
const double softened = std::sqrt(current->feather * current->feather + double(amount) * amount);
|
||||
setSelection(DocumentSelection{current->path, current->antialiased, std::min(250.0, softened)}, QStringLiteral("Feather Selection"));
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
void cancelLasso();
|
||||
void pressMarqueeKey();
|
||||
void pressLassoKey();
|
||||
void pressWandKey();
|
||||
void toggleMarqueeKind();
|
||||
void toggleLassoKind();
|
||||
// Closes the outline and combines it with the selection.
|
||||
|
||||
+11
-80
@@ -54,6 +54,11 @@ QString rawValue(SelectionAmountOperation operation)
|
||||
throw std::logic_error("unknown selection amount operation");
|
||||
}
|
||||
|
||||
QString rawValue(WandMode mode)
|
||||
{
|
||||
return mode == WandMode::object ? QStringLiteral("Object") : QStringLiteral("Wand");
|
||||
}
|
||||
|
||||
QString rawValue(SelectionMode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
@@ -173,31 +178,6 @@ void EditorSession::setSelectionAntialiased(bool antialiased)
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionExpandAmount(int amount)
|
||||
{
|
||||
m_selectionExpandAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionContractAmount(int amount)
|
||||
{
|
||||
m_selectionContractAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionFeatherAmount(int amount)
|
||||
{
|
||||
m_selectionFeatherAmount = amount;
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::setSelectionAmountOperation(std::optional<SelectionAmountOperation> operation)
|
||||
{
|
||||
m_selectionAmountOperation = operation;
|
||||
resumeFileRequests();
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::beginLasso(QPointF point, SelectionMode mode)
|
||||
{
|
||||
// The Magic Wand selects with a click, never an outline.
|
||||
@@ -266,6 +246,12 @@ void EditorSession::pressMarqueeKey()
|
||||
selectTool(NavigationTool::marquee);
|
||||
}
|
||||
|
||||
// W picks the Magic tool; Tab switches its two modes.
|
||||
void EditorSession::pressWandKey()
|
||||
{
|
||||
selectTool(NavigationTool::wand);
|
||||
}
|
||||
|
||||
void EditorSession::pressLassoKey()
|
||||
{
|
||||
selectTool(NavigationTool::lasso);
|
||||
@@ -400,61 +386,6 @@ void EditorSession::nudgeSelection(double dx, double dy)
|
||||
endSelectionMove();
|
||||
}
|
||||
|
||||
bool EditorSession::canModifySelection() const
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
return current && !current->isEmpty() && canEditSelection() && !m_lassoDraft;
|
||||
}
|
||||
|
||||
void EditorSession::expandSelection(int amount)
|
||||
{
|
||||
resizeSelection(amount, QStringLiteral("Expand Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::contractSelection(int amount)
|
||||
{
|
||||
resizeSelection(-amount, QStringLiteral("Contract Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::promptSelectionAmount(SelectionAmountOperation operation)
|
||||
{
|
||||
if (!canModifySelection())
|
||||
return;
|
||||
setSelectionAmountOperation(operation);
|
||||
}
|
||||
|
||||
void EditorSession::confirmSelectionAmount(int amount)
|
||||
{
|
||||
const std::optional<SelectionAmountOperation> operation = m_selectionAmountOperation;
|
||||
if (!operation || amount < 1 || amount > (*operation == SelectionAmountOperation::feather ? 250 : 500))
|
||||
return;
|
||||
setSelectionAmountOperation(std::nullopt);
|
||||
switch (*operation) {
|
||||
case SelectionAmountOperation::expand:
|
||||
setSelectionExpandAmount(amount);
|
||||
expandSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::contract:
|
||||
setSelectionContractAmount(amount);
|
||||
contractSelection(amount);
|
||||
break;
|
||||
case SelectionAmountOperation::feather:
|
||||
setSelectionFeatherAmount(amount);
|
||||
featherSelection(amount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorSession::featherSelection(int amount)
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
if (!canModifySelection() || !current || amount <= 0)
|
||||
return;
|
||||
// Two soft edges spread less than their sum.
|
||||
const double softened = std::sqrt(current->feather * current->feather + double(amount) * amount);
|
||||
setSelection(DocumentSelection{current->path, current->antialiased, std::min(250.0, softened)}, QStringLiteral("Feather Selection"));
|
||||
}
|
||||
|
||||
void EditorSession::resizeSelection(double delta, const QString &name)
|
||||
{
|
||||
const std::optional<DocumentSelection> current = selection();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
#include <QString>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
@@ -31,6 +32,11 @@ struct DocumentSelection {
|
||||
friend bool operator==(const DocumentSelection &, const DocumentSelection &) = default;
|
||||
};
|
||||
|
||||
// The Magic tool's modes: similar colours, or the object clicked.
|
||||
enum class WandMode { wand, object };
|
||||
inline constexpr std::array allWandModes{WandMode::wand, WandMode::object};
|
||||
QString rawValue(WandMode mode);
|
||||
|
||||
// The Marquee's outlines are rectangle and ellipse.
|
||||
enum class LassoKind { freehand, polygonal, rectangle, ellipse };
|
||||
// The name the tool bar shows.
|
||||
|
||||
@@ -222,6 +222,11 @@ QImage SubjectRemoval::subjectMask(const QImage &image, const std::optional<QIma
|
||||
return combined;
|
||||
}
|
||||
|
||||
QImage SubjectRemoval::foreground(const QImage &image)
|
||||
{
|
||||
return vision(image);
|
||||
}
|
||||
|
||||
QImage SubjectRemoval::run(const QImage &image, const FilterSettings &settings)
|
||||
{
|
||||
// The preview refines a copy 1400 long at most: quick.
|
||||
|
||||
@@ -23,6 +23,8 @@ std::vector<float> modelInput(const QImage &image);
|
||||
QImage modelMask(const float *prediction, QSize size);
|
||||
// White over the subject in the layer's grid, times `existing`.
|
||||
QImage subjectMask(const QImage &image, const std::optional<QImage> &existing, const FilterSettings &settings);
|
||||
// The model's mask at the image's size, cached by image.
|
||||
QImage foreground(const QImage &image);
|
||||
// The preview: the layer with its background cleared.
|
||||
QImage run(const QImage &image, const FilterSettings &settings);
|
||||
}
|
||||
Reference in new issue
Block a user