@pixerate/editor 0.205.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 +11 -0
- package/dist/image-editor/controller.d.ts +11 -0
- package/dist/image-editor/index.cjs +432 -302
- package/dist/image-editor/index.js +154 -24
- 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 +6 -4
- package/dist/image-editor/selection/refine.d.ts +6 -4
- 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;
|
|
@@ -5302,6 +5319,66 @@ function segmentsToSelection(width, height, results) {
|
|
|
5302
5319
|
return out;
|
|
5303
5320
|
}
|
|
5304
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
|
+
|
|
5305
5382
|
// src/image-editor/selection/stroke.ts
|
|
5306
5383
|
function selectionRing(sel, width, location) {
|
|
5307
5384
|
const half = location === "center" ? width / 2 : width;
|
|
@@ -10332,10 +10409,19 @@ var ImageEditorController = class {
|
|
|
10332
10409
|
* - `new-layer-with-mask`: a copy of the layer masked by the selection, the source hidden.
|
|
10333
10410
|
* `decontaminate` (0..100, new-layer outputs only) replaces the old background's colour in the
|
|
10334
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.
|
|
10335
10417
|
*/
|
|
10336
10418
|
async refineSelectionTo(params, output = "selection", options = {}) {
|
|
10337
|
-
const
|
|
10338
|
-
|
|
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
|
+
}
|
|
10339
10425
|
const amount = Math.min(100, Math.max(0, options.decontaminate ?? 0)) / 100;
|
|
10340
10426
|
const toLayer = output === "new-layer" || output === "new-layer-with-mask";
|
|
10341
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.");
|
|
@@ -10346,16 +10432,30 @@ var ImageEditorController = class {
|
|
|
10346
10432
|
const layer = requireLayer(this.state.layers, layerId).layer;
|
|
10347
10433
|
if (toLayer && layer.type !== "pixel") throw new LayerOperationError(`Output ${output} needs a pixel layer; "${layerId}" is a ${layer.type} layer.`);
|
|
10348
10434
|
}
|
|
10349
|
-
const
|
|
10350
|
-
const
|
|
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);
|
|
10351
10451
|
if (!refined.bounds) throw new Error("Refining left nothing selected; use a smaller shiftEdge or contrast.");
|
|
10352
10452
|
return this.asOneStep(() => {
|
|
10353
10453
|
this.setSelection(refined);
|
|
10354
|
-
if (output === "selection") return { output };
|
|
10454
|
+
if (output === "selection") return { output, refineMode };
|
|
10355
10455
|
if (output === "layer-mask") {
|
|
10356
10456
|
this.addLayerMask(layerId, "selection");
|
|
10357
10457
|
this.setSelection(null);
|
|
10358
|
-
return { output, layerId };
|
|
10458
|
+
return { output, layerId, refineMode };
|
|
10359
10459
|
}
|
|
10360
10460
|
let newLayerId2;
|
|
10361
10461
|
if (output === "new-layer") newLayerId2 = this.layerViaCopy({ layerId });
|
|
@@ -10367,7 +10467,7 @@ var ImageEditorController = class {
|
|
|
10367
10467
|
if (amount > 0) this.decontaminateLayer(newLayerId2, layerId, refined, amount, Math.max(4, params.radius ?? 0));
|
|
10368
10468
|
this.setLayerProperties(layerId, { visible: false });
|
|
10369
10469
|
this.setSelection(null);
|
|
10370
|
-
return { output, layerId, newLayerId: newLayerId2 };
|
|
10470
|
+
return { output, layerId, newLayerId: newLayerId2, refineMode };
|
|
10371
10471
|
});
|
|
10372
10472
|
}
|
|
10373
10473
|
/**
|
|
@@ -13586,7 +13686,7 @@ function createImageSelectionTools(controller) {
|
|
|
13586
13686
|
const refine = {
|
|
13587
13687
|
name: "refine_selection",
|
|
13588
13688
|
title: "Refine the selection edge",
|
|
13589
|
-
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. One undoable step. Returns the selection and, for layer outputs, layerId and newLayerId.",
|
|
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.",
|
|
13590
13690
|
inputSchema: {
|
|
13591
13691
|
type: "object",
|
|
13592
13692
|
properties: {
|
|
@@ -13602,13 +13702,35 @@ function createImageSelectionTools(controller) {
|
|
|
13602
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."
|
|
13603
13703
|
},
|
|
13604
13704
|
layerId: { type: "string", description: "The layer for layer outputs (default the active layer; see list_layers). New-layer outputs need a pixel layer." },
|
|
13605
|
-
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." }
|
|
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)." }
|
|
13606
13723
|
},
|
|
13607
13724
|
additionalProperties: false
|
|
13608
13725
|
},
|
|
13609
13726
|
annotations: writeAnnotations("Refine the selection edge"),
|
|
13610
13727
|
handler: async (params) => {
|
|
13611
|
-
|
|
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.");
|
|
13612
13734
|
const result = await controller.refineSelectionTo(
|
|
13613
13735
|
{
|
|
13614
13736
|
radius: num2(params.radius, 8),
|
|
@@ -13619,7 +13741,12 @@ function createImageSelectionTools(controller) {
|
|
|
13619
13741
|
shiftEdge: num2(params.shiftEdge, 0)
|
|
13620
13742
|
},
|
|
13621
13743
|
params.output ?? "selection",
|
|
13622
|
-
{
|
|
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
|
+
}
|
|
13623
13750
|
);
|
|
13624
13751
|
return { ...summary(controller), ...result };
|
|
13625
13752
|
}
|
|
@@ -20840,6 +20967,7 @@ export {
|
|
|
20840
20967
|
PsdError,
|
|
20841
20968
|
QUICK_MASK_TARGET,
|
|
20842
20969
|
REFINE_EDGE_CONTROLS,
|
|
20970
|
+
REFINE_EDIT_KINDS,
|
|
20843
20971
|
REFINE_OUTPUTS,
|
|
20844
20972
|
RETOUCH_CHOICES,
|
|
20845
20973
|
RETOUCH_OPTION_CONTROLS,
|
|
@@ -20952,6 +21080,7 @@ export {
|
|
|
20952
21080
|
applyParams,
|
|
20953
21081
|
applyProcess2Adjustments,
|
|
20954
21082
|
applyProcess2Float,
|
|
21083
|
+
applyRefineEdits,
|
|
20955
21084
|
applyRegionBlur,
|
|
20956
21085
|
applyRegionMosaic,
|
|
20957
21086
|
applyRetouchToTile,
|
|
@@ -20984,6 +21113,7 @@ export {
|
|
|
20984
21113
|
boxQuad,
|
|
20985
21114
|
boxToCurvePoint,
|
|
20986
21115
|
bradfordAdaptation,
|
|
21116
|
+
brushStrokeMask,
|
|
20987
21117
|
brushstrokeDataUrl,
|
|
20988
21118
|
brushstrokeKey,
|
|
20989
21119
|
brushstrokeSamples,
|
|
@@ -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,7 +55,7 @@ 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;
|
|
57
59
|
/**
|
|
58
60
|
* Decontaminate Colors: along a soft edge, a pixel's colour is part subject and part the old
|
|
59
61
|
* background (C = αF + (1 − α)B), which shows as a fringe once the subject is on a new background.
|
|
@@ -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,7 +55,7 @@ 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;
|
|
57
59
|
/**
|
|
58
60
|
* Decontaminate Colors: along a soft edge, a pixel's colour is part subject and part the old
|
|
59
61
|
* background (C = αF + (1 − α)B), which shows as a fringe once the subject is on a new background.
|
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. |
|
|
@@ -57,7 +57,10 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
|
|
|
57
57
|
`layerId` (white adds, black removes, a soft brush feathers; `blur_stroke` softens a part
|
|
58
58
|
of the edge), then `set_quick_mask` `enabled: false`.
|
|
59
59
|
- `refine_selection` snaps to real edges: radius 2–4 for hard edges, 8–20 for hair and fur;
|
|
60
|
-
`smartRadius: true` for mixed edges.
|
|
60
|
+
`smartRadius: true` for mixed edges. For flyaway hair beyond the radius, add `edits` with a
|
|
61
|
+
`refine-brush` stroke along it (size covering the strands); fix misses and leaks in the same
|
|
62
|
+
call with `quick-select` or `brush` strokes (`mode: "subtract"` to remove). With a host
|
|
63
|
+
model, `refineMode: "object"` refines from the model's outline.
|
|
61
64
|
- `shiftEdge` −10..−30 % tightens a refined edge that picked up background.
|
|
62
65
|
**Check:** `percent` changed by roughly what you intended (a few percent at most for a
|
|
63
66
|
cleanup).
|