@pixerate/editor 0.204.0 → 0.206.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image-editor/controller.d.cts +41 -0
- package/dist/image-editor/controller.d.ts +41 -0
- package/dist/image-editor/index.cjs +574 -304
- package/dist/image-editor/index.js +296 -26
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/selection/refine-edits.d.cts +41 -0
- package/dist/image-editor/selection/refine-edits.d.ts +41 -0
- package/dist/image-editor/selection/refine.d.cts +15 -4
- package/dist/image-editor/selection/refine.d.ts +15 -4
- package/dist/image-editor/selection/tool-options.d.cts +5 -0
- package/dist/image-editor/selection/tool-options.d.ts +5 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
- package/skills/pixerate-image-selection-masking/SKILL.md +4 -1
|
@@ -4015,10 +4015,10 @@ function edgeWidth(guide, w, h, r) {
|
|
|
4015
4015
|
for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
|
|
4016
4016
|
return out;
|
|
4017
4017
|
}
|
|
4018
|
-
function refineCoverage(guide, m, w, h, params = {}) {
|
|
4018
|
+
function refineCoverage(guide, m, w, h, params = {}, band = null) {
|
|
4019
4019
|
const p = normalizeRefineEdgeParams(params);
|
|
4020
4020
|
let a = Float32Array.from(m);
|
|
4021
|
-
if (p.radius > 0 && guide) {
|
|
4021
|
+
if ((p.radius > 0 || band) && guide) {
|
|
4022
4022
|
const n = w * h;
|
|
4023
4023
|
const boundary = new Uint8Array(n);
|
|
4024
4024
|
let any = false;
|
|
@@ -4033,14 +4033,15 @@ function refineCoverage(guide, m, w, h, params = {}) {
|
|
|
4033
4033
|
}
|
|
4034
4034
|
}
|
|
4035
4035
|
}
|
|
4036
|
-
if (any) {
|
|
4036
|
+
if (any || band) {
|
|
4037
4037
|
const dist = distanceTransform(boundary, w, h);
|
|
4038
|
-
const r = gfRadius(p);
|
|
4038
|
+
const r = band ? Math.max(4, gfRadius(p)) : gfRadius(p);
|
|
4039
4039
|
const q = guidedFilterColor(guide, m, w, h, r);
|
|
4040
4040
|
const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
|
|
4041
4041
|
for (let i = 0; i < n; i++) {
|
|
4042
4042
|
const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
|
|
4043
|
-
const
|
|
4043
|
+
const edge = p.radius > 0 ? Math.min(1, Math.max(0, reff + 1 - dist[i])) : 0;
|
|
4044
|
+
const wgt = band ? Math.max(edge, Math.min(1, Math.max(0, band[i]))) : edge;
|
|
4044
4045
|
if (wgt > 0) {
|
|
4045
4046
|
const qi = Math.min(1, Math.max(0, q[i]));
|
|
4046
4047
|
a[i] = m[i] + (qi - m[i]) * wgt;
|
|
@@ -4081,17 +4082,24 @@ function guidePlanes(pixels, win) {
|
|
|
4081
4082
|
}
|
|
4082
4083
|
return [r, g, b];
|
|
4083
4084
|
}
|
|
4084
|
-
function refineSelection(pixels, mask, params = {}) {
|
|
4085
|
+
function refineSelection(pixels, mask, params = {}, refineBrush = null) {
|
|
4085
4086
|
const p = normalizeRefineEdgeParams(params);
|
|
4086
4087
|
if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
|
|
4087
4088
|
throw new Error("Refine Edge needs pixels the size of the selection");
|
|
4088
4089
|
}
|
|
4089
4090
|
if (!mask.bounds) return mask;
|
|
4090
|
-
const
|
|
4091
|
-
const
|
|
4092
|
-
const
|
|
4093
|
-
|
|
4094
|
-
|
|
4091
|
+
const brush = refineBrush?.bounds ? refineBrush : null;
|
|
4092
|
+
const halo = refineEdgeHalo(p) + (brush ? 2 * Math.max(4, gfRadius(p)) : 0);
|
|
4093
|
+
const b = brush ? {
|
|
4094
|
+
x: Math.min(mask.bounds.x, brush.bounds.x),
|
|
4095
|
+
y: Math.min(mask.bounds.y, brush.bounds.y),
|
|
4096
|
+
r: Math.max(mask.bounds.x + mask.bounds.width, brush.bounds.x + brush.bounds.width),
|
|
4097
|
+
btm: Math.max(mask.bounds.y + mask.bounds.height, brush.bounds.y + brush.bounds.height)
|
|
4098
|
+
} : { x: mask.bounds.x, y: mask.bounds.y, r: mask.bounds.x + mask.bounds.width, btm: mask.bounds.y + mask.bounds.height };
|
|
4099
|
+
const x0 = Math.max(0, b.x - halo);
|
|
4100
|
+
const y0 = Math.max(0, b.y - halo);
|
|
4101
|
+
const x1 = Math.min(mask.width, b.r + halo);
|
|
4102
|
+
const y1 = Math.min(mask.height, b.btm + halo);
|
|
4095
4103
|
const ww = x1 - x0;
|
|
4096
4104
|
const wh = y1 - y0;
|
|
4097
4105
|
const m = new Float32Array(ww * wh);
|
|
@@ -4099,8 +4107,17 @@ function refineSelection(pixels, mask, params = {}) {
|
|
|
4099
4107
|
const src = (y0 + y) * mask.width + x0;
|
|
4100
4108
|
for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
|
|
4101
4109
|
}
|
|
4102
|
-
const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
|
|
4103
|
-
|
|
4110
|
+
const guide = pixels && (p.radius > 0 || brush) ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
|
|
4111
|
+
let band = null;
|
|
4112
|
+
if (brush) {
|
|
4113
|
+
band = new Float32Array(ww * wh);
|
|
4114
|
+
const bd = brush.data;
|
|
4115
|
+
for (let y = 0; y < wh; y++) {
|
|
4116
|
+
const src = (y0 + y) * mask.width + x0;
|
|
4117
|
+
for (let x = 0; x < ww; x++) band[y * ww + x] = bd[src + x] / 255;
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
const refined = refineCoverage(guide, m, ww, wh, p, band);
|
|
4104
4121
|
const out = new Uint8Array(mask.width * mask.height);
|
|
4105
4122
|
for (let y = 0; y < wh; y++) {
|
|
4106
4123
|
const dst = (y0 + y) * mask.width + x0;
|
|
@@ -4108,6 +4125,49 @@ function refineSelection(pixels, mask, params = {}) {
|
|
|
4108
4125
|
}
|
|
4109
4126
|
return createSelectionMask(mask.width, mask.height, out);
|
|
4110
4127
|
}
|
|
4128
|
+
function decontaminateColors(rgba, coverage, w, h, amount, radius = 8) {
|
|
4129
|
+
const out = Uint8ClampedArray.from(rgba);
|
|
4130
|
+
const n = w * h;
|
|
4131
|
+
const k = Math.min(1, Math.max(0, amount));
|
|
4132
|
+
if (k === 0) return out;
|
|
4133
|
+
const r = Math.max(2, Math.round(radius));
|
|
4134
|
+
const fgW = new Float32Array(n);
|
|
4135
|
+
const bgW = new Float32Array(n);
|
|
4136
|
+
for (let i = 0; i < n; i++) {
|
|
4137
|
+
fgW[i] = coverage[i] >= 250 ? 1 : 0;
|
|
4138
|
+
bgW[i] = coverage[i] <= 5 ? 1 : 0;
|
|
4139
|
+
}
|
|
4140
|
+
const meanOf = (weights, radiusPx) => {
|
|
4141
|
+
const wm = boxMean(weights, w, h, radiusPx);
|
|
4142
|
+
const planes = [0, 1, 2].map((c) => {
|
|
4143
|
+
const v = new Float32Array(n);
|
|
4144
|
+
for (let i = 0; i < n; i++) v[i] = rgba[i * 4 + c] * weights[i];
|
|
4145
|
+
const m = boxMean(v, w, h, radiusPx);
|
|
4146
|
+
for (let i = 0; i < n; i++) m[i] = wm[i] > 1e-6 ? m[i] / wm[i] : NaN;
|
|
4147
|
+
return m;
|
|
4148
|
+
});
|
|
4149
|
+
return { weight: wm, planes };
|
|
4150
|
+
};
|
|
4151
|
+
const fg = meanOf(fgW, r);
|
|
4152
|
+
const fgWide = meanOf(fgW, r * 3);
|
|
4153
|
+
const bg = meanOf(bgW, r);
|
|
4154
|
+
for (let i = 0; i < n; i++) {
|
|
4155
|
+
const c = coverage[i];
|
|
4156
|
+
if (c <= 5 || c >= 250) continue;
|
|
4157
|
+
const a = c / 255;
|
|
4158
|
+
for (let ch = 0; ch < 3; ch++) {
|
|
4159
|
+
const C = rgba[i * 4 + ch];
|
|
4160
|
+
const B = bg.planes[ch][i];
|
|
4161
|
+
const Fm = Number.isNaN(fg.planes[ch][i]) ? fgWide.planes[ch][i] : fg.planes[ch][i];
|
|
4162
|
+
let F;
|
|
4163
|
+
if (a >= 0.5 && !Number.isNaN(B)) F = (C - (1 - a) * B) / a;
|
|
4164
|
+
else if (!Number.isNaN(Fm)) F = Fm;
|
|
4165
|
+
else F = C;
|
|
4166
|
+
out[i * 4 + ch] = Math.round(C + k * (Math.min(255, Math.max(0, F)) - C));
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
return out;
|
|
4170
|
+
}
|
|
4111
4171
|
|
|
4112
4172
|
// src/image-editor/selection/segment/guided.ts
|
|
4113
4173
|
function workPlanes(work) {
|
|
@@ -5259,6 +5319,66 @@ function segmentsToSelection(width, height, results) {
|
|
|
5259
5319
|
return out;
|
|
5260
5320
|
}
|
|
5261
5321
|
|
|
5322
|
+
// src/image-editor/selection/refine-edits.ts
|
|
5323
|
+
var REFINE_EDIT_KINDS = ["quick-select", "brush", "refine-brush"];
|
|
5324
|
+
function brushStrokeMask(width, height, points, size, hardness = 1) {
|
|
5325
|
+
const r = Math.max(0.5, size / 2);
|
|
5326
|
+
const solid = r * Math.min(1, Math.max(0, hardness));
|
|
5327
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
5328
|
+
for (const p of points) {
|
|
5329
|
+
x0 = Math.min(x0, p.x);
|
|
5330
|
+
y0 = Math.min(y0, p.y);
|
|
5331
|
+
x1 = Math.max(x1, p.x);
|
|
5332
|
+
y1 = Math.max(y1, p.y);
|
|
5333
|
+
}
|
|
5334
|
+
const bx = Math.max(0, Math.floor(x0 - r - 1));
|
|
5335
|
+
const by = Math.max(0, Math.floor(y0 - r - 1));
|
|
5336
|
+
const bw = Math.min(width, Math.ceil(x1 + r + 1)) - bx;
|
|
5337
|
+
const bh = Math.min(height, Math.ceil(y1 + r + 1)) - by;
|
|
5338
|
+
if (!(bw > 0 && bh > 0)) return createSelectionMask(width, height);
|
|
5339
|
+
const out = new Uint8Array(bw * bh);
|
|
5340
|
+
const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
|
|
5341
|
+
for (let y = 0; y < bh; y++) {
|
|
5342
|
+
for (let x = 0; x < bw; x++) {
|
|
5343
|
+
const px2 = bx + x + 0.5;
|
|
5344
|
+
const py = by + y + 0.5;
|
|
5345
|
+
let d = Infinity;
|
|
5346
|
+
for (const [a, b] of segs) {
|
|
5347
|
+
const dx = b.x - a.x;
|
|
5348
|
+
const dy = b.y - a.y;
|
|
5349
|
+
const len2 = dx * dx + dy * dy;
|
|
5350
|
+
const t = len2 > 0 ? Math.min(1, Math.max(0, ((px2 - a.x) * dx + (py - a.y) * dy) / len2)) : 0;
|
|
5351
|
+
d = Math.min(d, Math.hypot(px2 - a.x - t * dx, py - a.y - t * dy));
|
|
5352
|
+
}
|
|
5353
|
+
let v = d <= solid ? 1 : solid >= r ? 0 : 1 - (d - solid) / (r - solid);
|
|
5354
|
+
v = Math.min(v, Math.min(1, Math.max(0, r + 0.5 - d)));
|
|
5355
|
+
out[y * bw + x] = Math.round(Math.max(0, v) * 255);
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
return selectionFromRegion(width, height, { x: bx, y: by, width: bw, height: bh }, out);
|
|
5359
|
+
}
|
|
5360
|
+
function applyRefineEdits(pixels, selection, edits, width, height) {
|
|
5361
|
+
let sel = selection?.bounds ? selection : null;
|
|
5362
|
+
let band = null;
|
|
5363
|
+
for (const e of edits) {
|
|
5364
|
+
if (!e.points.length) throw new Error(`A ${e.kind} stroke needs at least one point.`);
|
|
5365
|
+
if (e.kind === "refine-brush") {
|
|
5366
|
+
band = combineSelections(band, brushStrokeMask(width, height, e.points, e.size ?? 40), "add");
|
|
5367
|
+
continue;
|
|
5368
|
+
}
|
|
5369
|
+
const mode = e.mode ?? "add";
|
|
5370
|
+
let region;
|
|
5371
|
+
if (e.kind === "quick-select") {
|
|
5372
|
+
if (!pixels) throw new Error("Quick Selection strokes need the image pixels.");
|
|
5373
|
+
region = quickSelectStroke(pixels, e.points, { size: e.size, selection: sel, grow: mode === "add" });
|
|
5374
|
+
} else {
|
|
5375
|
+
region = brushStrokeMask(width, height, e.points, e.size ?? 20, e.hardness ?? 1);
|
|
5376
|
+
}
|
|
5377
|
+
sel = combineSelections(sel, region, mode);
|
|
5378
|
+
}
|
|
5379
|
+
return { selection: sel, refineBrush: band };
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5262
5382
|
// src/image-editor/selection/stroke.ts
|
|
5263
5383
|
function selectionRing(sel, width, location) {
|
|
5264
5384
|
const half = location === "center" ? width / 2 : width;
|
|
@@ -10280,6 +10400,107 @@ var ImageEditorController = class {
|
|
|
10280
10400
|
const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;
|
|
10281
10401
|
return this.setSelection(refineSelection(pixels, sel, params));
|
|
10282
10402
|
}
|
|
10403
|
+
/**
|
|
10404
|
+
* Select and Mask's output: refines the selection with `params`, then puts the result where
|
|
10405
|
+
* `output` says, as one undo step:
|
|
10406
|
+
* - `selection`: the refined selection;
|
|
10407
|
+
* - `layer-mask`: a mask on the layer (`layerId`, default the active one), then deselects;
|
|
10408
|
+
* - `new-layer`: a new layer of the layer's selected pixels (Layer via Copy), the source hidden;
|
|
10409
|
+
* - `new-layer-with-mask`: a copy of the layer masked by the selection, the source hidden.
|
|
10410
|
+
* `decontaminate` (0..100, new-layer outputs only) replaces the old background's colour in the
|
|
10411
|
+
* soft edge with the subject's own (see `decontaminateColors`).
|
|
10412
|
+
*
|
|
10413
|
+
* `edits` are the workspace's strokes, applied first in order (Quick Selection, Brush and
|
|
10414
|
+
* Refine Edge Brush; see `RefineEdit`); with them, an empty selection is a valid start.
|
|
10415
|
+
* `refineMode: 'object'` asks the segmentation provider for the object in the selection's
|
|
10416
|
+
* bounds and refines from its outline, kept to near the selection's edge.
|
|
10417
|
+
*/
|
|
10418
|
+
async refineSelectionTo(params, output = "selection", options = {}) {
|
|
10419
|
+
const edits = options.edits ?? [];
|
|
10420
|
+
const refineMode = options.refineMode ?? "color";
|
|
10421
|
+
if (!this.state.selection?.bounds && !edits.length) throw new Error("Nothing is selected; select first, or pass edits that paint a selection.");
|
|
10422
|
+
if (refineMode === "object" && !this.segmentation) {
|
|
10423
|
+
throw new Error("Object aware refinement needs a segmentation model (setSegmentationProvider); use refineMode color.");
|
|
10424
|
+
}
|
|
10425
|
+
const amount = Math.min(100, Math.max(0, options.decontaminate ?? 0)) / 100;
|
|
10426
|
+
const toLayer = output === "new-layer" || output === "new-layer-with-mask";
|
|
10427
|
+
if (amount > 0 && !toLayer) throw new Error("Decontaminate colors writes the cleaned colours to a new layer: use output new-layer or new-layer-with-mask.");
|
|
10428
|
+
let layerId;
|
|
10429
|
+
if (output !== "selection") {
|
|
10430
|
+
layerId = options.layerId ?? this.state.activeLayerId ?? void 0;
|
|
10431
|
+
if (!layerId) throw new LayerOperationError("No layer given and none is active; pass layerId (see list_layers).");
|
|
10432
|
+
const layer = requireLayer(this.state.layers, layerId).layer;
|
|
10433
|
+
if (toLayer && layer.type !== "pixel") throw new LayerOperationError(`Output ${output} needs a pixel layer; "${layerId}" is a ${layer.type} layer.`);
|
|
10434
|
+
}
|
|
10435
|
+
const needsPixels = (params.radius ?? 0) > 0 || edits.some((e) => e.kind !== "brush");
|
|
10436
|
+
const pixels = needsPixels ? await this.requirePixels(options) : null;
|
|
10437
|
+
const { width, height } = this.naturalSize;
|
|
10438
|
+
const edited = applyRefineEdits(pixels, this.state.selection ?? null, edits, width, height);
|
|
10439
|
+
let sel = edited.selection;
|
|
10440
|
+
if (!sel?.bounds) throw new Error("The edits leave nothing selected; paint with mode add first.");
|
|
10441
|
+
if (refineMode === "object") {
|
|
10442
|
+
const b = sel.bounds;
|
|
10443
|
+
const found = await this.modelObject({ rect: b }, options.signal);
|
|
10444
|
+
if (!found) throw new Error("The segmentation model found no object in the selection; use refineMode color.");
|
|
10445
|
+
const near = modifySelection(sel, "expand", Math.max(4, params.radius ?? 8));
|
|
10446
|
+
const clipped = combineSelections(found, near, "intersect");
|
|
10447
|
+
if (!clipped?.bounds) throw new Error("The segmentation model found no object in the selection; use refineMode color.");
|
|
10448
|
+
sel = clipped;
|
|
10449
|
+
}
|
|
10450
|
+
const refined = refineSelection(pixels, sel, params, edited.refineBrush);
|
|
10451
|
+
if (!refined.bounds) throw new Error("Refining left nothing selected; use a smaller shiftEdge or contrast.");
|
|
10452
|
+
return this.asOneStep(() => {
|
|
10453
|
+
this.setSelection(refined);
|
|
10454
|
+
if (output === "selection") return { output, refineMode };
|
|
10455
|
+
if (output === "layer-mask") {
|
|
10456
|
+
this.addLayerMask(layerId, "selection");
|
|
10457
|
+
this.setSelection(null);
|
|
10458
|
+
return { output, layerId, refineMode };
|
|
10459
|
+
}
|
|
10460
|
+
let newLayerId2;
|
|
10461
|
+
if (output === "new-layer") newLayerId2 = this.layerViaCopy({ layerId });
|
|
10462
|
+
else {
|
|
10463
|
+
newLayerId2 = this.duplicateLayer(layerId);
|
|
10464
|
+
this.addLayerMask(newLayerId2, "selection");
|
|
10465
|
+
this.state.activeTarget = "pixels";
|
|
10466
|
+
}
|
|
10467
|
+
if (amount > 0) this.decontaminateLayer(newLayerId2, layerId, refined, amount, Math.max(4, params.radius ?? 0));
|
|
10468
|
+
this.setLayerProperties(layerId, { visible: false });
|
|
10469
|
+
this.setSelection(null);
|
|
10470
|
+
return { output, layerId, newLayerId: newLayerId2, refineMode };
|
|
10471
|
+
});
|
|
10472
|
+
}
|
|
10473
|
+
/**
|
|
10474
|
+
* Decontaminate Colors along the soft edge of `coverage`: the colours are estimated from the
|
|
10475
|
+
* source layer (whose background is still there) and written into layer `id`'s RGB, keeping
|
|
10476
|
+
* its alpha.
|
|
10477
|
+
*/
|
|
10478
|
+
decontaminateLayer(id, sourceId, coverage, amount, radius) {
|
|
10479
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
10480
|
+
const source = requireLayer(this.state.layers, sourceId).layer;
|
|
10481
|
+
if (layer.type !== "pixel" || source.type !== "pixel" || !coverage.bounds) return;
|
|
10482
|
+
const { width, height } = this.naturalSize;
|
|
10483
|
+
const pad = Math.ceil(radius * 3);
|
|
10484
|
+
const b = coverage.bounds;
|
|
10485
|
+
const x0 = Math.max(0, b.x - pad);
|
|
10486
|
+
const y0 = Math.max(0, b.y - pad);
|
|
10487
|
+
const box = { x: x0, y: y0, width: Math.min(width, b.x + b.width + pad) - x0, height: Math.min(height, b.y + b.height + pad) - y0 };
|
|
10488
|
+
const src = { x: box.x - layer.offset.x, y: box.y - layer.offset.y, width: box.width, height: box.height };
|
|
10489
|
+
const surface = this.surfaceOf(layer);
|
|
10490
|
+
const rgba = readRegion(surface, src);
|
|
10491
|
+
const original = readRegion(this.surfaceOf(source), { x: box.x - source.offset.x, y: box.y - source.offset.y, width: box.width, height: box.height });
|
|
10492
|
+
const cov = new Uint8Array(box.width * box.height);
|
|
10493
|
+
const all = coverage.data;
|
|
10494
|
+
for (let y = 0; y < box.height; y++) cov.set(all.subarray((box.y + y) * width + box.x, (box.y + y) * width + box.x + box.width), y * box.width);
|
|
10495
|
+
const cleaned = decontaminateColors(original, cov, box.width, box.height, amount, radius);
|
|
10496
|
+
for (let i = 3; i < cleaned.length; i += 4) cleaned[i] = rgba[i];
|
|
10497
|
+
const next = mapLayer(this.state.layers, id, (l) => {
|
|
10498
|
+
const updated = { ...l, surface: writeRegion(surface, src, cleaned) };
|
|
10499
|
+
delete updated.source;
|
|
10500
|
+
return updated;
|
|
10501
|
+
});
|
|
10502
|
+
this.commitLayers(next);
|
|
10503
|
+
}
|
|
10283
10504
|
/**
|
|
10284
10505
|
* Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or
|
|
10285
10506
|
* RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.
|
|
@@ -13251,6 +13472,12 @@ var REFINE_EDGE_CONTROLS = [
|
|
|
13251
13472
|
{ id: "contrast", label: "Contrast", min: 0, max: 100, step: 1, unit: "%" },
|
|
13252
13473
|
{ id: "shiftEdge", label: "Shift Edge", min: -100, max: 100, step: 1, unit: "%" }
|
|
13253
13474
|
];
|
|
13475
|
+
var REFINE_OUTPUTS = [
|
|
13476
|
+
{ id: "selection", label: "Selection" },
|
|
13477
|
+
{ id: "layer-mask", label: "Layer Mask" },
|
|
13478
|
+
{ id: "new-layer", label: "New Layer" },
|
|
13479
|
+
{ id: "new-layer-with-mask", label: "New Layer with Layer Mask" }
|
|
13480
|
+
];
|
|
13254
13481
|
function describeSelection(mask) {
|
|
13255
13482
|
if (!mask || !mask.bounds) return "No selection";
|
|
13256
13483
|
const pct2 = selectionArea(mask) / (mask.width * mask.height) * 100;
|
|
@@ -13459,31 +13686,69 @@ function createImageSelectionTools(controller) {
|
|
|
13459
13686
|
const refine = {
|
|
13460
13687
|
name: "refine_selection",
|
|
13461
13688
|
title: "Refine the selection edge",
|
|
13462
|
-
description: "Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %). Use it after loading a coarse AI mask. One undoable step.",
|
|
13689
|
+
description: "Select and Mask / Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %), and puts the result where output says: the selection (default), a layer-mask on layerId (default the active layer; then deselects), a new-layer of its selected pixels, or a new-layer-with-mask (a masked copy); the new-layer outputs hide the source layer. decontaminate (0-100, new-layer outputs only) replaces the old background colour in soft edges (hair, fur) with the subject's own. Use it after loading a coarse AI mask, or to cut a subject out cleanly. edits are the Select and Mask workspace's strokes, applied first in order: quick-select (grows to the region's edges), brush (paints the selection in or out) and refine-brush (marks hair or fur for the edge to be re-estimated there); with edits, an empty selection is a valid start. refineMode object (needs a host segmentation model) refines from the model's outline of the object. One undoable step. Returns the selection, refineMode and, for layer outputs, layerId and newLayerId.",
|
|
13463
13690
|
inputSchema: {
|
|
13464
13691
|
type: "object",
|
|
13465
13692
|
properties: {
|
|
13466
|
-
radius: { type: "number", minimum: 0, maximum: 64, description: "Edge detection radius in pixels." },
|
|
13693
|
+
radius: { type: "number", minimum: 0, maximum: 64, description: "Edge detection radius in pixels (default 8)." },
|
|
13467
13694
|
smartRadius: { type: "boolean", description: "Adapt the radius to hard and soft edges." },
|
|
13468
13695
|
smooth: { type: "number", minimum: 0, maximum: 100, description: "Smooth the edge outline (0-100)." },
|
|
13469
13696
|
feather: { type: "number", minimum: 0, maximum: 250, description: "Feather the edge (pixels)." },
|
|
13470
13697
|
contrast: { type: "number", minimum: 0, maximum: 100, description: "Sharpen the edge transition (0-100)." },
|
|
13471
|
-
shiftEdge: { type: "number", minimum: -100, maximum: 100, description: "Move the edge out (+) or in (-), in percent." }
|
|
13698
|
+
shiftEdge: { type: "number", minimum: -100, maximum: 100, description: "Move the edge out (+) or in (-), in percent." },
|
|
13699
|
+
output: {
|
|
13700
|
+
type: "string",
|
|
13701
|
+
enum: ["selection", "layer-mask", "new-layer", "new-layer-with-mask"],
|
|
13702
|
+
description: "Where the result goes (default selection). layer-mask keeps every pixel and is easiest to revise; new-layer-with-mask too, on a copy."
|
|
13703
|
+
},
|
|
13704
|
+
layerId: { type: "string", description: "The layer for layer outputs (default the active layer; see list_layers). New-layer outputs need a pixel layer." },
|
|
13705
|
+
decontaminate: { type: "number", minimum: 0, maximum: 100, description: "Decontaminate Colors amount 0-100 (default 0), only with output new-layer or new-layer-with-mask." },
|
|
13706
|
+
edits: {
|
|
13707
|
+
type: "array",
|
|
13708
|
+
description: "Strokes applied before refining, in order.",
|
|
13709
|
+
items: {
|
|
13710
|
+
type: "object",
|
|
13711
|
+
properties: {
|
|
13712
|
+
kind: { type: "string", enum: ["quick-select", "brush", "refine-brush"], description: "quick-select grows to edges; brush paints directly; refine-brush marks soft detail (hair) for the edge to be re-estimated." },
|
|
13713
|
+
points: { type: "array", minItems: 1, items: pointSchema, description: "The stroke path in natural image pixels (one point is a dab)." },
|
|
13714
|
+
size: { type: "number", minimum: 1, maximum: 2500, description: "Brush diameter in natural image pixels (default 30 quick-select, 20 brush, 40 refine-brush)." },
|
|
13715
|
+
mode: { type: "string", enum: ["add", "subtract"], description: "quick-select and brush: add (default) or subtract." },
|
|
13716
|
+
hardness: { type: "number", minimum: 0, maximum: 1, description: "brush: the solid share of the radius, 0..1 (default 1)." }
|
|
13717
|
+
},
|
|
13718
|
+
required: ["kind", "points"],
|
|
13719
|
+
additionalProperties: false
|
|
13720
|
+
}
|
|
13721
|
+
},
|
|
13722
|
+
refineMode: { type: "string", enum: ["color", "object"], description: "color (default): refine from the image colours; object: from a host segmentation model's outline of the object (needs a model)." }
|
|
13472
13723
|
},
|
|
13473
13724
|
additionalProperties: false
|
|
13474
13725
|
},
|
|
13475
13726
|
annotations: writeAnnotations("Refine the selection edge"),
|
|
13476
13727
|
handler: async (params) => {
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13728
|
+
const edits = Array.isArray(params.edits) ? params.edits.map((e) => ({ ...e, points: (e.points ?? []).map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) })) : [];
|
|
13729
|
+
for (const e of edits) {
|
|
13730
|
+
if (!e.points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) throw new Error("Each edit needs points of { x, y } in natural image pixels.");
|
|
13731
|
+
if (e.kind === "refine-brush" && e.mode !== void 0) throw new Error("refine-brush strokes have no mode; they mark where the edge is re-estimated.");
|
|
13732
|
+
}
|
|
13733
|
+
if (!controller.getSelection() && !edits.length) throw new Error("Nothing is selected.");
|
|
13734
|
+
const result = await controller.refineSelectionTo(
|
|
13735
|
+
{
|
|
13736
|
+
radius: num2(params.radius, 8),
|
|
13737
|
+
smartRadius: params.smartRadius === true,
|
|
13738
|
+
smooth: num2(params.smooth, 0),
|
|
13739
|
+
feather: num2(params.feather, 0),
|
|
13740
|
+
contrast: num2(params.contrast, 0),
|
|
13741
|
+
shiftEdge: num2(params.shiftEdge, 0)
|
|
13742
|
+
},
|
|
13743
|
+
params.output ?? "selection",
|
|
13744
|
+
{
|
|
13745
|
+
layerId: typeof params.layerId === "string" ? params.layerId : void 0,
|
|
13746
|
+
decontaminate: num2(params.decontaminate, 0),
|
|
13747
|
+
edits,
|
|
13748
|
+
refineMode: params.refineMode === "object" ? "object" : "color"
|
|
13749
|
+
}
|
|
13750
|
+
);
|
|
13751
|
+
return { ...summary(controller), ...result };
|
|
13487
13752
|
}
|
|
13488
13753
|
};
|
|
13489
13754
|
const applyMask = {
|
|
@@ -20702,6 +20967,8 @@ export {
|
|
|
20702
20967
|
PsdError,
|
|
20703
20968
|
QUICK_MASK_TARGET,
|
|
20704
20969
|
REFINE_EDGE_CONTROLS,
|
|
20970
|
+
REFINE_EDIT_KINDS,
|
|
20971
|
+
REFINE_OUTPUTS,
|
|
20705
20972
|
RETOUCH_CHOICES,
|
|
20706
20973
|
RETOUCH_OPTION_CONTROLS,
|
|
20707
20974
|
RETOUCH_SAMPLES,
|
|
@@ -20813,6 +21080,7 @@ export {
|
|
|
20813
21080
|
applyParams,
|
|
20814
21081
|
applyProcess2Adjustments,
|
|
20815
21082
|
applyProcess2Float,
|
|
21083
|
+
applyRefineEdits,
|
|
20816
21084
|
applyRegionBlur,
|
|
20817
21085
|
applyRegionMosaic,
|
|
20818
21086
|
applyRetouchToTile,
|
|
@@ -20845,6 +21113,7 @@ export {
|
|
|
20845
21113
|
boxQuad,
|
|
20846
21114
|
boxToCurvePoint,
|
|
20847
21115
|
bradfordAdaptation,
|
|
21116
|
+
brushStrokeMask,
|
|
20848
21117
|
brushstrokeDataUrl,
|
|
20849
21118
|
brushstrokeKey,
|
|
20850
21119
|
brushstrokeSamples,
|
|
@@ -20950,6 +21219,7 @@ export {
|
|
|
20950
21219
|
decodePng,
|
|
20951
21220
|
decodePngDataUrl,
|
|
20952
21221
|
decodeSelectionRle,
|
|
21222
|
+
decontaminateColors,
|
|
20953
21223
|
deepBytes,
|
|
20954
21224
|
deepTile,
|
|
20955
21225
|
defaultActiveLayerId,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Select and Mask workspace's strokes as plain data, so an agent can send what a person
|
|
3
|
+
* paints: Quick Selection strokes (grow to the region's edges), Brush strokes (paint the
|
|
4
|
+
* selection in or out directly) and Refine Edge Brush strokes (mark where the edge should be
|
|
5
|
+
* re-estimated from the image, e.g. over hair).
|
|
6
|
+
*/
|
|
7
|
+
import type { ImagePoint } from '../types.cjs';
|
|
8
|
+
import { type PixelBuffer, type SelectionMask } from './mask.cjs';
|
|
9
|
+
/** One stroke in the workspace, in natural image pixels. */
|
|
10
|
+
export type RefineEdit = {
|
|
11
|
+
kind: 'quick-select';
|
|
12
|
+
points: ImagePoint[];
|
|
13
|
+
size?: number;
|
|
14
|
+
mode?: 'add' | 'subtract';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'brush';
|
|
17
|
+
points: ImagePoint[];
|
|
18
|
+
size?: number;
|
|
19
|
+
mode?: 'add' | 'subtract';
|
|
20
|
+
hardness?: number;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'refine-brush';
|
|
23
|
+
points: ImagePoint[];
|
|
24
|
+
size?: number;
|
|
25
|
+
};
|
|
26
|
+
export declare const REFINE_EDIT_KINDS: readonly ["quick-select", "brush", "refine-brush"];
|
|
27
|
+
/**
|
|
28
|
+
* Coverage of a round brush of diameter `size` dragged along `points` (one point is a dab), over
|
|
29
|
+
* a `width × height` image. `hardness` 0..1 (default 1): the solid share of the radius; the rest
|
|
30
|
+
* fades out. The edge is anti-aliased either way.
|
|
31
|
+
*/
|
|
32
|
+
export declare function brushStrokeMask(width: number, height: number, points: readonly ImagePoint[], size: number, hardness?: number): SelectionMask;
|
|
33
|
+
/**
|
|
34
|
+
* Applies `edits` in order to `selection` (`null` starts empty) over `pixels` (needed for
|
|
35
|
+
* Quick Selection strokes). Returns the edited selection and the Refine Edge Brush's painted
|
|
36
|
+
* area (`null` when there were no such strokes), for `refineSelection`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyRefineEdits(pixels: PixelBuffer | null, selection: SelectionMask | null, edits: readonly RefineEdit[], width: number, height: number): {
|
|
39
|
+
selection: SelectionMask | null;
|
|
40
|
+
refineBrush: SelectionMask | null;
|
|
41
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Select and Mask workspace's strokes as plain data, so an agent can send what a person
|
|
3
|
+
* paints: Quick Selection strokes (grow to the region's edges), Brush strokes (paint the
|
|
4
|
+
* selection in or out directly) and Refine Edge Brush strokes (mark where the edge should be
|
|
5
|
+
* re-estimated from the image, e.g. over hair).
|
|
6
|
+
*/
|
|
7
|
+
import type { ImagePoint } from '../types.js';
|
|
8
|
+
import { type PixelBuffer, type SelectionMask } from './mask.js';
|
|
9
|
+
/** One stroke in the workspace, in natural image pixels. */
|
|
10
|
+
export type RefineEdit = {
|
|
11
|
+
kind: 'quick-select';
|
|
12
|
+
points: ImagePoint[];
|
|
13
|
+
size?: number;
|
|
14
|
+
mode?: 'add' | 'subtract';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'brush';
|
|
17
|
+
points: ImagePoint[];
|
|
18
|
+
size?: number;
|
|
19
|
+
mode?: 'add' | 'subtract';
|
|
20
|
+
hardness?: number;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'refine-brush';
|
|
23
|
+
points: ImagePoint[];
|
|
24
|
+
size?: number;
|
|
25
|
+
};
|
|
26
|
+
export declare const REFINE_EDIT_KINDS: readonly ["quick-select", "brush", "refine-brush"];
|
|
27
|
+
/**
|
|
28
|
+
* Coverage of a round brush of diameter `size` dragged along `points` (one point is a dab), over
|
|
29
|
+
* a `width × height` image. `hardness` 0..1 (default 1): the solid share of the radius; the rest
|
|
30
|
+
* fades out. The edge is anti-aliased either way.
|
|
31
|
+
*/
|
|
32
|
+
export declare function brushStrokeMask(width: number, height: number, points: readonly ImagePoint[], size: number, hardness?: number): SelectionMask;
|
|
33
|
+
/**
|
|
34
|
+
* Applies `edits` in order to `selection` (`null` starts empty) over `pixels` (needed for
|
|
35
|
+
* Quick Selection strokes). Returns the edited selection and the Refine Edge Brush's painted
|
|
36
|
+
* area (`null` when there were no such strokes), for `refineSelection`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyRefineEdits(pixels: PixelBuffer | null, selection: SelectionMask | null, edits: readonly RefineEdit[], width: number, height: number): {
|
|
39
|
+
selection: SelectionMask | null;
|
|
40
|
+
refineBrush: SelectionMask | null;
|
|
41
|
+
};
|
|
@@ -42,10 +42,12 @@ export declare function guidedFilterColor(guide: readonly [Float32Array, Float32
|
|
|
42
42
|
export declare function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array;
|
|
43
43
|
/**
|
|
44
44
|
* Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when
|
|
45
|
-
* `radius > 0`): edge-aware re-estimation in a band around the edge, then
|
|
46
|
-
* contrast and shift edge, in that order.
|
|
45
|
+
* `radius > 0` or a `band` is given): edge-aware re-estimation in a band around the edge, then
|
|
46
|
+
* smooth, feather, contrast and shift edge, in that order. `band` (0..1 per pixel, the Refine
|
|
47
|
+
* Edge Brush) adds to the band where it is painted, so hair or fur far from the edge is
|
|
48
|
+
* re-estimated too.
|
|
47
49
|
*/
|
|
48
|
-
export declare function refineCoverage(guide: readonly [Float32Array, Float32Array, Float32Array] | null, m: Float32Array, w: number, h: number, params?: RefineEdgeParams): Float32Array;
|
|
50
|
+
export declare function refineCoverage(guide: readonly [Float32Array, Float32Array, Float32Array] | null, m: Float32Array, w: number, h: number, params?: RefineEdgeParams, band?: ArrayLike<number> | null): Float32Array;
|
|
49
51
|
/** Three 0..1 planes of a window of an RGBA buffer. */
|
|
50
52
|
export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array];
|
|
51
53
|
/**
|
|
@@ -53,4 +55,13 @@ export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float3
|
|
|
53
55
|
* same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small
|
|
54
56
|
* selection on a large image stays cheap.
|
|
55
57
|
*/
|
|
56
|
-
export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams): SelectionMask;
|
|
58
|
+
export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams, refineBrush?: SelectionMask | null): SelectionMask;
|
|
59
|
+
/**
|
|
60
|
+
* Decontaminate Colors: along a soft edge, a pixel's colour is part subject and part the old
|
|
61
|
+
* background (C = αF + (1 − α)B), which shows as a fringe once the subject is on a new background.
|
|
62
|
+
* Each edge pixel's colour is replaced (by `amount`, 0..1) with an estimate of the subject's own
|
|
63
|
+
* colour: unmixed from the background's local mean where the pixel is mostly subject, and the
|
|
64
|
+
* subject's local mean where it is mostly background. Only RGB changes; pixels fully in or out
|
|
65
|
+
* are kept. `rgba` and `coverage` (0..255) cover the same `w × h` region; returns new RGBA.
|
|
66
|
+
*/
|
|
67
|
+
export declare function decontaminateColors(rgba: ArrayLike<number>, coverage: ArrayLike<number>, w: number, h: number, amount: number, radius?: number): Uint8ClampedArray;
|
|
@@ -42,10 +42,12 @@ export declare function guidedFilterColor(guide: readonly [Float32Array, Float32
|
|
|
42
42
|
export declare function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array;
|
|
43
43
|
/**
|
|
44
44
|
* Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when
|
|
45
|
-
* `radius > 0`): edge-aware re-estimation in a band around the edge, then
|
|
46
|
-
* contrast and shift edge, in that order.
|
|
45
|
+
* `radius > 0` or a `band` is given): edge-aware re-estimation in a band around the edge, then
|
|
46
|
+
* smooth, feather, contrast and shift edge, in that order. `band` (0..1 per pixel, the Refine
|
|
47
|
+
* Edge Brush) adds to the band where it is painted, so hair or fur far from the edge is
|
|
48
|
+
* re-estimated too.
|
|
47
49
|
*/
|
|
48
|
-
export declare function refineCoverage(guide: readonly [Float32Array, Float32Array, Float32Array] | null, m: Float32Array, w: number, h: number, params?: RefineEdgeParams): Float32Array;
|
|
50
|
+
export declare function refineCoverage(guide: readonly [Float32Array, Float32Array, Float32Array] | null, m: Float32Array, w: number, h: number, params?: RefineEdgeParams, band?: ArrayLike<number> | null): Float32Array;
|
|
49
51
|
/** Three 0..1 planes of a window of an RGBA buffer. */
|
|
50
52
|
export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array];
|
|
51
53
|
/**
|
|
@@ -53,4 +55,13 @@ export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float3
|
|
|
53
55
|
* same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small
|
|
54
56
|
* selection on a large image stays cheap.
|
|
55
57
|
*/
|
|
56
|
-
export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams): SelectionMask;
|
|
58
|
+
export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams, refineBrush?: SelectionMask | null): SelectionMask;
|
|
59
|
+
/**
|
|
60
|
+
* Decontaminate Colors: along a soft edge, a pixel's colour is part subject and part the old
|
|
61
|
+
* background (C = αF + (1 − α)B), which shows as a fringe once the subject is on a new background.
|
|
62
|
+
* Each edge pixel's colour is replaced (by `amount`, 0..1) with an estimate of the subject's own
|
|
63
|
+
* colour: unmixed from the background's local mean where the pixel is mostly subject, and the
|
|
64
|
+
* subject's local mean where it is mostly background. Only RGB changes; pixels fully in or out
|
|
65
|
+
* are kept. `rgba` and `coverage` (0..255) cover the same `w × h` region; returns new RGBA.
|
|
66
|
+
*/
|
|
67
|
+
export declare function decontaminateColors(rgba: ArrayLike<number>, coverage: ArrayLike<number>, w: number, h: number, amount: number, radius?: number): Uint8ClampedArray;
|
|
@@ -79,5 +79,10 @@ export interface RefineEdgeControl {
|
|
|
79
79
|
}
|
|
80
80
|
/** Sliders of the Refine Edge panel. */
|
|
81
81
|
export declare const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[];
|
|
82
|
+
/** Select and Mask's outputs, in menu order. */
|
|
83
|
+
export declare const REFINE_OUTPUTS: readonly {
|
|
84
|
+
id: 'selection' | 'layer-mask' | 'new-layer' | 'new-layer-with-mask';
|
|
85
|
+
label: string;
|
|
86
|
+
}[];
|
|
82
87
|
/** A one-line status: "No selection" or "W × H px selected (n % of the image)". */
|
|
83
88
|
export declare function describeSelection(mask: SelectionMask | null | undefined): string;
|
|
@@ -79,5 +79,10 @@ export interface RefineEdgeControl {
|
|
|
79
79
|
}
|
|
80
80
|
/** Sliders of the Refine Edge panel. */
|
|
81
81
|
export declare const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[];
|
|
82
|
+
/** Select and Mask's outputs, in menu order. */
|
|
83
|
+
export declare const REFINE_OUTPUTS: readonly {
|
|
84
|
+
id: 'selection' | 'layer-mask' | 'new-layer' | 'new-layer-with-mask';
|
|
85
|
+
label: string;
|
|
86
|
+
}[];
|
|
82
87
|
/** A one-line status: "No selection" or "W × H px selected (n % of the image)". */
|
|
83
88
|
export declare function describeSelection(mask: SelectionMask | null | undefined): string;
|
package/package.json
CHANGED
|
@@ -269,7 +269,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
|
|
|
269
269
|
| `grow_selection` | Select › Grow: add connected pixels within `tolerance` (default 32) of a selected colour; follows colour, not distance. `sample`, `sampleMerged`. Fails when nothing is selected. Returns the summary. |
|
|
270
270
|
| `select_similar` | Select › Similar: add every pixel in the image within `tolerance` of a selected colour, connected or not. Fails when nothing is selected. Returns the summary. |
|
|
271
271
|
| `reselect` | Bring back the last selection `deselect` cleared (one undo step). Fails when there is none. Returns the summary. |
|
|
272
|
-
| `refine_selection` | Select and Mask: snap the selection edge to image edges within `radius` px (0..64, default 8), with `smartRadius`, `smooth` 0..100, `feather` px, `contrast` 0..100 and `shiftEdge` -100..100 %, then `output` to `selection` (default), `layer-mask`, `new-layer` or `new-layer-with-mask` (on `layerId`, default active; new layers hide the source), with `decontaminate` 0..100 for new layers. One step; returns `layerId` / `newLayerId`. Use on hair, fur, foliage or
|
|
272
|
+
| `refine_selection` | Select and Mask: snap the selection edge to image edges within `radius` px (0..64, default 8), with `smartRadius`, `smooth` 0..100, `feather` px, `contrast` 0..100 and `shiftEdge` -100..100 %, then `output` to `selection` (default), `layer-mask`, `new-layer` or `new-layer-with-mask` (on `layerId`, default active; new layers hide the source), with `decontaminate` 0..100 for new layers. `edits` (`quick-select`, `brush` add/subtract, `refine-brush` over hair) apply first; `refineMode: "object"` uses the host model. One step; returns `refineMode`, `layerId` / `newLayerId`. Use on hair, fur, foliage or a coarse AI mask. |
|
|
273
273
|
| `update_adjustment_layer` | Change a layer's `enabled` (false hides it without deleting), `blend`, `opacity` (0..1) or `name` in one undo step. Returns the layer. Its parameters change with `set_adjustments`. Toggle `enabled` to compare a layer's effect before and after. |
|
|
274
274
|
| `move_adjustment_layer` | Move a layer to index `to` (0 = bottom, applied first; past the top means top). Order matters: a curves layer above a hue/saturation layer works on its output. Fails when the layer is already there. Returns the ids bottom to top. |
|
|
275
275
|
| `remove_adjustment_layer` | Delete a layer and its mask. Prefer hiding it with `update_adjustment_layer` when the user may want it back. Returns the remaining ids. |
|