@pixerate/editor 0.207.0 → 0.208.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.
Files changed (69) hide show
  1. package/dist/animation-editor/index.cjs +60 -59
  2. package/dist/animation-editor/index.js +6 -5
  3. package/dist/{brushstrokeRender-7LXCU7PD.js → brushstrokeRender-C3UU6GVC.js} +5 -4
  4. package/dist/{brushstrokeRender-VO4ZTWVA.cjs → brushstrokeRender-EAJNE6WP.cjs} +12 -11
  5. package/dist/canvas/index.cjs +8 -8
  6. package/dist/canvas/index.js +1 -1
  7. package/dist/{chunk-2N2QPO3H.js → chunk-2PBZBRP5.js} +9 -7
  8. package/dist/{chunk-7NW46N7I.cjs → chunk-3EHPLDFQ.cjs} +54 -52
  9. package/dist/{chunk-H367YTXI.js → chunk-42TWUPNJ.js} +13 -387
  10. package/dist/chunk-474IQG74.cjs +384 -0
  11. package/dist/{chunk-MLC7HUHO.js → chunk-BGOCAUXH.js} +1 -1
  12. package/dist/chunk-DWYQPL2A.js +2010 -0
  13. package/dist/{chunk-YXKLVD56.js → chunk-F7YA3MMC.js} +2 -2
  14. package/dist/{chunk-ZZGPYNS2.js → chunk-G4E6RUPC.js} +1 -1
  15. package/dist/chunk-HVVR5PZW.js +384 -0
  16. package/dist/{chunk-FR2Q2LVF.cjs → chunk-I7G3RSUJ.cjs} +35 -35
  17. package/dist/chunk-K6PXS6DO.cjs +102 -0
  18. package/dist/{chunk-ZIDUPSP4.js → chunk-KFIOEOCQ.js} +24 -761
  19. package/dist/{chunk-V7GWZIZK.cjs → chunk-KWNMBMKG.cjs} +23 -21
  20. package/dist/chunk-L3G23N5Y.js +763 -0
  21. package/dist/{chunk-GHCXLQ47.js → chunk-NDMXB4PR.js} +2 -2
  22. package/dist/{chunk-CWM3CUUX.cjs → chunk-OLNIPTB3.cjs} +39 -413
  23. package/dist/{chunk-EBDCRTZI.cjs → chunk-PQ3H2AAN.cjs} +92 -90
  24. package/dist/{chunk-BRKVNXDZ.cjs → chunk-QAUIZPYI.cjs} +7 -7
  25. package/dist/{chunk-UYXL6ZWQ.cjs → chunk-QE7D7TZR.cjs} +494 -1231
  26. package/dist/chunk-QYLWG22N.cjs +763 -0
  27. package/dist/{chunk-YQ5C5BVK.cjs → chunk-ROVK6AXX.cjs} +60 -60
  28. package/dist/{chunk-HGTDMKNE.cjs → chunk-UOGKSVRQ.cjs} +126 -221
  29. package/dist/{chunk-EL5PAHNP.js → chunk-USQCKRZG.js} +6 -4
  30. package/dist/{chunk-36OKQDCF.js → chunk-WJ2RBZMM.js} +5 -3
  31. package/dist/chunk-X4LUGTQX.cjs +2010 -0
  32. package/dist/{chunk-NQMP3CWF.js → chunk-YBJXCQTR.js} +15 -110
  33. package/dist/chunk-ZIF4UJJ6.js +102 -0
  34. package/dist/{chunk-WTW7QRTS.cjs → chunk-ZMHPQ6HP.cjs} +1 -1
  35. package/dist/image-editor/controller.d.cts +28 -3
  36. package/dist/image-editor/controller.d.ts +28 -3
  37. package/dist/image-editor/index.cjs +1726 -3359
  38. package/dist/image-editor/index.js +241 -1874
  39. package/dist/image-editor/selection/index.d.cts +1 -0
  40. package/dist/image-editor/selection/index.d.ts +1 -0
  41. package/dist/image-editor/selection/segment/focus.d.cts +2 -0
  42. package/dist/image-editor/selection/segment/focus.d.ts +2 -0
  43. package/dist/image-editor/selection/segment/grabcut.d.cts +4 -2
  44. package/dist/image-editor/selection/segment/grabcut.d.ts +4 -2
  45. package/dist/image-editor/selection/segment/sky.d.cts +2 -0
  46. package/dist/image-editor/selection/segment/sky.d.ts +2 -0
  47. package/dist/image-editor/selection/segment/subject.d.cts +2 -1
  48. package/dist/image-editor/selection/segment/subject.d.ts +2 -1
  49. package/dist/image-editor/selection/segment/work.d.cts +5 -0
  50. package/dist/image-editor/selection/segment/work.d.ts +5 -0
  51. package/dist/image-editor/selection/select-and-mask.d.cts +12 -2
  52. package/dist/image-editor/selection/select-and-mask.d.ts +12 -2
  53. package/dist/image-editor/selection/worker.d.cts +126 -0
  54. package/dist/image-editor/selection/worker.d.ts +126 -0
  55. package/dist/image-editor/selection-worker.cjs +14 -0
  56. package/dist/image-editor/selection-worker.d.cts +12 -0
  57. package/dist/image-editor/selection-worker.d.ts +12 -0
  58. package/dist/image-editor/selection-worker.js +14 -0
  59. package/dist/image-editor/types.d.cts +8 -0
  60. package/dist/image-editor/types.d.ts +8 -0
  61. package/dist/paint/index.cjs +6 -5
  62. package/dist/paint/index.js +5 -4
  63. package/dist/paint-editor/index.cjs +394 -390
  64. package/dist/paint-editor/index.js +18 -14
  65. package/dist/raster/index.cjs +22 -18
  66. package/dist/raster/index.js +29 -25
  67. package/dist/vector/index.cjs +986 -985
  68. package/dist/vector/index.js +6 -5
  69. package/package.json +11 -1
