@pixerate/editor 0.198.0 → 0.199.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.
@@ -3672,6 +3672,209 @@ function selectObjectIn(pixels, target, options = {}) {
3672
3672
  }
3673
3673
  }
3674
3674
 
3675
+ // src/image-editor/selection/segment/subject.ts
3676
+ function toLab(r, g, b) {
3677
+ const lin = (v) => {
3678
+ v /= 255;
3679
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
3680
+ };
3681
+ const R = lin(r);
3682
+ const G = lin(g);
3683
+ const B = lin(b);
3684
+ const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
3685
+ const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
3686
+ const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
3687
+ const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
3688
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
3689
+ }
3690
+ function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
3691
+ const { width: w, height: h, rgb } = work;
3692
+ const n = w * h;
3693
+ const lab = new Float32Array(n * 3);
3694
+ for (let i = 0; i < n; i++) lab.set(toLab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]), i * 3);
3695
+ const S = Math.max(2, Math.round(Math.sqrt(n / target)));
3696
+ const seeds = [];
3697
+ for (let y = Math.floor(S / 2); y < h; y += S) {
3698
+ for (let x = Math.floor(S / 2); x < w; x += S) {
3699
+ const i = y * w + x;
3700
+ seeds.push([lab[i * 3], lab[i * 3 + 1], lab[i * 3 + 2], x, y]);
3701
+ }
3702
+ }
3703
+ const k = seeds.length;
3704
+ const label = new Int32Array(n).fill(-1);
3705
+ const best = new Float64Array(n);
3706
+ const m2 = (compactness / S) ** 2;
3707
+ for (let round6 = 0; round6 < rounds; round6++) {
3708
+ best.fill(Infinity);
3709
+ for (let c = 0; c < k; c++) {
3710
+ const [L, A, B, cx, cy] = seeds[c];
3711
+ const x0 = Math.max(0, Math.floor(cx - S));
3712
+ const x1 = Math.min(w - 1, Math.ceil(cx + S));
3713
+ const y0 = Math.max(0, Math.floor(cy - S));
3714
+ const y1 = Math.min(h - 1, Math.ceil(cy + S));
3715
+ for (let y = y0; y <= y1; y++) {
3716
+ for (let x = x0; x <= x1; x++) {
3717
+ const i = y * w + x;
3718
+ const d = (lab[i * 3] - L) ** 2 + (lab[i * 3 + 1] - A) ** 2 + (lab[i * 3 + 2] - B) ** 2 + m2 * ((x - cx) ** 2 + (y - cy) ** 2);
3719
+ if (d < best[i]) {
3720
+ best[i] = d;
3721
+ label[i] = c;
3722
+ }
3723
+ }
3724
+ }
3725
+ }
3726
+ const sum = new Float64Array(k * 6);
3727
+ for (let i = 0; i < n; i++) {
3728
+ const c = label[i];
3729
+ sum[c * 6] += lab[i * 3];
3730
+ sum[c * 6 + 1] += lab[i * 3 + 1];
3731
+ sum[c * 6 + 2] += lab[i * 3 + 2];
3732
+ sum[c * 6 + 3] += i % w;
3733
+ sum[c * 6 + 4] += Math.floor(i / w);
3734
+ sum[c * 6 + 5]++;
3735
+ }
3736
+ for (let c = 0; c < k; c++) {
3737
+ const s = sum[c * 6 + 5];
3738
+ if (s) seeds[c] = [sum[c * 6] / s, sum[c * 6 + 1] / s, sum[c * 6 + 2] / s, sum[c * 6 + 3] / s, sum[c * 6 + 4] / s];
3739
+ }
3740
+ }
3741
+ const remap = new Int32Array(k).fill(-1);
3742
+ let count = 0;
3743
+ for (let i = 0; i < n; i++) if (remap[label[i]] < 0) remap[label[i]] = count++;
3744
+ const meanLab = new Float64Array(count * 3);
3745
+ const centre = new Float64Array(count * 2);
3746
+ const size = new Int32Array(count);
3747
+ for (let i = 0; i < n; i++) {
3748
+ const c = label[i] = remap[label[i]];
3749
+ meanLab[c * 3] += lab[i * 3];
3750
+ meanLab[c * 3 + 1] += lab[i * 3 + 1];
3751
+ meanLab[c * 3 + 2] += lab[i * 3 + 2];
3752
+ centre[c * 2] += i % w;
3753
+ centre[c * 2 + 1] += Math.floor(i / w);
3754
+ size[c]++;
3755
+ }
3756
+ for (let c = 0; c < count; c++) {
3757
+ for (let j = 0; j < 3; j++) meanLab[c * 3 + j] /= size[c];
3758
+ centre[c * 2] /= size[c];
3759
+ centre[c * 2 + 1] /= size[c];
3760
+ }
3761
+ return { label, count, lab: meanLab, centre, size };
3762
+ }
3763
+ function superpixelSaliency(sp, width, height) {
3764
+ const { count, lab, centre, size, label } = sp;
3765
+ const dist = (a, b) => Math.hypot(lab[a * 3] - lab[b * 3], lab[a * 3 + 1] - lab[b * 3 + 1], lab[a * 3 + 2] - lab[b * 3 + 2]);
3766
+ const onBorder = new Uint8Array(count);
3767
+ for (let x = 0; x < width; x++) {
3768
+ onBorder[label[x]] = 1;
3769
+ onBorder[label[(height - 1) * width + x]] = 1;
3770
+ }
3771
+ for (let y = 0; y < height; y++) {
3772
+ onBorder[label[y * width]] = 1;
3773
+ onBorder[label[y * width + width - 1]] = 1;
3774
+ }
3775
+ const border = [];
3776
+ for (let c = 0; c < count; c++) if (onBorder[c]) border.push(c);
3777
+ const diag = Math.hypot(width, height);
3778
+ const unlikeBorder = new Float64Array(count);
3779
+ const contrast = new Float64Array(count);
3780
+ const centreBias = new Float64Array(count);
3781
+ for (let i = 0; i < count; i++) {
3782
+ const ds = border.map((j) => dist(i, j)).sort((a, b) => a - b);
3783
+ unlikeBorder[i] = ds.length ? ds[Math.floor(ds.length * 0.25)] : 0;
3784
+ let s = 0;
3785
+ let wsum = 0;
3786
+ for (let j = 0; j < count; j++) {
3787
+ if (j === i) continue;
3788
+ const dx = (centre[i * 2] - centre[j * 2]) / diag;
3789
+ const dy = (centre[i * 2 + 1] - centre[j * 2 + 1]) / diag;
3790
+ const wgt = size[j] * Math.exp(-(dx * dx + dy * dy) / (2 * 0.25 * 0.25));
3791
+ s += wgt * dist(i, j);
3792
+ wsum += wgt;
3793
+ }
3794
+ contrast[i] = wsum ? s / wsum : 0;
3795
+ const cx = centre[i * 2] / width - 0.5;
3796
+ const cy = centre[i * 2 + 1] / height - 0.5;
3797
+ centreBias[i] = 0.4 + 0.6 * Math.exp(-(cx * cx + cy * cy) / (2 * 0.3 * 0.3));
3798
+ }
3799
+ let farthest = 0;
3800
+ for (const v of unlikeBorder) farthest = Math.max(farthest, v);
3801
+ if (farthest < 6) return new Float64Array(count);
3802
+ const norm = (a) => {
3803
+ let max = 0;
3804
+ for (const v of a) max = Math.max(max, v);
3805
+ if (max > 0) for (let i = 0; i < a.length; i++) a[i] /= max;
3806
+ };
3807
+ norm(unlikeBorder);
3808
+ norm(contrast);
3809
+ const out = new Float64Array(count);
3810
+ for (let i = 0; i < count; i++) out[i] = unlikeBorder[i] * (0.3 + 0.7 * contrast[i]) * centreBias[i];
3811
+ norm(out);
3812
+ return out;
3813
+ }
3814
+ function otsuThreshold(values, weights) {
3815
+ const BINS2 = 64;
3816
+ const hist = new Float64Array(BINS2);
3817
+ let total = 0;
3818
+ for (let i = 0; i < values.length; i++) {
3819
+ hist[Math.min(BINS2 - 1, Math.floor(values[i] * BINS2))] += weights[i];
3820
+ total += weights[i];
3821
+ }
3822
+ let sumAll = 0;
3823
+ for (let b = 0; b < BINS2; b++) sumAll += b * hist[b];
3824
+ let wB = 0;
3825
+ let sumB = 0;
3826
+ let best = 0;
3827
+ let bestT = 0.5;
3828
+ for (let b = 0; b < BINS2 - 1; b++) {
3829
+ wB += hist[b];
3830
+ if (!wB) continue;
3831
+ const wF = total - wB;
3832
+ if (!wF) break;
3833
+ sumB += b * hist[b];
3834
+ const between = wB * wF * (sumB / wB - (sumAll - sumB) / wF) ** 2;
3835
+ if (between > best) {
3836
+ best = between;
3837
+ bestT = (b + 1) / BINS2;
3838
+ }
3839
+ }
3840
+ return bestT;
3841
+ }
3842
+ function findSubject(pixels, options = {}) {
3843
+ const work = workImage(pixels);
3844
+ const { width: w, height: h } = work;
3845
+ const sp = slicSuperpixels(work);
3846
+ const sal = superpixelSaliency(sp, w, h);
3847
+ const t = otsuThreshold(sal, sp.size);
3848
+ const n = w * h;
3849
+ const initial = new Uint8Array(n);
3850
+ const hardBg = new Uint8Array(n);
3851
+ const hardFg = new Uint8Array(n);
3852
+ const strong = t + 0.6 * (1 - t);
3853
+ for (let i = 0; i < n; i++) {
3854
+ const s = sal[sp.label[i]];
3855
+ if (s >= t) initial[i] = 1;
3856
+ if (s >= strong) hardFg[i] = 1;
3857
+ }
3858
+ for (let x = 0; x < w; x++) {
3859
+ for (const y of [0, h - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
3860
+ }
3861
+ for (let y = 0; y < h; y++) {
3862
+ for (const x of [0, w - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
3863
+ }
3864
+ const empty = (reason) => ({ selection: createSelectionMask(pixels.width, pixels.height), reason });
3865
+ let guessed = 0;
3866
+ for (let i = 0; i < n; i++) guessed += initial[i];
3867
+ if (!guessed || guessed > n * 0.95) return empty("Nothing stands out from the background.");
3868
+ const roi = { x: 0, y: 0, width: w, height: h };
3869
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, 5);
3870
+ keepMainPieces(label, w, h, null);
3871
+ let kept = 0;
3872
+ for (let i = 0; i < n; i++) kept += label[i];
3873
+ if (kept < n * 2e-3) return empty("Nothing stands out from the background.");
3874
+ if (kept > n * 0.97) return empty("The whole image looks like one subject; select it with select_all instead.");
3875
+ return { selection: upscaleRegion(work, pixels, roi, label, options.enhanceEdge ?? false) };
3876
+ }
3877
+
3675
3878
  // src/image-editor/layers/shape-type-ui.ts
3676
3879
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
3677
3880
  kind: "rect",
@@ -6459,7 +6662,7 @@ var ImageEditorController = class {
6459
6662
  this.state.layers = next;
6460
6663
  this.notify();
6461
6664
  }
6462
- /** The registered segmentation provider, or `null` (then `selectSubject` is unavailable). */
6665
+ /** The registered segmentation provider, or `null` (then `selectSubject` finds the most salient subject without a model). */
6463
6666
  get segmentationProvider() {
6464
6667
  return this.segmentation;
6465
6668
  }
@@ -9831,11 +10034,24 @@ var ImageEditorController = class {
9831
10034
  * Select › Subject: asks the segmentation provider for the objects matching `target` ("the
9832
10035
  * dog", "the sky") in the flattened image and selects their shapes, combined with the
9833
10036
  * selection by `mode`, as one undo step. Returns what was found (labels and bounds in natural
9834
- * pixels); an empty list leaves the selection as it was. Throws without a provider.
10037
+ * pixels); an empty list leaves the selection as it was. Without a provider, the subject that
10038
+ * stands out most is found classically (superpixels, saliency, Otsu, GrabCut): `target` can't
10039
+ * choose it, and `method: 'saliency'` and a `note` say so.
9835
10040
  */
9836
- async selectSubject(target, options = {}) {
10041
+ async selectSubject(target = "", options = {}) {
9837
10042
  const provider = this.segmentation;
9838
- if (!provider) throw new Error("Select Subject needs a segmentation provider (setSegmentationProvider).");
10043
+ if (!provider) {
10044
+ const pixels = await this.requirePixels({});
10045
+ options.signal?.throwIfAborted();
10046
+ const { selection, reason } = findSubject(pixels, { enhanceEdge: options.enhanceEdge });
10047
+ if (selection.bounds) this.shapeCommand(selection, options);
10048
+ const unused = target.trim() ? ` "${target.trim()}" was not used to choose it.` : "";
10049
+ return {
10050
+ segments: selection.bounds ? [{ label: "subject", bounds: selection.bounds }] : [],
10051
+ method: "saliency",
10052
+ note: reason ?? `No segmentation model is set, so the subject that stands out most was selected.${unused}`
10053
+ };
10054
+ }
9839
10055
  const description = target.trim();
9840
10056
  if (!description) throw new Error("Describe the subject to select.");
9841
10057
  const { width, height } = this.naturalSize;
@@ -9861,7 +10077,7 @@ var ImageEditorController = class {
9861
10077
  options.signal?.throwIfAborted();
9862
10078
  }
9863
10079
  if (mask) this.shapeCommand(mask, options);
9864
- return { segments: segments.filter((s) => s.bounds) };
10080
+ return { segments: segments.filter((s) => s.bounds), method: "model" };
9865
10081
  }
9866
10082
  /** Quick Mask view (tints the unselected area). View state: no undo step. */
9867
10083
  setQuickMask(on) {
@@ -11156,7 +11372,7 @@ var ImageEditorController = class {
11156
11372
  }
11157
11373
  if (src.colorSpace !== "rgb") throw new LayerOperationError("Colours are given as #rrggbb, so the source profile must be RGB.");
11158
11374
  const t = Transform.create(src, dst, { intent: options.intent ?? "relative", bpc: options.bpc ?? true });
11159
- const toLab = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
11375
+ const toLab2 = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
11160
11376
  return colors.map((input) => {
11161
11377
  const m = /^#?([0-9a-f]{6})$/i.exec(input.trim());
11162
11378
  if (!m) throw new LayerOperationError(`Invalid colour "${input}"; use #rrggbb.`);
@@ -11166,7 +11382,7 @@ var ImageEditorController = class {
11166
11382
  t.eval(rgb, out);
11167
11383
  const values = Array.from(out.subarray(0, t.outputs), (v) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4);
11168
11384
  const l = new Float32Array(3);
11169
- toLab.eval(rgb, l);
11385
+ toLab2.eval(rgb, l);
11170
11386
  const lab = [Math.round(l[0] * 1e4) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];
11171
11387
  const res = { input, values, lab };
11172
11388
  if (dst.colorSpace === "rgb") res.hex = `#${values.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("")}`;
@@ -13738,6 +13954,7 @@ function createImageSelectionTools(controller) {
13738
13954
  selectPolygonTool,
13739
13955
  quickSelectTool,
13740
13956
  selectObjectTool,
13957
+ createImageSubjectSelectionTool(controller),
13741
13958
  lineTool,
13742
13959
  magicWandTool,
13743
13960
  colorRangeTool,
@@ -13774,49 +13991,50 @@ function createImageSubjectSelectionTool(controller) {
13774
13991
  return {
13775
13992
  name: "select_subject",
13776
13993
  title: "Select a subject",
13777
- description: 'Select \u203A Subject: an AI segmentation model finds the objects matching target (a description such as "the dog", "the house", "the sky") in the flattened image and selects their outlines, combined with the selection by mode. Use it to cut a subject out by its contour (then layer_via_copy for a layer of just that subject); describe one object precisely to get only that one. One undoable step. Returns the selection and the objects found (label, bounds); none found leaves the selection unchanged.',
13994
+ 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.',
13778
13995
  inputSchema: {
13779
13996
  type: "object",
13780
13997
  properties: {
13781
- target: { type: "string", minLength: 1, description: "What to select, in words." },
13998
+ target: { type: "string", description: "What to select, in words. Needed with a model; ignored without one." },
13782
13999
  maxSegments: {
13783
14000
  type: "integer",
13784
14001
  minimum: 1,
13785
14002
  maximum: 32,
13786
- description: "Select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
14003
+ description: "With a model: select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
13787
14004
  },
13788
14005
  mode: modeSchema,
13789
- feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
14006
+ feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." },
14007
+ enhanceEdge: { type: "boolean", description: "Without a model: smoother, less blocky edges (Auto-Enhance; default false)." }
13790
14008
  },
13791
- required: ["target"],
13792
14009
  additionalProperties: false
13793
14010
  },
13794
14011
  annotations: { ...writeAnnotations("Select a subject"), openWorldHint: true },
13795
- handler: async ({ target, maxSegments, mode, feather }, context) => {
13796
- if (typeof target !== "string" || !target.trim()) throw new Error("target must describe what to select.");
14012
+ handler: async ({ target, maxSegments, mode, feather, enhanceEdge }, context) => {
14013
+ if (target !== void 0 && typeof target !== "string") throw new Error("target must be a description in words.");
14014
+ if (controller.segmentationProvider && !String(target ?? "").trim()) throw new Error("target must describe what to select (a model is set, and it selects by description).");
13797
14015
  const perform = context?.perform;
13798
- const { segments } = await controller.selectSubject(target, {
14016
+ const { segments, method, note } = await controller.selectSubject(String(target ?? ""), {
13799
14017
  mode: parseSelectionMode(mode),
13800
14018
  feather: num2(feather, 0),
13801
14019
  maxSegments: typeof maxSegments === "number" ? Math.max(1, Math.round(maxSegments)) : void 0,
14020
+ enhanceEdge: enhanceEdge === true,
13802
14021
  signal: context?.signal,
13803
14022
  // Under playback the cursor traces what the model found, as a lasso, before it is selected.
13804
14023
  beforeApply: perform ? (outlines) => perform(planOutlineTrace(outlines)) : void 0
13805
14024
  });
13806
- return { ...summary(controller), found: segments };
14025
+ return { ...summary(controller), found: segments, method, ...note ? { note } : {} };
13807
14026
  }
13808
14027
  };
13809
14028
  }
13810
14029
  function createImageSegmentationToolProvider(controller, options = {}) {
13811
- const tool = createImageSubjectSelectionTool(controller);
13812
14030
  return withImagePlayback(
13813
14031
  {
13814
14032
  id: options.id ?? "image-segmentation",
13815
14033
  namespace: options.namespace,
13816
14034
  title: "Image segmentation",
13817
- description: "Select \u203A Subject: select objects in the image by description through an AI segmentation model.",
14035
+ description: "Deprecated: select_subject is in the image editor tool provider.",
13818
14036
  skill: "pixerate-image-editor",
13819
- tools: () => controller.segmentationProvider ? [tool] : []
14037
+ tools: () => []
13820
14038
  },
13821
14039
  options.playbackSurface
13822
14040
  );
@@ -20450,6 +20668,7 @@ export {
20450
20668
  findAdjustmentControl,
20451
20669
  findLayer,
20452
20670
  findLayerControl,
20671
+ findSubject,
20453
20672
  findWebGpu,
20454
20673
  finishTile,
20455
20674
  fitParagraphRuns,
@@ -20471,6 +20690,7 @@ export {
20471
20690
  getViewportMetrics,
20472
20691
  getViewportProjection,
20473
20692
  globalImageCache,
20693
+ grabCutLabels,
20474
20694
  gradientDabColor,
20475
20695
  grainCatch,
20476
20696
  grainHeights,
@@ -20562,6 +20782,7 @@ export {
20562
20782
  isTransformAnchor,
20563
20783
  isValidWarp,
20564
20784
  keepDeepSamples,
20785
+ keepMainPieces,
20565
20786
  kelvinToTemperature,
20566
20787
  keysWeight,
20567
20788
  latticeViscosity,
@@ -20674,6 +20895,7 @@ export {
20674
20895
  offsetVectorMask,
20675
20896
  openMaskFile,
20676
20897
  openPsdFile,
20898
+ otsuThreshold,
20677
20899
  packRgba,
20678
20900
  packWetParams,
20679
20901
  paintGradientMappingFor,
@@ -20863,6 +21085,7 @@ export {
20863
21085
  shiftCoverage,
20864
21086
  simplifyPixelPath,
20865
21087
  skinToneRange,
21088
+ slicSuperpixels,
20866
21089
  smartFilterRows,
20867
21090
  smartObjectLayers,
20868
21091
  smoothCoverage,
@@ -20893,6 +21116,7 @@ export {
20893
21116
  styleAt,
20894
21117
  subpathsToSvgPath,
20895
21118
  sumHistograms,
21119
+ superpixelSaliency,
20896
21120
  surfaceBytes,
20897
21121
  surfaceDepth,
20898
21122
  surfaceFromDeepPixels,
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { CropBox, ImagePoint } from '../../types.cjs';
9
9
  import { type PixelBuffer, type SelectionMask } from '../mask.cjs';
10
+ import { type Rect, type WorkImage } from './work.cjs';
10
11
  /** A mixture of {@link K} full-covariance Gaussians over RGB. */
11
12
  export declare class ColorGmm {
12
13
  private readonly logWeight;
@@ -24,6 +25,23 @@ export declare class ColorGmm {
24
25
  /** The component most likely to have produced the colour. */
25
26
  nearest(r: number, g: number, b: number): number;
26
27
  }
28
+ /** Pixels fixed to one side, and a starting guess, over a window of the work image. */
29
+ export interface GrabCutSetup {
30
+ roi: Rect;
31
+ /** Per window pixel: 1 = starts inside the object. */
32
+ initial: Uint8Array;
33
+ /** Per window pixel: 1 = fixed outside. */
34
+ hardBg: Uint8Array;
35
+ /** Per window pixel: 1 = fixed inside. */
36
+ hardFg: Uint8Array | null;
37
+ }
38
+ /** GrabCut over the setup's window; returns 1 for the object per window pixel. */
39
+ export declare function grabCutLabels(work: WorkImage, setup: GrabCutSetup, iterations: number): Uint8Array;
40
+ /**
41
+ * Keeps the object's main parts: the pieces (4-connected) holding `seed` (window index), or
42
+ * without one, every piece at least a fifth the size of the largest.
43
+ */
44
+ export declare function keepMainPieces(label: Uint8Array, rw: number, rh: number, seed: number | null): void;
27
45
  export interface ObjectSelectionOptions {
28
46
  /** Smoother, less blocky edges (Auto-Enhance). Default false. */
29
47
  enhanceEdge?: boolean;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { CropBox, ImagePoint } from '../../types.js';
9
9
  import { type PixelBuffer, type SelectionMask } from '../mask.js';
10
+ import { type Rect, type WorkImage } from './work.js';
10
11
  /** A mixture of {@link K} full-covariance Gaussians over RGB. */
11
12
  export declare class ColorGmm {
12
13
  private readonly logWeight;
@@ -24,6 +25,23 @@ export declare class ColorGmm {
24
25
  /** The component most likely to have produced the colour. */
25
26
  nearest(r: number, g: number, b: number): number;
26
27
  }
28
+ /** Pixels fixed to one side, and a starting guess, over a window of the work image. */
29
+ export interface GrabCutSetup {
30
+ roi: Rect;
31
+ /** Per window pixel: 1 = starts inside the object. */
32
+ initial: Uint8Array;
33
+ /** Per window pixel: 1 = fixed outside. */
34
+ hardBg: Uint8Array;
35
+ /** Per window pixel: 1 = fixed inside. */
36
+ hardFg: Uint8Array | null;
37
+ }
38
+ /** GrabCut over the setup's window; returns 1 for the object per window pixel. */
39
+ export declare function grabCutLabels(work: WorkImage, setup: GrabCutSetup, iterations: number): Uint8Array;
40
+ /**
41
+ * Keeps the object's main parts: the pieces (4-connected) holding `seed` (window index), or
42
+ * without one, every piece at least a fifth the size of the largest.
43
+ */
44
+ export declare function keepMainPieces(label: Uint8Array, rw: number, rh: number, seed: number | null): void;
27
45
  export interface ObjectSelectionOptions {
28
46
  /** Smoother, less blocky edges (Auto-Enhance). Default false. */
29
47
  enhanceEdge?: boolean;
@@ -2,3 +2,4 @@ export * from './maxflow.cjs';
2
2
  export * from './work.cjs';
3
3
  export * from './quick.cjs';
4
4
  export * from './grabcut.cjs';
5
+ export * from './subject.cjs';
@@ -2,3 +2,4 @@ export * from './maxflow.js';
2
2
  export * from './work.js';
3
3
  export * from './quick.js';
4
4
  export * from './grabcut.js';
5
+ export * from './subject.js';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Select Subject without a segmentation model: the most salient subject, found classically.
3
+ *
4
+ * 1. SLIC superpixels (Achanta et al., 2012) group the reduced image into a few hundred patches of
5
+ * similar colour.
6
+ * 2. Each patch's saliency is how unlike the image's border it is (the border is mostly
7
+ * background), times how much it contrasts with the patches around it (region contrast, Cheng
8
+ * et al., 2015), times a mild preference for the centre.
9
+ * 3. Otsu's threshold splits the patches into a first guess at the subject.
10
+ * 4. GrabCut refines that guess along the image's edges, with the clearly-background border fixed.
11
+ *
12
+ * It cannot tell which object a description names: it selects what stands out.
13
+ */
14
+ import type { PixelBuffer, SelectionMask } from '../mask.cjs';
15
+ import { type WorkImage } from './work.cjs';
16
+ export interface Superpixels {
17
+ /** Superpixel of each work pixel. */
18
+ label: Int32Array;
19
+ count: number;
20
+ /** Mean L*a*b* per superpixel, 3 values each. */
21
+ lab: Float64Array;
22
+ /** Mean position per superpixel, in work pixels, 2 values each. */
23
+ centre: Float64Array;
24
+ size: Int32Array;
25
+ }
26
+ /** SLIC superpixels of the work image: about `target` of them, `compactness` 10 (L*a*b* units). */
27
+ export declare function slicSuperpixels(work: WorkImage, target?: number, compactness?: number, rounds?: number): Superpixels;
28
+ /** Saliency 0..1 of each superpixel (see the module comment). */
29
+ export declare function superpixelSaliency(sp: Superpixels, width: number, height: number): Float64Array;
30
+ /** Otsu's threshold of `values` (0..1) weighted by `weights`. */
31
+ export declare function otsuThreshold(values: ArrayLike<number>, weights: ArrayLike<number>): number;
32
+ /** What {@link findSubject} found. */
33
+ export interface FoundSubject {
34
+ selection: SelectionMask;
35
+ /** Why nothing was selected, when nothing was. */
36
+ reason?: string;
37
+ }
38
+ /**
39
+ * The most salient subject of `pixels` as a selection of the whole image (empty, with a reason,
40
+ * when nothing stands out from the background).
41
+ */
42
+ export declare function findSubject(pixels: PixelBuffer, options?: {
43
+ enhanceEdge?: boolean;
44
+ }): FoundSubject;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Select Subject without a segmentation model: the most salient subject, found classically.
3
+ *
4
+ * 1. SLIC superpixels (Achanta et al., 2012) group the reduced image into a few hundred patches of
5
+ * similar colour.
6
+ * 2. Each patch's saliency is how unlike the image's border it is (the border is mostly
7
+ * background), times how much it contrasts with the patches around it (region contrast, Cheng
8
+ * et al., 2015), times a mild preference for the centre.
9
+ * 3. Otsu's threshold splits the patches into a first guess at the subject.
10
+ * 4. GrabCut refines that guess along the image's edges, with the clearly-background border fixed.
11
+ *
12
+ * It cannot tell which object a description names: it selects what stands out.
13
+ */
14
+ import type { PixelBuffer, SelectionMask } from '../mask.js';
15
+ import { type WorkImage } from './work.js';
16
+ export interface Superpixels {
17
+ /** Superpixel of each work pixel. */
18
+ label: Int32Array;
19
+ count: number;
20
+ /** Mean L*a*b* per superpixel, 3 values each. */
21
+ lab: Float64Array;
22
+ /** Mean position per superpixel, in work pixels, 2 values each. */
23
+ centre: Float64Array;
24
+ size: Int32Array;
25
+ }
26
+ /** SLIC superpixels of the work image: about `target` of them, `compactness` 10 (L*a*b* units). */
27
+ export declare function slicSuperpixels(work: WorkImage, target?: number, compactness?: number, rounds?: number): Superpixels;
28
+ /** Saliency 0..1 of each superpixel (see the module comment). */
29
+ export declare function superpixelSaliency(sp: Superpixels, width: number, height: number): Float64Array;
30
+ /** Otsu's threshold of `values` (0..1) weighted by `weights`. */
31
+ export declare function otsuThreshold(values: ArrayLike<number>, weights: ArrayLike<number>): number;
32
+ /** What {@link findSubject} found. */
33
+ export interface FoundSubject {
34
+ selection: SelectionMask;
35
+ /** Why nothing was selected, when nothing was. */
36
+ reason?: string;
37
+ }
38
+ /**
39
+ * The most salient subject of `pixels` as a selection of the whole image (empty, with a reason,
40
+ * when nothing stands out from the background).
41
+ */
42
+ export declare function findSubject(pixels: PixelBuffer, options?: {
43
+ enhanceEdge?: boolean;
44
+ }): FoundSubject;
@@ -23,8 +23,8 @@ export interface ImageSegmentationToolProviderOptions {
23
23
  /** The `select_subject` tool: Select › Subject through the controller's segmentation provider. */
24
24
  export declare function createImageSubjectSelectionTool(controller: ImageEditorController): AgentTool;
25
25
  /**
26
- * Agent tools that need the controller's segmentation provider (`select_subject`). The list is
27
- * empty while no provider is set, so a host can register this once and the tool appears when a
28
- * model is available.
26
+ * @deprecated `select_subject` is now one of the selection tools of `createImageEditorToolProvider`
27
+ * (it works without a model too), so this provider lists no tools; it stays so hosts that register
28
+ * it keep working. Remove it from your registry.
29
29
  */
30
30
  export declare function createImageSegmentationToolProvider(controller: ImageEditorController, options?: ImageSegmentationToolProviderOptions): AgentToolProvider;
@@ -23,8 +23,8 @@ export interface ImageSegmentationToolProviderOptions {
23
23
  /** The `select_subject` tool: Select › Subject through the controller's segmentation provider. */
24
24
  export declare function createImageSubjectSelectionTool(controller: ImageEditorController): AgentTool;
25
25
  /**
26
- * Agent tools that need the controller's segmentation provider (`select_subject`). The list is
27
- * empty while no provider is set, so a host can register this once and the tool appears when a
28
- * model is available.
26
+ * @deprecated `select_subject` is now one of the selection tools of `createImageEditorToolProvider`
27
+ * (it works without a model too), so this provider lists no tools; it stays so hosts that register
28
+ * it keep working. Remove it from your registry.
29
29
  */
30
30
  export declare function createImageSegmentationToolProvider(controller: ImageEditorController, options?: ImageSegmentationToolProviderOptions): AgentToolProvider;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.198.0",
3
+ "version": "0.199.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -221,7 +221,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
221
221
  | `list_controls` | Get every base control (id, label, section, min, max, step, default, `requiresProcess`), each layer kind's settable controls (plus `opacity`), the blend mode ids and the `layers.<layerId>.<controlId>` path format. Call once per session before writing. |
222
222
  | `get_selection` | Read the active selection: `empty`, `bounds` (natural pixels), `area` in pixels, `percent` of the image and a description. Check it after every select or modify step and before masking: a wand that covered 90% of the image is the wrong selection. |
223
223
  | `list_channels` | Read the alpha channels (saved selections): id, name, bounds, percent, overlay and visibility, and whether Quick Mask mode is on. |
224
- | `select_subject` | Select › Subject (only when the host has a segmentation model): `target` describes what to select ("the dog", "the sky"); an AI model finds the matching objects and selects their outlines, combined by `mode`. `maxSegments` 1 for a single subject. Returns the selection and `found` (label, bounds per object); nothing found leaves the selection unchanged. |
224
+ | `select_subject` | Select › Subject. With a host segmentation model, `target` describes what to select ("the dog", "the sky") and the model's outlines are selected (`maxSegments` 1 for one). Without one, the subject that stands out most is found classically; `target` can't choose it (`method: "saliency"`, `note`). Combined by `mode`. Returns the selection, `found` (label, bounds) and `method`; nothing found leaves the selection unchanged. |
225
225
  | `list_layers` | Read the layer stack bottom to top as a tree: id, type (`pixel`, `adjustment`, `group`, `shape`, `annotation`, `smart`; smart objects add their `contents`, `smartFilters` and `filterMask`; shapes a `shape` summary of component ops and kinds and paint types, and `annotation` when migrated from one), name, document `bounds` of pixel layers, visibility, opacity, `fillOpacity`, blend, `clipped`, locks, a `mask` summary, an `effects` summary (master switch, scale, effect ids and kinds), pixel-layer `filters` (the Background's `develop` filter is the base adjustments), adjustment `kind` and `params`, group `children`, and which layer is `active`. Call it to understand a document with more than the Background before changing it; adjustment layer ids work with every adjustment-layer tool. |
226
226
  | `list_layer_effects` | Read one layer's effects: `enabled` (master switch), `scale` (Scale Effects), `margin` (px they reach beyond the shape), `fromPsd`, and every effect with `id`, `kind`, `label` and all its settings. Also returns the `globalLight`, the `patterns` pattern effects can use and the effect `kinds`. Call before changing effects you did not add. |
227
227
  | `get_image_info` | Read the geometry: source `image` size, `crop` box (source pixels, or null), `rotation` degrees, `flipH` / `flipV`, the `output` size after crop and rotation, `bitDepth` (8, 16 or 32), the layer count and `canUndo` / `canRedo`. Call before cropping or selecting so coordinates are right. |
@@ -97,7 +97,7 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
97
97
  | --- | --- |
98
98
  | Geometric areas: a frame, a screen, a band | `select_rect` / `select_ellipse`, with `feather` for soft transitions |
99
99
  | One object among others, where you can box it (a product on a table, a person in a group) | `select_object` with a `rect` a little larger than the object on every side; then `quick_select` add / subtract strokes for what it missed |
100
- | A subject with a clear outline (product, person, animal) | `select_subject` if the host has a model; else `select_object` around it, or `select_polygon` with `snapToEdges` (12–30 points within `width` of the edge), then `refine_selection` |
100
+ | A subject with a clear outline (product, person, animal) | `select_subject` (with a host model, by description; without one it takes whatever stands out most, so check `method` and the result); else `select_object` around it, or `select_polygon` with `snapToEdges` (12–30 points within `width` of the edge), then `refine_selection` |
101
101
  | An area or subject that differs from its surroundings (a shirt, a sky, a car) | `quick_select` with strokes painted inside it, away from the edge (`size` a fraction of its width); `add` / `subtract` strokes where it fell short or leaked |
102
102
  | A low-contrast or busy outline the edges don't follow | `select_polygon` without `snapToEdges`, along the contour (20–60 points) |
103
103
  | Flat backdrop or solid colour (studio white, green screen, sky without clouds) | `magic_wand` on the backdrop (`tolerance` 20–40; `distance: "perceptual"` for gradients), then `invert_selection` |
@@ -158,6 +158,9 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
158
158
  30–60), check `percent`, and grow with `mode: "add"`.
159
159
  - **Box instead of outline:** `select_rect` around a subject masks its background too. Trace
160
160
  the contour.
161
+ - **Select Subject without a model picks the wrong thing:** it selects what stands out most from
162
+ the image's border, which may be a bright sign rather than the person. When `method` is
163
+ `saliency`, check with `capture_view`; if it's wrong, use `select_object` with a box.
161
164
  - **Object Selection takes part of the background:** inside the box, anything that looks
162
165
  unlike the background around the box counts as the object. Tighten the box, or `subtract` a
163
166
  `quick_select` stroke on the extra. With `point`, it may stop at an inner edge of a
@@ -212,7 +215,7 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
212
215
  - `fill_pixels`, `blur_stroke`: fill or soften the selection in Quick Mask mode
213
216
  - `magic_wand`: similar colour from a seed
214
217
  - `select_color_range`: colour samples, hue, tone and skin presets
215
- - `select_subject`: a subject by description (needs a host model)
218
+ - `select_subject`: a subject by description with a host model; without one, the most salient subject
216
219
  - `load_selection_from_image`: AI or saved mattes
217
220
  - `load_selection_from_mask`: a layer mask back to a selection
218
221
  - `set_selection_display`: ants, Quick Mask tint or hidden