@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.
@@ -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 premul = (x, y, c) => {
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
- const refined = refineSelection(pixels, sel, params, edited.refineBrush);
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,
@@ -18,3 +18,4 @@ export * from './refine-edits.cjs';
18
18
  export * from './select-and-mask.cjs';
19
19
  export * from './worker.cjs';
20
20
  export * from './inspect.cjs';
21
+ export * from './uncertainty.cjs';
@@ -18,3 +18,4 @@ export * from './refine-edits.js';
18
18
  export * from './select-and-mask.js';
19
19
  export * from './worker.js';
20
20
  export * from './inspect.js';
21
+ export * from './uncertainty.js';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.215.0",
3
+ "version": "0.216.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -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