@pixerate/editor 0.198.0 → 0.200.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.
@@ -3049,8 +3049,9 @@ var GridGraph = class {
3049
3049
  var WORK_SIZE = 512;
3050
3050
  var workCache = /* @__PURE__ */ new WeakMap();
3051
3051
  function workImage(pixels, maxSide = WORK_SIZE) {
3052
- const cached = workCache.get(pixels.data);
3053
- if (cached && cached.maxSide === maxSide && cached.work.sourceWidth === pixels.width && cached.work.sourceHeight === pixels.height) return cached.work;
3052
+ const sizes = workCache.get(pixels.data);
3053
+ const cached = sizes?.get(maxSide);
3054
+ if (cached && cached.sourceWidth === pixels.width && cached.sourceHeight === pixels.height) return cached;
3054
3055
  const { width: W, height: H, data } = pixels;
3055
3056
  const scale = Math.min(1, maxSide / Math.max(W, H));
3056
3057
  const w = Math.max(1, Math.round(W * scale));
@@ -3079,7 +3080,8 @@ function workImage(pixels, maxSide = WORK_SIZE) {
3079
3080
  rgb[i * 3 + 2] = sum[i * 3 + 2] / c;
3080
3081
  }
3081
3082
  const work = { sourceWidth: W, sourceHeight: H, width: w, height: h, scale: w / W, rgb };
3082
- workCache.set(pixels.data, { maxSide, work });
3083
+ if (sizes) sizes.set(maxSide, work);
3084
+ else workCache.set(pixels.data, /* @__PURE__ */ new Map([[maxSide, work]]));
3083
3085
  return work;
3084
3086
  }
3085
3087
  var GAMMA = 50;
@@ -3672,6 +3674,545 @@ function selectObjectIn(pixels, target, options = {}) {
3672
3674
  }
3673
3675
  }
3674
3676
 
