@pixerate/editor 0.204.0 → 0.205.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 +30 -0
- package/dist/image-editor/controller.d.ts +30 -0
- package/dist/image-editor/index.cjs +298 -158
- package/dist/image-editor/index.js +152 -12
- package/dist/image-editor/selection/refine.d.cts +9 -0
- package/dist/image-editor/selection/refine.d.ts +9 -0
- 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
|
@@ -4108,6 +4108,49 @@ function refineSelection(pixels, mask, params = {}) {
|
|
|
4108
4108
|
}
|
|
4109
4109
|
return createSelectionMask(mask.width, mask.height, out);
|
|
4110
4110
|
}
|
|
4111
|
+
function decontaminateColors(rgba, coverage, w, h, amount, radius = 8) {
|
|
4112
|
+
const out = Uint8ClampedArray.from(rgba);
|
|
4113
|
+
const n = w * h;
|
|
4114
|
+
const k = Math.min(1, Math.max(0, amount));
|
|
4115
|
+
if (k === 0) return out;
|
|
4116
|
+
const r = Math.max(2, Math.round(radius));
|
|
4117
|
+
const fgW = new Float32Array(n);
|
|
4118
|
+
const bgW = new Float32Array(n);
|
|
4119
|
+
for (let i = 0; i < n; i++) {
|
|
4120
|
+
fgW[i] = coverage[i] >= 250 ? 1 : 0;
|
|
4121
|
+
bgW[i] = coverage[i] <= 5 ? 1 : 0;
|
|
4122
|
+
}
|
|
4123
|
+
const meanOf = (weights, radiusPx) => {
|
|
4124
|
+
const wm = boxMean(weights, w, h, radiusPx);
|
|
4125
|
+
const planes = [0, 1, 2].map((c) => {
|
|
4126
|
+
const v = new Float32Array(n);
|
|
4127
|
+
for (let i = 0; i < n; i++) v[i] = rgba[i * 4 + c] * weights[i];
|
|
4128
|
+
const m = boxMean(v, w, h, radiusPx);
|
|
4129
|
+
for (let i = 0; i < n; i++) m[i] = wm[i] > 1e-6 ? m[i] / wm[i] : NaN;
|
|
4130
|
+
return m;
|
|
4131
|
+
});
|
|
4132
|
+
return { weight: wm, planes };
|
|
4133
|
+
};
|
|
4134
|
+
const fg = meanOf(fgW, r);
|
|
4135
|
+
const fgWide = meanOf(fgW, r * 3);
|
|
4136
|
+
const bg = meanOf(bgW, r);
|
|
4137
|
+
for (let i = 0; i < n; i++) {
|
|
4138
|
+
const c = coverage[i];
|
|
4139
|
+
if (c <= 5 || c >= 250) continue;
|
|
4140
|
+
const a = c / 255;
|
|
4141
|
+
for (let ch = 0; ch < 3; ch++) {
|
|
4142
|
+
const C = rgba[i * 4 + ch];
|
|
4143
|
+
const B = bg.planes[ch][i];
|
|
4144
|
+
const Fm = Number.isNaN(fg.planes[ch][i]) ? fgWide.planes[ch][i] : fg.planes[ch][i];
|
|
4145
|
+
let F;
|
|
4146
|
+
if (a >= 0.5 && !Number.isNaN(B)) F = (C - (1 - a) * B) / a;
|
|
4147
|
+
else if (!Number.isNaN(Fm)) F = Fm;
|
|
4148
|
+
else F = C;
|
|
4149
|
+
out[i * 4 + ch] = Math.round(C + k * (Math.min(255, Math.max(0, F)) - C));
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
return out;
|
|
4153
|
+
}
|
|
4111
4154
|
|
|
4112
4155
|
// src/image-editor/selection/segment/guided.ts
|
|
4113
4156
|
function workPlanes(work) {
|
|
@@ -10280,6 +10323,84 @@ var ImageEditorController = class {
|
|
|
10280
10323
|
const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;
|
|
10281
10324
|
return this.setSelection(refineSelection(pixels, sel, params));
|
|
10282
10325
|
}
|
|
10326
|
+
/**
|
|
10327
|
+
* Select and Mask's output: refines the selection with `params`, then puts the result where
|
|
10328
|
+
* `output` says, as one undo step:
|
|
10329
|
+
* - `selection`: the refined selection;
|
|
10330
|
+
* - `layer-mask`: a mask on the layer (`layerId`, default the active one), then deselects;
|
|
10331
|
+
* - `new-layer`: a new layer of the layer's selected pixels (Layer via Copy), the source hidden;
|
|
10332
|
+
* - `new-layer-with-mask`: a copy of the layer masked by the selection, the source hidden.
|
|
10333
|
+
* `decontaminate` (0..100, new-layer outputs only) replaces the old background's colour in the
|
|
10334
|
+
* soft edge with the subject's own (see `decontaminateColors`).
|
|
10335
|
+
*/
|
|
10336
|
+
async refineSelectionTo(params, output = "selection", options = {}) {
|
|
10337
|
+
const sel = this.state.selection;
|
|
10338
|
+
if (!sel?.bounds) throw new Error("Nothing is selected; select first.");
|
|
10339
|
+
const amount = Math.min(100, Math.max(0, options.decontaminate ?? 0)) / 100;
|
|
10340
|
+
const toLayer = output === "new-layer" || output === "new-layer-with-mask";
|
|
10341
|
+
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.");
|
|
10342
|
+
let layerId;
|
|
10343
|
+
if (output !== "selection") {
|
|
10344
|
+
layerId = options.layerId ?? this.state.activeLayerId ?? void 0;
|
|
10345
|
+
if (!layerId) throw new LayerOperationError("No layer given and none is active; pass layerId (see list_layers).");
|
|
10346
|
+
const layer = requireLayer(this.state.layers, layerId).layer;
|
|
10347
|
+
if (toLayer && layer.type !== "pixel") throw new LayerOperationError(`Output ${output} needs a pixel layer; "${layerId}" is a ${layer.type} layer.`);
|
|
10348
|
+
}
|
|
10349
|
+
const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;
|
|
10350
|
+
const refined = refineSelection(pixels, sel, params);
|
|
10351
|
+
if (!refined.bounds) throw new Error("Refining left nothing selected; use a smaller shiftEdge or contrast.");
|
|
10352
|
+
return this.asOneStep(() => {
|
|
10353
|
+
this.setSelection(refined);
|
|
10354
|
+
if (output === "selection") return { output };
|
|
10355
|
+
if (output === "layer-mask") {
|
|
10356
|
+
this.addLayerMask(layerId, "selection");
|
|
10357
|
+
this.setSelection(null);
|
|
10358
|
+
return { output, layerId };
|
|
10359
|
+
}
|
|
10360
|
+
let newLayerId2;
|
|
10361
|
+
if (output === "new-layer") newLayerId2 = this.layerViaCopy({ layerId });
|
|
10362
|
+
else {
|
|
10363
|
+
newLayerId2 = this.duplicateLayer(layerId);
|
|
10364
|
+
this.addLayerMask(newLayerId2, "selection");
|
|
10365
|
+
this.state.activeTarget = "pixels";
|
|
10366
|
+
}
|
|
10367
|
+
if (amount > 0) this.decontaminateLayer(newLayerId2, layerId, refined, amount, Math.max(4, params.radius ?? 0));
|
|
10368
|
+
this.setLayerProperties(layerId, { visible: false });
|
|
10369
|
+
this.setSelection(null);
|
|
10370
|
+
return { output, layerId, newLayerId: newLayerId2 };
|
|
10371
|
+
});
|
|
10372
|
+
}
|
|
10373
|
+
/**
|
|
10374
|
+
* Decontaminate Colors along the soft edge of `coverage`: the colours are estimated from the
|
|
10375
|
+
* source layer (whose background is still there) and written into layer `id`'s RGB, keeping
|
|
10376
|
+
* its alpha.
|
|
10377
|
+
*/
|
|
10378
|
+
decontaminateLayer(id, sourceId, coverage, amount, radius) {
|
|
10379
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
10380
|
+
const source = requireLayer(this.state.layers, sourceId).layer;
|
|
10381
|
+
if (layer.type !== "pixel" || source.type !== "pixel" || !coverage.bounds) return;
|
|
10382
|
+
const { width, height } = this.naturalSize;
|
|
10383
|
+
const pad = Math.ceil(radius * 3);
|
|
10384
|
+
const b = coverage.bounds;
|
|
10385
|
+
const x0 = Math.max(0, b.x - pad);
|
|
10386
|
+
const y0 = Math.max(0, b.y - pad);
|
|
10387
|
+
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 };
|
|
10388
|
+
const src = { x: box.x - layer.offset.x, y: box.y - layer.offset.y, width: box.width, height: box.height };
|
|
10389
|
+
const surface = this.surfaceOf(layer);
|
|
10390
|
+
const rgba = readRegion(surface, src);
|
|
10391
|
+
const original = readRegion(this.surfaceOf(source), { x: box.x - source.offset.x, y: box.y - source.offset.y, width: box.width, height: box.height });
|
|
10392
|
+
const cov = new Uint8Array(box.width * box.height);
|
|
10393
|
+
const all = coverage.data;
|
|
10394
|
+
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);
|
|
10395
|
+
const cleaned = decontaminateColors(original, cov, box.width, box.height, amount, radius);
|
|
10396
|
+
for (let i = 3; i < cleaned.length; i += 4) cleaned[i] = rgba[i];
|
|
10397
|
+
const next = mapLayer(this.state.layers, id, (l) => {
|
|
10398
|
+
const updated = { ...l, surface: writeRegion(surface, src, cleaned) };
|
|
10399
|
+
delete updated.source;
|
|
10400
|
+
return updated;
|
|
10401
|
+
});
|
|
10402
|
+
this.commitLayers(next);
|
|
10403
|
+
}
|
|
10283
10404
|
/**
|
|
10284
10405
|
* Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or
|
|
10285
10406
|
* RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.
|
|
@@ -13251,6 +13372,12 @@ var REFINE_EDGE_CONTROLS = [
|
|
|
13251
13372
|
{ id: "contrast", label: "Contrast", min: 0, max: 100, step: 1, unit: "%" },
|
|
13252
13373
|
{ id: "shiftEdge", label: "Shift Edge", min: -100, max: 100, step: 1, unit: "%" }
|
|
13253
13374
|
];
|
|
13375
|
+
var REFINE_OUTPUTS = [
|
|
13376
|
+
{ id: "selection", label: "Selection" },
|
|
13377
|
+
{ id: "layer-mask", label: "Layer Mask" },
|
|
13378
|
+
{ id: "new-layer", label: "New Layer" },
|
|
13379
|
+
{ id: "new-layer-with-mask", label: "New Layer with Layer Mask" }
|
|
13380
|
+
];
|
|
13254
13381
|
function describeSelection(mask) {
|
|
13255
13382
|
if (!mask || !mask.bounds) return "No selection";
|
|
13256
13383
|
const pct2 = selectionArea(mask) / (mask.width * mask.height) * 100;
|
|
@@ -13459,31 +13586,42 @@ function createImageSelectionTools(controller) {
|
|
|
13459
13586
|
const refine = {
|
|
13460
13587
|
name: "refine_selection",
|
|
13461
13588
|
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.",
|
|
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.",
|
|
13463
13590
|
inputSchema: {
|
|
13464
13591
|
type: "object",
|
|
13465
13592
|
properties: {
|
|
13466
|
-
radius: { type: "number", minimum: 0, maximum: 64, description: "Edge detection radius in pixels." },
|
|
13593
|
+
radius: { type: "number", minimum: 0, maximum: 64, description: "Edge detection radius in pixels (default 8)." },
|
|
13467
13594
|
smartRadius: { type: "boolean", description: "Adapt the radius to hard and soft edges." },
|
|
13468
13595
|
smooth: { type: "number", minimum: 0, maximum: 100, description: "Smooth the edge outline (0-100)." },
|
|
13469
13596
|
feather: { type: "number", minimum: 0, maximum: 250, description: "Feather the edge (pixels)." },
|
|
13470
13597
|
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." }
|
|
13598
|
+
shiftEdge: { type: "number", minimum: -100, maximum: 100, description: "Move the edge out (+) or in (-), in percent." },
|
|
13599
|
+
output: {
|
|
13600
|
+
type: "string",
|
|
13601
|
+
enum: ["selection", "layer-mask", "new-layer", "new-layer-with-mask"],
|
|
13602
|
+
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
|
+
},
|
|
13604
|
+
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." }
|
|
13472
13606
|
},
|
|
13473
13607
|
additionalProperties: false
|
|
13474
13608
|
},
|
|
13475
13609
|
annotations: writeAnnotations("Refine the selection edge"),
|
|
13476
13610
|
handler: async (params) => {
|
|
13477
13611
|
if (!controller.getSelection()) throw new Error("Nothing is selected.");
|
|
13478
|
-
await controller.
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13612
|
+
const result = await controller.refineSelectionTo(
|
|
13613
|
+
{
|
|
13614
|
+
radius: num2(params.radius, 8),
|
|
13615
|
+
smartRadius: params.smartRadius === true,
|
|
13616
|
+
smooth: num2(params.smooth, 0),
|
|
13617
|
+
feather: num2(params.feather, 0),
|
|
13618
|
+
contrast: num2(params.contrast, 0),
|
|
13619
|
+
shiftEdge: num2(params.shiftEdge, 0)
|
|
13620
|
+
},
|
|
13621
|
+
params.output ?? "selection",
|
|
13622
|
+
{ layerId: typeof params.layerId === "string" ? params.layerId : void 0, decontaminate: num2(params.decontaminate, 0) }
|
|
13623
|
+
);
|
|
13624
|
+
return { ...summary(controller), ...result };
|
|
13487
13625
|
}
|
|
13488
13626
|
};
|
|
13489
13627
|
const applyMask = {
|
|
@@ -20702,6 +20840,7 @@ export {
|
|
|
20702
20840
|
PsdError,
|
|
20703
20841
|
QUICK_MASK_TARGET,
|
|
20704
20842
|
REFINE_EDGE_CONTROLS,
|
|
20843
|
+
REFINE_OUTPUTS,
|
|
20705
20844
|
RETOUCH_CHOICES,
|
|
20706
20845
|
RETOUCH_OPTION_CONTROLS,
|
|
20707
20846
|
RETOUCH_SAMPLES,
|
|
@@ -20950,6 +21089,7 @@ export {
|
|
|
20950
21089
|
decodePng,
|
|
20951
21090
|
decodePngDataUrl,
|
|
20952
21091
|
decodeSelectionRle,
|
|
21092
|
+
decontaminateColors,
|
|
20953
21093
|
deepBytes,
|
|
20954
21094
|
deepTile,
|
|
20955
21095
|
defaultActiveLayerId,
|
|
@@ -54,3 +54,12 @@ export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float3
|
|
|
54
54
|
* selection on a large image stays cheap.
|
|
55
55
|
*/
|
|
56
56
|
export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams): SelectionMask;
|
|
57
|
+
/**
|
|
58
|
+
* Decontaminate Colors: along a soft edge, a pixel's colour is part subject and part the old
|
|
59
|
+
* background (C = αF + (1 − α)B), which shows as a fringe once the subject is on a new background.
|
|
60
|
+
* Each edge pixel's colour is replaced (by `amount`, 0..1) with an estimate of the subject's own
|
|
61
|
+
* colour: unmixed from the background's local mean where the pixel is mostly subject, and the
|
|
62
|
+
* subject's local mean where it is mostly background. Only RGB changes; pixels fully in or out
|
|
63
|
+
* are kept. `rgba` and `coverage` (0..255) cover the same `w × h` region; returns new RGBA.
|
|
64
|
+
*/
|
|
65
|
+
export declare function decontaminateColors(rgba: ArrayLike<number>, coverage: ArrayLike<number>, w: number, h: number, amount: number, radius?: number): Uint8ClampedArray;
|
|
@@ -54,3 +54,12 @@ export declare function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float3
|
|
|
54
54
|
* selection on a large image stays cheap.
|
|
55
55
|
*/
|
|
56
56
|
export declare function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params?: RefineEdgeParams): SelectionMask;
|
|
57
|
+
/**
|
|
58
|
+
* Decontaminate Colors: along a soft edge, a pixel's colour is part subject and part the old
|
|
59
|
+
* background (C = αF + (1 − α)B), which shows as a fringe once the subject is on a new background.
|
|
60
|
+
* Each edge pixel's colour is replaced (by `amount`, 0..1) with an estimate of the subject's own
|
|
61
|
+
* colour: unmixed from the background's local mean where the pixel is mostly subject, and the
|
|
62
|
+
* subject's local mean where it is mostly background. Only RGB changes; pixels fully in or out
|
|
63
|
+
* are kept. `rgba` and `coverage` (0..255) cover the same `w × h` region; returns new RGBA.
|
|
64
|
+
*/
|
|
65
|
+
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;
|