@pixerate/editor 0.199.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;
@@ -3875,6 +3877,342 @@ function findSubject(pixels, options = {}) {
3875
3877
  return { selection: upscaleRegion(work, pixels, roi, label, options.enhanceEdge ?? false) };
3876
3878
  }
3877
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
+
3878
4216
  // src/image-editor/layers/shape-type-ui.ts
3879
4217
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
3880
4218
  kind: "rect",
@@ -4800,238 +5138,6 @@ function segmentsToSelection(width, height, results) {
4800
5138
  return out;
4801
5139
  }
4802
5140
 
4803
- // src/image-editor/selection/refine.ts
4804
- var DEFAULT_REFINE_EDGE_PARAMS = {
4805
- radius: 0,
4806
- smartRadius: false,
4807
- smooth: 0,
4808
- feather: 0,
4809
- contrast: 0,
4810
- shiftEdge: 0
4811
- };
4812
- var GUIDED_FILTER_EPS = 1e-4;
4813
- var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
4814
- function normalizeRefineEdgeParams(p = {}) {
4815
- return {
4816
- radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
4817
- smartRadius: p.smartRadius === true,
4818
- smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
4819
- feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
4820
- contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
4821
- shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
4822
- };
4823
- }
4824
- var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
4825
- var smoothSigma = (p) => p.smooth / 100 * 4;
4826
- var featherSigma = (p) => p.feather / 2;
4827
- function shiftAmount(p) {
4828
- const px2 = p.shiftEdge / 100 * Math.max(p.radius, 4);
4829
- return Number.isFinite(px2) ? Math.min(64, Math.max(-64, px2)) : 0;
4830
- }
4831
- function refineEdgeHalo(params = {}) {
4832
- const p = normalizeRefineEdgeParams(params);
4833
- const g = gfRadius(p);
4834
- const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
4835
- return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
4836
- }
4837
- function boxMean(src, w, h, r) {
4838
- const tmp = new Float32Array(w * h);
4839
- const pre = new Float64Array(Math.max(w, h) + 1);
4840
- for (let y = 0; y < h; y++) {
4841
- const row = y * w;
4842
- for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
4843
- for (let x = 0; x < w; x++) {
4844
- const lo = Math.max(0, x - r);
4845
- const hi = Math.min(w, x + r + 1);
4846
- tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
4847
- }
4848
- }
4849
- const out = new Float32Array(w * h);
4850
- for (let x = 0; x < w; x++) {
4851
- for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
4852
- for (let y = 0; y < h; y++) {
4853
- const lo = Math.max(0, y - r);
4854
- const hi = Math.min(h, y + r + 1);
4855
- out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
4856
- }
4857
- }
4858
- return out;
4859
- }
4860
- function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
4861
- const n = w * h;
4862
- const [c0, c1, c2] = guide;
4863
- const prod = (a, b2) => {
4864
- const o = new Float32Array(n);
4865
- for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
4866
- return o;
4867
- };
4868
- const m0 = boxMean(c0, w, h, r);
4869
- const m1 = boxMean(c1, w, h, r);
4870
- const m2 = boxMean(c2, w, h, r);
4871
- const mp = boxMean(p, w, h, r);
4872
- const mip0 = boxMean(prod(c0, p), w, h, r);
4873
- const mip1 = boxMean(prod(c1, p), w, h, r);
4874
- const mip2 = boxMean(prod(c2, p), w, h, r);
4875
- const s00 = boxMean(prod(c0, c0), w, h, r);
4876
- const s01 = boxMean(prod(c0, c1), w, h, r);
4877
- const s02 = boxMean(prod(c0, c2), w, h, r);
4878
- const s11 = boxMean(prod(c1, c1), w, h, r);
4879
- const s12 = boxMean(prod(c1, c2), w, h, r);
4880
- const s22 = boxMean(prod(c2, c2), w, h, r);
4881
- const a0 = new Float32Array(n);
4882
- const a1 = new Float32Array(n);
4883
- const a2 = new Float32Array(n);
4884
- const b = new Float32Array(n);
4885
- for (let i = 0; i < n; i++) {
4886
- const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
4887
- const rr = s00[i] - u0 * u0 + eps;
4888
- const rg = s01[i] - u0 * u1;
4889
- const rb = s02[i] - u0 * u2;
4890
- const gg = s11[i] - u1 * u1 + eps;
4891
- const gb = s12[i] - u1 * u2;
4892
- const bb = s22[i] - u2 * u2 + eps;
4893
- const v0 = mip0[i] - u0 * up;
4894
- const v1 = mip1[i] - u1 * up;
4895
- const v2 = mip2[i] - u2 * up;
4896
- const i00 = gg * bb - gb * gb;
4897
- const i01 = gb * rb - rg * bb;
4898
- const i02 = rg * gb - gg * rb;
4899
- const i11 = rr * bb - rb * rb;
4900
- const i12 = rb * rg - rr * gb;
4901
- const i22 = rr * gg - rg * rg;
4902
- const det = rr * i00 + rg * i01 + rb * i02;
4903
- let x0 = 0, x1 = 0, x2 = 0;
4904
- if (Math.abs(det) >= 1e-30) {
4905
- x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
4906
- x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
4907
- x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
4908
- }
4909
- a0[i] = x0;
4910
- a1[i] = x1;
4911
- a2[i] = x2;
4912
- b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
4913
- }
4914
- const ma0 = boxMean(a0, w, h, r);
4915
- const ma1 = boxMean(a1, w, h, r);
4916
- const ma2 = boxMean(a2, w, h, r);
4917
- const mb = boxMean(b, w, h, r);
4918
- const out = new Float32Array(n);
4919
- for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
4920
- return out;
4921
- }
4922
- function edgeWidth(guide, w, h, r) {
4923
- const n = w * h;
4924
- const y = new Float32Array(n);
4925
- for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
4926
- const g = new Float32Array(n);
4927
- for (let yy = 0; yy < h; yy++) {
4928
- for (let xx = 0; xx < w; xx++) {
4929
- const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
4930
- const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
4931
- g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
4932
- }
4933
- }
4934
- const gm = greyMorphology(g, w, h, r, true);
4935
- const hi = greyMorphology(y, w, h, r, true);
4936
- const lo = greyMorphology(y, w, h, r, false);
4937
- const out = new Float32Array(n);
4938
- for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
4939
- return out;
4940
- }
4941
- function refineCoverage(guide, m, w, h, params = {}) {
4942
- const p = normalizeRefineEdgeParams(params);
4943
- let a = Float32Array.from(m);
4944
- if (p.radius > 0 && guide) {
4945
- const n = w * h;
4946
- const boundary = new Uint8Array(n);
4947
- let any = false;
4948
- for (let y = 0; y < h; y++) {
4949
- for (let x = 0; x < w; x++) {
4950
- const i = y * w + x;
4951
- const v = m[i] >= 0.5;
4952
- 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;
4953
- if (diff) {
4954
- boundary[i] = 1;
4955
- any = true;
4956
- }
4957
- }
4958
- }
4959
- if (any) {
4960
- const dist = distanceTransform(boundary, w, h);
4961
- const r = gfRadius(p);
4962
- const q = guidedFilterColor(guide, m, w, h, r);
4963
- const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
4964
- for (let i = 0; i < n; i++) {
4965
- const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
4966
- const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));
4967
- if (wgt > 0) {
4968
- const qi = Math.min(1, Math.max(0, q[i]));
4969
- a[i] = m[i] + (qi - m[i]) * wgt;
4970
- }
4971
- }
4972
- }
4973
- }
4974
- const ss = smoothSigma(p);
4975
- if (ss >= 0.3) {
4976
- a = gaussianBlur(a, w, h, ss, "clamp");
4977
- const gain = 1 + ss;
4978
- for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
4979
- }
4980
- a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
4981
- if (p.contrast > 0) {
4982
- const gain = 1 / (1 - 0.99 * p.contrast / 100);
4983
- for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
4984
- }
4985
- const amount = shiftAmount(p);
4986
- if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
4987
- return a;
4988
- }
4989
- function guidePlanes(pixels, win) {
4990
- const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
4991
- const n = box.width * box.height;
4992
- const r = new Float32Array(n);
4993
- const g = new Float32Array(n);
4994
- const b = new Float32Array(n);
4995
- const d = pixels.data;
4996
- for (let y = 0; y < box.height; y++) {
4997
- for (let x = 0; x < box.width; x++) {
4998
- const i = y * box.width + x;
4999
- const p = ((box.y + y) * pixels.width + box.x + x) * 4;
5000
- r[i] = d[p] / 255;
5001
- g[i] = d[p + 1] / 255;
5002
- b[i] = d[p + 2] / 255;
5003
- }
5004
- }
5005
- return [r, g, b];
5006
- }
5007
- function refineSelection(pixels, mask, params = {}) {
5008
- const p = normalizeRefineEdgeParams(params);
5009
- if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
5010
- throw new Error("Refine Edge needs pixels the size of the selection");
5011
- }
5012
- if (!mask.bounds) return mask;
5013
- const halo = refineEdgeHalo(p);
5014
- const x0 = Math.max(0, mask.bounds.x - halo);
5015
- const y0 = Math.max(0, mask.bounds.y - halo);
5016
- const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);
5017
- const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);
5018
- const ww = x1 - x0;
5019
- const wh = y1 - y0;
5020
- const m = new Float32Array(ww * wh);
5021
- for (let y = 0; y < wh; y++) {
5022
- const src = (y0 + y) * mask.width + x0;
5023
- for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
5024
- }
5025
- const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
5026
- const refined = refineCoverage(guide, m, ww, wh, p);
5027
- const out = new Uint8Array(mask.width * mask.height);
5028
- for (let y = 0; y < wh; y++) {
5029
- const dst = (y0 + y) * mask.width + x0;
5030
- for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
5031
- }
5032
- return createSelectionMask(mask.width, mask.height, out);
5033
- }
5034
-
5035
5141
  // src/image-editor/selection/stroke.ts