3677
+ // src/image-editor/selection/segment/subject.ts
3678
+ function toLab(r, g, b) {
3679
+ const lin = (v) => {
3680
+ v /= 255;
3681
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
3682
+ };
3683
+ const R = lin(r);
3684
+ const G = lin(g);
3685
+ const B = lin(b);
3686
+ const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
3687
+ const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
3688
+ const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
3689
+ const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
3690
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
3691
+ }
3692
+ function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
3693
+ const { width: w, height: h, rgb } = work;
3694
+ const n = w * h;
3695
+ const lab = new Float32Array(n * 3);
3696
+ for (let i = 0; i < n; i++) lab.set(toLab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]), i * 3);
3697
+ const S = Math.max(2, Math.round(Math.sqrt(n / target)));
3698
+ const seeds = [];
3699
+ for (let y = Math.floor(S / 2); y < h; y += S) {
3700
+ for (let x = Math.floor(S / 2); x < w; x += S) {
3701
+ const i = y * w + x;
3702
+ seeds.push([lab[i * 3], lab[i * 3 + 1], lab[i * 3 + 2], x, y]);
3703
+ }
3704
+ }
3705
+ const k = seeds.length;
3706
+ const label = new Int32Array(n).fill(-1);
3707
+ const best = new Float64Array(n);
3708
+ const m2 = (compactness / S) ** 2;
3709
+ for (let round6 = 0; round6 < rounds; round6++) {
3710
+ best.fill(Infinity);
3711
+ for (let c = 0; c < k; c++) {
3712
+ const [L, A, B, cx, cy] = seeds[c];
3713
+ const x0 = Math.max(0, Math.floor(cx - S));
3714
+ const x1 = Math.min(w - 1, Math.ceil(cx + S));
3715
+ const y0 = Math.max(0, Math.floor(cy - S));
3716
+ const y1 = Math.min(h - 1, Math.ceil(cy + S));
3717
+ for (let y = y0; y <= y1; y++) {
3718
+ for (let x = x0; x <= x1; x++) {
3719
+ const i = y * w + x;
3720
+ 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);
3721
+ if (d < best[i]) {
3722
+ best[i] = d;
3723
+ label[i] = c;
3724
+ }
3725
+ }
3726
+ }
3727
+ }
3728
+ const sum = new Float64Array(k * 6);
3729
+ for (let i = 0; i < n; i++) {
3730
+ const c = label[i];
3731
+ sum[c * 6] += lab[i * 3];
3732
+ sum[c * 6 + 1] += lab[i * 3 + 1];
3733
+ sum[c * 6 + 2] += lab[i * 3 + 2];
3734
+ sum[c * 6 + 3] += i % w;
3735
+ sum[c * 6 + 4] += Math.floor(i / w);
3736
+ sum[c * 6 + 5]++;
3737
+ }
3738
+ for (let c = 0; c < k; c++) {
3739
+ const s = sum[c * 6 + 5];
3740
+ 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];
3741
+ }
3742
+ }
3743
+ const remap = new Int32Array(k).fill(-1);
3744
+ let count = 0;
3745
+ for (let i = 0; i < n; i++) if (remap[label[i]] < 0) remap[label[i]] = count++;
3746
+ const meanLab = new Float64Array(count * 3);
3747
+ const centre = new Float64Array(count * 2);
3748
+ const size = new Int32Array(count);
3749
+ for (let i = 0; i < n; i++) {
3750
+ const c = label[i] = remap[label[i]];
3751
+ meanLab[c * 3] += lab[i * 3];
3752
+ meanLab[c * 3 + 1] += lab[i * 3 + 1];
3753
+ meanLab[c * 3 + 2] += lab[i * 3 + 2];
3754
+ centre[c * 2] += i % w;
3755
+ centre[c * 2 + 1] += Math.floor(i / w);
3756
+ size[c]++;
3757
+ }
3758
+ for (let c = 0; c < count; c++) {
3759
+ for (let j = 0; j < 3; j++) meanLab[c * 3 + j] /= size[c];
3760
+ centre[c * 2] /= size[c];
3761
+ centre[c * 2 + 1] /= size[c];
3762
+ }
3763
+ return { label, count, lab: meanLab, centre, size };
3764
+ }
3765
+ function superpixelSaliency(sp, width, height) {
3766
+ const { count, lab, centre, size, label } = sp;
3767
+ 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]);
3768
+ const onBorder = new Uint8Array(count);
3769
+ for (let x = 0; x < width; x++) {
3770
+ onBorder[label[x]] = 1;
3771
+ onBorder[label[(height - 1) * width + x]] = 1;
3772
+ }
3773
+ for (let y = 0; y < height; y++) {
3774
+ onBorder[label[y * width]] = 1;
3775
+ onBorder[label[y * width + width - 1]] = 1;
3776
+ }
3777
+ const border = [];
3778
+ for (let c = 0; c < count; c++) if (onBorder[c]) border.push(c);
3779
+ const diag = Math.hypot(width, height);
3780
+ const unlikeBorder = new Float64Array(count);
3781
+ const contrast = new Float64Array(count);
3782
+ const centreBias = new Float64Array(count);
3783
+ for (let i = 0; i < count; i++) {
3784
+ const ds = border.map((j) => dist(i, j)).sort((a, b) => a - b);
3785
+ unlikeBorder[i] = ds.length ? ds[Math.floor(ds.length * 0.25)] : 0;
3786
+ let s = 0;
3787
+ let wsum = 0;
3788
+ for (let j = 0; j < count; j++) {
3789
+ if (j === i) continue;
3790
+ const dx = (centre[i * 2] - centre[j * 2]) / diag;
3791
+ const dy = (centre[i * 2 + 1] - centre[j * 2 + 1]) / diag;
3792
+ const wgt = size[j] * Math.exp(-(dx * dx + dy * dy) / (2 * 0.25 * 0.25));
3793
+ s += wgt * dist(i, j);
3794
+ wsum += wgt;
3795
+ }
3796
+ contrast[i] = wsum ? s / wsum : 0;
3797
+ const cx = centre[i * 2] / width - 0.5;
3798
+ const cy = centre[i * 2 + 1] / height - 0.5;
3799
+ centreBias[i] = 0.4 + 0.6 * Math.exp(-(cx * cx + cy * cy) / (2 * 0.3 * 0.3));
3800
+ }
3801
+ let farthest = 0;
3802
+ for (const v of unlikeBorder) farthest = Math.max(farthest, v);
3803
+ if (farthest < 6) return new Float64Array(count);
3804
+ const norm = (a) => {
3805
+ let max = 0;
3806
+ for (const v of a) max = Math.max(max, v);
3807
+ if (max > 0) for (let i = 0; i < a.length; i++) a[i] /= max;
3808
+ };
3809
+ norm(unlikeBorder);
3810
+ norm(contrast);
3811
+ const out = new Float64Array(count);
3812
+ for (let i = 0; i < count; i++) out[i] = unlikeBorder[i] * (0.3 + 0.7 * contrast[i]) * centreBias[i];
3813
+ norm(out);
3814
+ return out;
3815
+ }
3816
+ function otsuThreshold(values, weights) {
3817
+ const BINS2 = 64;
3818
+ const hist = new Float64Array(BINS2);
3819
+ let total = 0;
3820
+ for (let i = 0; i < values.length; i++) {
3821
+ hist[Math.min(BINS2 - 1, Math.floor(values[i] * BINS2))] += weights[i];
3822
+ total += weights[i];
3823
+ }
3824
+ let sumAll = 0;
3825
+ for (let b = 0; b < BINS2; b++) sumAll += b * hist[b];
3826
+ let wB = 0;
3827
+ let sumB = 0;
3828
+ let best = 0;
3829
+ let bestT = 0.5;
3830
+ for (let b = 0; b < BINS2 - 1; b++) {
3831
+ wB += hist[b];
3832
+ if (!wB) continue;
3833
+ const wF = total - wB;
3834
+ if (!wF) break;
3835
+ sumB += b * hist[b];
3836
+ const between = wB * wF * (sumB / wB - (sumAll - sumB) / wF) ** 2;
3837
+ if (between > best) {
3838
+ best = between;
3839
+ bestT = (b + 1) / BINS2;
3840
+ }
3841
+ }
3842
+ return bestT;
3843
+ }
3844
+ function findSubject(pixels, options = {}) {
3845
+ const work = workImage(pixels);
3846
+ const { width: w, height: h } = work;
3847
+ const sp = slicSuperpixels(work);
3848
+ const sal = superpixelSaliency(sp, w, h);
3849
+ const t = otsuThreshold(sal, sp.size);
3850
+ const n = w * h;
3851
+ const initial = new Uint8Array(n);
3852
+ const hardBg = new Uint8Array(n);
3853
+ const hardFg = new Uint8Array(n);
3854
+ const strong = t + 0.6 * (1 - t);
3855
+ for (let i = 0; i < n; i++) {
3856
+ const s = sal[sp.label[i]];
3857
+ if (s >= t) initial[i] = 1;
3858
+ if (s >= strong) hardFg[i] = 1;
3859
+ }
3860
+ for (let x = 0; x < w; x++) {
3861
+ for (const y of [0, h - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
3862
+ }
3863
+ for (let y = 0; y < h; y++) {
3864
+ for (const x of [0, w - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
3865
+ }
3866
+ const empty = (reason) => ({ selection: createSelectionMask(pixels.width, pixels.height), reason });
3867
+ let guessed = 0;
3868
+ for (let i = 0; i < n; i++) guessed += initial[i];
3869
+ if (!guessed || guessed > n * 0.95) return empty("Nothing stands out from the background.");
3870
+ const roi = { x: 0, y: 0, width: w, height: h };
3871
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, 5);
3872
+ keepMainPieces(label, w, h, null);
3873
+ let kept = 0;
3874
+ for (let i = 0; i < n; i++) kept += label[i];
3875
+ if (kept < n * 2e-3) return empty("Nothing stands out from the background.");
3876
+ if (kept > n * 0.97) return empty("The whole image looks like one subject; select it with select_all instead.");
3877
+ return { selection: upscaleRegion(work, pixels, roi, label, options.enhanceEdge ?? false) };
3878
+ }
3879
+
3880
+ // src/image-editor/selection/refine.ts
3881
+ var DEFAULT_REFINE_EDGE_PARAMS = {
3882
+ radius: 0,
3883
+ smartRadius: false,
3884
+ smooth: 0,
3885
+ feather: 0,
3886
+ contrast: 0,
3887
+ shiftEdge: 0
3888
+ };
3889
+ var GUIDED_FILTER_EPS = 1e-4;
3890
+ var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
3891
+ function normalizeRefineEdgeParams(p = {}) {
3892
+ return {
3893
+ radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
3894
+ smartRadius: p.smartRadius === true,
3895
+ smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
3896
+ feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
3897
+ contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
3898
+ shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
3899
+ };
3900
+ }
3901
+ var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
3902
+ var smoothSigma = (p) => p.smooth / 100 * 4;
3903
+ var featherSigma = (p) => p.feather / 2;
3904
+ function shiftAmount(p) {
3905
+ const px2 = p.shiftEdge / 100 * Math.max(p.radius, 4);
3906
+ return Number.isFinite(px2) ? Math.min(64, Math.max(-64, px2)) : 0;
3907
+ }
3908
+ function refineEdgeHalo(params = {}) {
3909
+ const p = normalizeRefineEdgeParams(params);
3910
+ const g = gfRadius(p);
3911
+ const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
3912
+ return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
3913
+ }
3914
+ function boxMean(src, w, h, r) {
3915
+ const tmp = new Float32Array(w * h);
3916
+ const pre = new Float64Array(Math.max(w, h) + 1);
3917
+ for (let y = 0; y < h; y++) {
3918
+ const row = y * w;
3919
+ for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
3920
+ for (let x = 0; x < w; x++) {
3921
+ const lo = Math.max(0, x - r);
3922
+ const hi = Math.min(w, x + r + 1);
3923
+ tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
3924
+ }
3925
+ }
3926
+ const out = new Float32Array(w * h);
3927
+ for (let x = 0; x < w; x++) {
3928
+ for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
3929
+ for (let y = 0; y < h; y++) {
3930
+ const lo = Math.max(0, y - r);
3931
+ const hi = Math.min(h, y + r + 1);
3932
+ out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
3933
+ }
3934
+ }
3935
+ return out;
3936
+ }
3937
+ function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
3938
+ const n = w * h;
3939
+ const [c0, c1, c2] = guide;
3940
+ const prod = (a, b2) => {
3941
+ const o = new Float32Array(n);
3942
+ for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
3943
+ return o;
3944
+ };
3945
+ const m0 = boxMean(c0, w, h, r);
3946
+ const m1 = boxMean(c1, w, h, r);
3947
+ const m2 = boxMean(c2, w, h, r);
3948
+ const mp = boxMean(p, w, h, r);
3949
+ const mip0 = boxMean(prod(c0, p), w, h, r);
3950
+ const mip1 = boxMean(prod(c1, p), w, h, r);
3951
+ const mip2 = boxMean(prod(c2, p), w, h, r);
3952
+ const s00 = boxMean(prod(c0, c0), w, h, r);
3953
+ const s01 = boxMean(prod(c0, c1), w, h, r);
3954
+ const s02 = boxMean(prod(c0, c2), w, h, r);
3955
+ const s11 = boxMean(prod(c1, c1), w, h, r);
3956
+ const s12 = boxMean(prod(c1, c2), w, h, r);
3957
+ const s22 = boxMean(prod(c2, c2), w, h, r);
3958
+ const a0 = new Float32Array(n);
3959
+ const a1 = new Float32Array(n);
3960
+ const a2 = new Float32Array(n);
3961
+ const b = new Float32Array(n);
3962
+ for (let i = 0; i < n; i++) {
3963
+ const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
3964
+ const rr = s00[i] - u0 * u0 + eps;
3965
+ const rg = s01[i] - u0 * u1;
3966
+ const rb = s02[i] - u0 * u2;
3967
+ const gg = s11[i] - u1 * u1 + eps;
3968
+ const gb = s12[i] - u1 * u2;
3969
+ const bb = s22[i] - u2 * u2 + eps;
3970
+ const v0 = mip0[i] - u0 * up;
3971
+ const v1 = mip1[i] - u1 * up;
3972
+ const v2 = mip2[i] - u2 * up;
3973
+ const i00 = gg * bb - gb * gb;
3974
+ const i01 = gb * rb - rg * bb;
3975
+ const i02 = rg * gb - gg * rb;
3976
+ const i11 = rr * bb - rb * rb;
3977
+ const i12 = rb * rg - rr * gb;
3978
+ const i22 = rr * gg - rg * rg;
3979
+ const det = rr * i00 + rg * i01 + rb * i02;
3980
+ let x0 = 0, x1 = 0, x2 = 0;
3981
+ if (Math.abs(det) >= 1e-30) {
3982
+ x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
3983
+ x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
3984
+ x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
3985
+ }
3986
+ a0[i] = x0;
3987
+ a1[i] = x1;
3988
+ a2[i] = x2;
3989
+ b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
3990
+ }
3991
+ const ma0 = boxMean(a0, w, h, r);
3992
+ const ma1 = boxMean(a1, w, h, r);
3993
+ const ma2 = boxMean(a2, w, h, r);
3994
+ const mb = boxMean(b, w, h, r);
3995
+ const out = new Float32Array(n);
3996
+ for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
3997
+ return out;
3998
+ }
3999
+ function edgeWidth(guide, w, h, r) {
4000
+ const n = w * h;
4001
+ const y = new Float32Array(n);
4002
+ for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
4003
+ const g = new Float32Array(n);
4004
+ for (let yy = 0; yy < h; yy++) {
4005
+ for (let xx = 0; xx < w; xx++) {
4006
+ const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
4007
+ const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
4008
+ g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
4009
+ }
4010
+ }
4011
+ const gm = greyMorphology(g, w, h, r, true);
4012
+ const hi = greyMorphology(y, w, h, r, true);
4013
+ const lo = greyMorphology(y, w, h, r, false);
4014
+ const out = new Float32Array(n);
4015
+ for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
4016
+ return out;
4017
+ }
4018
+ function refineCoverage(guide, m, w, h, params = {}) {
4019
+ const p = normalizeRefineEdgeParams(params);
4020
+ let a = Float32Array.from(m);
4021
+ if (p.radius > 0 && guide) {
4022
+ const n = w * h;
4023
+ const boundary = new Uint8Array(n);
4024
+ let any = false;
4025
+ for (let y = 0; y < h; y++) {
4026
+ for (let x = 0; x < w; x++) {
4027
+ const i = y * w + x;
4028
+ const v = m[i] >= 0.5;
4029
+ const diff = x > 0 && m[i - 1] >= 0.5 !== v || x + 1 < w && m[i + 1] >= 0.5 !== v || y > 0 && m[i - w] >= 0.5 !== v || y + 1 < h && m[i + w] >= 0.5 !== v;
4030
+ if (diff) {
4031
+ boundary[i] = 1;
4032
+ any = true;
4033
+ }
4034
+ }
4035
+ }
4036
+ if (any) {
4037
+ const dist = distanceTransform(boundary, w, h);
4038
+ const r = gfRadius(p);
4039
+ const q = guidedFilterColor(guide, m, w, h, r);
4040
+ const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
4041
+ for (let i = 0; i < n; i++) {
4042
+ const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
4043
+ const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));
4044
+ if (wgt > 0) {
4045
+ const qi = Math.min(1, Math.max(0, q[i]));
4046
+ a[i] = m[i] + (qi - m[i]) * wgt;
4047
+ }
4048
+ }
4049
+ }
4050
+ }
4051
+ const ss = smoothSigma(p);
4052
+ if (ss >= 0.3) {
4053
+ a = gaussianBlur(a, w, h, ss, "clamp");
4054
+ const gain = 1 + ss;
4055
+ for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
4056
+ }
4057
+ a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
4058
+ if (p.contrast > 0) {
4059
+ const gain = 1 / (1 - 0.99 * p.contrast / 100);
4060
+ for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
4061
+ }
4062
+ const amount = shiftAmount(p);
4063
+ if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
4064
+ return a;
4065
+ }
4066
+ function guidePlanes(pixels, win) {
4067
+ const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
4068
+ const n = box.width * box.height;
4069
+ const r = new Float32Array(n);
4070
+ const g = new Float32Array(n);
4071
+ const b = new Float32Array(n);
4072
+ const d = pixels.data;
4073
+ for (let y = 0; y < box.height; y++) {
4074
+ for (let x = 0; x < box.width; x++) {
4075
+ const i = y * box.width + x;
4076
+ const p = ((box.y + y) * pixels.width + box.x + x) * 4;
4077
+ r[i] = d[p] / 255;
4078
+ g[i] = d[p + 1] / 255;
4079
+ b[i] = d[p + 2] / 255;
4080
+ }
4081
+ }
4082
+ return [r, g, b];
4083
+ }
4084
+ function refineSelection(pixels, mask, params = {}) {
4085
+ const p = normalizeRefineEdgeParams(params);
4086
+ if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
4087
+ throw new Error("Refine Edge needs pixels the size of the selection");
4088
+ }
4089
+ if (!mask.bounds) return mask;
4090
+ const halo = refineEdgeHalo(p);
4091
+ const x0 = Math.max(0, mask.bounds.x - halo);
4092
+ const y0 = Math.max(0, mask.bounds.y - halo);
4093
+ const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);
4094
+ const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);
4095
+ const ww = x1 - x0;
4096
+ const wh = y1 - y0;
4097
+ const m = new Float32Array(ww * wh);
4098
+ for (let y = 0; y < wh; y++) {
4099
+ const src = (y0 + y) * mask.width + x0;
4100
+ for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
4101
+ }
4102
+ const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
4103
+ const refined = refineCoverage(guide, m, ww, wh, p);
4104
+ const out = new Uint8Array(mask.width * mask.height);
4105
+ for (let y = 0; y < wh; y++) {
4106
+ const dst = (y0 + y) * mask.width + x0;
4107
+ for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
4108
+ }
4109
+ return createSelectionMask(mask.width, mask.height, out);
4110
+ }
4111
+
4112
+ // src/image-editor/selection/segment/guided.ts
4113
+ function workPlanes(work) {
4114
+ const n = work.width * work.height;
4115
+ const planes = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];
4116
+ for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] = work.rgb[i * 3 + c] / 255;
4117
+ return planes;
4118
+ }
4119
+ function workLuma(work) {
4120
+ const n = work.width * work.height;
4121
+ const out = new Float32Array(n);
4122
+ const { rgb } = work;
4123
+ for (let i = 0; i < n; i++) out[i] = (0.299 * rgb[i * 3] + 0.587 * rgb[i * 3 + 1] + 0.114 * rgb[i * 3 + 2]) / 255;
4124
+ return out;
4125
+ }
4126
+ function softMaskToSelection(work, mask) {
4127
+ const { sourceWidth: W, sourceHeight: H, width: w, height: h, scale } = work;
4128
+ const out = new Uint8Array(W * H);
4129
+ const col = new Int32Array(W);
4130
+ const fx = new Float32Array(W);
4131
+ for (let X = 0; X < W; X++) {
4132
+ const u = Math.min(w - 1, Math.max(0, (X + 0.5) * scale - 0.5));
4133
+ col[X] = Math.min(w - 2, Math.floor(u));
4134
+ fx[X] = u - col[X];
4135
+ if (w === 1) {
4136
+ col[X] = 0;
4137
+ fx[X] = 0;
4138
+ }
4139
+ }
4140
+ for (let Y = 0; Y < H; Y++) {
4141
+ const v = Math.min(h - 1, Math.max(0, (Y + 0.5) * scale - 0.5));
4142
+ const j = h === 1 ? 0 : Math.min(h - 2, Math.floor(v));
4143
+ const fy = h === 1 ? 0 : v - j;
4144
+ const r0 = j * w;
4145
+ const r1 = h === 1 ? r0 : r0 + w;
4146
+ for (let X = 0; X < W; X++) {
4147
+ const i = col[X];
4148
+ const i1 = w === 1 ? i : i + 1;
4149
+ const t = fx[X];
4150
+ const m = (mask[r0 + i] * (1 - t) + mask[r0 + i1] * t) * (1 - fy) + (mask[r1 + i] * (1 - t) + mask[r1 + i1] * t) * fy;
4151
+ out[Y * W + X] = Math.round(Math.min(1, Math.max(0, m)) * 255);
4152
+ }
4153
+ }
4154
+ return selectionFromRegion(W, H, { x: 0, y: 0, width: W, height: H }, out);
4155
+ }
4156
+
4157
+ // src/image-editor/selection/segment/focus.ts
4158
+ var FOCUS_SIZE = 1024;
4159
+ function estimateNoise(luma2, w, h) {
4160
+ if (w < 3 || h < 3) return 0;
4161
+ let sum = 0;
4162
+ for (let y = 1; y < h - 1; y++) {
4163
+ for (let x = 1; x < w - 1; x++) {
4164
+ const i = y * w + x;
4165
+ const v = luma2[i - w - 1] - 2 * luma2[i - w] + luma2[i - w + 1] - 2 * luma2[i - 1] + 4 * luma2[i] - 2 * luma2[i + 1] + luma2[i + w - 1] - 2 * luma2[i + w] + luma2[i + w + 1];
4166
+ sum += Math.abs(v);
4167
+ }
4168
+ }
4169
+ return Math.sqrt(Math.PI / 2) * sum / (6 * (w - 2) * (h - 2));
4170
+ }
4171
+ function focusArea(pixels, options = {}) {
4172
+ const work = workImage(pixels, FOCUS_SIZE);
4173
+ const { width: w, height: h, scale } = work;
4174
+ const n = w * h;
4175
+ const luma2 = workLuma(work);
4176
+ const sigma = estimateNoise(luma2, w, h);
4177
+ const noise = Math.min(100, Math.max(0, options.noise ?? 0));
4178
+ const floor = 2 * sigma * (1 + noise / 25);
4179
+ const detail = new Float32Array(n);
4180
+ for (let y = 1; y < h - 1; y++) {
4181
+ for (let x = 1; x < w - 1; x++) {
4182
+ const i = y * w + x;
4183
+ const lap = Math.abs(4 * luma2[i] - luma2[i - 1] - luma2[i + 1] - luma2[i - w] - luma2[i + w]);
4184
+ const d = Math.max(0, lap - floor);
4185
+ detail[i] = d * d;
4186
+ }
4187
+ }
4188
+ const r = Math.max(2, Math.round(0.012 * Math.max(w, h)));
4189
+ const energy = boxMean(detail, w, h, r);
4190
+ const sharp = new Float32Array(n);
4191
+ for (let i = 0; i < n; i++) sharp[i] = Math.log1p(Math.sqrt(energy[i]) * 400);
4192
+ const sorted = Float32Array.from(sharp).sort();
4193
+ const top = sorted[Math.floor(n * 0.995)] || 1;
4194
+ for (let i = 0; i < n; i++) sharp[i] = Math.min(1, sharp[i] / top);
4195
+ const ones = new Float32Array(n).fill(1);
4196
+ const otsu = otsuThreshold(sharp, ones);
4197
+ const range = Math.min(100, Math.max(0, options.range ?? 50));
4198
+ const t = Math.min(0.98, otsu * Math.pow(2, (50 - range) / 25));
4199
+ let mask = new Float32Array(n);
4200
+ for (let i = 0; i < n; i++) mask[i] = sharp[i] >= t ? 1 : 0;
4201
+ const c = 2 * r;
4202
+ const grown = boxMean(mask, w, h, c);
4203
+ for (let i = 0; i < n; i++) grown[i] = grown[i] > 1e-3 ? 1 : 0;
4204
+ const shrunk = boxMean(grown, w, h, c);
4205
+ for (let i = 0; i < n; i++) mask[i] = shrunk[i] >= 0.999 ? 1 : 0;
4206
+ const pulled = boxMean(mask, w, h, Math.max(1, Math.round(r / 2)));
4207
+ for (let i = 0; i < n; i++) mask[i] = pulled[i] >= 0.999 ? 1 : 0;
4208
+ const softness = Math.max(0, options.softness ?? 4) * scale;
4209
+ if (softness > 0) {
4210
+ const fitted = guidedFilterColor(workPlanes(work), mask, w, h, Math.max(1, Math.round(softness * 2)), 1e-3);
4211
+ for (let i = 0; i < n; i++) mask[i] = Math.min(1, Math.max(0, fitted[i]));
4212
+ }
4213
+ return { selection: softMaskToSelection(work, mask), noiseLevel: Math.round(sigma * 255 * 10) / 10 };
4214
+ }
4215
+
3675
4216
  // src/image-editor/layers/shape-type-ui.ts
