@pixerate/editor 0.202.0 → 0.203.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 +24 -1
- package/dist/image-editor/controller.d.ts +24 -1
- package/dist/image-editor/index.cjs +226 -171
- package/dist/image-editor/index.js +78 -23
- package/dist/image-editor/selection/segmentation.d.cts +7 -0
- package/dist/image-editor/selection/segmentation.d.ts +7 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +2 -2
|
@@ -10071,6 +10071,46 @@ var ImageEditorController = class {
|
|
|
10071
10071
|
this.setSelection(sel, "replace");
|
|
10072
10072
|
return this.state.selection ?? null;
|
|
10073
10073
|
}
|
|
10074
|
+
/**
|
|
10075
|
+
* Asks the segmentation provider (which must be set) for `request.target` in the flattened image
|
|
10076
|
+
* and returns its results with masks decoded to pixels.
|
|
10077
|
+
*/
|
|
10078
|
+
async modelSegments(request) {
|
|
10079
|
+
const provider = this.segmentation;
|
|
10080
|
+
const { width, height } = this.naturalSize;
|
|
10081
|
+
const image = await this.toDataURL({ format: "image/png" });
|
|
10082
|
+
const results = await provider.segment({ ...request, image, width, height });
|
|
10083
|
+
request.signal?.throwIfAborted();
|
|
10084
|
+
const decoded = [];
|
|
10085
|
+
for (const r of results ?? []) decoded.push({ ...r, mask: r.mask !== void 0 ? await this.decodeMaskImage(r.mask) : void 0 });
|
|
10086
|
+
return decoded;
|
|
10087
|
+
}
|
|
10088
|
+
/**
|
|
10089
|
+
* The model's object for Object Selection: of what it returns, the pieces mostly inside the box
|
|
10090
|
+
* (clipped to the box grown by a tenth) or those under the point. `null` when none fits.
|
|
10091
|
+
*/
|
|
10092
|
+
async modelObject(target, signal) {
|
|
10093
|
+
const { width, height } = this.naturalSize;
|
|
10094
|
+
const decoded = await this.modelSegments(
|
|
10095
|
+
"rect" in target ? { target: "the main object inside the box", box: target.rect, maxSegments: 4, signal } : { target: "the object at the point", point: target.point, maxSegments: 4, signal }
|
|
10096
|
+
);
|
|
10097
|
+
let union = null;
|
|
10098
|
+
for (const r of decoded) {
|
|
10099
|
+
const one = segmentsToSelection(width, height, [r]);
|
|
10100
|
+
if (!one?.bounds) continue;
|
|
10101
|
+
if ("rect" in target) {
|
|
10102
|
+
const r0 = target.rect;
|
|
10103
|
+
const grow = 0.1 * Math.max(r0.width, r0.height);
|
|
10104
|
+
const box = rectSelection(width, height, { x: r0.x - grow, y: r0.y - grow, width: r0.width + 2 * grow, height: r0.height + 2 * grow }, { antiAlias: false });
|
|
10105
|
+
const inside = combineSelections(one, box, "intersect");
|
|
10106
|
+
if (!inside || selectionArea(inside) < selectionArea(one) * 0.5) continue;
|
|
10107
|
+
union = combineSelections(union, inside, "add");
|
|
10108
|
+
} else if (selectionValueAt(one, target.point.x, target.point.y) >= 0.5) {
|
|
10109
|
+
union = combineSelections(union, one, "add");
|
|
10110
|
+
}
|
|
10111
|
+
}
|
|
10112
|
+
return union;
|
|
10113
|
+
}
|
|
10074
10114
|
/**
|
|
10075
10115
|
* Object Selection: the object inside `target.rect` (natural pixels; what is outside the box is
|
|
10076
10116
|
* background) or under `target.point`, found by GrabCut (colour models of object and background
|
|
@@ -10088,10 +10128,17 @@ var ImageEditorController = class {
|
|
|
10088
10128
|
} else if (!(target.point.x >= 0 && target.point.y >= 0 && target.point.x < width && target.point.y < height)) {
|
|
10089
10129
|
throw new Error(`(${target.point.x}, ${target.point.y}) is outside the image (0..${width - 1}, 0..${height - 1}); click on the object.`);
|
|
10090
10130
|
}
|
|
10131
|
+
if (this.segmentation && options.useModel !== false) {
|
|
10132
|
+
const found = await this.modelObject(target, options.signal);
|
|
10133
|
+
if (found) {
|
|
10134
|
+
this.setSelection(found, options.mode ?? "replace");
|
|
10135
|
+
return { selection: this.state.selection ?? null, method: "model" };
|
|
10136
|
+
}
|
|
10137
|
+
}
|
|
10091
10138
|
const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
|
|
10092
10139
|
const region = selectObjectIn(pixels, target, { enhanceEdge: options.enhanceEdge });
|
|
10093
10140
|
this.setSelection(region, options.mode ?? "replace");
|
|
10094
|
-
return this.state.selection ?? null;
|
|
10141
|
+
return { selection: this.state.selection ?? null, method: "grabcut" };
|
|
10095
10142
|
}
|
|
10096
10143
|
/**
|
|
10097
10144
|
* Select › Focus Area: selects the parts of the image that are in focus (fine detail above the
|
|
@@ -10111,11 +10158,18 @@ var ImageEditorController = class {
|
|
|
10111
10158
|
* (the selection is then unchanged), why.
|
|
10112
10159
|
*/
|
|
10113
10160
|
async selectSky(options = {}) {
|
|
10161
|
+
if (this.segmentation && options.useModel !== false) {
|
|
10162
|
+
const { width, height } = this.naturalSize;
|
|
10163
|
+
const sky = segmentsToSelection(width, height, await this.modelSegments({ target: "the sky", signal: options.signal }));
|
|
10164
|
+
if (!sky?.bounds) return { selection: this.state.selection ?? null, method: "model", note: "The segmentation model found no sky." };
|
|
10165
|
+
this.setSelection(sky, options.mode ?? "replace");
|
|
10166
|
+
return { selection: this.state.selection ?? null, method: "model" };
|
|
10167
|
+
}
|
|
10114
10168
|
const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
|
|
10115
10169
|
const { selection, reason } = findSky(pixels, options);
|
|
10116
|
-
if (!selection.bounds) return { selection: this.state.selection ?? null, note: reason };
|
|
10170
|
+
if (!selection.bounds) return { selection: this.state.selection ?? null, method: "classical", note: reason };
|
|
10117
10171
|
this.setSelection(selection, options.mode ?? "replace");
|
|
10118
|
-
return { selection: this.state.selection ?? null };
|
|
10172
|
+
return { selection: this.state.selection ?? null, method: "classical" };
|
|
10119
10173
|
}
|
|
10120
10174
|
/**
|
|
10121
10175
|
* Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
|
|
@@ -10306,13 +10360,7 @@ var ImageEditorController = class {
|
|
|
10306
10360
|
const description = target.trim();
|
|
10307
10361
|
if (!description) throw new Error("Describe the subject to select.");
|
|
10308
10362
|
const { width, height } = this.naturalSize;
|
|
10309
|
-
const
|
|
10310
|
-
const results = await provider.segment({ image, width, height, target: description, maxSegments: options.maxSegments, signal: options.signal });
|
|
10311
|
-
options.signal?.throwIfAborted();
|
|
10312
|
-
const decoded = [];
|
|
10313
|
-
for (const r of results ?? []) {
|
|
10314
|
-
decoded.push({ ...r, mask: r.mask !== void 0 ? await this.decodeMaskImage(r.mask) : void 0 });
|
|
10315
|
-
}
|
|
10363
|
+
const decoded = await this.modelSegments({ target: description, maxSegments: options.maxSegments, signal: options.signal });
|
|
10316
10364
|
const segments = decoded.map((r) => {
|
|
10317
10365
|
const one = segmentsToSelection(width, height, [r]);
|
|
10318
10366
|
return { label: r.label || description, bounds: one?.bounds ?? null, ...r.score !== void 0 ? { score: r.score } : {} };
|
|
@@ -13624,7 +13672,7 @@ function createImageSelectionTools(controller) {
|
|
|
13624
13672
|
const selectObjectTool = {
|
|
13625
13673
|
name: "select_object",
|
|
13626
13674
|
title: "Object Selection",
|
|
13627
|
-
description:
|
|
13675
|
+
description: 'Object Selection: selects the object inside rect (a box drawn loosely around it, natural image pixels; what lies outside the box is treated as background) or the object under point. With a host segmentation model it asks the model (falling back if nothing it returns fits); otherwise it finds the object from the colours of object and background and the edges between them (GrabCut). Combined with the selection by mode. A box is more reliable than a point: make it a little larger than the object on every side. Check with get_selection and capture_view, then fix with quick_select strokes or select_object with mode add / subtract. One undoable step. Returns the selection and method ("model" or "grabcut").',
|
|
13628
13676
|
inputSchema: {
|
|
13629
13677
|
type: "object",
|
|
13630
13678
|
properties: {
|
|
@@ -13638,20 +13686,25 @@ function createImageSelectionTools(controller) {
|
|
|
13638
13686
|
point: { ...pointSchema, description: "A point on the object (natural image pixels). Pass this or rect." },
|
|
13639
13687
|
mode: modeSchema,
|
|
13640
13688
|
sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
|
|
13641
|
-
enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false)." }
|
|
13689
|
+
enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false)." },
|
|
13690
|
+
useModel: { type: "boolean", description: "Use the host segmentation model when there is one (default true); false always uses GrabCut." }
|
|
13642
13691
|
},
|
|
13643
13692
|
additionalProperties: false
|
|
13644
13693
|
},
|
|
13645
|
-
annotations: writeAnnotations("Object Selection"),
|
|
13694
|
+
annotations: { ...writeAnnotations("Object Selection"), openWorldHint: true },
|
|
13646
13695
|
handler: async (args) => {
|
|
13647
13696
|
const rect = args.rect;
|
|
13648
13697
|
const point = args.point;
|
|
13649
13698
|
if (!rect && !point) throw new Error("Pass rect (a box around the object) or point (a point on it).");
|
|
13650
13699
|
if (rect && point) throw new Error("Pass rect or point, not both.");
|
|
13651
|
-
const options = {
|
|
13652
|
-
|
|
13653
|
-
|
|
13654
|
-
|
|
13700
|
+
const options = {
|
|
13701
|
+
mode: parseSelectionMode(args.mode),
|
|
13702
|
+
sampleAllLayers: args.sampleAllLayers,
|
|
13703
|
+
enhanceEdge: args.enhanceEdge,
|
|
13704
|
+
useModel: args.useModel
|
|
13705
|
+
};
|
|
13706
|
+
const { method } = rect ? await controller.selectObject({ rect: { x: num2(rect.x, NaN), y: num2(rect.y, NaN), width: num2(rect.width, NaN), height: num2(rect.height, NaN) } }, options) : await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
|
|
13707
|
+
return { ...summary(controller), method };
|
|
13655
13708
|
}
|
|
13656
13709
|
};
|
|
13657
13710
|
const selectFocusAreaTool = {
|
|
@@ -13684,26 +13737,28 @@ function createImageSelectionTools(controller) {
|
|
|
13684
13737
|
const selectSkyTool = {
|
|
13685
13738
|
name: "select_sky",
|
|
13686
13739
|
title: "Select the sky",
|
|
13687
|
-
description:
|
|
13740
|
+
description: 'Select \u203A Sky: selects the sky, combined with the selection by mode. With a host segmentation model it asks the model; otherwise it finds it classically (blue, or bright and grey when overcast, smooth, reaching down from the top of the image, including patches seen through branches or gaps) with the edge fitted to the skyline. Use it to replace or recolour a sky or to adjust the land without it (invert_selection). Raise threshold to take in haze or a sunset glow, lower it if bright buildings come in. One undoable step. Returns the selection, method ("model" or "classical"; threshold and softness apply to classical), and a note when no sky was found (the selection is then unchanged).',
|
|
13688
13741
|
inputSchema: {
|
|
13689
13742
|
type: "object",
|
|
13690
13743
|
properties: {
|
|
13691
13744
|
threshold: { type: "number", minimum: 0, maximum: 100, description: "How sky-like a pixel must be, 0..100 (default 50): higher takes in less sky-like pixels." },
|
|
13692
13745
|
softness: { type: "number", minimum: 0, maximum: 100, description: "Soften the edge over this many natural pixels (default 4; 0 for a hard edge)." },
|
|
13693
13746
|
mode: modeSchema,
|
|
13694
|
-
sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." }
|
|
13747
|
+
sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
|
|
13748
|
+
useModel: { type: "boolean", description: "Use the host segmentation model when there is one (default true); false finds the sky classically." }
|
|
13695
13749
|
},
|
|
13696
13750
|
additionalProperties: false
|
|
13697
13751
|
},
|
|
13698
|
-
annotations: writeAnnotations("Select the sky", true),
|
|
13752
|
+
annotations: { ...writeAnnotations("Select the sky", true), openWorldHint: true },
|
|
13699
13753
|
handler: async (args) => {
|
|
13700
|
-
const { note } = await controller.selectSky({
|
|
13754
|
+
const { note, method } = await controller.selectSky({
|
|
13701
13755
|
threshold: args.threshold === void 0 ? void 0 : num2(args.threshold, 50),
|
|
13702
13756
|
softness: args.softness === void 0 ? void 0 : num2(args.softness, 4),
|
|
13703
13757
|
mode: parseSelectionMode(args.mode),
|
|
13704
|
-
sampleAllLayers: args.sampleAllLayers
|
|
13758
|
+
sampleAllLayers: args.sampleAllLayers,
|
|
13759
|
+
useModel: args.useModel
|
|
13705
13760
|
});
|
|
13706
|
-
return { ...summary(controller), ...note ? { note } : {} };
|
|
13761
|
+
return { ...summary(controller), method, ...note ? { note } : {} };
|
|
13707
13762
|
}
|
|
13708
13763
|
};
|
|
13709
13764
|
const magicWandTool = {
|
|
@@ -11,6 +11,13 @@ export interface SegmentationRequest {
|
|
|
11
11
|
target: string;
|
|
12
12
|
/** Return at most this many objects (the provider's default when absent). */
|
|
13
13
|
maxSegments?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Object Selection: the box (natural pixels) the wanted object is in. A box-prompted model (SAM)
|
|
16
|
+
* should segment the object inside it; others can read `target`, which describes it too.
|
|
17
|
+
*/
|
|
18
|
+
box?: CropBox;
|
|
19
|
+
/** Object Selection: a point (natural pixels) on the wanted object, for a point-prompted model. */
|
|
20
|
+
point?: ImagePoint;
|
|
14
21
|
signal?: AbortSignal;
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
@@ -11,6 +11,13 @@ export interface SegmentationRequest {
|
|
|
11
11
|
target: string;
|
|
12
12
|
/** Return at most this many objects (the provider's default when absent). */
|
|
13
13
|
maxSegments?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Object Selection: the box (natural pixels) the wanted object is in. A box-prompted model (SAM)
|
|
16
|
+
* should segment the object inside it; others can read `target`, which describes it too.
|
|
17
|
+
*/
|
|
18
|
+
box?: CropBox;
|
|
19
|
+
/** Object Selection: a point (natural pixels) on the wanted object, for a point-prompted model. */
|
|
20
|
+
point?: ImagePoint;
|
|
14
21
|
signal?: AbortSignal;
|
|
15
22
|
}
|
|
16
23
|
/**
|
package/package.json
CHANGED
|
@@ -253,9 +253,9 @@ in the source frame at the document's bit depth, colour mode and profile. Read
|
|
|
253
253
|
| `select_ellipse` | Elliptical marquee inscribed in `x`, `y`, `width`, `height`; same options. A feathered ellipse makes a vignette. Returns the summary. |
|
|
254
254
|
| `select_line` | Single row or column marquee: `orientation` `row` (at y) or `column` (at x), `position` in natural px, `mode`. A 1 px line across the image. Returns the summary. |
|
|
255
255
|
| `quick_select` | Quick Selection: `strokes` (brush paths of `{x, y}`), `size` (diameter, default 30), `mode`, `sampleAllLayers`, `enhanceEdge`. Each stroke grows out to its region's edges; with `replace` the first replaces and the rest add. Paint inside the area, then check and fix with `add` / `subtract` strokes. Returns the summary. |
|
|
256
|
-
| `select_object` | Object Selection
|
|
256
|
+
| `select_object` | Object Selection: `rect` (a box around the object) or `point` (on it), `mode`, `sampleAllLayers`, `enhanceEdge`, `useModel`. Uses the host model when set (else, or when nothing fits, GrabCut; outside the box is background). Make the box a little larger than the object. Returns the summary and `method`. |
|
|
257
257
|
| `select_focus_area` | Select › Focus Area: the in-focus (sharp) parts, edge fitted to the image. `range` 0..100 (higher takes in softer areas), `noise` 0..100 (raise when grain is selected), `softness` px, `mode`. Returns the summary and `noiseLevel`. |
|
|
258
|
-
| `select_sky` | Select › Sky: the sky (blue or overcast, from the top down, with patches through gaps)
|
|
258
|
+
| `select_sky` | Select › Sky: the sky, from the host model when set, else classically (blue or overcast, from the top down, with patches through gaps). `threshold` 0..100 (higher takes in haze) and `softness` px for classical, `mode`, `useModel`. Returns the summary, `method`, and `note` when none was found. |
|
|
259
259
|
| `select_polygon` | The lasso: `points` (at least 3 `{x, y}` along the outline, closed automatically), `mode`, `feather`, `antiAlias`; `snapToEdges: {width, contrast}` makes it the Magnetic Lasso (points snap to edges, the outline follows them). Trace a subject's contour, not its box. Returns the summary (plus `outlinePoints` when snapped). |
|
|
260
260
|
| `magic_wand` | Select similar colour from `x`, `y`: `tolerance` 0..255 (default 32), `contiguous` (default true), `distance` `rgb` or `perceptual`, `sampleMerged`, `sample` (`composite` or `active-layer`), `mode`, `feather`. For flat backdrops and solid colours. Returns the summary. |
|
|
261
261
|
| `select_color_range` | Color Range, soft: `samples` (`[{color, x?, y?}]`) or a `preset` (reds … magentas, highlights, midtones, shadows, skinTones); `fuzziness` (samples 0..200, default 40; tone presets' falloff, default 20), `localized` px radius around positioned samples, `invert`, `sample`, `mode`, `feather`. Returns the summary. |
|