5036
5142
  function selectionRing(sel, width, location) {
5037
5143
  const half = location === "center" ? width / 2 : width;
@@ -9866,6 +9972,17 @@ var ImageEditorController = class {
9866
9972
  this.setSelection(region, options.mode ?? "replace");
9867
9973
  return this.state.selection ?? null;
9868
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
+ }
9869
9986
  /**
9870
9987
  * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
9871
9988
  * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)
@@ -12766,7 +12883,7 @@ var DEPTH_MASK_CONTROLS = [
12766
12883
  // src/image-editor/playback.ts
12767
12884
  var IMAGE_PLAYBACK_KIND = "layer";
12768
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)$/;
12769
- 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)$/;
12770
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)$/;
12771
12888
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
12772
12889
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -12932,6 +13049,9 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
12932
13049
  quickEnhance: false,
12933
13050
  objectSampleAll: true,
12934
13051
  objectEnhance: false,
13052
+ focusRange: 50,
13053
+ focusNoise: 0,
13054
+ focusSoftness: 4,
12935
13055
  typeText: "Type",
12936
13056
  typeSize: 72,
12937
13057
  typeFamily: "sans-serif"
@@ -13400,6 +13520,33 @@ function createImageSelectionTools(controller) {
13400
13520
  return summary(controller);
13401
13521
  }
13402
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
+ };
13403
13550
  const magicWandTool = {
13404
13551
  name: "magic_wand",
13405
13552
  title: "Magic wand",
@@ -13955,6 +14102,7 @@ function createImageSelectionTools(controller) {
13955
14102
  quickSelectTool,
13956
14103
  selectObjectTool,
13957
14104
  createImageSubjectSelectionTool(controller),
14105
+ selectFocusAreaTool,
13958
14106
  lineTool,
13959
14107
  magicWandTool,
13960
14108
  colorRangeTool,
@@ -20677,6 +20825,7 @@ export {
20677
20825
  flattenPath,
20678
20826
  flipCorners,
20679
20827
  flowField,
20828
+ focusArea,
20680
20829
  followLayerMove,
20681
20830
  fontOf,
20682
20831
  formatControlValue,
@@ -21095,6 +21244,7 @@ export {
21095
21244
  snapPolygonToEdges,
21096
21245
  snapTo45,
21097
21246
  snapToEdge,
21247
+ softMaskToSelection,
21098
21248
  solidPaint,
21099
21249
  solidSurface,
21100
21250
  solveMembrane,
@@ -21209,6 +21359,8 @@ export {
21209
21359
  withImagePlayback,
21210
21360
  withNewIds,
21211
21361
  workImage,
21362
+ workLuma,
21363
+ workPlanes,
21212
21364
  writeAdjustmentBlock,
21213
21365
  writeDeepRegion,
21214
21366
  writeLfx2,