3676
4217
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
3677
4218
  kind: "rect",
@@ -4597,238 +5138,6 @@ function segmentsToSelection(width, height, results) {
4597
5138
  return out;
4598
5139
  }
4599
5140
 
4600
- // src/image-editor/selection/refine.ts
4601
- var DEFAULT_REFINE_EDGE_PARAMS = {
4602
- radius: 0,
4603
- smartRadius: false,
4604
- smooth: 0,
4605
- feather: 0,
4606
- contrast: 0,
4607
- shiftEdge: 0
4608
- };
4609
- var GUIDED_FILTER_EPS = 1e-4;
4610
- var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
4611
- function normalizeRefineEdgeParams(p = {}) {
4612
- return {
4613
- radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
4614
- smartRadius: p.smartRadius === true,
4615
- smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
4616
- feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
4617
- contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
4618
- shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
4619
- };
4620
- }
4621
- var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
4622
- var smoothSigma = (p) => p.smooth / 100 * 4;
4623
- var featherSigma = (p) => p.feather / 2;
4624
- function shiftAmount(p) {
4625
- const px2 = p.shiftEdge / 100 * Math.max(p.radius, 4);
4626
- return Number.isFinite(px2) ? Math.min(64, Math.max(-64, px2)) : 0;
4627
- }
4628
- function refineEdgeHalo(params = {}) {
4629
- const p = normalizeRefineEdgeParams(params);
4630
- const g = gfRadius(p);
4631
- const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
4632
- return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
4633
- }
4634
- function boxMean(src, w, h, r) {
4635
- const tmp = new Float32Array(w * h);
4636
- const pre = new Float64Array(Math.max(w, h) + 1);
4637
- for (let y = 0; y < h; y++) {
4638
- const row = y * w;
4639
- for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
4640
- for (let x = 0; x < w; x++) {
4641
- const lo = Math.max(0, x - r);
4642
- const hi = Math.min(w, x + r + 1);
4643
- tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
4644
- }
4645
- }
4646
- const out = new Float32Array(w * h);
4647
- for (let x = 0; x < w; x++) {
4648
- for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
4649
- for (let y = 0; y < h; y++) {
4650
- const lo = Math.max(0, y - r);
4651
- const hi = Math.min(h, y + r + 1);
4652
- out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
4653
- }
4654
- }
4655
- return out;
4656
- }
4657
- function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
4658
- const n = w * h;
4659
- const [c0, c1, c2] = guide;
4660
- const prod = (a, b2) => {
4661
- const o = new Float32Array(n);
4662
- for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
4663
- return o;
4664
- };
4665
- const m0 = boxMean(c0, w, h, r);
4666
- const m1 = boxMean(c1, w, h, r);
4667
- const m2 = boxMean(c2, w, h, r);
4668
- const mp = boxMean(p, w, h, r);
4669
- const mip0 = boxMean(prod(c0, p), w, h, r);
4670
- const mip1 = boxMean(prod(c1, p), w, h, r);
4671
- const mip2 = boxMean(prod(c2, p), w, h, r);
4672
- const s00 = boxMean(prod(c0, c0), w, h, r);
4673
- const s01 = boxMean(prod(c0, c1), w, h, r);
4674
- const s02 = boxMean(prod(c0, c2), w, h, r);
4675
- const s11 = boxMean(prod(c1, c1), w, h, r);
4676
- const s12 = boxMean(prod(c1, c2), w, h, r);
4677
- const s22 = boxMean(prod(c2, c2), w, h, r);
4678
- const a0 = new Float32Array(n);
4679
- const a1 = new Float32Array(n);
4680
- const a2 = new Float32Array(n);
4681
- const b = new Float32Array(n);
4682
- for (let i = 0; i < n; i++) {
4683
- const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
4684
- const rr = s00[i] - u0 * u0 + eps;
4685
- const rg = s01[i] - u0 * u1;
4686
- const rb = s02[i] - u0 * u2;
4687
- const gg = s11[i] - u1 * u1 + eps;
4688
- const gb = s12[i] - u1 * u2;
4689
- const bb = s22[i] - u2 * u2 + eps;
4690
- const v0 = mip0[i] - u0 * up;
4691
- const v1 = mip1[i] - u1 * up;
4692
- const v2 = mip2[i] - u2 * up;
4693
- const i00 = gg * bb - gb * gb;
4694
- const i01 = gb * rb - rg * bb;
4695
- const i02 = rg * gb - gg * rb;
4696
- const i11 = rr * bb - rb * rb;
4697
- const i12 = rb * rg - rr * gb;
4698
- const i22 = rr * gg - rg * rg;
4699
- const det = rr * i00 + rg * i01 + rb * i02;
4700
- let x0 = 0, x1 = 0, x2 = 0;
4701
- if (Math.abs(det) >= 1e-30) {
4702
- x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
4703
- x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
4704
- x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
4705
- }
4706
- a0[i] = x0;
4707
- a1[i] = x1;
4708
- a2[i] = x2;
4709
- b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
4710
- }
4711
- const ma0 = boxMean(a0, w, h, r);
4712
- const ma1 = boxMean(a1, w, h, r);
4713
- const ma2 = boxMean(a2, w, h, r);
4714
- const mb = boxMean(b, w, h, r);
4715
- const out = new Float32Array(n);
4716
- for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
4717
- return out;
4718
- }
4719
- function edgeWidth(guide, w, h, r) {
4720
- const n = w * h;
4721
- const y = new Float32Array(n);
4722
- for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
4723
- const g = new Float32Array(n);
4724
- for (let yy = 0; yy < h; yy++) {
4725
- for (let xx = 0; xx < w; xx++) {
4726
- const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
4727
- const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
4728
- g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
4729
- }
4730
- }
4731
- const gm = greyMorphology(g, w, h, r, true);
4732
- const hi = greyMorphology(y, w, h, r, true);
4733
- const lo = greyMorphology(y, w, h, r, false);
4734
- const out = new Float32Array(n);
4735
- for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
4736
- return out;
4737
- }
4738
- function refineCoverage(guide, m, w, h, params = {}) {
4739
- const p = normalizeRefineEdgeParams(params);
4740
- let a = Float32Array.from(m);
4741
- if (p.radius > 0 && guide) {
4742
- const n = w * h;
4743
- const boundary = new Uint8Array(n);
4744
- let any = false;
4745
- for (let y = 0; y < h; y++) {
4746
- for (let x = 0; x < w; x++) {
4747
- const i = y * w + x;
4748
- const v = m[i] >= 0.5;
4749
- const diff = x > 0 && m[i - 1] >= 0.5 !== v || x + 1 < w && m[i + 1] >= 0.5 !== v || y > 0 && m[i - w] >= 0.5 !== v || y + 1 < h && m[i + w] >= 0.5 !== v;
4750
- if (diff) {
4751
- boundary[i] = 1;
4752
- any = true;
4753
- }
4754
- }
4755
- }
4756
- if (any) {
4757
- const dist = distanceTransform(boundary, w, h);
4758
- const r = gfRadius(p);
4759
- const q = guidedFilterColor(guide, m, w, h, r);
4760
- const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
4761
- for (let i = 0; i < n; i++) {
4762
- const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
4763
- const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));
4764
- if (wgt > 0) {
4765
- const qi = Math.min(1, Math.max(0, q[i]));
4766
- a[i] = m[i] + (qi - m[i]) * wgt;
4767
- }
4768
- }
4769
- }
4770
- }
4771
- const ss = smoothSigma(p);
4772
- if (ss >= 0.3) {
4773
- a = gaussianBlur(a, w, h, ss, "clamp");
4774
- const gain = 1 + ss;
4775
- for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
4776
- }
4777
- a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
4778
- if (p.contrast > 0) {
4779
- const gain = 1 / (1 - 0.99 * p.contrast / 100);
4780
- for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
4781
- }
4782
- const amount = shiftAmount(p);
4783
- if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
4784
- return a;
4785
- }
4786
- function guidePlanes(pixels, win) {
4787
- const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
4788
- const n = box.width * box.height;
4789
- const r = new Float32Array(n);
4790
- const g = new Float32Array(n);
4791
- const b = new Float32Array(n);
4792
- const d = pixels.data;
4793
- for (let y = 0; y < box.height; y++) {
4794
- for (let x = 0; x < box.width; x++) {
4795
- const i = y * box.width + x;
4796
- const p = ((box.y + y) * pixels.width + box.x + x) * 4;
4797
- r[i] = d[p] / 255;
4798
- g[i] = d[p + 1] / 255;
4799
- b[i] = d[p + 2] / 255;
4800
- }
4801
- }
4802
- return [r, g, b];
4803
- }
4804
- function refineSelection(pixels, mask, params = {}) {
4805
- const p = normalizeRefineEdgeParams(params);
4806
- if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
4807
- throw new Error("Refine Edge needs pixels the size of the selection");
4808
- }
4809
- if (!mask.bounds) return mask;
4810
- const halo = refineEdgeHalo(p);
4811
- const x0 = Math.max(0, mask.bounds.x - halo);
4812
- const y0 = Math.max(0, mask.bounds.y - halo);
4813
- const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);
4814
- const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);
4815
- const ww = x1 - x0;
4816
- const wh = y1 - y0;
4817
- const m = new Float32Array(ww * wh);
4818
- for (let y = 0; y < wh; y++) {
4819
- const src = (y0 + y) * mask.width + x0;
4820
- for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
4821
- }
4822
- const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
4823
- const refined = refineCoverage(guide, m, ww, wh, p);
4824
- const out = new Uint8Array(mask.width * mask.height);
4825
- for (let y = 0; y < wh; y++) {
4826
- const dst = (y0 + y) * mask.width + x0;
4827
- for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
4828
- }
4829
- return createSelectionMask(mask.width, mask.height, out);
4830
- }
4831
-
4832
5141
  // src/image-editor/selection/stroke.ts