@@ -0,0 +1,2010 @@
1
+ import {
2
+ combineSelections,
3
+ coverageToByte,
4
+ createSelectionMask,
5
+ distanceTransform,
6
+ greyMorphology,
7
+ selectionFromRegion,
8
+ selectionValueAt,
9
+ shiftCoverage
10
+ } from "./chunk-L3G23N5Y.js";
11
+ import {
12
+ gaussianBlur
13
+ } from "./chunk-ZIF4UJJ6.js";
14
+
15
+ // src/image-editor/selection/refine.ts
16
+ var DEFAULT_REFINE_EDGE_PARAMS = {
17
+ radius: 0,
18
+ smartRadius: false,
19
+ smooth: 0,
20
+ feather: 0,
21
+ contrast: 0,
22
+ shiftEdge: 0
23
+ };
24
+ var GUIDED_FILTER_EPS = 1e-4;
25
+ var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
26
+ function normalizeRefineEdgeParams(p = {}) {
27
+ return {
28
+ radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
29
+ smartRadius: p.smartRadius === true,
30
+ smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
31
+ feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
32
+ contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
33
+ shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
34
+ };
35
+ }
36
+ var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
37
+ var smoothSigma = (p) => p.smooth / 100 * 4;
38
+ var featherSigma = (p) => p.feather / 2;
39
+ function shiftAmount(p) {
40
+ const px = p.shiftEdge / 100 * Math.max(p.radius, 4);
41
+ return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;
42
+ }
43
+ function refineEdgeHalo(params = {}) {
44
+ const p = normalizeRefineEdgeParams(params);
45
+ const g = gfRadius(p);
46
+ const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
47
+ return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
48
+ }
49
+ function boxMean(src, w, h, r) {
50
+ const tmp = new Float32Array(w * h);
51
+ const pre = new Float64Array(Math.max(w, h) + 1);
52
+ for (let y = 0; y < h; y++) {
53
+ const row = y * w;
54
+ for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
55
+ for (let x = 0; x < w; x++) {
56
+ const lo = Math.max(0, x - r);
57
+ const hi = Math.min(w, x + r + 1);
58
+ tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
59
+ }
60
+ }
61
+ const out = new Float32Array(w * h);
62
+ for (let x = 0; x < w; x++) {
63
+ for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
64
+ for (let y = 0; y < h; y++) {
65
+ const lo = Math.max(0, y - r);
66
+ const hi = Math.min(h, y + r + 1);
67
+ out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
73
+ const n = w * h;
74
+ const [c0, c1, c2] = guide;
75
+ const prod = (a, b2) => {
76
+ const o = new Float32Array(n);
77
+ for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
78
+ return o;
79
+ };
80
+ const m0 = boxMean(c0, w, h, r);
81
+ const m1 = boxMean(c1, w, h, r);
82
+ const m2 = boxMean(c2, w, h, r);
83
+ const mp = boxMean(p, w, h, r);
84
+ const mip0 = boxMean(prod(c0, p), w, h, r);
85
+ const mip1 = boxMean(prod(c1, p), w, h, r);
86
+ const mip2 = boxMean(prod(c2, p), w, h, r);
87
+ const s00 = boxMean(prod(c0, c0), w, h, r);
88
+ const s01 = boxMean(prod(c0, c1), w, h, r);
89
+ const s02 = boxMean(prod(c0, c2), w, h, r);
90
+ const s11 = boxMean(prod(c1, c1), w, h, r);
91
+ const s12 = boxMean(prod(c1, c2), w, h, r);
92
+ const s22 = boxMean(prod(c2, c2), w, h, r);
93
+ const a0 = new Float32Array(n);
94
+ const a1 = new Float32Array(n);
95
+ const a2 = new Float32Array(n);
96
+ const b = new Float32Array(n);
97
+ for (let i = 0; i < n; i++) {
98
+ const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
99
+ const rr = s00[i] - u0 * u0 + eps;
100
+ const rg = s01[i] - u0 * u1;
101
+ const rb = s02[i] - u0 * u2;
102
+ const gg = s11[i] - u1 * u1 + eps;
103
+ const gb = s12[i] - u1 * u2;
104
+ const bb = s22[i] - u2 * u2 + eps;
105
+ const v0 = mip0[i] - u0 * up;
106
+ const v1 = mip1[i] - u1 * up;
107
+ const v2 = mip2[i] - u2 * up;
108
+ const i00 = gg * bb - gb * gb;
109
+ const i01 = gb * rb - rg * bb;
110
+ const i02 = rg * gb - gg * rb;
111
+ const i11 = rr * bb - rb * rb;
112
+ const i12 = rb * rg - rr * gb;
113
+ const i22 = rr * gg - rg * rg;
114
+ const det = rr * i00 + rg * i01 + rb * i02;
115
+ let x0 = 0, x1 = 0, x2 = 0;
116
+ if (Math.abs(det) >= 1e-30) {
117
+ x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
118
+ x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
119
+ x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
120
+ }
121
+ a0[i] = x0;
122
+ a1[i] = x1;
123
+ a2[i] = x2;
124
+ b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
125
+ }
126
+ const ma0 = boxMean(a0, w, h, r);
127
+ const ma1 = boxMean(a1, w, h, r);
128
+ const ma2 = boxMean(a2, w, h, r);
129
+ const mb = boxMean(b, w, h, r);
130
+ const out = new Float32Array(n);
131
+ for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
132
+ return out;
133
+ }
134
+ function edgeWidth(guide, w, h, r) {
135
+ const n = w * h;
136
+ const y = new Float32Array(n);
137
+ for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
138
+ const g = new Float32Array(n);
139
+ for (let yy = 0; yy < h; yy++) {
140
+ for (let xx = 0; xx < w; xx++) {
141
+ const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
142
+ const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
143
+ g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
144
+ }
145
+ }
146
+ const gm = greyMorphology(g, w, h, r, true);
147
+ const hi = greyMorphology(y, w, h, r, true);
148
+ const lo = greyMorphology(y, w, h, r, false);
149
+ const out = new Float32Array(n);
150
+ for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
151
+ return out;
152
+ }
153
+ function refineCoverage(guide, m, w, h, params = {}, band = null) {
154
+ const p = normalizeRefineEdgeParams(params);
155
+ let a = Float32Array.from(m);
156
+ if ((p.radius > 0 || band) && guide) {
157
+ const n = w * h;
158
+ const boundary = new Uint8Array(n);
159
+ let any = false;
160
+ for (let y = 0; y < h; y++) {
161
+ for (let x = 0; x < w; x++) {
162
+ const i = y * w + x;
163
+ const v = m[i] >= 0.5;
164
+ 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;
165
+ if (diff) {
166
+ boundary[i] = 1;
167
+ any = true;
168
+ }
169
+ }
170
+ }
171
+ if (any || band) {
172
+ const dist = distanceTransform(boundary, w, h);
173
+ const r = band ? Math.max(4, gfRadius(p)) : gfRadius(p);
174
+ const q = guidedFilterColor(guide, m, w, h, r);
175
+ const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
176
+ for (let i = 0; i < n; i++) {
177
+ const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
178
+ const edge = p.radius > 0 ? Math.min(1, Math.max(0, reff + 1 - dist[i])) : 0;
179
+ const wgt = band ? Math.max(edge, Math.min(1, Math.max(0, band[i]))) : edge;
180
+ if (wgt > 0) {
181
+ const qi = Math.min(1, Math.max(0, q[i]));
182
+ a[i] = m[i] + (qi - m[i]) * wgt;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ const ss = smoothSigma(p);
188
+ if (ss >= 0.3) {
189
+ a = gaussianBlur(a, w, h, ss, "clamp");
190
+ const gain = 1 + ss;
191
+ for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
192
+ }
193
+ a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
194
+ if (p.contrast > 0) {
195
+ const gain = 1 / (1 - 0.99 * p.contrast / 100);
196
+ for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
197
+ }
198
+ const amount = shiftAmount(p);
199
+ if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
200
+ return a;
201
+ }
202
+ function guidePlanes(pixels, win) {
203
+ const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
204
+ const n = box.width * box.height;
205
+ const r = new Float32Array(n);
206
+ const g = new Float32Array(n);
207
+ const b = new Float32Array(n);
208
+ const d = pixels.data;
209
+ for (let y = 0; y < box.height; y++) {
210
+ for (let x = 0; x < box.width; x++) {
211
+ const i = y * box.width + x;
212
+ const p = ((box.y + y) * pixels.width + box.x + x) * 4;
213
+ r[i] = d[p] / 255;
214
+ g[i] = d[p + 1] / 255;
215
+ b[i] = d[p + 2] / 255;
216
+ }
217
+ }
218
+ return [r, g, b];
219
+ }
220
+ function refineSelection(pixels, mask, params = {}, refineBrush = null) {
221
+ const p = normalizeRefineEdgeParams(params);
222
+ if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
223
+ throw new Error("Refine Edge needs pixels the size of the selection");
224
+ }
225
+ if (!mask.bounds) return mask;
226
+ const brush = refineBrush?.bounds ? refineBrush : null;
227
+ const halo = refineEdgeHalo(p) + (brush ? 2 * Math.max(4, gfRadius(p)) : 0);
228
+ const b = brush ? {
229
+ x: Math.min(mask.bounds.x, brush.bounds.x),
230
+ y: Math.min(mask.bounds.y, brush.bounds.y),
231
+ r: Math.max(mask.bounds.x + mask.bounds.width, brush.bounds.x + brush.bounds.width),
232
+ btm: Math.max(mask.bounds.y + mask.bounds.height, brush.bounds.y + brush.bounds.height)
233
+ } : { x: mask.bounds.x, y: mask.bounds.y, r: mask.bounds.x + mask.bounds.width, btm: mask.bounds.y + mask.bounds.height };
234
+ const x0 = Math.max(0, b.x - halo);
235
+ const y0 = Math.max(0, b.y - halo);
236
+ const x1 = Math.min(mask.width, b.r + halo);
237
+ const y1 = Math.min(mask.height, b.btm + halo);
238
+ const ww = x1 - x0;
239
+ const wh = y1 - y0;
240
+ const m = new Float32Array(ww * wh);
241
+ for (let y = 0; y < wh; y++) {
242
+ const src = (y0 + y) * mask.width + x0;
243
+ for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
244
+ }
245
+ const guide = pixels && (p.radius > 0 || brush) ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
246
+ let band = null;
247
+ if (brush) {
248
+ band = new Float32Array(ww * wh);
249
+ const bd = brush.data;
250
+ for (let y = 0; y < wh; y++) {
251
+ const src = (y0 + y) * mask.width + x0;
252
+ for (let x = 0; x < ww; x++) band[y * ww + x] = bd[src + x] / 255;
253
+ }
254
+ }
255
+ const refined = refineCoverage(guide, m, ww, wh, p, band);
256
+ const out = new Uint8Array(mask.width * mask.height);
257
+ for (let y = 0; y < wh; y++) {
258
+ const dst = (y0 + y) * mask.width + x0;
259
+ for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
260
+ }
261
+ return createSelectionMask(mask.width, mask.height, out);
262
+ }
263
+ function decontaminateColors(rgba, coverage, w, h, amount, radius = 8) {
264
+ const out = Uint8ClampedArray.from(rgba);
265
+ const n = w * h;
266
+ const k = Math.min(1, Math.max(0, amount));
267
+ if (k === 0) return out;
268
+ const r = Math.max(2, Math.round(radius));
269
+ const fgW = new Float32Array(n);
270
+ const bgW = new Float32Array(n);
271
+ for (let i = 0; i < n; i++) {
272
+ fgW[i] = coverage[i] >= 250 ? 1 : 0;
273
+ bgW[i] = coverage[i] <= 5 ? 1 : 0;
274
+ }
275
+ const meanOf = (weights, radiusPx) => {
276
+ const wm = boxMean(weights, w, h, radiusPx);
277
+ const planes = [0, 1, 2].map((c) => {
278
+ const v = new Float32Array(n);
279
+ for (let i = 0; i < n; i++) v[i] = rgba[i * 4 + c] * weights[i];
280
+ const m = boxMean(v, w, h, radiusPx);
281
+ for (let i = 0; i < n; i++) m[i] = wm[i] > 1e-6 ? m[i] / wm[i] : NaN;
282
+ return m;
283
+ });
284
+ return { weight: wm, planes };
285
+ };
286
+ const fg = meanOf(fgW, r);
287
+ const fgWide = meanOf(fgW, r * 3);
288
+ const bg = meanOf(bgW, r);
289
+ for (let i = 0; i < n; i++) {
290
+ const c = coverage[i];
291
+ if (c <= 5 || c >= 250) continue;
292
+ const a = c / 255;
293
+ for (let ch = 0; ch < 3; ch++) {
294
+ const C = rgba[i * 4 + ch];
295
+ const B = bg.planes[ch][i];
296
+ const Fm = Number.isNaN(fg.planes[ch][i]) ? fgWide.planes[ch][i] : fg.planes[ch][i];
297
+ let F;
298
+ if (a >= 0.5 && !Number.isNaN(B)) F = (C - (1 - a) * B) / a;
299
+ else if (!Number.isNaN(Fm)) F = Fm;
300
+ else F = C;
301
+ out[i * 4 + ch] = Math.round(C + k * (Math.min(255, Math.max(0, F)) - C));
302
+ }
303
+ }
304
+ return out;
305
+ }
306
+
307
+ // src/image-editor/selection/shapes.ts
308
+ function polygonCoverage(points, width, height, antiAlias = true) {
309
+ const { box, coverage } = polygonCoverageBox(points, width, height, antiAlias);
310
+ if (!box || box.width === width && box.height === height) return box ? coverage : new Float32Array(width * height);
311
+ const m = new Float32Array(width * height);
312
+ for (let y = 0; y < box.height; y++) m.set(coverage.subarray(y * box.width, (y + 1) * box.width), (box.y + y) * width + box.x);
313
+ return m;
314
+ }
315
+ function polygonCoverageBox(points, width, height, antiAlias = true) {
316
+ const none = { box: null, coverage: new Float32Array(0) };
317
+ if (points.length < 3 || width <= 0 || height <= 0) return none;
318
+ let minX = Infinity;
319
+ let minY = Infinity;
320
+ let maxX = -Infinity;
321
+ let maxY = -Infinity;
322
+ for (const p of points) {
323
+ if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
324
+ minX = Math.min(minX, p.x);
325
+ minY = Math.min(minY, p.y);
326
+ maxX = Math.max(maxX, p.x);
327
+ maxY = Math.max(maxY, p.y);
328
+ }
329
+ const bx0 = Math.max(0, Math.floor(minX));
330
+ const by0 = Math.max(0, Math.floor(minY));
331
+ const bx1 = Math.min(width, Math.ceil(maxX));
332
+ const by1 = Math.min(height, Math.ceil(maxY));
333
+ if (!(bx1 > bx0 && by1 > by0)) return none;
334
+ const bw = bx1 - bx0;
335
+ const m = new Float32Array(bw * (by1 - by0));
336
+ const sub = antiAlias ? 4 : 1;
337
+ const edges = [];
338
+ for (let i = 0; i < points.length; i++) {
339
+ const a = points[i];
340
+ const b = points[(i + 1) % points.length];
341
+ if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;
342
+ if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });
343
+ else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });
344
+ }
345
+ edges.sort((p, q) => p.top - q.top);
346
+ let next = 0;
347
+ let active = [];
348
+ const xs = [];
349
+ for (let py = by0; py < by1; py++) {
350
+ const row = (py - by0) * bw - bx0;
351
+ for (let s = 0; s < sub; s++) {
352
+ const y = py + (s + 0.5) / sub;
353
+ while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
354
+ if (active.length) active = active.filter((e) => e.bottom > y);
355
+ if (!active.length) continue;
356
+ xs.length = 0;
357
+ for (const e of active) {
358
+ if (e.top <= y) xs.push(e.ax + (y - e.ay) / (e.by - e.ay) * (e.bx - e.ax));
359
+ }
360
+ xs.sort((a, b) => a - b);
361
+ for (let k = 0; k + 1 < xs.length; k += 2) {
362
+ const x0 = Math.max(0, xs[k]);
363
+ const x1 = Math.min(width, xs[k + 1]);
364
+ if (x1 <= x0) continue;
365
+ const c0 = Math.floor(x0);
366
+ const c1 = Math.min(width, Math.ceil(x1));
367
+ for (let cx = c0; cx < c1; cx++) {
368
+ let cov;
369
+ if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);
370
+ else {
371
+ const c = cx + 0.5;
372
+ cov = c >= x0 && c < x1 ? 1 : 0;
373
+ }
374
+ if (cov > 0) m[row + cx] += cov / sub;
375
+ }
376
+ }
377
+ }
378
+ }
379
+ for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;
380
+ return { box: { x: bx0, y: by0, width: bw, height: by1 - by0 }, coverage: m };
381
+ }
382
+ function polygonSelection(width, height, points, options = {}) {
383
+ const { box, coverage } = polygonCoverageBox(points, width, height, options.antiAlias ?? true);
384
+ if (!box) return createSelectionMask(width, height);
385
+ const out = new Uint8Array(coverage.length);
386
+ for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
387
+ return selectionFromRegion(width, height, box, out);
388
+ }
389
+ function normalizeRect(rect) {
390
+ const x = rect.width < 0 ? rect.x + rect.width : rect.x;
391
+ const y = rect.height < 0 ? rect.y + rect.height : rect.y;
392
+ return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };
393
+ }
394
+ function rectSelection(width, height, rect, options = {}) {
395
+ const r = normalizeRect(rect);
396
+ const pts = [
397
+ { x: r.x, y: r.y },
398
+ { x: r.x + r.width, y: r.y },
399
+ { x: r.x + r.width, y: r.y + r.height },
400
+ { x: r.x, y: r.y + r.height }
401
+ ];
402
+ return polygonSelection(width, height, pts, options);
403
+ }
404
+ function ellipsePoints(rect) {
405
+ const r = normalizeRect(rect);
406
+ const rx = r.width / 2;
407
+ const ry = r.height / 2;
408
+ const cx = r.x + rx;
409
+ const cy = r.y + ry;
410
+ const rmax = Math.max(rx, ry, 0.5);
411
+ const step = Math.sqrt(0.4 / rmax);
412
+ const n = Math.min(4096, Math.max(16, Math.ceil(2 * Math.PI / step)));
413
+ const pts = [];
414
+ for (let i = 0; i < n; i++) {
415
+ const t = i / n * 2 * Math.PI;
416
+ pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
417
+ }
418
+ return pts;
419
+ }
420
+ function ellipseSelection(width, height, rect, options = {}) {
421
+ return polygonSelection(width, height, ellipsePoints(rect), options);
422
+ }
423
+
424
+ // src/image-editor/selection/segment/maxflow.ts
425
+ var TERMINAL = -2;
426
+ var NONE = -1;
427
+ var FREE = 0;
428
+ var SOURCE = 1;
429
+ var SINK = 2;
430
+ var GridGraph = class {
431
+ width;
432
+ height;
433
+ /** Residual capacity of the edge leaving node `p` in direction `d`, at `p * 4 + d`. */
434
+ cap;
435
+ /** Net terminal capacity: above 0 to the source, below 0 to the sink. */
436
+ tr;
437
+ tree;
438
+ constructor(width, height) {
439
+ this.width = width;
440
+ this.height = height;
441
+ const n = width * height;
442
+ this.cap = new Float64Array(n * 4);
443
+ this.tr = new Float64Array(n);
444
+ this.tree = new Uint8Array(n);
445
+ }
446
+ /** Sets the capacity of the edge between `p` and its neighbour in direction `d`, both ways. */
447
+ setEdge(p, d, capacity) {
448
+ const q = d === 0 ? p + 1 : p + this.width;
449
+ this.cap[p * 4 + d] = capacity;
450
+ this.cap[q * 4 + (d ^ 2)] = capacity;
451
+ }
452
+ /** Adds `source` (cost of putting `p` on the sink side) and `sink` (the other way) to `p`. */
453
+ addTerminal(p, source, sink) {
454
+ this.tr[p] += source - sink;
455
+ }
456
+ /** Whether `p` is on the source side of the minimum cut (after {@link maxflow}). */
457
+ inSource(p) {
458
+ return this.tree[p] === SOURCE;
459
+ }
460
+ /** Runs max-flow and returns the flow (the cut's cost, less the constant terminal parts). */
461
+ maxflow() {
462
+ const { width: w, height: h, cap, tr, tree } = this;
463
+ const n = w * h;
464
+ const nb = (p, d) => {
465
+ const x = p % w;
466
+ if (d === 0) return x < w - 1 ? p + 1 : -1;
467
+ if (d === 1) return p + w < n ? p + w : -1;
468
+ if (d === 2) return x > 0 ? p - 1 : -1;
469
+ return p - w >= 0 ? p - w : -1;
470
+ };
471
+ const parent = new Int8Array(n).fill(NONE);
472
+ const ts = new Int32Array(n);
473
+ const dist = new Int32Array(n);
474
+ const queue = new Int32Array(n + 1);
475
+ const queued = new Uint8Array(n);
476
+ let qHead = 0;
477
+ let qTail = 0;
478
+ const activate = (p) => {
479
+ if (queued[p]) return;
480
+ queued[p] = 1;
481
+ queue[qTail] = p;
482
+ qTail = qTail === n ? 0 : qTail + 1;
483
+ };
484
+ const nextActive = () => {
485
+ while (qHead !== qTail) {
486
+ const p = queue[qHead];
487
+ qHead = qHead === n ? 0 : qHead + 1;
488
+ queued[p] = 0;
489
+ if (tree[p] !== FREE) return p;
490
+ }
491
+ return -1;
492
+ };
493
+ let orphans = new Int32Array(64);
494
+ let orphanCount = 0;
495
+ const orphan = (p) => {
496
+ parent[p] = NONE;
497
+ if (orphanCount === orphans.length) {
498
+ const grown = new Int32Array(orphans.length * 2);
499
+ grown.set(orphans);
500
+ orphans = grown;
501
+ }
502
+ orphans[orphanCount++] = p;
503
+ };
504
+ tree.fill(FREE);
505
+ for (let p = 0; p < n; p++) {
506
+ if (tr[p] > 0) tree[p] = SOURCE;
507
+ else if (tr[p] < 0) tree[p] = SINK;
508
+ else continue;
509
+ parent[p] = TERMINAL;
510
+ dist[p] = 1;
511
+ activate(p);
512
+ }
513
+ let flow = 0;
514
+ let time = 0;
515
+ let current = -1;
516
+ for (; ; ) {
517
+ if (current < 0 || tree[current] === FREE) {
518
+ current = nextActive();
519
+ if (current < 0) break;
520
+ }
521
+ const p = current;
522
+ const side = tree[p];
523
+ let s = -1;
524
+ let t = -1;
525
+ let dir = 0;
526
+ for (let d = 0; d < 4; d++) {
527
+ const q = nb(p, d);
528
+ if (q < 0) continue;
529
+ if (side === SOURCE ? cap[p * 4 + d] <= 0 : cap[q * 4 + (d ^ 2)] <= 0) continue;
530
+ if (tree[q] === FREE) {
531
+ tree[q] = side;
532
+ parent[q] = d ^ 2;
533
+ ts[q] = ts[p];
534
+ dist[q] = dist[p] + 1;
535
+ activate(q);
536
+ } else if (tree[q] !== side) {
537
+ if (side === SOURCE) {
538
+ s = p;
539
+ t = q;
540
+ dir = d;
541
+ } else {
542
+ s = q;
543
+ t = p;
544
+ dir = d ^ 2;
545
+ }
546
+ break;
547
+ } else if (ts[q] <= ts[p] && dist[q] > dist[p]) {
548
+ parent[q] = d ^ 2;
549
+ ts[q] = ts[p];
550
+ dist[q] = dist[p] + 1;
551
+ }
552
+ }
553
+ if (s < 0) {
554
+ current = -1;
555
+ continue;
556
+ }
557
+ time++;
558
+ let f = cap[s * 4 + dir];
559
+ for (let x = s; ; ) {
560
+ const a = parent[x];
561
+ if (a === TERMINAL) {
562
+ f = Math.min(f, tr[x]);
563
+ break;
564
+ }
565
+ const up = nb(x, a);
566
+ f = Math.min(f, cap[up * 4 + (a ^ 2)]);
567
+ x = up;
568
+ }
569
+ for (let x = t; ; ) {
570
+ const a = parent[x];
571
+ if (a === TERMINAL) {
572
+ f = Math.min(f, -tr[x]);
573
+ break;
574
+ }
575
+ f = Math.min(f, cap[x * 4 + a]);
576
+ x = nb(x, a);
577
+ }
578
+ cap[s * 4 + dir] -= f;
579
+ cap[t * 4 + (dir ^ 2)] += f;
580
+ for (let x = s; ; ) {
581
+ const a = parent[x];
582
+ if (a === TERMINAL) {
583
+ tr[x] -= f;
584
+ if (tr[x] <= 0) orphan(x);
585
+ break;
586
+ }
587
+ const up = nb(x, a);
588
+ cap[up * 4 + (a ^ 2)] -= f;
589
+ cap[x * 4 + a] += f;
590
+ if (cap[up * 4 + (a ^ 2)] <= 0) orphan(x);
591
+ x = up;
592
+ }
593
+ for (let x = t; ; ) {
594
+ const a = parent[x];
595
+ if (a === TERMINAL) {
596
+ tr[x] += f;
597
+ if (tr[x] >= 0) orphan(x);
598
+ break;
599
+ }
600
+ const up = nb(x, a);
601
+ cap[x * 4 + a] -= f;
602
+ cap[up * 4 + (a ^ 2)] += f;
603
+ if (cap[x * 4 + a] <= 0) orphan(x);
604
+ x = up;
605
+ }
606
+ flow += f;
607
+ while (orphanCount) {
608
+ const o = orphans[--orphanCount];
609
+ const oSide = tree[o];
610
+ let best = NONE;
611
+ let bestDist = Infinity;
612
+ for (let d = 0; d < 4; d++) {
613
+ const q = nb(o, d);
614
+ if (q < 0 || tree[q] !== oSide) continue;
615
+ if (oSide === SOURCE ? cap[q * 4 + (d ^ 2)] <= 0 : cap[o * 4 + d] <= 0) continue;
616
+ let k = 0;
617
+ let x = q;
618
+ for (; ; ) {
619
+ if (ts[x] === time) {
620
+ k += dist[x];
621
+ break;
622
+ }
623
+ const a = parent[x];
624
+ k++;
625
+ if (a === TERMINAL) {
626
+ ts[x] = time;
627
+ dist[x] = 1;
628
+ break;
629
+ }
630
+ if (a === NONE) {
631
+ k = Infinity;
632
+ break;
633
+ }
634
+ x = nb(x, a);
635
+ }
636
+ if (k === Infinity) continue;
637
+ if (k < bestDist) {
638
+ bestDist = k;
639
+ best = d;
640
+ }
641
+ for (let y = q, dd = k; ts[y] !== time; y = nb(y, parent[y]), dd--) {
642
+ ts[y] = time;
643
+ dist[y] = dd;
644
+ }
645
+ }
646
+ if (best !== NONE) {
647
+ parent[o] = best;
648
+ ts[o] = time;
649
+ dist[o] = bestDist + 1;
650
+ continue;
651
+ }
652
+ for (let d = 0; d < 4; d++) {
653
+ const q = nb(o, d);
654
+ if (q < 0 || tree[q] !== oSide) continue;
655
+ if (oSide === SOURCE ? cap[q * 4 + (d ^ 2)] > 0 : cap[o * 4 + d] > 0) activate(q);
656
+ const a = parent[q];
657
+ if (a !== TERMINAL && a !== NONE && nb(q, a) === o) orphan(q);
658
+ }
659
+ tree[o] = FREE;
660
+ }
661
+ }
662
+ return flow;
663
+ }
664
+ };
665
+
666
+ // src/image-editor/selection/segment/work.ts
667
+ var WORK_SIZE = 512;
668
+ var workCache = /* @__PURE__ */ new WeakMap();
669
+ function workImage(pixels, maxSide = WORK_SIZE) {
670
+ const sizes = workCache.get(pixels.data);
671
+ const cached = sizes?.get(maxSide);
672
+ if (cached && cached.sourceWidth === pixels.width && cached.sourceHeight === pixels.height) return cached;
673
+ const { width: W, height: H, data } = pixels;
674
+ const scale = Math.min(1, maxSide / Math.max(W, H));
675
+ const w = Math.max(1, Math.round(W * scale));
676
+ const h = Math.max(1, Math.round(H * scale));
677
+ const sum = new Float64Array(w * h * 3);
678
+ const count = new Float64Array(w * h);
679
+ const xs = new Int32Array(W);
680
+ for (let x = 0; x < W; x++) xs[x] = Math.min(w - 1, Math.floor(x * w / W));
681
+ for (let y = 0; y < H; y++) {
682
+ const row = Math.min(h - 1, Math.floor(y * h / H)) * w;
683
+ for (let x = 0, p = y * W * 4; x < W; x++, p += 4) {
684
+ const o = row + xs[x];
685
+ const a = data[p + 3] / 255;
686
+ const k = o * 3;
687
+ sum[k] += data[p] * a + 255 * (1 - a);
688
+ sum[k + 1] += data[p + 1] * a + 255 * (1 - a);
689
+ sum[k + 2] += data[p + 2] * a + 255 * (1 - a);
690
+ count[o]++;
691
+ }
692
+ }
693
+ const rgb = new Float32Array(w * h * 3);
694
+ for (let i = 0; i < w * h; i++) {
695
+ const c = count[i] || 1;
696
+ rgb[i * 3] = sum[i * 3] / c;
697
+ rgb[i * 3 + 1] = sum[i * 3 + 1] / c;
698
+ rgb[i * 3 + 2] = sum[i * 3 + 2] / c;
699
+ }
700
+ const work = { sourceWidth: W, sourceHeight: H, width: w, height: h, scale: w / W, rgb };
701
+ if (sizes) sizes.set(maxSide, work);
702
+ else workCache.set(pixels.data, /* @__PURE__ */ new Map([[maxSide, work]]));
703
+ return work;
704
+ }
705
+ var GAMMA = 50;
706
+ var UNITS = 64;
707
+ function smoothnessWeights(work, roi) {
708
+ const { width: w, rgb } = work;
709
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
710
+ const right = new Float64Array(rw * rh);
711
+ const down = new Float64Array(rw * rh);
712
+ let sumSq = 0;
713
+ let pairs = 0;
714
+ for (let y = 0; y < rh; y++) {
715
+ for (let x = 0; x < rw; x++) {
716
+ const p = y * rw + x;
717
+ const c = ((ry + y) * w + rx + x) * 3;
718
+ if (x + 1 < rw) {
719
+ right[p] = (rgb[c] - rgb[c + 3]) ** 2 + (rgb[c + 1] - rgb[c + 4]) ** 2 + (rgb[c + 2] - rgb[c + 5]) ** 2;
720
+ sumSq += right[p];
721
+ pairs++;
722
+ }
723
+ if (y + 1 < rh) {
724
+ const k = c + w * 3;
725
+ down[p] = (rgb[c] - rgb[k]) ** 2 + (rgb[c + 1] - rgb[k + 1]) ** 2 + (rgb[c + 2] - rgb[k + 2]) ** 2;
726
+ sumSq += down[p];
727
+ pairs++;
728
+ }
729
+ }
730
+ }
731
+ const beta = 1 / (2 * Math.max(1e-6, sumSq / Math.max(1, pairs)));
732
+ for (let p = 0; p < right.length; p++) {
733
+ right[p] = Math.round(GAMMA * UNITS * Math.exp(-beta * right[p]));
734
+ down[p] = Math.round(GAMMA * UNITS * Math.exp(-beta * down[p]));
735
+ }
736
+ return { right, down };
737
+ }
738
+ function boxBlur(src, w, h, passes) {
739
+ let a = src;
740
+ for (let k = 0; k < passes; k++) {
741
+ const b = new Float32Array(a.length);
742
+ for (let y = 0; y < h; y++) {
743
+ for (let x = 0; x < w; x++) {
744
+ let s = 0;
745
+ let c = 0;
746
+ for (let j = Math.max(0, y - 1); j <= Math.min(h - 1, y + 1); j++) {
747
+ for (let i = Math.max(0, x - 1); i <= Math.min(w - 1, x + 1); i++) {
748
+ s += a[j * w + i];
749
+ c++;
750
+ }
751
+ }
752
+ b[y * w + x] = s / c;
753
+ }
754
+ }
755
+ a = b;
756
+ }
757
+ return a;
758
+ }
759
+ function upscaleRegion(work, pixels, roi, label, enhance) {
760
+ const { sourceWidth: W, sourceHeight: H, scale, rgb, width: ww } = work;
761
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
762
+ let soft = Float32Array.from(label);
763
+ if (enhance) {
764
+ soft = boxBlur(soft, rw, rh, 2);
765
+ for (let i = 0; i < soft.length; i++) soft[i] = Math.min(1, Math.max(0, (soft[i] - 0.5) * 3 + 0.5));
766
+ }
767
+ const X0 = Math.max(0, Math.floor(rx / scale));
768
+ const Y0 = Math.max(0, Math.floor(ry / scale));
769
+ const X1 = Math.min(W, Math.ceil((rx + rw) / scale));
770
+ const Y1 = Math.min(H, Math.ceil((ry + rh) / scale));
771
+ const RW = X1 - X0;
772
+ const RH = Y1 - Y0;
773
+ const out = new Uint8Array(RW * RH);
774
+ const at = (x, y) => soft[Math.min(rh - 1, Math.max(0, y)) * rw + Math.min(rw - 1, Math.max(0, x))];
775
+ const data = pixels.data;
776
+ const colOf = new Int32Array(RW);
777
+ const fxOf = new Float32Array(RW);
778
+ for (let X = 0; X < RW; X++) {
779
+ const u = (X0 + X + 0.5) * scale - 0.5 - rx;
780
+ colOf[X] = Math.floor(u);
781
+ fxOf[X] = u - colOf[X];
782
+ }
783
+ const cellState = (i, j) => {
784
+ const a = at(i, j), b = at(i + 1, j), c = at(i, j + 1), d = at(i + 1, j + 1);
785
+ if (a === 1 && b === 1 && c === 1 && d === 1) return 1;
786
+ if (a === 0 && b === 0 && c === 0 && d === 0) return 0;
787
+ return 2;
788
+ };
789
+ for (let Y = 0; Y < RH; Y++) {
790
+ const v = (Y0 + Y + 0.5) * scale - 0.5 - ry;
791
+ const j = Math.floor(v);
792
+ const fy = v - j;
793
+ for (let X = 0; X < RW; X++) {
794
+ const i = colOf[X];
795
+ const state = cellState(i, j);
796
+ if (state !== 2) {
797
+ let end = X + 1;
798
+ while (end < RW && colOf[end] === i) end++;
799
+ if (state === 1) out.fill(255, Y * RW + X, Y * RW + end);
800
+ X = end - 1;
801
+ continue;
802
+ }
803
+ const fx = fxOf[X];
804
+ const m = (at(i, j) * (1 - fx) + at(i + 1, j) * fx) * (1 - fy) + (at(i, j + 1) * (1 - fx) + at(i + 1, j + 1) * fx) * fy;
805
+ if (m <= 1e-3 || m >= 0.999) {
806
+ out[Y * RW + X] = m >= 0.999 ? 255 : 0;
807
+ continue;
808
+ }
809
+ let fr = 0, fg = 0, fb = 0, fn = 0, br = 0, bg = 0, bb = 0, bn = 0;
810
+ for (let jj = j - 1; jj <= j + 2; jj++) {
811
+ for (let ii = i - 1; ii <= i + 2; ii++) {
812
+ if (ii < 0 || jj < 0 || ii >= rw || jj >= rh) continue;
813
+ const k = ((ry + jj) * ww + rx + ii) * 3;
814
+ if (soft[jj * rw + ii] >= 0.5) {
815
+ fr += rgb[k];
816
+ fg += rgb[k + 1];
817
+ fb += rgb[k + 2];
818
+ fn++;
819
+ } else {
820
+ br += rgb[k];
821
+ bg += rgb[k + 1];
822
+ bb += rgb[k + 2];
823
+ bn++;
824
+ }
825
+ }
826
+ }
827
+ let alpha = m;
828
+ if (fn && bn) {
829
+ fr /= fn;
830
+ fg /= fn;
831
+ fb /= fn;
832
+ br /= bn;
833
+ bg /= bn;
834
+ bb /= bn;
835
+ const dr = fr - br, dg = fg - bg, db = fb - bb;
836
+ const len2 = dr * dr + dg * dg + db * db;
837
+ if (len2 > 400) {
838
+ const p = ((Y0 + Y) * W + X0 + X) * 4;
839
+ const a = data[p + 3] / 255;
840
+ const cr = data[p] * a + 255 * (1 - a) - br;
841
+ const cg = data[p + 1] * a + 255 * (1 - a) - bg;
842
+ const cb = data[p + 2] * a + 255 * (1 - a) - bb;
843
+ const proj = Math.min(1, Math.max(0, (cr * dr + cg * dg + cb * db) / len2));
844
+ alpha = Math.min(1, Math.max(0, proj * 0.75 + m * 0.25));
845
+ } else alpha = Math.min(1, Math.max(0, (m - 0.5) * 2 + 0.5));
846
+ }
847
+ out[Y * RW + X] = Math.round(alpha * 255);
848
+ }
849
+ }
850
+ return selectionFromRegion(W, H, { x: X0, y: Y0, width: RW, height: RH }, out);
851
+ }
852
+
853
+ // src/image-editor/selection/segment/grabcut.ts
854
+ var K = 5;
855
+ var ColorGmm = class {
856
+ logWeight = new Float64Array(K);
857
+ mean = new Float64Array(K * 3);
858
+ /** Inverse covariances, row-major 3 × 3. */
859
+ inv = new Float64Array(K * 9);
860
+ /** ln(weight) − ½ ln((2π)³ |Σ|) per component; −∞ for an empty one. */
861
+ logNorm = new Float64Array(K).fill(-Infinity);
862
+ /** Fits the components to the pixels `ids` of `rgb`, each in component `comp[id]`. */
863
+ fit(rgb, ids, comp) {
864
+ const n = new Float64Array(K);
865
+ const sum = new Float64Array(K * 3);
866
+ const prod = new Float64Array(K * 9);
867
+ for (let t = 0; t < ids.length; t++) {
868
+ const i = ids[t];
869
+ const k = comp[i];
870
+ const c = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
871
+ n[k]++;
872
+ for (let a = 0; a < 3; a++) {
873
+ sum[k * 3 + a] += c[a];
874
+ for (let b = 0; b < 3; b++) prod[k * 9 + a * 3 + b] += c[a] * c[b];
875
+ }
876
+ }
877
+ const total = ids.length || 1;
878
+ for (let k = 0; k < K; k++) {
879
+ if (!n[k]) {
880
+ this.logNorm[k] = -Infinity;
881
+ continue;
882
+ }
883
+ const m = [sum[k * 3] / n[k], sum[k * 3 + 1] / n[k], sum[k * 3 + 2] / n[k]];
884
+ const cov = new Float64Array(9);
885
+ for (let a = 0; a < 3; a++) {
886
+ for (let b = 0; b < 3; b++) cov[a * 3 + b] = prod[k * 9 + a * 3 + b] / n[k] - m[a] * m[b];
887
+ cov[a * 4] += 4;
888
+ }
889
+ const det = cov[0] * (cov[4] * cov[8] - cov[5] * cov[7]) - cov[1] * (cov[3] * cov[8] - cov[5] * cov[6]) + cov[2] * (cov[3] * cov[7] - cov[4] * cov[6]);
890
+ const d = Math.max(det, 1e-6);
891
+ const inv = [
892
+ (cov[4] * cov[8] - cov[5] * cov[7]) / d,
893
+ (cov[2] * cov[7] - cov[1] * cov[8]) / d,
894
+ (cov[1] * cov[5] - cov[2] * cov[4]) / d,
895
+ (cov[5] * cov[6] - cov[3] * cov[8]) / d,
896
+ (cov[0] * cov[8] - cov[2] * cov[6]) / d,
897
+ (cov[2] * cov[3] - cov[0] * cov[5]) / d,
898
+ (cov[3] * cov[7] - cov[4] * cov[6]) / d,
899
+ (cov[1] * cov[6] - cov[0] * cov[7]) / d,
900
+ (cov[0] * cov[4] - cov[1] * cov[3]) / d
901
+ ];
902
+ this.mean.set(m, k * 3);
903
+ this.inv.set(inv, k * 9);
904
+ this.logWeight[k] = Math.log(n[k] / total);
905
+ this.logNorm[k] = this.logWeight[k] - 0.5 * Math.log(Math.pow(2 * Math.PI, 3) * d);
906
+ }
907
+ }
908
+ /** ln p(colour | component k), up to the shared constant. */
909
+ componentLog(k, r, g, b) {
910
+ const ln = this.logNorm[k];
911
+ if (ln === -Infinity) return -Infinity;
912
+ const dr = r - this.mean[k * 3];
913
+ const dg = g - this.mean[k * 3 + 1];
914
+ const db = b - this.mean[k * 3 + 2];
915
+ const v = this.inv;
916
+ const o = k * 9;
917
+ const q = dr * (v[o] * dr + v[o + 1] * dg + v[o + 2] * db) + dg * (v[o + 3] * dr + v[o + 4] * dg + v[o + 5] * db) + db * (v[o + 6] * dr + v[o + 7] * dg + v[o + 8] * db);
918
+ return ln - 0.5 * q;
919
+ }
920
+ /** −ln p(colour) under the mixture (large for colours the model hasn't seen). */
921
+ cost(r, g, b) {
922
+ let best = -Infinity;
923
+ const logs = new Array(K);
924
+ for (let k = 0; k < K; k++) {
925
+ logs[k] = this.componentLog(k, r, g, b);
926
+ if (logs[k] > best) best = logs[k];
927
+ }
928
+ if (best === -Infinity) return 50;
929
+ let s = 0;
930
+ for (let k = 0; k < K; k++) if (logs[k] > -Infinity) s += Math.exp(logs[k] - best);
931
+ return Math.min(50, -(best + Math.log(s)));
932
+ }
933
+ /** The component most likely to have produced the colour. */
934
+ nearest(r, g, b) {
935
+ let best = 0;
936
+ let bestLog = -Infinity;
937
+ for (let k = 0; k < K; k++) {
938
+ const l = this.componentLog(k, r, g, b);
939
+ if (l > bestLog) {
940
+ bestLog = l;
941
+ best = k;
942
+ }
943
+ }
944
+ return best;
945
+ }
946
+ };
947
+ function initialComponents(rgb, ids, comp) {
948
+ if (!ids.length) return;
949
+ const step = Math.max(1, Math.floor(ids.length / 6e3));
950
+ const sample = [];
951
+ for (let t = 0; t < ids.length; t += step) sample.push(ids[t]);
952
+ const centres = [];
953
+ const dist2 = (i, c) => (rgb[i * 3] - c[0]) ** 2 + (rgb[i * 3 + 1] - c[1]) ** 2 + (rgb[i * 3 + 2] - c[2]) ** 2;
954
+ centres.push([rgb[sample[0] * 3], rgb[sample[0] * 3 + 1], rgb[sample[0] * 3 + 2]]);
955
+ const near = sample.map((i) => dist2(i, centres[0]));
956
+ while (centres.length < K) {
957
+ let far = 0;
958
+ for (let t = 1; t < sample.length; t++) if (near[t] > near[far]) far = t;
959
+ const i = sample[far];
960
+ const c = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
961
+ centres.push(c);
962
+ for (let t = 0; t < sample.length; t++) near[t] = Math.min(near[t], dist2(sample[t], c));
963
+ }
964
+ const closest = (i) => {
965
+ let best = 0;
966
+ for (let k = 1; k < K; k++) if (dist2(i, centres[k]) < dist2(i, centres[best])) best = k;
967
+ return best;
968
+ };
969
+ for (let round = 0; round < 6; round++) {
970
+ const sum = Array.from({ length: K }, () => [0, 0, 0, 0]);
971
+ for (const i of sample) {
972
+ const s = sum[closest(i)];
973
+ s[0] += rgb[i * 3];
974
+ s[1] += rgb[i * 3 + 1];
975
+ s[2] += rgb[i * 3 + 2];
976
+ s[3]++;
977
+ }
978
+ for (let k = 0; k < K; k++) if (sum[k][3]) centres[k] = [sum[k][0] / sum[k][3], sum[k][1] / sum[k][3], sum[k][2] / sum[k][3]];
979
+ }
980
+ for (const i of ids) comp[i] = closest(i);
981
+ }
982
+ function grabCutLabels(work, setup, iterations, onRound) {
983
+ const { width: w, rgb } = work;
984
+ const { roi, hardBg, hardFg } = setup;
985
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
986
+ const n = rw * rh;
987
+ const label = setup.initial.slice();
988
+ const global = new Int32Array(n);
989
+ for (let y = 0; y < rh; y++) for (let x = 0; x < rw; x++) global[y * rw + x] = (ry + y) * w + rx + x;
990
+ const comp = new Uint8Array(w * work.height);
991
+ const fg = new ColorGmm();
992
+ const bg = new ColorGmm();
993
+ const { right, down } = smoothnessWeights(work, roi);
994
+ const HARD = 1e9;
995
+ for (let iter = 0; iter < iterations; iter++) {
996
+ const fgIds = [];
997
+ const bgIds = [];
998
+ for (let p = 0; p < n; p++) (label[p] ? fgIds : bgIds).push(global[p]);
999
+ if (!fgIds.length) return label;
1000
+ if (iter === 0) {
1001
+ initialComponents(rgb, fgIds, comp);
1002
+ initialComponents(rgb, bgIds, comp);
1003
+ } else {
1004
+ for (const i of fgIds) comp[i] = fg.nearest(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
1005
+ for (const i of bgIds) comp[i] = bg.nearest(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
1006
+ }
1007
+ fg.fit(rgb, fgIds, comp);
1008
+ bg.fit(rgb, bgIds, comp);
1009
+ const graph = new GridGraph(rw, rh);
1010
+ for (let p = 0; p < n; p++) {
1011
+ if (hardBg[p]) graph.addTerminal(p, 0, HARD);
1012
+ else if (hardFg?.[p]) graph.addTerminal(p, HARD, 0);
1013
+ else {
1014
+ const i = global[p] * 3;
1015
+ graph.addTerminal(p, Math.round(bg.cost(rgb[i], rgb[i + 1], rgb[i + 2]) * UNITS), Math.round(fg.cost(rgb[i], rgb[i + 1], rgb[i + 2]) * UNITS));
1016
+ }
1017
+ if (p % rw + 1 < rw) graph.setEdge(p, 0, right[p]);
1018
+ if (p + rw < n) graph.setEdge(p, 1, down[p]);
1019
+ }
1020
+ graph.maxflow();
1021
+ let changed = 0;
1022
+ for (let p = 0; p < n; p++) {
1023
+ const v = graph.inSource(p) ? 1 : 0;
1024
+ if (v !== label[p]) changed++;
1025
+ label[p] = v;
1026
+ }
1027
+ onRound?.((iter + 1) / iterations);
1028
+ if (changed <= n * 1e-3) break;
1029
+ }
1030
+ return label;
1031
+ }
1032
+ function keepMainPieces(label, rw, rh, seed) {
1033
+ const n = rw * rh;
1034
+ const piece = new Int32Array(n).fill(-1);
1035
+ const sizes = [];
1036
+ const stack = [];
1037
+ for (let s = 0; s < n; s++) {
1038
+ if (!label[s] || piece[s] >= 0) continue;
1039
+ const id = sizes.length;
1040
+ let size = 0;
1041
+ piece[s] = id;
1042
+ stack.push(s);
1043
+ while (stack.length) {
1044
+ const p = stack.pop();
1045
+ size++;
1046
+ const x = p % rw;
1047
+ for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
1048
+ if (q >= 0 && label[q] && piece[q] < 0) {
1049
+ piece[q] = id;
1050
+ stack.push(q);
1051
+ }
1052
+ }
1053
+ }
1054
+ sizes.push(size);
1055
+ }
1056
+ if (!sizes.length) return;
1057
+ const largest = Math.max(...sizes);
1058
+ const keep = (id) => seed !== null && piece[seed] >= 0 ? id === piece[seed] : sizes[id] * 5 >= largest;
1059
+ for (let p = 0; p < n; p++) if (label[p] && !keep(piece[p])) label[p] = 0;
1060
+ }
1061
+ function selectObjectIn(pixels, target, options = {}) {
1062
+ const report = options.onProgress;
1063
+ report?.(0, "Reducing the image");
1064
+ const work = workImage(pixels);
1065
+ report?.(0.1, "Finding the object");
1066
+ const round = (fraction) => report?.(0.1 + 0.8 * fraction, "Finding the object");
1067
+ const { width: w, height: h, scale } = work;
1068
+ const iterations = Math.max(1, Math.round(options.iterations ?? 5));
1069
+ const enhance = options.enhanceEdge ?? false;
1070
+ if ("rect" in target) {
1071
+ const r = target.rect;
1072
+ const bx0 = Math.max(0, Math.floor(r.x * scale));
1073
+ const by0 = Math.max(0, Math.floor(r.y * scale));
1074
+ const bx1 = Math.min(w, Math.ceil((r.x + r.width) * scale));
1075
+ const by1 = Math.min(h, Math.ceil((r.y + r.height) * scale));
1076
+ if (bx1 - bx0 < 2 || by1 - by0 < 2) return createSelectionMask(pixels.width, pixels.height);
1077
+ const m = Math.max(8, Math.round(0.25 * Math.max(bx1 - bx0, by1 - by0)));
1078
+ const rx = Math.max(0, bx0 - m);
1079
+ const ry = Math.max(0, by0 - m);
1080
+ const roi = { x: rx, y: ry, width: Math.min(w, bx1 + m) - rx, height: Math.min(h, by1 + m) - ry };
1081
+ const n = roi.width * roi.height;
1082
+ const initial = new Uint8Array(n);
1083
+ const hardBg = new Uint8Array(n);
1084
+ for (let y = 0; y < roi.height; y++) {
1085
+ for (let x = 0; x < roi.width; x++) {
1086
+ const gx = rx + x;
1087
+ const gy = ry + y;
1088
+ const inside = gx >= bx0 && gx < bx1 && gy >= by0 && gy < by1;
1089
+ initial[y * roi.width + x] = inside ? 1 : 0;
1090
+ hardBg[y * roi.width + x] = inside ? 0 : 1;
1091
+ }
1092
+ }
1093
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg: null }, iterations, round);
1094
+ keepMainPieces(label, roi.width, roi.height, null);
1095
+ report?.(0.9, "Scaling up");
1096
+ const out = upscaleRegion(work, pixels, roi, label, enhance);
1097
+ report?.(1, "Done");
1098
+ return out;
1099
+ }
1100
+ const px = Math.min(w - 1, Math.max(0, Math.floor(target.point.x * scale)));
1101
+ const py = Math.min(h - 1, Math.max(0, Math.floor(target.point.y * scale)));
1102
+ let half = Math.max(24, Math.round(0.3 * Math.max(w, h)));
1103
+ for (; ; ) {
1104
+ const rx = Math.max(0, px - half);
1105
+ const ry = Math.max(0, py - half);
1106
+ const roi = { x: rx, y: ry, width: Math.min(w, px + half + 1) - rx, height: Math.min(h, py + half + 1) - ry };
1107
+ const whole = roi.width === w && roi.height === h;
1108
+ const band = Math.max(4, Math.round(0.15 * half));
1109
+ const n = roi.width * roi.height;
1110
+ const initial = new Uint8Array(n);
1111
+ const hardBg = new Uint8Array(n);
1112
+ const hardFg = new Uint8Array(n);
1113
+ const inBand = (x, y) => rx > 0 && x < band || ry > 0 && y < band || rx + roi.width < w && x >= roi.width - band || ry + roi.height < h && y >= roi.height - band;
1114
+ for (let y = 0; y < roi.height; y++) {
1115
+ for (let x = 0; x < roi.width; x++) {
1116
+ const p = y * roi.width + x;
1117
+ if (inBand(x, y)) hardBg[p] = 1;
1118
+ else initial[p] = 1;
1119
+ if (Math.hypot(rx + x - px, ry + y - py) <= 1.5) hardFg[p] = 1;
1120
+ }
1121
+ }
1122
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, iterations, round);
1123
+ keepMainPieces(label, roi.width, roi.height, (py - ry) * roi.width + (px - rx));
1124
+ let touches = false;
1125
+ for (let y = 0; y < roi.height && !touches; y++) {
1126
+ for (let x = 0; x < roi.width; x++) {
1127
+ if (label[y * roi.width + x] && (inBand(x - 1, y) || inBand(x + 1, y) || inBand(x, y - 1) || inBand(x, y + 1))) {
1128
+ touches = true;
1129
+ break;
1130
+ }
1131
+ }
1132
+ }
1133
+ if (!touches || whole) {
1134
+ report?.(0.9, "Scaling up");
1135
+ const out = upscaleRegion(work, pixels, roi, label, enhance);
1136
+ report?.(1, "Done");
1137
+ return out;
1138
+ }
1139
+ half = Math.round(half * 1.6);
1140
+ }
1141
+ }
1142
+
1143
+ // src/image-editor/selection/segment/quick.ts
1144
+ var BINS = 16;
1145
+ var binOf = (rgb, i) => (Math.min(BINS - 1, rgb[i * 3] >> 4) * BINS + Math.min(BINS - 1, rgb[i * 3 + 1] >> 4)) * BINS + Math.min(BINS - 1, rgb[i * 3 + 2] >> 4);
1146
+ function costTable(hist, total) {
1147
+ let a = hist;
1148
+ for (const stride of [1, BINS, BINS * BINS]) {
1149
+ const b = new Float64Array(a.length);
1150
+ for (let i = 0; i < a.length; i++) {
1151
+ const pos = Math.floor(i / stride) % BINS;
1152
+ const lo = pos > 0 ? a[i - stride] : a[i];
1153
+ const hi = pos < BINS - 1 ? a[i + stride] : a[i];
1154
+ b[i] = (lo + 2 * a[i] + hi) / 4;
1155
+ }
1156
+ a = b;
1157
+ }
1158
+ const out = new Float64Array(a.length);
1159
+ const floor = 1e-4;
1160
+ const denom = Math.max(1, total) * (1 + floor * a.length);
1161
+ for (let i = 0; i < a.length; i++) out[i] = -Math.log((a[i] + floor * Math.max(1, total)) / denom);
1162
+ return out;
1163
+ }
1164
+ function strokePixels(points, r, w, h) {
1165
+ const mask = new Uint8Array(w * h);
1166
+ const list = [];
1167
+ const add = (g) => {
1168
+ if (!mask[g]) {
1169
+ mask[g] = 1;
1170
+ list.push(g);
1171
+ }
1172
+ };
1173
+ const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
1174
+ for (const [a, b] of segs) {
1175
+ const x0 = Math.max(0, Math.floor(Math.min(a.x, b.x) - r));
1176
+ const x1 = Math.min(w - 1, Math.ceil(Math.max(a.x, b.x) + r));
1177
+ const y0 = Math.max(0, Math.floor(Math.min(a.y, b.y) - r));
1178
+ const y1 = Math.min(h - 1, Math.ceil(Math.max(a.y, b.y) + r));
1179
+ const dx = b.x - a.x;
1180
+ const dy = b.y - a.y;
1181
+ const len2 = dx * dx + dy * dy;
1182
+ for (let y = y0; y <= y1; y++) {
1183
+ for (let x = x0; x <= x1; x++) {
1184
+ const px = x + 0.5 - a.x;
1185
+ const py = y + 0.5 - a.y;
1186
+ const t = len2 > 0 ? Math.min(1, Math.max(0, (px * dx + py * dy) / len2)) : 0;
1187
+ if ((px - t * dx) ** 2 + (py - t * dy) ** 2 <= r * r) add(y * w + x);
1188
+ }
1189
+ }
1190
+ }
1191
+ if (!list.length) {
1192
+ for (const p of points) add(Math.min(h - 1, Math.max(0, Math.floor(p.y))) * w + Math.min(w - 1, Math.max(0, Math.floor(p.x))));
1193
+ }
1194
+ return { mask, list };
1195
+ }
1196
+ function segmentStroke(work, stroke, grown, start) {
1197
+ const { width: w, height: h, rgb } = work;
1198
+ let roi = start;
1199
+ for (; ; ) {
1200
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
1201
+ const n = rw * rh;
1202
+ const fgHist = new Float64Array(BINS ** 3);
1203
+ const bgHist = new Float64Array(BINS ** 3);
1204
+ let fgN = 0;
1205
+ let bgN = 0;
1206
+ const atImageEdge = { l: rx === 0, t: ry === 0, r: rx + rw === w, b: ry + rh === h };
1207
+ for (let y = 0; y < rh; y++) {
1208
+ for (let x = 0; x < rw; x++) {
1209
+ const g = (ry + y) * w + (rx + x);
1210
+ const bin = binOf(rgb, g);
1211
+ bgHist[bin]++;
1212
+ bgN++;
1213
+ if (stroke.mask[g] || grown?.[g]) {
1214
+ fgHist[bin]++;
1215
+ fgN++;
1216
+ }
1217
+ }
1218
+ }
1219
+ const fgCost = costTable(fgHist, fgN);
1220
+ const bgCost = costTable(bgHist, bgN);
1221
+ const { right, down } = smoothnessWeights(work, roi);
1222
+ const graph = new GridGraph(rw, rh);
1223
+ const HARD = 1e9;
1224
+ for (let y = 0; y < rh; y++) {
1225
+ for (let x = 0; x < rw; x++) {
1226
+ const p = y * rw + x;
1227
+ const g = (ry + y) * w + rx + x;
1228
+ if (stroke.mask[g]) graph.addTerminal(p, HARD, 0);
1229
+ else {
1230
+ const bin = binOf(rgb, g);
1231
+ graph.addTerminal(p, Math.round(bgCost[bin] * UNITS), Math.round(fgCost[bin] * UNITS));
1232
+ }
1233
+ if (x + 1 < rw) graph.setEdge(p, 0, right[p]);
1234
+ if (y + 1 < rh) graph.setEdge(p, 1, down[p]);
1235
+ }
1236
+ }
1237
+ graph.maxflow();
1238
+ const label = new Uint8Array(n);
1239
+ const stack = [];
1240
+ for (const g of stroke.list) {
1241
+ const x = g % w - rx;
1242
+ const y = Math.floor(g / w) - ry;
1243
+ if (x < 0 || y < 0 || x >= rw || y >= rh) continue;
1244
+ const p = y * rw + x;
1245
+ if (!label[p]) {
1246
+ label[p] = 1;
1247
+ stack.push(p);
1248
+ }
1249
+ }
1250
+ while (stack.length) {
1251
+ const p = stack.pop();
1252
+ const x = p % rw;
1253
+ for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
1254
+ if (q >= 0 && !label[q] && graph.inSource(q)) {
1255
+ label[q] = 1;
1256
+ stack.push(q);
1257
+ }
1258
+ }
1259
+ }
1260
+ let touches = false;
1261
+ for (let y = 0; y < rh; y++) {
1262
+ for (let x = 0; x < rw; x++) {
1263
+ const p = y * rw + x;
1264
+ if (!label[p]) continue;
1265
+ if (!atImageEdge.l && x === 0 || !atImageEdge.t && y === 0 || !atImageEdge.r && x === rw - 1 || !atImageEdge.b && y === rh - 1) touches = true;
1266
+ }
1267
+ }
1268
+ if (!touches || atImageEdge.l && atImageEdge.t && atImageEdge.r && atImageEdge.b) return { roi, label };
1269
+ const nx = Math.max(0, rx - Math.ceil(rw / 2));
1270
+ const ny = Math.max(0, ry - Math.ceil(rh / 2));
1271
+ roi = { x: nx, y: ny, width: Math.min(w, rx + rw + Math.ceil(rw / 2)) - nx, height: Math.min(h, ry + rh + Math.ceil(rh / 2)) - ny };
1272
+ if (roi.width * roi.height * 2.5 > w * h) roi = { x: 0, y: 0, width: w, height: h };
1273
+ }
1274
+ }
1275
+ function quickSelectStroke(pixels, stroke, options = {}) {
1276
+ if (!stroke.length) throw new Error("A stroke needs at least one point.");
1277
+ const work = workImage(pixels);
1278
+ const { width: w, height: h, scale } = work;
1279
+ const r = Math.max(0.5, (options.size ?? 30) / 2 * scale);
1280
+ const pts = stroke.map((p) => ({ x: p.x * scale, y: p.y * scale }));
1281
+ const seeds = strokePixels(pts, r, w, h);
1282
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
1283
+ for (const p of pts) {
1284
+ x0 = Math.min(x0, p.x);
1285
+ y0 = Math.min(y0, p.y);
1286
+ x1 = Math.max(x1, p.x);
1287
+ y1 = Math.max(y1, p.y);
1288
+ }
1289
+ const margin = Math.max(24, 4 * r);
1290
+ const rx = Math.max(0, Math.floor(x0 - margin));
1291
+ const ry = Math.max(0, Math.floor(y0 - margin));
1292
+ const roi = { x: rx, y: ry, width: Math.min(w, Math.ceil(x1 + margin)) - rx, height: Math.min(h, Math.ceil(y1 + margin)) - ry };
1293
+ let grown = null;
1294
+ const sel = options.grow ? options.selection : null;
1295
+ if (sel?.bounds) {
1296
+ grown = new Uint8Array(w * h);
1297
+ const b = sel.bounds;
1298
+ for (let y = Math.max(0, Math.floor(b.y * scale)); y < Math.min(h, Math.ceil((b.y + b.height) * scale)); y++) {
1299
+ for (let x = Math.max(0, Math.floor(b.x * scale)); x < Math.min(w, Math.ceil((b.x + b.width) * scale)); x++) {
1300
+ if (selectionValueAt(sel, Math.min(sel.width - 1, Math.floor((x + 0.5) / scale)), Math.min(sel.height - 1, Math.floor((y + 0.5) / scale))) >= 0.5) grown[y * w + x] = 1;
1301
+ }
1302
+ }
1303
+ }
1304
+ const { roi: done, label } = segmentStroke(work, seeds, grown, roi);
1305
+ return upscaleRegion(work, pixels, done, label, options.enhanceEdge ?? false);
1306
+ }
1307
+
1308
+ // src/image-editor/selection/refine-edits.ts
1309
+ var REFINE_EDIT_KINDS = ["quick-select", "brush", "refine-brush", "lasso", "object"];
1310
+ function brushStrokeMask(width, height, points, size, hardness = 1) {
1311
+ const r = Math.max(0.5, size / 2);
1312
+ const solid = r * Math.min(1, Math.max(0, hardness));
1313
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
1314
+ for (const p of points) {
1315
+ x0 = Math.min(x0, p.x);
1316
+ y0 = Math.min(y0, p.y);
1317
+ x1 = Math.max(x1, p.x);
1318
+ y1 = Math.max(y1, p.y);
1319
+ }
1320
+ const bx = Math.max(0, Math.floor(x0 - r - 1));
1321
+ const by = Math.max(0, Math.floor(y0 - r - 1));
1322
+ const bw = Math.min(width, Math.ceil(x1 + r + 1)) - bx;
1323
+ const bh = Math.min(height, Math.ceil(y1 + r + 1)) - by;
1324
+ if (!(bw > 0 && bh > 0)) return createSelectionMask(width, height);
1325
+ const out = new Uint8Array(bw * bh);
1326
+ const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
1327
+ for (let y = 0; y < bh; y++) {
1328
+ for (let x = 0; x < bw; x++) {
1329
+ const px = bx + x + 0.5;
1330
+ const py = by + y + 0.5;
1331
+ let d = Infinity;
1332
+ for (const [a, b] of segs) {
1333
+ const dx = b.x - a.x;
1334
+ const dy = b.y - a.y;
1335
+ const len2 = dx * dx + dy * dy;
1336
+ const t = len2 > 0 ? Math.min(1, Math.max(0, ((px - a.x) * dx + (py - a.y) * dy) / len2)) : 0;
1337
+ d = Math.min(d, Math.hypot(px - a.x - t * dx, py - a.y - t * dy));
1338
+ }
1339
+ let v = d <= solid ? 1 : solid >= r ? 0 : 1 - (d - solid) / (r - solid);
1340
+ v = Math.min(v, Math.min(1, Math.max(0, r + 0.5 - d)));
1341
+ out[y * bw + x] = Math.round(Math.max(0, v) * 255);
1342
+ }
1343
+ }
1344
+ return selectionFromRegion(width, height, { x: bx, y: by, width: bw, height: bh }, out);
1345
+ }
1346
+ function applyRefineEdits(pixels, selection, edits, width, height) {
1347
+ let sel = selection?.bounds ? selection : null;
1348
+ let band = null;
1349
+ for (const e of edits) {
1350
+ if (e.kind === "object") {
1351
+ if (!pixels) throw new Error("Object Selection needs the image pixels.");
1352
+ if (!e.rect === !e.point) throw new Error("An object edit takes rect (a box around the object) or point (on it), not both.");
1353
+ const region2 = selectObjectIn(pixels, e.rect ? { rect: e.rect } : { point: e.point });
1354
+ sel = combineSelections(sel, region2, e.mode ?? "add");
1355
+ continue;
1356
+ }
1357
+ if (!e.points.length) throw new Error(`A ${e.kind} stroke needs at least one point.`);
1358
+ if (e.kind === "lasso") {
1359
+ if (e.points.length < 3) throw new Error("A lasso edit needs at least 3 points.");
1360
+ sel = combineSelections(sel, polygonSelection(width, height, e.points, {}), e.mode ?? "add");
1361
+ continue;
1362
+ }
1363
+ if (e.kind === "refine-brush") {
1364
+ band = combineSelections(band, brushStrokeMask(width, height, e.points, e.size ?? 40), "add");
1365
+ continue;
1366
+ }
1367
+ const mode = e.mode ?? "add";
1368
+ let region;
1369
+ if (e.kind === "quick-select") {
1370
+ if (!pixels) throw new Error("Quick Selection strokes need the image pixels.");
1371
+ region = quickSelectStroke(pixels, e.points, { size: e.size, selection: sel, grow: mode === "add" });
1372
+ } else {
1373
+ region = brushStrokeMask(width, height, e.points, e.size ?? 20, e.hardness ?? 1);
1374
+ }
1375
+ sel = combineSelections(sel, region, mode);
1376
+ }
1377
+ return { selection: sel, refineBrush: band };
1378
+ }
1379
+
1380
+ // src/image-editor/selection/segment/guided.ts
1381
+ function workPlanes(work) {
1382
+ const n = work.width * work.height;
1383
+ const planes = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];
1384
+ for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] = work.rgb[i * 3 + c] / 255;
1385
+ return planes;
1386
+ }
1387
+ function workLuma(work) {
1388
+ const n = work.width * work.height;
1389
+ const out = new Float32Array(n);
1390
+ const { rgb } = work;
1391
+ 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;
1392
+ return out;
1393
+ }
1394
+ function softMaskToSelection(work, mask) {
1395
+ const { sourceWidth: W, sourceHeight: H, width: w, height: h, scale } = work;
1396
+ const out = new Uint8Array(W * H);
1397
+ const col = new Int32Array(W);
1398
+ const fx = new Float32Array(W);
1399
+ for (let X = 0; X < W; X++) {
1400
+ const u = Math.min(w - 1, Math.max(0, (X + 0.5) * scale - 0.5));
1401
+ col[X] = Math.min(w - 2, Math.floor(u));
1402
+ fx[X] = u - col[X];
1403
+ if (w === 1) {
1404
+ col[X] = 0;
1405
+ fx[X] = 0;
1406
+ }
1407
+ }
1408
+ for (let Y = 0; Y < H; Y++) {
1409
+ const v = Math.min(h - 1, Math.max(0, (Y + 0.5) * scale - 0.5));
1410
+ const j = h === 1 ? 0 : Math.min(h - 2, Math.floor(v));
1411
+ const fy = h === 1 ? 0 : v - j;
1412
+ const r0 = j * w;
1413
+ const r1 = h === 1 ? r0 : r0 + w;
1414
+ for (let X = 0; X < W; X++) {
1415
+ const i = col[X];
1416
+ const i1 = w === 1 ? i : i + 1;
1417
+ const t = fx[X];
1418
+ const m = (mask[r0 + i] * (1 - t) + mask[r0 + i1] * t) * (1 - fy) + (mask[r1 + i] * (1 - t) + mask[r1 + i1] * t) * fy;
1419
+ out[Y * W + X] = Math.round(Math.min(1, Math.max(0, m)) * 255);
1420
+ }
1421
+ }
1422
+ return selectionFromRegion(W, H, { x: 0, y: 0, width: W, height: H }, out);
1423
+ }
1424
+
1425
+ // src/image-editor/selection/segment/subject.ts
1426
+ function toLab(r, g, b) {
1427
+ const lin = (v) => {
1428
+ v /= 255;
1429
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
1430
+ };
1431
+ const R = lin(r);
1432
+ const G = lin(g);
1433
+ const B = lin(b);
1434
+ const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
1435
+ const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
1436
+ const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
1437
+ const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
1438
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
1439
+ }
1440
+ function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
1441
+ const { width: w, height: h, rgb } = work;
1442
+ const n = w * h;
1443
+ const lab2 = new Float32Array(n * 3);
1444
+ for (let i = 0; i < n; i++) lab2.set(toLab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]), i * 3);
1445
+ const S = Math.max(2, Math.round(Math.sqrt(n / target)));
1446
+ const seeds = [];
1447
+ for (let y = Math.floor(S / 2); y < h; y += S) {
1448
+ for (let x = Math.floor(S / 2); x < w; x += S) {
1449
+ const i = y * w + x;
1450
+ seeds.push([lab2[i * 3], lab2[i * 3 + 1], lab2[i * 3 + 2], x, y]);
1451
+ }
1452
+ }
1453
+ const k = seeds.length;
1454
+ const label = new Int32Array(n).fill(-1);
1455
+ const best = new Float64Array(n);
1456
+ const m2 = (compactness / S) ** 2;
1457
+ for (let round = 0; round < rounds; round++) {
1458
+ best.fill(Infinity);
1459
+ for (let c = 0; c < k; c++) {
1460
+ const [L, A, B, cx, cy] = seeds[c];
1461
+ const x0 = Math.max(0, Math.floor(cx - S));
1462
+ const x1 = Math.min(w - 1, Math.ceil(cx + S));
1463
+ const y0 = Math.max(0, Math.floor(cy - S));
1464
+ const y1 = Math.min(h - 1, Math.ceil(cy + S));
1465
+ for (let y = y0; y <= y1; y++) {
1466
+ for (let x = x0; x <= x1; x++) {
1467
+ const i = y * w + x;
1468
+ const d = (lab2[i * 3] - L) ** 2 + (lab2[i * 3 + 1] - A) ** 2 + (lab2[i * 3 + 2] - B) ** 2 + m2 * ((x - cx) ** 2 + (y - cy) ** 2);
1469
+ if (d < best[i]) {
1470
+ best[i] = d;
1471
+ label[i] = c;
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ const sum = new Float64Array(k * 6);
1477
+ for (let i = 0; i < n; i++) {
1478
+ const c = label[i];
1479
+ sum[c * 6] += lab2[i * 3];
1480
+ sum[c * 6 + 1] += lab2[i * 3 + 1];
1481
+ sum[c * 6 + 2] += lab2[i * 3 + 2];
1482
+ sum[c * 6 + 3] += i % w;
1483
+ sum[c * 6 + 4] += Math.floor(i / w);
1484
+ sum[c * 6 + 5]++;
1485
+ }
1486
+ for (let c = 0; c < k; c++) {
1487
+ const s = sum[c * 6 + 5];
1488
+ 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];
1489
+ }
1490
+ }
1491
+ const remap = new Int32Array(k).fill(-1);
1492
+ let count = 0;
1493
+ for (let i = 0; i < n; i++) if (remap[label[i]] < 0) remap[label[i]] = count++;
1494
+ const meanLab = new Float64Array(count * 3);
1495
+ const centre = new Float64Array(count * 2);
1496
+ const size = new Int32Array(count);
1497
+ for (let i = 0; i < n; i++) {
1498
+ const c = label[i] = remap[label[i]];
1499
+ meanLab[c * 3] += lab2[i * 3];
1500
+ meanLab[c * 3 + 1] += lab2[i * 3 + 1];
1501
+ meanLab[c * 3 + 2] += lab2[i * 3 + 2];
1502
+ centre[c * 2] += i % w;
1503
+ centre[c * 2 + 1] += Math.floor(i / w);
1504
+ size[c]++;
1505
+ }
1506
+ for (let c = 0; c < count; c++) {
1507
+ for (let j = 0; j < 3; j++) meanLab[c * 3 + j] /= size[c];
1508
+ centre[c * 2] /= size[c];
1509
+ centre[c * 2 + 1] /= size[c];
1510
+ }
1511
+ return { label, count, lab: meanLab, centre, size };
1512
+ }
1513
+ function superpixelSaliency(sp, width, height) {
1514
+ const { count, lab: lab2, centre, size, label } = sp;
1515
+ const dist = (a, b) => Math.hypot(lab2[a * 3] - lab2[b * 3], lab2[a * 3 + 1] - lab2[b * 3 + 1], lab2[a * 3 + 2] - lab2[b * 3 + 2]);
1516
+ const onBorder = new Uint8Array(count);
1517
+ for (let x = 0; x < width; x++) {
1518
+ onBorder[label[x]] = 1;
1519
+ onBorder[label[(height - 1) * width + x]] = 1;
1520
+ }
1521
+ for (let y = 0; y < height; y++) {
1522
+ onBorder[label[y * width]] = 1;
1523
+ onBorder[label[y * width + width - 1]] = 1;
1524
+ }
1525
+ const border = [];
1526
+ for (let c = 0; c < count; c++) if (onBorder[c]) border.push(c);
1527
+ const diag = Math.hypot(width, height);
1528
+ const unlikeBorder = new Float64Array(count);
1529
+ const contrast = new Float64Array(count);
1530
+ const centreBias = new Float64Array(count);
1531
+ for (let i = 0; i < count; i++) {
1532
+ const ds = border.map((j) => dist(i, j)).sort((a, b) => a - b);
1533
+ unlikeBorder[i] = ds.length ? ds[Math.floor(ds.length * 0.25)] : 0;
1534
+ let s = 0;
1535
+ let wsum = 0;
1536
+ for (let j = 0; j < count; j++) {
1537
+ if (j === i) continue;
1538
+ const dx = (centre[i * 2] - centre[j * 2]) / diag;
1539
+ const dy = (centre[i * 2 + 1] - centre[j * 2 + 1]) / diag;
1540
+ const wgt = size[j] * Math.exp(-(dx * dx + dy * dy) / (2 * 0.25 * 0.25));
1541
+ s += wgt * dist(i, j);
1542
+ wsum += wgt;
1543
+ }
1544
+ contrast[i] = wsum ? s / wsum : 0;
1545
+ const cx = centre[i * 2] / width - 0.5;
1546
+ const cy = centre[i * 2 + 1] / height - 0.5;
1547
+ centreBias[i] = 0.4 + 0.6 * Math.exp(-(cx * cx + cy * cy) / (2 * 0.3 * 0.3));
1548
+ }
1549
+ let farthest = 0;
1550
+ for (const v of unlikeBorder) farthest = Math.max(farthest, v);
1551
+ if (farthest < 6) return new Float64Array(count);
1552
+ const norm = (a) => {
1553
+ let max = 0;
1554
+ for (const v of a) max = Math.max(max, v);
1555
+ if (max > 0) for (let i = 0; i < a.length; i++) a[i] /= max;
1556
+ };
1557
+ norm(unlikeBorder);
1558
+ norm(contrast);
1559
+ const out = new Float64Array(count);
1560
+ for (let i = 0; i < count; i++) out[i] = unlikeBorder[i] * (0.3 + 0.7 * contrast[i]) * centreBias[i];
1561
+ norm(out);
1562
+ return out;
1563
+ }
1564
+ function otsuThreshold(values, weights) {
1565
+ const BINS2 = 64;
1566
+ const hist = new Float64Array(BINS2);
1567
+ let total = 0;
1568
+ for (let i = 0; i < values.length; i++) {
1569
+ hist[Math.min(BINS2 - 1, Math.floor(values[i] * BINS2))] += weights[i];
1570
+ total += weights[i];
1571
+ }
1572
+ let sumAll = 0;
1573
+ for (let b = 0; b < BINS2; b++) sumAll += b * hist[b];
1574
+ let wB = 0;
1575
+ let sumB = 0;
1576
+ let best = 0;
1577
+ let bestT = 0.5;
1578
+ for (let b = 0; b < BINS2 - 1; b++) {
1579
+ wB += hist[b];
1580
+ if (!wB) continue;
1581
+ const wF = total - wB;
1582
+ if (!wF) break;
1583
+ sumB += b * hist[b];
1584
+ const between = wB * wF * (sumB / wB - (sumAll - sumB) / wF) ** 2;
1585
+ if (between > best) {
1586
+ best = between;
1587
+ bestT = (b + 1) / BINS2;
1588
+ }
1589
+ }
1590
+ return bestT;
1591
+ }
1592
+ function findSubject(pixels, options = {}) {
1593
+ const report = options.onProgress;
1594
+ report?.(0, "Reducing the image");
1595
+ const work = workImage(pixels);
1596
+ const { width: w, height: h } = work;
1597
+ report?.(0.1, "Grouping into superpixels");
1598
+ const sp = slicSuperpixels(work);
1599
+ report?.(0.3, "Measuring what stands out");
1600
+ const sal = superpixelSaliency(sp, w, h);
1601
+ report?.(0.4, "Finding the subject");
1602
+ const t = otsuThreshold(sal, sp.size);
1603
+ const n = w * h;
1604
+ const initial = new Uint8Array(n);
1605
+ const hardBg = new Uint8Array(n);
1606
+ const hardFg = new Uint8Array(n);
1607
+ const strong = t + 0.6 * (1 - t);
1608
+ for (let i = 0; i < n; i++) {
1609
+ const s = sal[sp.label[i]];
1610
+ if (s >= t) initial[i] = 1;
1611
+ if (s >= strong) hardFg[i] = 1;
1612
+ }
1613
+ for (let x = 0; x < w; x++) {
1614
+ for (const y of [0, h - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
1615
+ }
1616
+ for (let y = 0; y < h; y++) {
1617
+ for (const x of [0, w - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
1618
+ }
1619
+ const empty = (reason) => ({ selection: createSelectionMask(pixels.width, pixels.height), reason });
1620
+ let guessed = 0;
1621
+ for (let i = 0; i < n; i++) guessed += initial[i];
1622
+ if (!guessed || guessed > n * 0.95) return empty("Nothing stands out from the background.");
1623
+ const roi = { x: 0, y: 0, width: w, height: h };
1624
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, 5, (f) => report?.(0.4 + 0.5 * f, "Finding the subject"));
1625
+ keepMainPieces(label, w, h, null);
1626
+ let kept = 0;
1627
+ for (let i = 0; i < n; i++) kept += label[i];
1628
+ if (kept < n * 2e-3) return empty("Nothing stands out from the background.");
1629
+ if (kept > n * 0.97) return empty("The whole image looks like one subject; select it with select_all instead.");
1630
+ report?.(0.9, "Scaling up");
1631
+ const selection = upscaleRegion(work, pixels, roi, label, options.enhanceEdge ?? false);
1632
+ report?.(1, "Done");
1633
+ return { selection };
1634
+ }
1635
+
1636
+ // src/image-editor/selection/segment/focus.ts
1637
+ var FOCUS_SIZE = 1024;
1638
+ function estimateNoise(luma, w, h) {
1639
+ if (w < 3 || h < 3) return 0;
1640
+ let sum = 0;
1641
+ for (let y = 1; y < h - 1; y++) {
1642
+ for (let x = 1; x < w - 1; x++) {
1643
+ const i = y * w + x;
1644
+ const v = luma[i - w - 1] - 2 * luma[i - w] + luma[i - w + 1] - 2 * luma[i - 1] + 4 * luma[i] - 2 * luma[i + 1] + luma[i + w - 1] - 2 * luma[i + w] + luma[i + w + 1];
1645
+ sum += Math.abs(v);
1646
+ }
1647
+ }
1648
+ return Math.sqrt(Math.PI / 2) * sum / (6 * (w - 2) * (h - 2));
1649
+ }
1650
+ function focusArea(pixels, options = {}) {
1651
+ const report = options.onProgress;
1652
+ report?.(0, "Reducing the image");
1653
+ const work = workImage(pixels, FOCUS_SIZE);
1654
+ report?.(0.2, "Measuring sharpness");
1655
+ const { width: w, height: h, scale } = work;
1656
+ const n = w * h;
1657
+ const luma = workLuma(work);
1658
+ const sigma = estimateNoise(luma, w, h);
1659
+ const noise = Math.min(100, Math.max(0, options.noise ?? 0));
1660
+ const floor = 2 * sigma * (1 + noise / 25);
1661
+ const detail = new Float32Array(n);
1662
+ for (let y = 1; y < h - 1; y++) {
1663
+ for (let x = 1; x < w - 1; x++) {
1664
+ const i = y * w + x;
1665
+ const lap = Math.abs(4 * luma[i] - luma[i - 1] - luma[i + 1] - luma[i - w] - luma[i + w]);
1666
+ const d = Math.max(0, lap - floor);
1667
+ detail[i] = d * d;
1668
+ }
1669
+ }
1670
+ const r = Math.max(2, Math.round(0.012 * Math.max(w, h)));
1671
+ const energy = boxMean(detail, w, h, r);
1672
+ const sharp = new Float32Array(n);
1673
+ for (let i = 0; i < n; i++) sharp[i] = Math.log1p(Math.sqrt(energy[i]) * 400);
1674
+ report?.(0.5, "Separating what is in focus");
1675
+ const sorted = Float32Array.from(sharp).sort();
1676
+ const top = sorted[Math.floor(n * 0.995)] || 1;
1677
+ for (let i = 0; i < n; i++) sharp[i] = Math.min(1, sharp[i] / top);
1678
+ const ones = new Float32Array(n).fill(1);
1679
+ const otsu = otsuThreshold(sharp, ones);
1680
+ const range = Math.min(100, Math.max(0, options.range ?? 50));
1681
+ const t = Math.min(0.98, otsu * Math.pow(2, (50 - range) / 25));
1682
+ let mask = new Float32Array(n);
1683
+ for (let i = 0; i < n; i++) mask[i] = sharp[i] >= t ? 1 : 0;
1684
+ const c = 2 * r;
1685
+ const grown = boxMean(mask, w, h, c);
1686
+ for (let i = 0; i < n; i++) grown[i] = grown[i] > 1e-3 ? 1 : 0;
1687
+ const shrunk = boxMean(grown, w, h, c);
1688
+ for (let i = 0; i < n; i++) mask[i] = shrunk[i] >= 0.999 ? 1 : 0;
1689
+ const pulled = boxMean(mask, w, h, Math.max(1, Math.round(r / 2)));
1690
+ for (let i = 0; i < n; i++) mask[i] = pulled[i] >= 0.999 ? 1 : 0;
1691
+ report?.(0.7, "Fitting the edge");
1692
+ const softness = Math.max(0, options.softness ?? 4) * scale;
1693
+ if (softness > 0) {
1694
+ const fitted = guidedFilterColor(workPlanes(work), mask, w, h, Math.max(1, Math.round(softness * 2)), 1e-3);
1695
+ for (let i = 0; i < n; i++) mask[i] = Math.min(1, Math.max(0, fitted[i]));
1696
+ }
1697
+ const selection = softMaskToSelection(work, mask);
1698
+ report?.(1, "Done");
1699
+ return { selection, noiseLevel: Math.round(sigma * 255 * 10) / 10 };
1700
+ }
1701
+
1702
+ // src/image-editor/selection/segment/sky.ts
1703
+ var SKY_SIZE = 768;
1704
+ function lab(r, g, b) {
1705
+ const lin = (v) => {
1706
+ v /= 255;
1707
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
1708
+ };
1709
+ const R = lin(r);
1710
+ const G = lin(g);
1711
+ const B = lin(b);
1712
+ const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
1713
+ const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
1714
+ const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
1715
+ const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
1716
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
1717
+ }
1718
+ var sigmoid = (v) => 1 / (1 + Math.exp(-v));
1719
+ function findSky(pixels, options = {}) {
1720
+ const report = options.onProgress;
1721
+ report?.(0, "Reducing the image");
1722
+ const work = workImage(pixels, SKY_SIZE);
1723
+ report?.(0.2, "Scoring the sky");
1724
+ const { width: w, height: h, rgb, scale } = work;
1725
+ const n = w * h;
1726
+ const L = new Float32Array(n);
1727
+ const A = new Float32Array(n);
1728
+ const B = new Float32Array(n);
1729
+ for (let i = 0; i < n; i++) {
1730
+ const [l, a, b] = lab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
1731
+ L[i] = l;
1732
+ A[i] = a;
1733
+ B[i] = b;
1734
+ }
1735
+ const luma = boxMean(workLuma(work), w, h, 1);
1736
+ const grad = new Float32Array(n);
1737
+ for (let y = 0; y < h; y++) {
1738
+ for (let x = 0; x < w; x++) {
1739
+ const i = y * w + x;
1740
+ const gx = x + 1 < w ? luma[i + 1] - luma[i] : 0;
1741
+ const gy = y + 1 < h ? luma[i + w] - luma[i] : 0;
1742
+ grad[i] = Math.hypot(gx, gy);
1743
+ }
1744
+ }
1745
+ const roughR = Math.max(1, Math.round(6e-3 * Math.max(w, h)));
1746
+ const rough = boxMean(grad, w, h, roughR);
1747
+ const score = new Float32Array(n);
1748
+ for (let i = 0; i < n; i++) {
1749
+ const chroma = Math.hypot(A[i], B[i]);
1750
+ const blue = sigmoid((-B[i] - 6) / 4) * sigmoid((L[i] - 35) / 6);
1751
+ const grey = sigmoid((L[i] - 68) / 5) * sigmoid((18 - chroma) / 4);
1752
+ const smooth = Math.exp(-rough[i] / 0.02);
1753
+ const y = Math.floor(i / w) / h;
1754
+ const high = 1 - 0.35 * y;
1755
+ score[i] = Math.max(blue, grey) * smooth * high;
1756
+ }
1757
+ const threshold = Math.min(100, Math.max(0, options.threshold ?? 50));
1758
+ const t = 0.55 - (threshold - 50) * 6e-3;
1759
+ report?.(0.5, "Growing the sky");
1760
+ const sky = new Uint8Array(n);
1761
+ const stack = [];
1762
+ const seedRows = Math.max(1, Math.round(h * 0.04));
1763
+ for (let y = 0; y < seedRows; y++) {
1764
+ for (let x = 0; x < w; x++) {
1765
+ const i = y * w + x;
1766
+ if (score[i] >= t) {
1767
+ sky[i] = 1;
1768
+ stack.push(i);
1769
+ }
1770
+ }
1771
+ }
1772
+ if (!stack.length) return { selection: createSelectionMask(pixels.width, pixels.height), reason: "No sky was found along the top of the image." };
1773
+ const low = t * 0.7;
1774
+ while (stack.length) {
1775
+ const p = stack.pop();
1776
+ const x = p % w;
1777
+ for (const q of [x + 1 < w ? p + 1 : -1, x > 0 ? p - 1 : -1, p + w < n ? p + w : -1, p - w]) {
1778
+ if (q < 0 || sky[q] || score[q] < low) continue;
1779
+ if (Math.hypot(L[q] - L[p], A[q] - A[p], B[q] - B[p]) > 6) continue;
1780
+ sky[q] = 1;
1781
+ stack.push(q);
1782
+ }
1783
+ }
1784
+ let count = 0;
1785
+ const mean = [0, 0, 0];
1786
+ for (let i = 0; i < n; i++) {
1787
+ if (!sky[i]) continue;
1788
+ mean[0] += L[i];
1789
+ mean[1] += A[i];
1790
+ mean[2] += B[i];
1791
+ count++;
1792
+ }
1793
+ for (let k = 0; k < 3; k++) mean[k] /= count;
1794
+ let spread = 0;
1795
+ for (let i = 0; i < n; i++) if (sky[i]) spread += (L[i] - mean[0]) ** 2 + (A[i] - mean[1]) ** 2 + (B[i] - mean[2]) ** 2;
1796
+ const limit = Math.max(8, 2.5 * Math.sqrt(spread / count));
1797
+ for (let i = 0; i < n; i++) {
1798
+ if (sky[i] || score[i] < t) continue;
1799
+ if (Math.hypot(L[i] - mean[0], A[i] - mean[1], B[i] - mean[2]) <= limit) sky[i] = 1;
1800
+ }
1801
+ let front = [];
1802
+ for (let i = 0; i < n; i++) if (sky[i]) front.push(i);
1803
+ for (let step = 0; step <= roughR + 1 && front.length; step++) {
1804
+ const next = [];
1805
+ for (const p of front) {
1806
+ const x = p % w;
1807
+ for (const q of [x + 1 < w ? p + 1 : -1, x > 0 ? p - 1 : -1, p + w < n ? p + w : -1, p - w]) {
1808
+ if (q < 0 || sky[q]) continue;
1809
+ if (Math.hypot(L[q] - mean[0], A[q] - mean[1], B[q] - mean[2]) > limit) continue;
1810
+ sky[q] = 1;
1811
+ next.push(q);
1812
+ }
1813
+ }
1814
+ front = next;
1815
+ }
1816
+ if (count < n * 5e-3) return { selection: createSelectionMask(pixels.width, pixels.height), reason: "No sky was found along the top of the image." };
1817
+ report?.(0.7, "Fitting the edge");
1818
+ let mask = Float32Array.from(sky);
1819
+ const softness = Math.max(0, options.softness ?? 4) * scale;
1820
+ if (softness > 0) {
1821
+ const fitted = guidedFilterColor(workPlanes(work), mask, w, h, Math.max(1, Math.round(softness * 2)), 1e-3);
1822
+ mask = new Float32Array(n);
1823
+ for (let i = 0; i < n; i++) mask[i] = Math.min(1, Math.max(0, fitted[i]));
1824
+ }
1825
+ const selection = softMaskToSelection(work, mask);
1826
+ report?.(1, "Done");
1827
+ return { selection };
1828
+ }
1829
+
1830
+ // src/image-editor/selection/worker.ts
1831
+ function serializeMask(mask) {
1832
+ const b = mask.bounds;
1833
+ if (!b) return { width: mask.width, height: mask.height, bounds: null, bytes: new Uint8Array(0) };
1834
+ const bytes = new Uint8Array(b.width * b.height);
1835
+ const data = mask.data;
1836
+ for (let y = 0; y < b.height; y++) bytes.set(data.subarray((b.y + y) * mask.width + b.x, (b.y + y) * mask.width + b.x + b.width), y * b.width);
1837
+ return { width: mask.width, height: mask.height, bounds: { ...b }, bytes };
1838
+ }
1839
+ function deserializeMask(s) {
1840
+ return s.bounds ? selectionFromRegion(s.width, s.height, s.bounds, s.bytes) : createSelectionMask(s.width, s.height);
1841
+ }
1842
+ function runSelectionJob(job, pixels, onProgress) {
1843
+ switch (job.op) {
1844
+ case "subject": {
1845
+ const { selection, reason } = findSubject(pixels, { enhanceEdge: job.enhanceEdge, onProgress });
1846
+ return { mask: selection, reason };
1847
+ }
1848
+ case "object":
1849
+ return { mask: selectObjectIn(pixels, job.target, { enhanceEdge: job.enhanceEdge, onProgress }) };
1850
+ case "focus": {
1851
+ const { selection, noiseLevel } = focusArea(pixels, { ...job.options, onProgress });
1852
+ return { mask: selection, noiseLevel };
1853
+ }
1854
+ case "sky": {
1855
+ const { selection, reason } = findSky(pixels, { ...job.options, onProgress });
1856
+ return { mask: selection, reason };
1857
+ }
1858
+ case "quick":
1859
+ return {
1860
+ mask: quickSelectStroke(pixels, job.stroke, {
1861
+ size: job.size,
1862
+ grow: job.grow,
1863
+ enhanceEdge: job.enhanceEdge,
1864
+ selection: job.selection ? deserializeMask(job.selection) : null
1865
+ })
1866
+ };
1867
+ case "refine-preview": {
1868
+ onProgress?.(0, "Refining");
1869
+ const base = job.base ? deserializeMask(job.base) : null;
1870
+ const { selection, refineBrush } = applyRefineEdits(pixels, base, job.edits, pixels.width, pixels.height);
1871
+ const mask = selection?.bounds ? refineSelection(pixels, selection, job.params, refineBrush) : createSelectionMask(pixels.width, pixels.height);
1872
+ onProgress?.(1, "Done");
1873
+ return { mask };
1874
+ }
1875
+ }
1876
+ }
1877
+ function startSelectionWorker(scope) {
1878
+ const images = /* @__PURE__ */ new Map();
1879
+ scope.onmessage = (event) => {
1880
+ const msg = event.data;
1881
+ if (msg.type === "pixels") {
1882
+ images.set(msg.key, msg.pixels);
1883
+ while (images.size > 2) images.delete(images.keys().next().value);
1884
+ return;
1885
+ }
1886
+ const pixels = images.get(msg.key);
1887
+ try {
1888
+ if (!pixels) throw new Error("The selection worker has no image for this job; send the pixels first.");
1889
+ const result = runSelectionJob(msg.job, pixels, (fraction, stage) => scope.postMessage({ type: "progress", id: msg.id, fraction, stage }));
1890
+ const mask = serializeMask(result.mask);
1891
+ scope.postMessage({ type: "done", id: msg.id, mask, reason: result.reason, noiseLevel: result.noiseLevel }, [mask.bytes.buffer]);
1892
+ } catch (err) {
1893
+ scope.postMessage({ type: "error", id: msg.id, message: err instanceof Error ? err.message : String(err) });
1894
+ }
1895
+ };
1896
+ }
1897
+ var abortError = () => typeof DOMException === "function" ? new DOMException("The selection was cancelled.", "AbortError") : Object.assign(new Error("The selection was cancelled."), { name: "AbortError" });
1898
+ var SelectionWorkerClient = class {
1899
+ constructor(factory) {
1900
+ this.factory = factory;
1901
+ }
1902
+ factory;
1903
+ worker = null;
1904
+ /** Pixel buffers this worker already has, by their data array. */
1905
+ sent = /* @__PURE__ */ new WeakMap();
1906
+ nextKey = 1;
1907
+ nextId = 1;
1908
+ pending = /* @__PURE__ */ new Map();
1909
+ /** Runs `job` on `pixels` in the worker. Aborting `signal` terminates the worker and rejects with an `AbortError`. */
1910
+ run(job, pixels, options = {}) {
1911
+ if (options.signal?.aborted) return Promise.reject(abortError());
1912
+ const worker = this.ensure();
1913
+ let key = this.sent.get(pixels.data);
1914
+ if (key === void 0) {
1915
+ key = this.nextKey++;
1916
+ this.sent.set(pixels.data, key);
1917
+ worker.postMessage({ type: "pixels", key, pixels: { width: pixels.width, height: pixels.height, data: pixels.data } });
1918
+ }
1919
+ const id = this.nextId++;
1920
+ return new Promise((resolve, reject) => {
1921
+ this.pending.set(id, { resolve, reject, onProgress: options.onProgress });
1922
+ options.signal?.addEventListener(
1923
+ "abort",
1924
+ () => {
1925
+ if (this.pending.has(id)) this.terminate(abortError());
1926
+ },
1927
+ { once: true }
1928
+ );
1929
+ worker.postMessage({ type: "job", id, key, job });
1930
+ });
1931
+ }
1932
+ /** Stops the worker; jobs still running reject with `reason` (default an `AbortError`). */
1933
+ terminate(reason = abortError()) {
1934
+ this.worker?.terminate?.();
1935
+ this.worker = null;
1936
+ this.sent = /* @__PURE__ */ new WeakMap();
1937
+ const pending = [...this.pending.values()];
1938
+ this.pending.clear();
1939
+ for (const p of pending) p.reject(reason);
1940
+ }
1941
+ ensure() {
1942
+ if (this.worker) return this.worker;
1943
+ const worker = this.factory();
1944
+ worker.onmessage = (event) => {
1945
+ const msg = event.data;
1946
+ const p = this.pending.get(msg.id);
1947
+ if (!p) return;
1948
+ if (msg.type === "progress") p.onProgress?.(msg.fraction, msg.stage);
1949
+ else {
1950
+ this.pending.delete(msg.id);
1951
+ if (msg.type === "error") p.reject(new Error(msg.message));
1952
+ else p.resolve({ mask: deserializeMask(msg.mask), reason: msg.reason, noiseLevel: msg.noiseLevel });
1953
+ }
1954
+ };
1955
+ worker.onerror = (event) => this.terminate(new Error(event?.message || "The selection worker failed."));
1956
+ this.worker = worker;
1957
+ return worker;
1958
+ }
1959
+ };
1960
+
1961
+ export {
1962
+ DEFAULT_REFINE_EDGE_PARAMS,
1963
+ GUIDED_FILTER_EPS,
1964
+ normalizeRefineEdgeParams,
1965
+ refineEdgeHalo,
1966
+ boxMean,
1967
+ guidedFilterColor,
1968
+ edgeWidth,
1969
+ refineCoverage,
1970
+ guidePlanes,
1971
+ refineSelection,
1972
+ decontaminateColors,
1973
+ polygonCoverage,
1974
+ polygonCoverageBox,
1975
+ polygonSelection,
1976
+ normalizeRect,
1977
+ rectSelection,
1978
+ ellipsePoints,
1979
+ ellipseSelection,
1980
+ GridGraph,
1981
+ WORK_SIZE,
1982
+ workImage,
1983
+ GAMMA,
1984
+ UNITS,
1985
+ smoothnessWeights,
1986
+ boxBlur,
1987
+ upscaleRegion,
1988
+ ColorGmm,
1989
+ grabCutLabels,
1990
+ keepMainPieces,
1991
+ selectObjectIn,
1992
+ quickSelectStroke,
1993
+ REFINE_EDIT_KINDS,
1994
+ brushStrokeMask,
1995
+ applyRefineEdits,
1996
+ workPlanes,
1997
+ workLuma,
1998
+ softMaskToSelection,
1999
+ slicSuperpixels,
2000
+ superpixelSaliency,
2001
+ otsuThreshold,
2002
+ findSubject,
2003
+ focusArea,
2004
+ findSky,
2005
+ serializeMask,
2006
+ deserializeMask,
2007
+ runSelectionJob,
2008
+ startSelectionWorker,
2009
+ SelectionWorkerClient
2010
+ };