@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.
- package/dist/image-editor/controller.d.cts +9 -3
- package/dist/image-editor/controller.d.ts +9 -3
- package/dist/image-editor/index.cjs +387 -163
- package/dist/image-editor/index.js +243 -19
- package/dist/image-editor/selection/segment/grabcut.d.cts +18 -0
- package/dist/image-editor/selection/segment/grabcut.d.ts +18 -0
- package/dist/image-editor/selection/segment/index.d.cts +1 -0
- package/dist/image-editor/selection/segment/index.d.ts +1 -0
- package/dist/image-editor/selection/segment/subject.d.cts +44 -0
- package/dist/image-editor/selection/segment/subject.d.ts +44 -0
- package/dist/image-editor/selection-tools.d.cts +3 -3
- package/dist/image-editor/selection-tools.d.ts +3 -3
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
- package/skills/pixerate-image-selection-masking/SKILL.md +5 -2
|
@@ -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`
|
|
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.
|
|
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)
|
|
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
|
|
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
|
-
|
|
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:
|
|
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",
|
|
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: "
|
|
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"
|
|
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: "
|
|
14035
|
+
description: "Deprecated: select_subject is in the image editor tool provider.",
|
|
13818
14036
|
skill: "pixerate-image-editor",
|
|
13819
|
-
tools: () =>
|
|
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;
|
|
@@ -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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
@@ -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
|
|
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`
|
|
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
|
|
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
|