4833
5142
  function selectionRing(sel, width, location) {
4834
5143
  const half = location === "center" ? width / 2 : width;
@@ -6459,7 +6768,7 @@ var ImageEditorController = class {
6459
6768
  this.state.layers = next;
6460
6769
  this.notify();
6461
6770
  }
6462
- /** The registered segmentation provider, or `null` (then `selectSubject` is unavailable). */
6771
+ /** The registered segmentation provider, or `null` (then `selectSubject` finds the most salient subject without a model). */
6463
6772
  get segmentationProvider() {
6464
6773
  return this.segmentation;
6465
6774
  }
@@ -9663,6 +9972,17 @@ var ImageEditorController = class {
9663
9972
  this.setSelection(region, options.mode ?? "replace");
9664
9973
  return this.state.selection ?? null;
9665
9974
  }
9975
+ /**
9976
+ * Select › Focus Area: selects the parts of the image that are in focus (fine detail above the
9977
+ * image's noise), fitted to its edges, combined with the selection by `mode` as one undo step.
9978
+ * Resolves to the selection and the noise level measured (σ of luminance in levels).
9979
+ */
9980
+ async selectFocusArea(options = {}) {
9981
+ const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
9982
+ const { selection, noiseLevel } = focusArea(pixels, options);
9983
+ this.setSelection(selection, options.mode ?? "replace");
9984
+ return { selection: this.state.selection ?? null, noiseLevel };
9985
+ }
9666
9986
  /**
9667
9987
  * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
9668
9988
  * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)
@@ -9831,11 +10151,24 @@ var ImageEditorController = class {
9831
10151
  * Select › Subject: asks the segmentation provider for the objects matching `target` ("the
9832
10152
  * dog", "the sky") in the flattened image and selects their shapes, combined with the
9833
10153
  * 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.
10154
+ * pixels); an empty list leaves the selection as it was. Without a provider, the subject that
10155
+ * stands out most is found classically (superpixels, saliency, Otsu, GrabCut): `target` can't
10156
+ * choose it, and `method: 'saliency'` and a `note` say so.
9835
10157
  */
9836
- async selectSubject(target, options = {}) {
10158
+ async selectSubject(target = "", options = {}) {
9837
10159
  const provider = this.segmentation;
9838
- if (!provider) throw new Error("Select Subject needs a segmentation provider (setSegmentationProvider).");
10160
+ if (!provider) {
10161
+ const pixels = await this.requirePixels({});
10162
+ options.signal?.throwIfAborted();
10163
+ const { selection, reason } = findSubject(pixels, { enhanceEdge: options.enhanceEdge });
10164
+ if (selection.bounds) this.shapeCommand(selection, options);
10165
+ const unused = target.trim() ? ` "${target.trim()}" was not used to choose it.` : "";
10166
+ return {
10167
+ segments: selection.bounds ? [{ label: "subject", bounds: selection.bounds }] : [],
10168
+ method: "saliency",
10169
+ note: reason ?? `No segmentation model is set, so the subject that stands out most was selected.${unused}`
10170
+ };
10171
+ }
9839
10172
  const description = target.trim();
9840
10173
  if (!description) throw new Error("Describe the subject to select.");
9841
10174
  const { width, height } = this.naturalSize;
@@ -9861,7 +10194,7 @@ var ImageEditorController = class {
9861
10194
  options.signal?.throwIfAborted();
9862
10195
  }
9863
10196
  if (mask) this.shapeCommand(mask, options);
9864
- return { segments: segments.filter((s) => s.bounds) };
10197
+ return { segments: segments.filter((s) => s.bounds), method: "model" };
9865
10198
  }
9866
10199
  /** Quick Mask view (tints the unselected area). View state: no undo step. */
9867
10200
  setQuickMask(on) {
@@ -11156,7 +11489,7 @@ var ImageEditorController = class {
11156
11489
  }
11157
11490
  if (src.colorSpace !== "rgb") throw new LayerOperationError("Colours are given as #rrggbb, so the source profile must be RGB.");
11158
11491
  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 });
11492
+ const toLab2 = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
11160
11493
  return colors.map((input) => {
11161
11494
  const m = /^#?([0-9a-f]{6})$/i.exec(input.trim());
11162
11495
  if (!m) throw new LayerOperationError(`Invalid colour "${input}"; use #rrggbb.`);
@@ -11166,7 +11499,7 @@ var ImageEditorController = class {
11166
11499
  t.eval(rgb, out);
11167
11500
  const values = Array.from(out.subarray(0, t.outputs), (v) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4);
11168
11501
  const l = new Float32Array(3);
11169
- toLab.eval(rgb, l);
11502
+ toLab2.eval(rgb, l);
11170
11503
  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
11504
  const res = { input, values, lab };
11172
11505
  if (dst.colorSpace === "rgb") res.hex = `#${values.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("")}`;
@@ -12550,7 +12883,7 @@ var DEPTH_MASK_CONTROLS = [
12550
12883
  // src/image-editor/playback.ts
12551
12884
  var IMAGE_PLAYBACK_KIND = "layer";
12552
12885
  var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
12553
- var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
12886
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_focus_area|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
12554
12887
  var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|set_text_orientation|convert_text_to_shape)$/;
12555
12888
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
12556
12889
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -12716,6 +13049,9 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
12716
13049
  quickEnhance: false,
12717
13050
  objectSampleAll: true,
12718
13051
  objectEnhance: false,
13052
+ focusRange: 50,
13053
+ focusNoise: 0,
13054
+ focusSoftness: 4,
12719
13055
  typeText: "Type",
12720
13056
  typeSize: 72,
12721
13057
  typeFamily: "sans-serif"
@@ -13184,6 +13520,33 @@ function createImageSelectionTools(controller) {
13184
13520
  return summary(controller);
13185
13521
  }
13186
13522
  };
13523
+ const selectFocusAreaTool = {
13524
+ name: "select_focus_area",
13525
+ title: "Select the focus area",
13526
+ description: "Select \u203A Focus Area: selects the parts of the image that are in focus (sharp detail, above the image noise), with the edge fitted to the image, combined with the selection by mode. Use it to separate a sharp subject from a blurred background (shallow depth of field), e.g. before blurring or darkening the background. Raise range to take in less sharp areas, raise noise if grain in the background gets selected. One undoable step. Returns the selection and noiseLevel (\u03C3 of the image noise in levels of 255).",
13527
+ inputSchema: {
13528
+ type: "object",
13529
+ properties: {
13530
+ range: { type: "number", minimum: 0, maximum: 100, description: "In-Focus Range 0..100 (default 50): higher takes in less sharp areas too." },
13531
+ noise: { type: "number", minimum: 0, maximum: 100, description: "Image Noise Level 0..100 (default 0): raise it when grain is selected as detail." },
13532
+ softness: { type: "number", minimum: 0, maximum: 100, description: "Soften the edge over this many natural pixels (default 4; 0 for a hard edge)." },
13533
+ mode: modeSchema,
13534
+ sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." }
13535
+ },
13536
+ additionalProperties: false
13537
+ },
13538
+ annotations: writeAnnotations("Select the focus area", true),
13539
+ handler: async (args) => {
13540
+ const { noiseLevel } = await controller.selectFocusArea({
13541
+ range: args.range === void 0 ? void 0 : num2(args.range, 50),
13542
+ noise: args.noise === void 0 ? void 0 : num2(args.noise, 0),
13543
+ softness: args.softness === void 0 ? void 0 : num2(args.softness, 4),
13544
+ mode: parseSelectionMode(args.mode),
13545
+ sampleAllLayers: args.sampleAllLayers
13546
+ });
13547
+ return { ...summary(controller), noiseLevel };
13548
+ }
13549
+ };
13187
13550
  const magicWandTool = {
13188
13551
  name: "magic_wand",
13189
13552
  title: "Magic wand",
@@ -13738,6 +14101,8 @@ function createImageSelectionTools(controller) {
13738
14101
  selectPolygonTool,
13739
14102
  quickSelectTool,
13740
14103
  selectObjectTool,
14104
+ createImageSubjectSelectionTool(controller),
14105
+ selectFocusAreaTool,
13741
14106
  lineTool,
13742
14107
  magicWandTool,
13743
14108
  colorRangeTool,
@@ -13774,49 +14139,50 @@ function createImageSubjectSelectionTool(controller) {
13774
14139
  return {
13775
14140
  name: "select_subject",
13776
14141
  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.',
14142
+ 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
14143
  inputSchema: {
13779
14144
  type: "object",
13780
14145
  properties: {
13781
- target: { type: "string", minLength: 1, description: "What to select, in words." },
14146
+ target: { type: "string", description: "What to select, in words. Needed with a model; ignored without one." },
13782
14147
  maxSegments: {
13783
14148
  type: "integer",
13784
14149
  minimum: 1,
13785
14150
  maximum: 32,
13786
- description: "Select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
14151
+ description: "With a model: select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
13787
14152
  },
13788
14153
  mode: modeSchema,
13789
- feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
14154
+ feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." },
14155
+ enhanceEdge: { type: "boolean", description: "Without a model: smoother, less blocky edges (Auto-Enhance; default false)." }
13790
14156
  },
13791
- required: ["target"],
13792
14157
  additionalProperties: false
13793
14158
  },
13794
14159
  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.");
14160
+ handler: async ({ target, maxSegments, mode, feather, enhanceEdge }, context) => {
14161
+ if (target !== void 0 && typeof target !== "string") throw new Error("target must be a description in words.");
14162
+ 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
14163
  const perform = context?.perform;
13798
- const { segments } = await controller.selectSubject(target, {
14164
+ const { segments, method, note } = await controller.selectSubject(String(target ?? ""), {
13799
14165
  mode: parseSelectionMode(mode),
13800
14166
  feather: num2(feather, 0),
13801
14167
  maxSegments: typeof maxSegments === "number" ? Math.max(1, Math.round(maxSegments)) : void 0,
14168
+ enhanceEdge: enhanceEdge === true,
13802
14169
  signal: context?.signal,
13803
14170
  // Under playback the cursor traces what the model found, as a lasso, before it is selected.
13804
14171
  beforeApply: perform ? (outlines) => perform(planOutlineTrace(outlines)) : void 0
13805
14172
  });
13806
- return { ...summary(controller), found: segments };
14173
+ return { ...summary(controller), found: segments, method, ...note ? { note } : {} };
13807
14174
  }
13808
14175
  };
13809
14176
  }
13810
14177
  function createImageSegmentationToolProvider(controller, options = {}) {
13811
- const tool = createImageSubjectSelectionTool(controller);
13812
14178
  return withImagePlayback(
13813
14179
  {
13814
14180
  id: options.id ?? "image-segmentation",
13815
14181
  namespace: options.namespace,
13816
14182
  title: "Image segmentation",
13817
- description: "Select \u203A Subject: select objects in the image by description through an AI segmentation model.",
14183
+ description: "Deprecated: select_subject is in the image editor tool provider.",
13818
14184
  skill: "pixerate-image-editor",
13819
- tools: () => controller.segmentationProvider ? [tool] : []
14185
+ tools: () => []
13820
14186
  },
13821
14187
  options.playbackSurface
13822
14188
  );
@@ -20450,6 +20816,7 @@ export {
20450
20816
  findAdjustmentControl,
20451
20817
  findLayer,
20452
20818
  findLayerControl,
20819
+ findSubject,
20453
20820
  findWebGpu,
20454
20821
  finishTile,
20455
20822
  fitParagraphRuns,
@@ -20458,6 +20825,7 @@ export {
20458
20825
  flattenPath,
20459
20826
  flipCorners,
20460
20827
  flowField,
20828
+ focusArea,
20461
20829
  followLayerMove,
20462
20830
  fontOf,
20463
20831
  formatControlValue,
@@ -20471,6 +20839,7 @@ export {
20471
20839
  getViewportMetrics,
20472
20840
  getViewportProjection,
20473
20841
  globalImageCache,
20842
+ grabCutLabels,
20474
20843
  gradientDabColor,
20475
20844
  grainCatch,
20476
20845
  grainHeights,
@@ -20562,6 +20931,7 @@ export {
20562
20931
  isTransformAnchor,
20563
20932
  isValidWarp,
20564
20933
  keepDeepSamples,
20934
+ keepMainPieces,
20565
20935
  kelvinToTemperature,
20566
20936
  keysWeight,
20567
20937
  latticeViscosity,
@@ -20674,6 +21044,7 @@ export {
20674
21044
  offsetVectorMask,
20675
21045
  openMaskFile,
20676
21046
  openPsdFile,
21047
+ otsuThreshold,
20677
21048
  packRgba,
20678
21049
  packWetParams,
20679
21050
  paintGradientMappingFor,
@@ -20863,6 +21234,7 @@ export {
20863
21234
  shiftCoverage,
20864
21235
  simplifyPixelPath,
20865
21236
  skinToneRange,
21237
+ slicSuperpixels,
20866
21238
  smartFilterRows,
20867
21239
  smartObjectLayers,
20868
21240
  smoothCoverage,
@@ -20872,6 +21244,7 @@ export {
20872
21244
  snapPolygonToEdges,
20873
21245
  snapTo45,
20874
21246
  snapToEdge,
21247
+ softMaskToSelection,
20875
21248
  solidPaint,
20876
21249
  solidSurface,
20877
21250
  solveMembrane,
@@ -20893,6 +21266,7 @@ export {
20893
21266
  styleAt,
20894
21267
  subpathsToSvgPath,
20895
21268
  sumHistograms,
21269
+ superpixelSaliency,
20896
21270
  surfaceBytes,
20897
21271
  surfaceDepth,
20898
21272
  surfaceFromDeepPixels,
@@ -20985,6 +21359,8 @@ export {
20985
21359
  withImagePlayback,
20986
21360
  withNewIds,
20987
21361
  workImage,
21362
+ workLuma,
21363
+ workPlanes,
20988
21364
  writeAdjustmentBlock,
20989
21365
  writeDeepRegion,
20990
21366
  writeLfx2,