@pixerate/editor 0.215.0 → 0.216.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 +12 -0
- package/dist/image-editor/controller.d.ts +12 -0
- package/dist/image-editor/index.cjs +374 -188
- package/dist/image-editor/index.js +218 -32
- 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/inspect.d.cts +9 -0
- package/dist/image-editor/selection/inspect.d.ts +9 -0
- package/dist/image-editor/selection/uncertainty.d.cts +40 -0
- package/dist/image-editor/selection/uncertainty.d.ts +40 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
- package/skills/pixerate-image-selection-masking/SKILL.md +3 -3
|
@@ -2609,27 +2609,7 @@ function inspectMaskEdge(mask, image, options = {}) {
|
|
|
2609
2609
|
if (img[i + 3] < 128) return null;
|
|
2610
2610
|
return [img[i], img[i + 1], img[i + 2]];
|
|
2611
2611
|
};
|
|
2612
|
-
const
|
|
2613
|
-
const ix = Math.min(image.width - 1, Math.max(0, x - image.x));
|
|
2614
|
-
const iy = Math.min(image.height - 1, Math.max(0, y - image.y));
|
|
2615
|
-
const i = (iy * image.width + ix) * 4;
|
|
2616
|
-
return img[i + c] * img[i + 3] / 255;
|
|
2617
|
-
};
|
|
2618
|
-
const gradCache = /* @__PURE__ */ new Map();
|
|
2619
|
-
const gradient = (x, y) => {
|
|
2620
|
-
const k = y * W + x;
|
|
2621
|
-
const hit = gradCache.get(k);
|
|
2622
|
-
if (hit !== void 0) return hit;
|
|
2623
|
-
let best = 0;
|
|
2624
|
-
for (let c = 0; c < 3; c++) {
|
|
2625
|
-
const p = (dx, dy) => premul(x + dx, y + dy, c);
|
|
2626
|
-
const gx = p(1, -1) + 2 * p(1, 0) + p(1, 1) - p(-1, -1) - 2 * p(-1, 0) - p(-1, 1);
|
|
2627
|
-
const gy = p(-1, 1) + 2 * p(0, 1) + p(1, 1) - p(-1, -1) - 2 * p(0, -1) - p(1, -1);
|
|
2628
|
-
best = Math.max(best, Math.hypot(gx, gy) / 1020);
|
|
2629
|
-
}
|
|
2630
|
-
gradCache.set(k, best);
|
|
2631
|
-
return best;
|
|
2632
|
-
};
|
|
2612
|
+
const gradient = createGradientSampler(image, W);
|
|
2633
2613
|
const B = Math.min(256, Math.max(24, Math.round(Math.max(W, H) / 12)));
|
|
2634
2614
|
const BW = Math.ceil(W / B);
|
|
2635
2615
|
const blocks = /* @__PURE__ */ new Map();
|
|
@@ -2806,6 +2786,30 @@ function inspectMaskEdge(mask, image, options = {}) {
|
|
|
2806
2786
|
issues
|
|
2807
2787
|
};
|
|
2808
2788
|
}
|
|
2789
|
+
function createGradientSampler(image, W) {
|
|
2790
|
+
const img = image.data;
|
|
2791
|
+
const premul = (x, y, c) => {
|
|
2792
|
+
const ix = Math.min(image.width - 1, Math.max(0, x - image.x));
|
|
2793
|
+
const iy = Math.min(image.height - 1, Math.max(0, y - image.y));
|
|
2794
|
+
const i = (iy * image.width + ix) * 4;
|
|
2795
|
+
return img[i + c] * img[i + 3] / 255;
|
|
2796
|
+
};
|
|
2797
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2798
|
+
return (x, y) => {
|
|
2799
|
+
const k = y * W + x;
|
|
2800
|
+
const hit = cache.get(k);
|
|
2801
|
+
if (hit !== void 0) return hit;
|
|
2802
|
+
let best = 0;
|
|
2803
|
+
for (let c = 0; c < 3; c++) {
|
|
2804
|
+
const p = (dx, dy) => premul(x + dx, y + dy, c);
|
|
2805
|
+
const gx = p(1, -1) + 2 * p(1, 0) + p(1, 1) - p(-1, -1) - 2 * p(-1, 0) - p(-1, 1);
|
|
2806
|
+
const gy = p(-1, 1) + 2 * p(0, 1) + p(1, 1) - p(-1, -1) - 2 * p(0, -1) - p(1, -1);
|
|
2807
|
+
best = Math.max(best, Math.hypot(gx, gy) / 1020);
|
|
2808
|
+
}
|
|
2809
|
+
cache.set(k, best);
|
|
2810
|
+
return best;
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2809
2813
|
function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
|
|
2810
2814
|
const w = rx1 - rx0;
|
|
2811
2815
|
const h = ry1 - ry0;
|
|
@@ -2990,6 +2994,135 @@ function regions(a, W, H, b, minArea) {
|
|
|
2990
2994
|
};
|
|
2991
2995
|
}
|
|
2992
2996
|
|
|
2997
|
+
// src/image-editor/selection/uncertainty.ts
|
|
2998
|
+
var BACKED = 0.2;
|
|
2999
|
+
function selectionUncertainty(mask, image, options = {}) {
|
|
3000
|
+
const W = mask.width;
|
|
3001
|
+
const H = mask.height;
|
|
3002
|
+
const empty = () => ({ unknown: createSelectionMask(W, H), regions: [], count: 0, confidence: 1 });
|
|
3003
|
+
const b = mask.bounds;
|
|
3004
|
+
if (!b) return empty();
|
|
3005
|
+
const band = Math.max(1, Math.round(options.band ?? Math.max(2, Math.min(W, H) / 200)));
|
|
3006
|
+
const minArea = Math.max(1, options.minArea ?? 16);
|
|
3007
|
+
const maxRegions = Math.max(1, options.maxRegions ?? 20);
|
|
3008
|
+
const a = mask.data;
|
|
3009
|
+
const gradient = createGradientSampler(image, W);
|
|
3010
|
+
const x0 = Math.max(0, b.x - band - 1);
|
|
3011
|
+
const y0 = Math.max(0, b.y - band - 1);
|
|
3012
|
+
const x1 = Math.min(W, b.x + b.width + band + 1);
|
|
3013
|
+
const y1 = Math.min(H, b.y + b.height + band + 1);
|
|
3014
|
+
const w = x1 - x0;
|
|
3015
|
+
const h = y1 - y0;
|
|
3016
|
+
const evidence = new Float32Array(w * h).fill(NaN);
|
|
3017
|
+
const seeds = new Uint8Array(w * h);
|
|
3018
|
+
let sum = 0;
|
|
3019
|
+
let n = 0;
|
|
3020
|
+
const on = (x, y) => a[y * W + x] >= 128;
|
|
3021
|
+
for (let y = y0; y < y1; y++) {
|
|
3022
|
+
for (let x = x0; x < x1; x++) {
|
|
3023
|
+
const v = on(x, y);
|
|
3024
|
+
const differs = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && on(nx, ny) !== v;
|
|
3025
|
+
if (!(differs(x - 1, y) || differs(x + 1, y) || differs(x, y - 1) || differs(x, y + 1))) continue;
|
|
3026
|
+
let g = 0;
|
|
3027
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
3028
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
3029
|
+
const nx = x + dx;
|
|
3030
|
+
const ny = y + dy;
|
|
3031
|
+
if (nx >= 0 && ny >= 0 && nx < W && ny < H) g = Math.max(g, gradient(nx, ny));
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
const e = Math.min(1, g / BACKED);
|
|
3035
|
+
const k = (y - y0) * w + (x - x0);
|
|
3036
|
+
evidence[k] = e;
|
|
3037
|
+
sum += e;
|
|
3038
|
+
n++;
|
|
3039
|
+
if (e < 0.5) seeds[k] = 1;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (!n) return empty();
|
|
3043
|
+
const rows = new Uint8Array(w * h);
|
|
3044
|
+
for (let y = 0; y < h; y++) {
|
|
3045
|
+
let last = -Infinity;
|
|
3046
|
+
for (let x = 0; x < w; x++) {
|
|
3047
|
+
if (seeds[y * w + x]) last = x;
|
|
3048
|
+
if (x - last <= band) rows[y * w + x] = 1;
|
|
3049
|
+
}
|
|
3050
|
+
last = Infinity;
|
|
3051
|
+
for (let x = w - 1; x >= 0; x--) {
|
|
3052
|
+
if (seeds[y * w + x]) last = x;
|
|
3053
|
+
if (last - x <= band) rows[y * w + x] = 1;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
const grown = new Uint8Array(w * h);
|
|
3057
|
+
for (let x = 0; x < w; x++) {
|
|
3058
|
+
let last = -Infinity;
|
|
3059
|
+
for (let y = 0; y < h; y++) {
|
|
3060
|
+
if (rows[y * w + x]) last = y;
|
|
3061
|
+
if (y - last <= band) grown[y * w + x] = 1;
|
|
3062
|
+
}
|
|
3063
|
+
last = Infinity;
|
|
3064
|
+
for (let y = h - 1; y >= 0; y--) {
|
|
3065
|
+
if (rows[y * w + x]) last = y;
|
|
3066
|
+
if (last - y <= band) grown[y * w + x] = 1;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
const label = new Int32Array(w * h);
|
|
3070
|
+
const stack = new Int32Array(w * h);
|
|
3071
|
+
const found = [];
|
|
3072
|
+
for (let start = 0; start < w * h; start++) {
|
|
3073
|
+
if (!grown[start] || label[start]) continue;
|
|
3074
|
+
const r = { area: 0, e: 0, en: 0, bx0: w, by0: h, bx1: -1, by1: -1 };
|
|
3075
|
+
const id = found.length + 1;
|
|
3076
|
+
let top = 0;
|
|
3077
|
+
stack[top++] = start;
|
|
3078
|
+
label[start] = id;
|
|
3079
|
+
while (top) {
|
|
3080
|
+
const k = stack[--top];
|
|
3081
|
+
const i = k % w;
|
|
3082
|
+
const j = (k - i) / w;
|
|
3083
|
+
r.area++;
|
|
3084
|
+
if (!Number.isNaN(evidence[k])) {
|
|
3085
|
+
r.e += evidence[k];
|
|
3086
|
+
r.en++;
|
|
3087
|
+
}
|
|
3088
|
+
if (i < r.bx0) r.bx0 = i;
|
|
3089
|
+
if (i > r.bx1) r.bx1 = i;
|
|
3090
|
+
if (j < r.by0) r.by0 = j;
|
|
3091
|
+
if (j > r.by1) r.by1 = j;
|
|
3092
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
3093
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
3094
|
+
const ni = i + dx;
|
|
3095
|
+
const nj = j + dy;
|
|
3096
|
+
if (ni < 0 || nj < 0 || ni >= w || nj >= h) continue;
|
|
3097
|
+
const nk = nj * w + ni;
|
|
3098
|
+
if (!grown[nk] || label[nk]) continue;
|
|
3099
|
+
label[nk] = id;
|
|
3100
|
+
stack[top++] = nk;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
found.push(r);
|
|
3105
|
+
}
|
|
3106
|
+
const unknownData = new Uint8Array(W * H);
|
|
3107
|
+
for (let y = 0; y < h; y++) {
|
|
3108
|
+
for (let x = 0; x < w; x++) {
|
|
3109
|
+
const id = label[y * w + x];
|
|
3110
|
+
if (id && found[id - 1].area >= minArea) unknownData[(y + y0) * W + x + x0] = 255;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
const kept = found.filter((r) => r.area >= minArea).map((r) => ({
|
|
3114
|
+
bounds: { x: x0 + r.bx0, y: y0 + r.by0, width: r.bx1 - r.bx0 + 1, height: r.by1 - r.by0 + 1 },
|
|
3115
|
+
area: r.area,
|
|
3116
|
+
confidence: Math.round((r.en ? r.e / r.en : 0) * 1e3) / 1e3
|
|
3117
|
+
})).sort((p, q) => p.confidence - q.confidence || q.area - p.area);
|
|
3118
|
+
return {
|
|
3119
|
+
unknown: createSelectionMask(W, H, unknownData),
|
|
3120
|
+
regions: kept.slice(0, maxRegions),
|
|
3121
|
+
count: kept.length,
|
|
3122
|
+
confidence: Math.round(sum / n * 1e3) / 1e3
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
|
|
2993
3126
|
// src/image-editor/selection/path.ts
|
|
2994
3127
|
function selectionToSubpaths(mask, options = {}) {
|
|
2995
3128
|
const b = mask.bounds;
|
|
@@ -9730,6 +9863,7 @@ var ImageEditorController = class {
|
|
|
9730
9863
|
*/
|
|
9731
9864
|
async refineSelectionTo(params, output = "selection", options = {}) {
|
|
9732
9865
|
const edits = options.edits ?? [];
|
|
9866
|
+
const regions2 = this.checkRegions(options.regions);
|
|
9733
9867
|
const refineMode = options.refineMode ?? "color";
|
|
9734
9868
|
if (!this.state.selection?.bounds && !edits.length) throw new Error("Nothing is selected; select first, or pass edits that paint a selection.");
|
|
9735
9869
|
if (refineMode === "object" && !this.segmentation) {
|
|
@@ -9764,7 +9898,14 @@ var ImageEditorController = class {
|
|
|
9764
9898
|
if (!clipped?.bounds) throw new Error("The segmentation model found no object in the selection; use refineMode color.");
|
|
9765
9899
|
sel = clipped;
|
|
9766
9900
|
}
|
|
9767
|
-
|
|
9901
|
+
let refined = refineSelection(pixels, sel, params, edited.refineBrush);
|
|
9902
|
+
if (regions2) {
|
|
9903
|
+
const data = Uint8Array.from(sel.data);
|
|
9904
|
+
for (const r of regions2) {
|
|
9905
|
+
for (let y = r.y; y < r.y + r.height; y++) data.set(refined.data.subarray(y * width + r.x, y * width + r.x + r.width), y * width + r.x);
|
|
9906
|
+
}
|
|
9907
|
+
refined = createSelectionMask(width, height, data);
|
|
9908
|
+
}
|
|
9768
9909
|
if (!refined.bounds) throw new Error("Refining left nothing selected; use a smaller shiftEdge or contrast.");
|
|
9769
9910
|
if (toDocument) {
|
|
9770
9911
|
const document2 = this.documentFromSelection({ layerId, withMask: output === "new-document-with-mask", selection: refined, decontaminate: amount * 100, radius: params.radius });
|
|
@@ -10209,6 +10350,37 @@ var ImageEditorController = class {
|
|
|
10209
10350
|
const report = inspectMaskEdge(mask, image, options);
|
|
10210
10351
|
return { target, ...layer ? { layerId: layer.id } : {}, ...report };
|
|
10211
10352
|
}
|
|
10353
|
+
/** Whole-pixel rectangles inside the canvas, or a useful error; undefined stays undefined. */
|
|
10354
|
+
checkRegions(regions2) {
|
|
10355
|
+
if (regions2 === void 0) return void 0;
|
|
10356
|
+
const { width, height } = this.naturalSize;
|
|
10357
|
+
if (!Array.isArray(regions2) || !regions2.length) throw new Error("regions must be a non-empty list of { x, y, width, height } (e.g. the uncertainRegions bounds); leave it out to refine everywhere.");
|
|
10358
|
+
return regions2.map((r, i) => {
|
|
10359
|
+
const x = Math.max(0, Math.floor(r?.x));
|
|
10360
|
+
const y = Math.max(0, Math.floor(r?.y));
|
|
10361
|
+
const x1 = Math.min(width, Math.ceil(r?.x + r?.width));
|
|
10362
|
+
const y1 = Math.min(height, Math.ceil(r?.y + r?.height));
|
|
10363
|
+
if (![x, y, x1, y1].every(Number.isFinite) || x1 <= x || y1 <= y) throw new Error(`regions[${i}] must overlap the image (0..${width} \xD7 0..${height}), got ${JSON.stringify(r)}.`);
|
|
10364
|
+
return { x, y, width: x1 - x, height: y1 - y };
|
|
10365
|
+
});
|
|
10366
|
+
}
|
|
10367
|
+
/**
|
|
10368
|
+
* Where the selection is unsure of itself: the stretches of its edge with no image edge under
|
|
10369
|
+
* them, widened into a band (a trimap's unknown), as a mask and regions with bounds and
|
|
10370
|
+
* confidence, least confident first. Measured against the composite. Read-only; an empty
|
|
10371
|
+
* selection has none.
|
|
10372
|
+
*/
|
|
10373
|
+
selectionUncertainty(options = {}) {
|
|
10374
|
+
const { width, height } = this.naturalSize;
|
|
10375
|
+
const sel = this.getSelection();
|
|
10376
|
+
if (!sel?.bounds) return selectionUncertainty(createSelectionMask(width, height), { x: 0, y: 0, width: 0, height: 0, data: new Uint8ClampedArray(0) }, options);
|
|
10377
|
+
const b = sel.bounds;
|
|
10378
|
+
const m = Math.max(8, Math.ceil(options.band ?? Math.max(2, Math.min(width, height) / 200)) + 4);
|
|
10379
|
+
const x = Math.max(0, b.x - m);
|
|
10380
|
+
const y = Math.max(0, b.y - m);
|
|
10381
|
+
const region = { x, y, width: Math.min(width, b.x + b.width + m) - x, height: Math.min(height, b.y + b.height + m) - y };
|
|
10382
|
+
return selectionUncertainty(sel, { ...this.compositePixels(region), x, y }, options);
|
|
10383
|
+
}
|
|
10212
10384
|
/** A layer's painted mask over the canvas as a selection mask; null without one (or for a depth mask). */
|
|
10213
10385
|
layerMaskCoverage(layerId) {
|
|
10214
10386
|
const layer = this.getLayer(layerId);
|
|
@@ -12921,6 +13093,10 @@ function rectOf(args) {
|
|
|
12921
13093
|
if (rect2.width <= 0 || rect2.height <= 0) throw new Error("width and height must be greater than 0.");
|
|
12922
13094
|
return rect2;
|
|
12923
13095
|
}
|
|
13096
|
+
function uncertainty(controller) {
|
|
13097
|
+
const u = controller.selectionUncertainty();
|
|
13098
|
+
return { uncertainRegions: u.regions, uncertainCount: u.count, edgeConfidence: u.confidence };
|
|
13099
|
+
}
|
|
12924
13100
|
function summary(controller) {
|
|
12925
13101
|
const sel = controller.getSelection();
|
|
12926
13102
|
const { width, height } = controller.getState().imageDimensions;
|
|
@@ -13145,7 +13321,14 @@ function createImageSelectionTools(controller) {
|
|
|
13145
13321
|
additionalProperties: false
|
|
13146
13322
|
}
|
|
13147
13323
|
},
|
|
13148
|
-
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)." }
|
|
13324
|
+
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)." },
|
|
13325
|
+
regions: {
|
|
13326
|
+
type: "array",
|
|
13327
|
+
minItems: 1,
|
|
13328
|
+
maxItems: 50,
|
|
13329
|
+
items: rectSchemaStrict,
|
|
13330
|
+
description: "Refine only inside these rectangles (natural image pixels), e.g. the uncertainRegions bounds a smart selection returned; the rest of the selection stays as it is. Default: everywhere."
|
|
13331
|
+
}
|
|
13149
13332
|
},
|
|
13150
13333
|
additionalProperties: false
|
|
13151
13334
|
},
|
|
@@ -13176,7 +13359,8 @@ function createImageSelectionTools(controller) {
|
|
|
13176
13359
|
layerId: typeof params.layerId === "string" ? params.layerId : void 0,
|
|
13177
13360
|
decontaminate: num2(params.decontaminate, 0),
|
|
13178
13361
|
edits,
|
|
13179
|
-
refineMode: params.refineMode === "object" ? "object" : "color"
|
|
13362
|
+
refineMode: params.refineMode === "object" ? "object" : "color",
|
|
13363
|
+
...params.regions !== void 0 ? { regions: params.regions } : {}
|
|
13180
13364
|
}
|
|
13181
13365
|
);
|
|
13182
13366
|
return { ...summary(controller), ...result };
|
|
@@ -13332,7 +13516,7 @@ function createImageSelectionTools(controller) {
|
|
|
13332
13516
|
const quickSelectTool = {
|
|
13333
13517
|
name: "quick_select",
|
|
13334
13518
|
title: "Quick Selection",
|
|
13335
|
-
description: "Quick Selection: paints brush strokes (paths in natural image pixels) that each grow into the region they are painted on, out to its edges (a graph cut on colour and edges), combined with the selection by mode. With replace (default) the first stroke replaces the selection and later ones add, as the brush does; use mode subtract to take an area back out. Best for a subject or area that differs from its surroundings: paint inside it, away from the edge, then check with get_selection and capture_view and add or subtract strokes where it leaked or fell short. All strokes are one undoable step. Returns the selection.",
|
|
13519
|
+
description: "Quick Selection: paints brush strokes (paths in natural image pixels) that each grow into the region they are painted on, out to its edges (a graph cut on colour and edges), combined with the selection by mode. With replace (default) the first stroke replaces the selection and later ones add, as the brush does; use mode subtract to take an area back out. Best for a subject or area that differs from its surroundings: paint inside it, away from the edge, then check with get_selection and capture_view and add or subtract strokes where it leaked or fell short. All strokes are one undoable step. Returns the selection. Also returns uncertainRegions (bounds, area, confidence 0-1; up to 20, least confident first), uncertainCount and edgeConfidence: where the edge has no image edge under it. Look at those with capture_view and fix only them (refine_selection with regions).",
|
|
13336
13520
|
inputSchema: {
|
|
13337
13521
|
type: "object",
|
|
13338
13522
|
properties: {
|
|
@@ -13363,13 +13547,13 @@ function createImageSelectionTools(controller) {
|
|
|
13363
13547
|
enhanceEdge: args.enhanceEdge,
|
|
13364
13548
|
signal: context?.signal
|
|
13365
13549
|
});
|
|
13366
|
-
return summary(controller);
|
|
13550
|
+
return { ...summary(controller), ...uncertainty(controller) };
|
|
13367
13551
|
}
|
|
13368
13552
|
};
|
|
13369
13553
|
const selectObjectTool = {
|
|
13370
13554
|
name: "select_object",
|
|
13371
13555
|
title: "Object Selection",
|
|
13372
|
-
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").',
|
|
13556
|
+
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"). Also returns uncertainRegions (bounds, area, confidence 0-1; up to 20, least confident first), uncertainCount and edgeConfidence: where the edge has no image edge under it. Look at those with capture_view and fix only them (refine_selection with regions).',
|
|
13373
13557
|
inputSchema: {
|
|
13374
13558
|
type: "object",
|
|
13375
13559
|
properties: {
|
|
@@ -13402,7 +13586,7 @@ function createImageSelectionTools(controller) {
|
|
|
13402
13586
|
signal: context?.signal
|
|
13403
13587
|
};
|
|
13404
13588
|
const { method } = rect2 ? await controller.selectObject({ rect: { x: num2(rect2.x, NaN), y: num2(rect2.y, NaN), width: num2(rect2.width, NaN), height: num2(rect2.height, NaN) } }, options) : await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
|
|
13405
|
-
return { ...summary(controller), method };
|
|
13589
|
+
return { ...summary(controller), method, ...uncertainty(controller) };
|
|
13406
13590
|
}
|
|
13407
13591
|
};
|
|
13408
13592
|
const selectFocusAreaTool = {
|
|
@@ -13436,7 +13620,7 @@ function createImageSelectionTools(controller) {
|
|
|
13436
13620
|
const selectSkyTool = {
|
|
13437
13621
|
name: "select_sky",
|
|
13438
13622
|
title: "Select the sky",
|
|
13439
|
-
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).',
|
|
13623
|
+
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). Also returns uncertainRegions (bounds, area, confidence 0-1; up to 20, least confident first), uncertainCount and edgeConfidence: where the edge has no image edge under it. Look at those with capture_view and fix only them (refine_selection with regions).',
|
|
13440
13624
|
inputSchema: {
|
|
13441
13625
|
type: "object",
|
|
13442
13626
|
properties: {
|
|
@@ -13458,7 +13642,7 @@ function createImageSelectionTools(controller) {
|
|
|
13458
13642
|
useModel: args.useModel,
|
|
13459
13643
|
signal: context?.signal
|
|
13460
13644
|
});
|
|
13461
|
-
return { ...summary(controller), method, ...note ? { note } : {} };
|
|
13645
|
+
return { ...summary(controller), method, ...note ? { note } : {}, ...uncertainty(controller) };
|
|
13462
13646
|
}
|
|
13463
13647
|
};
|
|
13464
13648
|
const magicWandTool = {
|
|
@@ -14063,7 +14247,7 @@ function createImageSubjectSelectionTool(controller) {
|
|
|
14063
14247
|
return {
|
|
14064
14248
|
name: "select_subject",
|
|
14065
14249
|
title: "Select a subject",
|
|
14066
|
-
description: 'Select \u203A Subject: selects the main subject of the flattened image, combined with the selection by mode. With a segmentation model set by the host, target (a description such as "the dog" or "the house") chooses which objects; describe one object precisely to get only that one. Without a model, the subject that stands out most from the background is found classically (superpixel saliency and GrabCut): target cannot choose it, and method "saliency" and a note say so. Then check it with capture_view and fix it with quick_select or select_object. One undoable step. Returns the selection, the objects found (label, bounds), method ("model" or "saliency") and any note; none found leaves the selection unchanged.',
|
|
14250
|
+
description: 'Select \u203A Subject: selects the main subject of the flattened image, combined with the selection by mode. With a segmentation model set by the host, target (a description such as "the dog" or "the house") chooses which objects; describe one object precisely to get only that one. Without a model, the subject that stands out most from the background is found classically (superpixel saliency and GrabCut): target cannot choose it, and method "saliency" and a note say so. Then check it with capture_view and fix it with quick_select or select_object. One undoable step. Returns the selection, the objects found (label, bounds), method ("model" or "saliency") and any note; none found leaves the selection unchanged. Also returns uncertainRegions (bounds, area, confidence 0-1; up to 20, least confident first), uncertainCount and edgeConfidence: where the edge has no image edge under it. Look at those with capture_view and fix only them (refine_selection with regions).',
|
|
14067
14251
|
inputSchema: {
|
|
14068
14252
|
type: "object",
|
|
14069
14253
|
properties: {
|
|
@@ -14094,7 +14278,7 @@ function createImageSubjectSelectionTool(controller) {
|
|
|
14094
14278
|
// Under playback the cursor traces what the model found, as a lasso, before it is selected.
|
|
14095
14279
|
beforeApply: perform ? (outlines) => perform(planOutlineTrace(outlines)) : void 0
|
|
14096
14280
|
});
|
|
14097
|
-
return { ...summary(controller), found: segments, method, ...note ? { note } : {} };
|
|
14281
|
+
return { ...summary(controller), found: segments, method, ...note ? { note } : {}, ...uncertainty(controller) };
|
|
14098
14282
|
}
|
|
14099
14283
|
};
|
|
14100
14284
|
}
|
|
@@ -22070,6 +22254,7 @@ export {
|
|
|
22070
22254
|
createAdjustmentLayer,
|
|
22071
22255
|
createBlenderScratch,
|
|
22072
22256
|
createCanvasTextShaper,
|
|
22257
|
+
createGradientSampler,
|
|
22073
22258
|
createImageCaptureSurface,
|
|
22074
22259
|
createImageColorTools,
|
|
22075
22260
|
createImageEditTools,
|
|
@@ -22597,6 +22782,7 @@ export {
|
|
|
22597
22782
|
selectionToCoverage,
|
|
22598
22783
|
selectionToPixels,
|
|
22599
22784
|
selectionToSubpaths,
|
|
22785
|
+
selectionUncertainty,
|
|
22600
22786
|
selectionValueAt,
|
|
22601
22787
|
serializeChannels,
|
|
22602
22788
|
serializeColorMode,
|
|
@@ -68,3 +68,12 @@ export declare function inspectMaskEdge(mask: SelectionMask, image: PixelBuffer
|
|
|
68
68
|
x: number;
|
|
69
69
|
y: number;
|
|
70
70
|
}, options?: InspectMaskEdgeOptions): MaskEdgeReport;
|
|
71
|
+
/**
|
|
72
|
+
* A cached sampler of the image's edge strength at document pixels: the Sobel magnitude of
|
|
73
|
+
* alpha-weighted R, G and B, the largest of the three, as a fraction of a full 0–255 step.
|
|
74
|
+
* Reads clamp at the image's edges. `W` is the canvas width (for the cache key).
|
|
75
|
+
*/
|
|
76
|
+
export declare function createGradientSampler(image: PixelBuffer & {
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
}, W: number): (x: number, y: number) => number;
|
|
@@ -68,3 +68,12 @@ export declare function inspectMaskEdge(mask: SelectionMask, image: PixelBuffer
|
|
|
68
68
|
x: number;
|
|
69
69
|
y: number;
|
|
70
70
|
}, options?: InspectMaskEdgeOptions): MaskEdgeReport;
|
|
71
|
+
/**
|
|
72
|
+
* A cached sampler of the image's edge strength at document pixels: the Sobel magnitude of
|
|
73
|
+
* alpha-weighted R, G and B, the largest of the three, as a fraction of a full 0–255 step.
|
|
74
|
+
* Reads clamp at the image's edges. `W` is the canvas width (for the cache key).
|
|
75
|
+
*/
|
|
76
|
+
export declare function createGradientSampler(image: PixelBuffer & {
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
}, W: number): (x: number, y: number) => number;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a selection is unsure of itself (a trimap's unknown band): the stretches of its edge that
|
|
3
|
+
* run where the image shows no edge, widened into a band. The smart selection tools report these
|
|
4
|
+
* as `uncertainRegions`, so an agent looks at and refines only them (`refine_selection` `regions`).
|
|
5
|
+
*/
|
|
6
|
+
import type { CropBox } from '../types.cjs';
|
|
7
|
+
import { type PixelBuffer, type SelectionMask } from './mask.cjs';
|
|
8
|
+
export interface UncertainRegion {
|
|
9
|
+
bounds: CropBox;
|
|
10
|
+
/** Pixels in the unknown band. */
|
|
11
|
+
area: number;
|
|
12
|
+
/** 0–1: how strongly the image backs the edge here (low = unsure). */
|
|
13
|
+
confidence: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SelectionUncertainty {
|
|
16
|
+
/** The unknown band over the canvas (255 = unsure). */
|
|
17
|
+
unknown: SelectionMask;
|
|
18
|
+
/** The least confident regions first (at most `maxRegions`). */
|
|
19
|
+
regions: UncertainRegion[];
|
|
20
|
+
/** How many regions there are in all. */
|
|
21
|
+
count: number;
|
|
22
|
+
/** 0–1 over the whole edge. */
|
|
23
|
+
confidence: number;
|
|
24
|
+
}
|
|
25
|
+
export interface SelectionUncertaintyOptions {
|
|
26
|
+
/** Half-width of the unknown band in px (default 1/200 of the shorter side, at least 2). */
|
|
27
|
+
band?: number;
|
|
28
|
+
/** Regions smaller than this many pixels are left out (default 16). */
|
|
29
|
+
minArea?: number;
|
|
30
|
+
/** At most this many regions are listed (default 20). */
|
|
31
|
+
maxRegions?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The unknown band of `mask` against `image` (RGBA, straight alpha, at `image.x`, `image.y` in
|
|
35
|
+
* document pixels). An empty selection has no band.
|
|
36
|
+
*/
|
|
37
|
+
export declare function selectionUncertainty(mask: SelectionMask, image: PixelBuffer & {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
}, options?: SelectionUncertaintyOptions): SelectionUncertainty;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a selection is unsure of itself (a trimap's unknown band): the stretches of its edge that
|
|
3
|
+
* run where the image shows no edge, widened into a band. The smart selection tools report these
|
|
4
|
+
* as `uncertainRegions`, so an agent looks at and refines only them (`refine_selection` `regions`).
|
|
5
|
+
*/
|
|
6
|
+
import type { CropBox } from '../types.js';
|
|
7
|
+
import { type PixelBuffer, type SelectionMask } from './mask.js';
|
|
8
|
+
export interface UncertainRegion {
|
|
9
|
+
bounds: CropBox;
|
|
10
|
+
/** Pixels in the unknown band. */
|
|
11
|
+
area: number;
|
|
12
|
+
/** 0–1: how strongly the image backs the edge here (low = unsure). */
|
|
13
|
+
confidence: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SelectionUncertainty {
|
|
16
|
+
/** The unknown band over the canvas (255 = unsure). */
|
|
17
|
+
unknown: SelectionMask;
|
|
18
|
+
/** The least confident regions first (at most `maxRegions`). */
|
|
19
|
+
regions: UncertainRegion[];
|
|
20
|
+
/** How many regions there are in all. */
|
|
21
|
+
count: number;
|
|
22
|
+
/** 0–1 over the whole edge. */
|
|
23
|
+
confidence: number;
|
|
24
|
+
}
|
|
25
|
+
export interface SelectionUncertaintyOptions {
|
|
26
|
+
/** Half-width of the unknown band in px (default 1/200 of the shorter side, at least 2). */
|
|
27
|
+
band?: number;
|
|
28
|
+
/** Regions smaller than this many pixels are left out (default 16). */
|
|
29
|
+
minArea?: number;
|
|
30
|
+
/** At most this many regions are listed (default 20). */
|
|
31
|
+
maxRegions?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The unknown band of `mask` against `image` (RGBA, straight alpha, at `image.x`, `image.y` in
|
|
35
|
+
* document pixels). An empty selection has no band.
|
|
36
|
+
*/
|
|
37
|
+
export declare function selectionUncertainty(mask: SelectionMask, image: PixelBuffer & {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
}, options?: SelectionUncertaintyOptions): SelectionUncertainty;
|
package/package.json
CHANGED
|
@@ -258,7 +258,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
|
|
|
258
258
|
| `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. |
|
|
259
259
|
| `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. |
|
|
260
260
|
| `reselect` | Bring back the last selection `deselect` cleared (one undo step). Fails when there is none. Returns the summary. |
|
|
261
|
-
| `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`, `new-layer-with-mask` (on `layerId`, default active; new layers hide the source), or `new-document` / `new-document-with-mask` (host must open documents; returns `documentId`), with `decontaminate` 0..100 for new layers and documents. `edits` (`quick-select`, `brush`, `lasso`, `object` 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. |
|
|
261
|
+
| `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`, `new-layer-with-mask` (on `layerId`, default active; new layers hide the source), or `new-document` / `new-document-with-mask` (host must open documents; returns `documentId`), with `decontaminate` 0..100 for new layers and documents. `edits` (`quick-select`, `brush`, `lasso`, `object` add/subtract; `refine-brush` over hair) apply first; `refineMode: "object"` uses the host model; `regions` (rects, e.g. `uncertainRegions` bounds) limits it to those. One step; returns `refineMode`, `layerId` / `newLayerId`. Use on hair, fur, foliage or a coarse AI mask. |
|
|
262
262
|
| `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. |
|
|
263
263
|
| `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. |
|
|
264
264
|
| `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. |
|
|
@@ -43,7 +43,8 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
|
|
|
43
43
|
2. **Build it in steps** with `mode`: start with `replace`, `add` what was missed, `subtract`
|
|
44
44
|
what leaked, and `intersect` to limit it to a region (for example `select_rect` around the
|
|
45
45
|
subject).
|
|
46
|
-
**Check:** `bounds` hug the subject. A bounds as large as the image means a leak.
|
|
46
|
+
**Check:** `bounds` hug the subject. A bounds as large as the image means a leak. Smart tools
|
|
47
|
+
return `uncertainRegions` (edge with no image edge under it): look there, refine with `regions`.
|
|
47
48
|
3. **Measure the edge, then look.** `inspect_mask_edge` scores it: `haloScore` ≥ 0.35 is old
|
|
48
49
|
background inside the edge, `edgeAlignment` < 0.5 is off the image's edges, `jaggedness` ≥ 15
|
|
49
50
|
zigzags; it counts `holes` and `islands`, `issues` names fixes, `worst` gives `capture_view` rects.
|
|
@@ -210,13 +211,12 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
|
|
|
210
211
|
|
|
211
212
|
- `get_selection`: empty, bounds, area and percent after every step
|
|
212
213
|
- `select_all`, `deselect`, `invert_selection`: whole-image selections
|
|
213
|
-
- `select_rect`, `select_ellipse`: marquees
|
|
214
|
+
- `select_rect`, `select_ellipse`, `select_line`: marquees, and a single row or column
|
|
214
215
|
- `select_polygon`: the lasso, along a contour; with `snapToEdges`, the Magnetic Lasso
|
|
215
216
|
- `quick_select`: brush strokes that grow out to the region's edges
|
|
216
217
|
- `select_object`: the object in a box or under a point
|
|
217
218
|
- `select_focus_area`: what is in focus
|
|
218
219
|
- `select_sky`: the sky
|
|
219
|
-
- `select_line`: a single row or column
|
|
220
220
|
- `select_layer_transparency`: a layer's own pixels
|
|
221
221
|
- `grow_selection`, `select_similar`: add similar colour, connected or anywhere
|
|
222
222
|
- `reselect`: undo a deselect
|