@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
@@ -3,6 +3,56 @@ import {
3
3
  parseOpenTypeFont,
4
4
  traceField
5
5
  } from "../chunk-5OC22IEO.js";
6
+ import {
7
+ ColorGmm,
8
+ DEFAULT_REFINE_EDGE_PARAMS,
9
+ GAMMA,
10
+ GUIDED_FILTER_EPS,
11
+ GridGraph,
12
+ REFINE_EDIT_KINDS,
13
+ SelectionWorkerClient,
14
+ UNITS,
15
+ WORK_SIZE,
16
+ applyRefineEdits,
17
+ boxBlur,
18
+ boxMean,
19
+ brushStrokeMask,
20
+ decontaminateColors,
21
+ deserializeMask,
22
+ edgeWidth,
23
+ ellipsePoints,
24
+ ellipseSelection,
25
+ findSky,
26
+ findSubject,
27
+ focusArea,
28
+ grabCutLabels,
29
+ guidePlanes,
30
+ guidedFilterColor,
31
+ keepMainPieces,
32
+ normalizeRect,
33
+ normalizeRefineEdgeParams,
34
+ otsuThreshold,
35
+ polygonCoverage,
36
+ polygonCoverageBox,
37
+ polygonSelection,
38
+ quickSelectStroke,
39
+ rectSelection,
40
+ refineCoverage,
41
+ refineEdgeHalo,
42
+ refineSelection,
43
+ runSelectionJob,
44
+ selectObjectIn,
45
+ serializeMask,
46
+ slicSuperpixels,
47
+ smoothnessWeights,
48
+ softMaskToSelection,
49
+ startSelectionWorker,
50
+ superpixelSaliency,
51
+ upscaleRegion,
52
+ workImage,
53
+ workLuma,
54
+ workPlanes
55
+ } from "../chunk-DWYQPL2A.js";
6
56
  import {
7
57
  ADJUSTMENT_KEYS,
8
58
  ADJUSTMENT_KINDS,
@@ -20,7 +70,6 @@ import {
20
70
  DEFAULT_MITER_LIMIT,
21
71
  DEFAULT_TRANSFORM_VALUES,
22
72
  DEVELOP_FILTER_ID,
23
- EMPTY_DISTANCE,
24
73
  FREE_TRANSFORM_MODES,
25
74
  HUE_RANGE_LABELS,
26
75
  IDENTITY_MATRIX3,
@@ -39,8 +88,6 @@ import {
39
88
  PATTERN_BLOCK_KEYS,
40
89
  PSD_BLEND_KEYS,
41
90
  PsdError,
42
- SELECTION_MODES,
43
- SELECTION_MODIFY_OPS,
44
91
  SHAPE_OPERATIONS,
45
92
  SHAPE_RENDER_MARGIN,
46
93
  SMART_RENDER_MARGIN,
@@ -89,7 +136,6 @@ import {
89
136
  backgroundLayer,
90
137
  blackWhiteGray,
91
138
  blurFilter,
92
- borderCoverage,
93
139
  boxPointOf,
94
140
  bradfordAdaptation,
95
141
  buildLayerMaskPlanes,
@@ -98,21 +144,16 @@ import {
98
144
  canvasToImagePoint,
99
145
  clampCropToImage,
100
146
  colorBalanceChannel,
101
- combineSelections,
102
147
  compileAdjustmentLayer,
103
148
  componentPath,
104
149
  componentsCoverage,
105
150
  compositeDeep,
106
- computeSelectionBounds,
107
151
  contentsBox,
108
152
  contentsSurface,
109
- contractCoverage,
110
153
  convertToSmartObject,
111
154
  copySmartObject,
112
155
  cornersFromMatrix,
113
- coverageToByte,
114
156
  createAdjustmentLayer,
115
- createSelectionMask,
116
157
  createShapeLayer,
117
158
  createTypeLayer,
118
159
  curveLutFromLevels,
@@ -122,7 +163,6 @@ import {
122
163
  decodeMergedImage,
123
164
  decodePlanes,
124
165
  decodePngDataUrl,
125
- decodeSelectionRle,
126
166
  defaultActiveLayerId,
127
167
  defaultAdjustmentParams,
128
168
  defaultShapeName,
@@ -133,39 +173,31 @@ import {
133
173
  deserializeLayer,
134
174
  deserializeLayers,
135
175
  deserializePatterns,
136
- deserializeSelection,
137
176
  developFilter,
138
177
  developFromAdjustments,
139
178
  developTarget,
140
- distanceTransform,
141
179
  documentContents,
142
180
  duplicateLayer,
143
181
  effectiveLayerMask,
144
182
  encodeDeepSurface,
145
183
  encodeRle,
146
- encodeSelectionRle,
147
184
  encodeSurface,
148
185
  evaluateAdjustmentLayers,
149
- expandCoverage,
150
186
  exportPsd,
151
- featherCoverage,
152
187
  filterMaskCoverage,
153
188
  flattenPath,
154
189
  flipCorners,
155
190
  followLayerMove,
156
- fullSelection,
157
191
  geometryHomography,
158
192
  getAnnotationBounds,
159
193
  getCropCursor,
160
194
  getRotatedSize,
161
195
  getViewportMetrics,
162
196
  getViewportProjection,
163
- greyMorphology,
164
197
  groupLayers,
165
198
  hasResampledLegacyMask,
166
199
  hasSurface,
167
200
  hasWarp,
168
- hashMaskContent,
169
201
  hexToRgb,
170
202
  hitTestAnnotation,
171
203
  hitTestCrop,
@@ -180,7 +212,6 @@ import {
180
212
  importPsd,
181
213
  instancesOf,
182
214
  invertMatrix3,
183
- invertSelection,
184
215
  isActiveFilter,
185
216
  isActiveVectorMask,
186
217
  isAdjustmentKind,
@@ -191,8 +222,6 @@ import {
191
222
  isPaintableLayer,
192
223
  isQuarterTurnSwapped,
193
224
  isRenderedLayer,
194
- isSelectionEmpty,
195
- isSelectionMode,
196
225
  isSmartSurfaceIncomplete,
197
226
  isSpatialFilter,
198
227
  isTransformAnchor,
@@ -220,7 +249,6 @@ import {
220
249
  matrixFromCorners,
221
250
  matrixFromParams,
222
251
  mergeShapeComponents,
223
- modifySelection,
224
252
  moveLayer,
225
253
  moveSmartFilter,
226
254
  mulMatrix3,
@@ -249,7 +277,6 @@ import {
249
277
  parseLfx2,
250
278
  parseLrfx,
251
279
  parsePatternBlock,
252
- parseSelectionMode,
253
280
  parseVersionedDescriptor,
254
281
  patchParagraphStyle,
255
282
  patchTextStyle,
@@ -295,7 +322,6 @@ import {
295
322
  rerender,
296
323
  rerenderShape,
297
324
  rerenderText,
298
- resizeSelection,
299
325
  resolveExternalAssets,
300
326
  resolveVectorMasks,
301
327
  rgbToGray,
@@ -304,21 +330,12 @@ import {
304
330
  runLayerFiltersFloat,
305
331
  sampleLut,
306
332
  sampleToneLut,
307
- selectionArea,
308
- selectionFromCoverage,
309
- selectionFromPixels,
310
- selectionFromRegion,
311
- selectionFromTiles,
312
- selectionToCoverage,
313
- selectionToPixels,
314
- selectionValueAt,
315
333
  serializeContents,
316
334
  serializeEffects,
317
335
  serializeLayer,
318
336
  serializeLayers,
319
337
  serializeLayersExternal,
320
338
  serializePatterns,
321
- serializeSelection,
322
339
  setLayerEffects,
323
340
  setLayerEffectsEnabled,
324
341
  setLayerMask,
@@ -332,10 +349,8 @@ import {
332
349
  shapeLayers,
333
350
  shapeOutline,
334
351
  shapeRenderClip,
335
- shiftCoverage,
336
352
  smartContentsFile,
337
353
  smartObjectLayers,
338
- smoothCoverage,
339
354
  srgbToLinear,
340
355
  tempTintToXy,
341
356
  temperatureToKelvin,
@@ -386,10 +401,47 @@ import {
386
401
  writePsd,
387
402
  xyToTempTint,
388
403
  xyToUv
389
- } from "../chunk-ZIDUPSP4.js";
404
+ } from "../chunk-KFIOEOCQ.js";
390
405
  import {
391
406
  parseCssColor
392
407
  } from "../chunk-3CI7FRMK.js";
408
+ import {
409
+ EMPTY_DISTANCE,
410
+ SELECTION_MODES,
411
+ SELECTION_MODIFY_OPS,
412
+ borderCoverage,
413
+ combineSelections,
414
+ computeSelectionBounds,
415
+ contractCoverage,
416
+ coverageToByte,
417
+ createSelectionMask,
418
+ decodeSelectionRle,
419
+ deserializeSelection,
420
+ distanceTransform,
421
+ encodeSelectionRle,
422
+ expandCoverage,
423
+ featherCoverage,
424
+ fullSelection,
425
+ greyMorphology,
426
+ hashMaskContent,
427
+ invertSelection,
428
+ isSelectionEmpty,
429
+ isSelectionMode,
430
+ modifySelection,
431
+ parseSelectionMode,
432
+ resizeSelection,
433
+ selectionArea,
434
+ selectionFromCoverage,
435
+ selectionFromPixels,
436
+ selectionFromRegion,
437
+ selectionFromTiles,
438
+ selectionToCoverage,
439
+ selectionToPixels,
440
+ selectionValueAt,
441
+ serializeSelection,
442
+ shiftCoverage,
443
+ smoothCoverage
444
+ } from "../chunk-L3G23N5Y.js";
393
445
  import {
394
446
  EffectRendererPool
395
447
  } from "../chunk-EHLOCVWB.js";
@@ -398,7 +450,6 @@ import {
398
450
  } from "../chunk-5ED73EC6.js";
399
451
  import {
400
452
  DEFAULT_GLOBAL_LIGHT,
401
- DIRECT_GAUSSIAN_MAX_SIGMA,
402
453
  INTERPOLATIONS,
403
454
  LAYER_EFFECT_KINDS,
404
455
  LayerEffectError,
@@ -422,7 +473,6 @@ import {
422
473
  effectsScale,
423
474
  findLayer,
424
475
  flattenLayers,
425
- gaussianBlur,
426
476
  hasActiveEffects,
427
477
  hashNoise,
428
478
  insertLayer,
@@ -447,7 +497,7 @@ import {
447
497
  validateLayerEffects,
448
498
  warpSurface,
449
499
  workingToSamples
450
- } from "../chunk-NQMP3CWF.js";
500
+ } from "../chunk-YBJXCQTR.js";
451
501
  import {
452
502
  applyHomography,
453
503
  boxQuad,
@@ -457,6 +507,10 @@ import {
457
507
  multiplyHomographies,
458
508
  rectToQuad
459
509
  } from "../chunk-OHKOFKBY.js";
510
+ import {
511
+ DIRECT_GAUSSIAN_MAX_SIGMA,
512
+ gaussianBlur
513
+ } from "../chunk-ZIF4UJJ6.js";
460
514
  import {
461
515
  DEFAULT_RETOUCH,
462
516
  IDENTITY_CLONE_TRANSFORM,
@@ -495,7 +549,7 @@ import {
495
549
  spotHealPixels,
496
550
  spotHealWindow,
497
551
  toneOf
498
- } from "../chunk-EL5PAHNP.js";
552
+ } from "../chunk-USQCKRZG.js";
499
553
  import {
500
554
  BODY_SCATTER,
501
555
  DEFAULT_FLOW,
@@ -586,7 +640,7 @@ import {
586
640
  wetGpuSupport,
587
641
  wetSimdSupported,
588
642
  wetSupport
589
- } from "../chunk-36OKQDCF.js";
643
+ } from "../chunk-WJ2RBZMM.js";
590
644
  import {
591
645
  BUILTIN_PROFILES,
592
646
  CmsError,
@@ -646,8 +700,8 @@ import {
646
700
  warpMeshParamAt,
647
701
  warpMeshRows,
648
702
  warpPresetMesh
649
- } from "../chunk-GHCXLQ47.js";
650
- import "../chunk-YXKLVD56.js";
703
+ } from "../chunk-NDMXB4PR.js";
704
+ import "../chunk-F7YA3MMC.js";
651
705
  import "../chunk-M6DNZNVY.js";
652
706
  import {
653
707
  bezierPathToSvg
@@ -758,7 +812,7 @@ import {
758
812
  tipShape,
759
813
  updatePaintGradientStop,
760
814
  walkDabs
761
- } from "../chunk-2N2QPO3H.js";
815
+ } from "../chunk-2PBZBRP5.js";
762
816
  import {
763
817
  BRUSHSTROKE_MAX_SCALE,
764
818
  BRUSHSTROKE_MIN_SCALE,
@@ -773,10 +827,6 @@ import {
773
827
  PIGMENT_LAMBDA_STEP,
774
828
  PIGMENT_WAVELENGTHS,
775
829
  SHADOW_AMBIENT,
776
- SurfaceDraft,
777
- TILE,
778
- TILE_SHIFT,
779
- TiledSurface,
780
830
  base64ToBytes,
781
831
  blendChannel,
782
832
  blendImageData,
@@ -785,18 +835,13 @@ import {
785
835
  blendRgb,
786
836
  bytesToBase64,
787
837
  canvasCompositeOperation,
788
- collectTiles,
789
- contentBounds,
790
838
  convertSurfaceDepth,
791
839
  createPigment,
792
840
  decodePng,
793
841
  deepBytes,
794
842
  deepTile,
795
843
  depthOf,
796
- emptySurface,
797
844
  encodePng,
798
- fillRect,
799
- finishTile,
800
845
  isBitDepth,
801
846
  isBlendMode,
802
847
  isPainterlyBlendMode,
@@ -809,45 +854,56 @@ import {
809
854
  mixPigmentRgb,
810
855
  mixPigments,
811
856
  normalizeBlendMode,
812
- packRgba,
813
857
  parseDataUrl,
814
858
  pigment8,
815
859
  pngDataUrl,
816
860
  proxyOf,
817
861
  readDeepRegion,
818
- readPixel,
819
- readRegion,
820
862
  reflectanceToRgb,
821
863
  rgbToReflectance,
822
864
  setLum,
823
865
  setPigmentColor,
824
- solidSurface,
825
866
  srgbDecode,
826
867
  srgbEncode,
827
868
  standardBlendMode,
828
- surfaceBytes,
829
869
  surfaceDepth,
830
870
  surfaceFromDeepPixels,
831
- surfaceFromPixels,
832
- surfacesEqual,
833
- tileCoord,
834
- tileKey,
835
- tileRect,
836
871
  tileSamples,
837
872
  tileSamples16,
838
873
  tileSamples32,
839
- tilesInRect,
840
874
  to16From32,
841
875
  to16From8,
842
876
  to32From16,
843
877
  to32From8,
844
878
  to8From16,
845
879
  to8From32,
880
+ writeDeepRegion
881
+ } from "../chunk-42TWUPNJ.js";
882
+ import {
883
+ SurfaceDraft,
884
+ TILE,
885
+ TILE_SHIFT,
886
+ TiledSurface,
887
+ collectTiles,
888
+ contentBounds,
889
+ emptySurface,
890
+ fillRect,
891
+ finishTile,
892
+ packRgba,
893
+ readPixel,
894
+ readRegion,
895
+ solidSurface,
896
+ surfaceBytes,
897
+ surfaceFromPixels,
898
+ surfacesEqual,
899
+ tileCoord,
900
+ tileKey,
901
+ tileRect,
902
+ tilesInRect,
846
903
  translateSurface,
847
904
  uniformTile,
848
- writeDeepRegion,
849
905
  writeRegion
850
- } from "../chunk-H367YTXI.js";
906
+ } from "../chunk-HVVR5PZW.js";
851
907
  import {
852
908
  ZlibError,
853
909
  adler32,
@@ -2803,1597 +2859,6 @@ function magneticAnchorSpacing(frequency = 57) {
2803
2859
  return 8 + (100 - f);
2804
2860
  }
2805
2861
 
2806
- // src/image-editor/selection/segment/maxflow.ts
2807
- var TERMINAL = -2;
2808
- var NONE = -1;
2809
- var FREE = 0;
2810
- var SOURCE = 1;
2811
- var SINK = 2;
2812
- var GridGraph = class {
2813
- width;
2814
- height;
2815
- /** Residual capacity of the edge leaving node `p` in direction `d`, at `p * 4 + d`. */
2816
- cap;
2817
- /** Net terminal capacity: above 0 to the source, below 0 to the sink. */
2818
- tr;
2819
- tree;
2820
- constructor(width, height) {
2821
- this.width = width;
2822
- this.height = height;
2823
- const n = width * height;
2824
- this.cap = new Float64Array(n * 4);
2825
- this.tr = new Float64Array(n);
2826
- this.tree = new Uint8Array(n);
2827
- }
2828
- /** Sets the capacity of the edge between `p` and its neighbour in direction `d`, both ways. */
2829
- setEdge(p, d, capacity) {
2830
- const q = d === 0 ? p + 1 : p + this.width;
2831
- this.cap[p * 4 + d] = capacity;
2832
- this.cap[q * 4 + (d ^ 2)] = capacity;
2833
- }
2834
- /** Adds `source` (cost of putting `p` on the sink side) and `sink` (the other way) to `p`. */
2835
- addTerminal(p, source, sink) {
2836
- this.tr[p] += source - sink;
2837
- }
2838
- /** Whether `p` is on the source side of the minimum cut (after {@link maxflow}). */
2839
- inSource(p) {
2840
- return this.tree[p] === SOURCE;
2841
- }
2842
- /** Runs max-flow and returns the flow (the cut's cost, less the constant terminal parts). */
2843
- maxflow() {
2844
- const { width: w, height: h, cap, tr, tree } = this;
2845
- const n = w * h;
2846
- const nb = (p, d) => {
2847
- const x = p % w;
2848
- if (d === 0) return x < w - 1 ? p + 1 : -1;
2849
- if (d === 1) return p + w < n ? p + w : -1;
2850
- if (d === 2) return x > 0 ? p - 1 : -1;
2851
- return p - w >= 0 ? p - w : -1;
2852
- };
2853
- const parent = new Int8Array(n).fill(NONE);
2854
- const ts = new Int32Array(n);
2855
- const dist = new Int32Array(n);
2856
- const queue = new Int32Array(n + 1);
2857
- const queued = new Uint8Array(n);
2858
- let qHead = 0;
2859
- let qTail = 0;
2860
- const activate = (p) => {
2861
- if (queued[p]) return;
2862
- queued[p] = 1;
2863
- queue[qTail] = p;
2864
- qTail = qTail === n ? 0 : qTail + 1;
2865
- };
2866
- const nextActive = () => {
2867
- while (qHead !== qTail) {
2868
- const p = queue[qHead];
2869
- qHead = qHead === n ? 0 : qHead + 1;
2870
- queued[p] = 0;
2871
- if (tree[p] !== FREE) return p;
2872
- }
2873
- return -1;
2874
- };
2875
- let orphans = new Int32Array(64);
2876
- let orphanCount = 0;
2877
- const orphan = (p) => {
2878
- parent[p] = NONE;
2879
- if (orphanCount === orphans.length) {
2880
- const grown = new Int32Array(orphans.length * 2);
2881
- grown.set(orphans);
2882
- orphans = grown;
2883
- }
2884
- orphans[orphanCount++] = p;
2885
- };
2886
- tree.fill(FREE);
2887
- for (let p = 0; p < n; p++) {
2888
- if (tr[p] > 0) tree[p] = SOURCE;
2889
- else if (tr[p] < 0) tree[p] = SINK;
2890
- else continue;
2891
- parent[p] = TERMINAL;
2892
- dist[p] = 1;
2893
- activate(p);
2894
- }
2895
- let flow = 0;
2896
- let time = 0;
2897
- let current = -1;
2898
- for (; ; ) {
2899
- if (current < 0 || tree[current] === FREE) {
2900
- current = nextActive();
2901
- if (current < 0) break;
2902
- }
2903
- const p = current;
2904
- const side = tree[p];
2905
- let s = -1;
2906
- let t = -1;
2907
- let dir = 0;
2908
- for (let d = 0; d < 4; d++) {
2909
- const q = nb(p, d);
2910
- if (q < 0) continue;
2911
- if (side === SOURCE ? cap[p * 4 + d] <= 0 : cap[q * 4 + (d ^ 2)] <= 0) continue;
2912
- if (tree[q] === FREE) {
2913
- tree[q] = side;
2914
- parent[q] = d ^ 2;
2915
- ts[q] = ts[p];
2916
- dist[q] = dist[p] + 1;
2917
- activate(q);
2918
- } else if (tree[q] !== side) {
2919
- if (side === SOURCE) {
2920
- s = p;
2921
- t = q;
2922
- dir = d;
2923
- } else {
2924
- s = q;
2925
- t = p;
2926
- dir = d ^ 2;
2927
- }
2928
- break;
2929
- } else if (ts[q] <= ts[p] && dist[q] > dist[p]) {
2930
- parent[q] = d ^ 2;
2931
- ts[q] = ts[p];
2932
- dist[q] = dist[p] + 1;
2933
- }
2934
- }
2935
- if (s < 0) {
2936
- current = -1;
2937
- continue;
2938
- }
2939
- time++;
2940
- let f = cap[s * 4 + dir];
2941
- for (let x = s; ; ) {
2942
- const a = parent[x];
2943
- if (a === TERMINAL) {
2944
- f = Math.min(f, tr[x]);
2945
- break;
2946
- }
2947
- const up = nb(x, a);
2948
- f = Math.min(f, cap[up * 4 + (a ^ 2)]);
2949
- x = up;
2950
- }
2951
- for (let x = t; ; ) {
2952
- const a = parent[x];
2953
- if (a === TERMINAL) {
2954
- f = Math.min(f, -tr[x]);
2955
- break;
2956
- }
2957
- f = Math.min(f, cap[x * 4 + a]);
2958
- x = nb(x, a);
2959
- }
2960
- cap[s * 4 + dir] -= f;
2961
- cap[t * 4 + (dir ^ 2)] += f;
2962
- for (let x = s; ; ) {
2963
- const a = parent[x];
2964
- if (a === TERMINAL) {
2965
- tr[x] -= f;
2966
- if (tr[x] <= 0) orphan(x);
2967
- break;
2968
- }
2969
- const up = nb(x, a);
2970
- cap[up * 4 + (a ^ 2)] -= f;
2971
- cap[x * 4 + a] += f;
2972
- if (cap[up * 4 + (a ^ 2)] <= 0) orphan(x);
2973
- x = up;
2974
- }
2975
- for (let x = t; ; ) {
2976
- const a = parent[x];
2977
- if (a === TERMINAL) {
2978
- tr[x] += f;
2979
- if (tr[x] >= 0) orphan(x);
2980
- break;
2981
- }
2982
- const up = nb(x, a);
2983
- cap[x * 4 + a] -= f;
2984
- cap[up * 4 + (a ^ 2)] += f;
2985
- if (cap[x * 4 + a] <= 0) orphan(x);
2986
- x = up;
2987
- }
2988
- flow += f;
2989
- while (orphanCount) {
2990
- const o = orphans[--orphanCount];
2991
- const oSide = tree[o];
2992
- let best = NONE;
2993
- let bestDist = Infinity;
2994
- for (let d = 0; d < 4; d++) {
2995
- const q = nb(o, d);
2996
- if (q < 0 || tree[q] !== oSide) continue;
2997
- if (oSide === SOURCE ? cap[q * 4 + (d ^ 2)] <= 0 : cap[o * 4 + d] <= 0) continue;
2998
- let k = 0;
2999
- let x = q;
3000
- for (; ; ) {
3001
- if (ts[x] === time) {
3002
- k += dist[x];
3003
- break;
3004
- }
3005
- const a = parent[x];
3006
- k++;
3007
- if (a === TERMINAL) {
3008
- ts[x] = time;
3009
- dist[x] = 1;
3010
- break;
3011
- }
3012
- if (a === NONE) {
3013
- k = Infinity;
3014
- break;
3015
- }
3016
- x = nb(x, a);
3017
- }
3018
- if (k === Infinity) continue;
3019
- if (k < bestDist) {
3020
- bestDist = k;
3021
- best = d;
3022
- }
3023
- for (let y = q, dd = k; ts[y] !== time; y = nb(y, parent[y]), dd--) {
3024
- ts[y] = time;
3025
- dist[y] = dd;
3026
- }
3027
- }
3028
- if (best !== NONE) {
3029
- parent[o] = best;
3030
- ts[o] = time;
3031
- dist[o] = bestDist + 1;
3032
- continue;
3033
- }
3034
- for (let d = 0; d < 4; d++) {
3035
- const q = nb(o, d);
3036
- if (q < 0 || tree[q] !== oSide) continue;
3037
- if (oSide === SOURCE ? cap[q * 4 + (d ^ 2)] > 0 : cap[o * 4 + d] > 0) activate(q);
3038
- const a = parent[q];
3039
- if (a !== TERMINAL && a !== NONE && nb(q, a) === o) orphan(q);
3040
- }
3041
- tree[o] = FREE;
3042
- }
3043
- }
3044
- return flow;
3045
- }
3046
- };
3047
-
3048
- // src/image-editor/selection/segment/work.ts
3049
- var WORK_SIZE = 512;
3050
- var workCache = /* @__PURE__ */ new WeakMap();
3051
- function workImage(pixels, maxSide = WORK_SIZE) {
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;
3055
- const { width: W, height: H, data } = pixels;
3056
- const scale = Math.min(1, maxSide / Math.max(W, H));
3057
- const w = Math.max(1, Math.round(W * scale));
3058
- const h = Math.max(1, Math.round(H * scale));
3059
- const sum = new Float64Array(w * h * 3);
3060
- const count = new Float64Array(w * h);
3061
- const xs = new Int32Array(W);
3062
- for (let x = 0; x < W; x++) xs[x] = Math.min(w - 1, Math.floor(x * w / W));
3063
- for (let y = 0; y < H; y++) {
3064
- const row = Math.min(h - 1, Math.floor(y * h / H)) * w;
3065
- for (let x = 0, p = y * W * 4; x < W; x++, p += 4) {
3066
- const o = row + xs[x];
3067
- const a = data[p + 3] / 255;
3068
- const k = o * 3;
3069
- sum[k] += data[p] * a + 255 * (1 - a);
3070
- sum[k + 1] += data[p + 1] * a + 255 * (1 - a);
3071
- sum[k + 2] += data[p + 2] * a + 255 * (1 - a);
3072
- count[o]++;
3073
- }
3074
- }
3075
- const rgb = new Float32Array(w * h * 3);
3076
- for (let i = 0; i < w * h; i++) {
3077
- const c = count[i] || 1;
3078
- rgb[i * 3] = sum[i * 3] / c;
3079
- rgb[i * 3 + 1] = sum[i * 3 + 1] / c;
3080
- rgb[i * 3 + 2] = sum[i * 3 + 2] / c;
3081
- }
3082
- const work = { sourceWidth: W, sourceHeight: H, width: w, height: h, scale: w / W, rgb };
3083
- if (sizes) sizes.set(maxSide, work);
3084
- else workCache.set(pixels.data, /* @__PURE__ */ new Map([[maxSide, work]]));
3085
- return work;
3086
- }
3087
- var GAMMA = 50;
3088
- var UNITS = 64;
3089
- function smoothnessWeights(work, roi) {
3090
- const { width: w, rgb } = work;
3091
- const { x: rx, y: ry, width: rw, height: rh } = roi;
3092
- const right = new Float64Array(rw * rh);
3093
- const down = new Float64Array(rw * rh);
3094
- let sumSq = 0;
3095
- let pairs = 0;
3096
- for (let y = 0; y < rh; y++) {
3097
- for (let x = 0; x < rw; x++) {
3098
- const p = y * rw + x;
3099
- const c = ((ry + y) * w + rx + x) * 3;
3100
- if (x + 1 < rw) {
3101
- right[p] = (rgb[c] - rgb[c + 3]) ** 2 + (rgb[c + 1] - rgb[c + 4]) ** 2 + (rgb[c + 2] - rgb[c + 5]) ** 2;
3102
- sumSq += right[p];
3103
- pairs++;
3104
- }
3105
- if (y + 1 < rh) {
3106
- const k = c + w * 3;
3107
- down[p] = (rgb[c] - rgb[k]) ** 2 + (rgb[c + 1] - rgb[k + 1]) ** 2 + (rgb[c + 2] - rgb[k + 2]) ** 2;
3108
- sumSq += down[p];
3109
- pairs++;
3110
- }
3111
- }
3112
- }
3113
- const beta = 1 / (2 * Math.max(1e-6, sumSq / Math.max(1, pairs)));
3114
- for (let p = 0; p < right.length; p++) {
3115
- right[p] = Math.round(GAMMA * UNITS * Math.exp(-beta * right[p]));
3116
- down[p] = Math.round(GAMMA * UNITS * Math.exp(-beta * down[p]));
3117
- }
3118
- return { right, down };
3119
- }
3120
- function boxBlur(src, w, h, passes) {
3121
- let a = src;
3122
- for (let k = 0; k < passes; k++) {
3123
- const b = new Float32Array(a.length);
3124
- for (let y = 0; y < h; y++) {
3125
- for (let x = 0; x < w; x++) {
3126
- let s = 0;
3127
- let c = 0;
3128
- for (let j = Math.max(0, y - 1); j <= Math.min(h - 1, y + 1); j++) {
3129
- for (let i = Math.max(0, x - 1); i <= Math.min(w - 1, x + 1); i++) {
3130
- s += a[j * w + i];
3131
- c++;
3132
- }
3133
- }
3134
- b[y * w + x] = s / c;
3135
- }
3136
- }
3137
- a = b;
3138
- }
3139
- return a;
3140
- }
3141
- function upscaleRegion(work, pixels, roi, label, enhance) {
3142
- const { sourceWidth: W, sourceHeight: H, scale, rgb, width: ww } = work;
3143
- const { x: rx, y: ry, width: rw, height: rh } = roi;
3144
- let soft = Float32Array.from(label);
3145
- if (enhance) {
3146
- soft = boxBlur(soft, rw, rh, 2);
3147
- for (let i = 0; i < soft.length; i++) soft[i] = Math.min(1, Math.max(0, (soft[i] - 0.5) * 3 + 0.5));
3148
- }
3149
- const X0 = Math.max(0, Math.floor(rx / scale));
3150
- const Y0 = Math.max(0, Math.floor(ry / scale));
3151
- const X1 = Math.min(W, Math.ceil((rx + rw) / scale));
3152
- const Y1 = Math.min(H, Math.ceil((ry + rh) / scale));
3153
- const RW = X1 - X0;
3154
- const RH = Y1 - Y0;
3155
- const out = new Uint8Array(RW * RH);
3156
- const at = (x, y) => soft[Math.min(rh - 1, Math.max(0, y)) * rw + Math.min(rw - 1, Math.max(0, x))];
3157
- const data = pixels.data;
3158
- const colOf = new Int32Array(RW);
3159
- const fxOf = new Float32Array(RW);
3160
- for (let X = 0; X < RW; X++) {
3161
- const u = (X0 + X + 0.5) * scale - 0.5 - rx;
3162
- colOf[X] = Math.floor(u);
3163
- fxOf[X] = u - colOf[X];
3164
- }
3165
- const cellState = (i, j) => {
3166
- const a = at(i, j), b = at(i + 1, j), c = at(i, j + 1), d = at(i + 1, j + 1);
3167
- if (a === 1 && b === 1 && c === 1 && d === 1) return 1;
3168
- if (a === 0 && b === 0 && c === 0 && d === 0) return 0;
3169
- return 2;
3170
- };
3171
- for (let Y = 0; Y < RH; Y++) {
3172
- const v = (Y0 + Y + 0.5) * scale - 0.5 - ry;
3173
- const j = Math.floor(v);
3174
- const fy = v - j;
3175
- for (let X = 0; X < RW; X++) {
3176
- const i = colOf[X];
3177
- const state = cellState(i, j);
3178
- if (state !== 2) {
3179
- let end = X + 1;
3180
- while (end < RW && colOf[end] === i) end++;
3181
- if (state === 1) out.fill(255, Y * RW + X, Y * RW + end);
3182
- X = end - 1;
3183
- continue;
3184
- }
3185
- const fx = fxOf[X];
3186
- 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;
3187
- if (m <= 1e-3 || m >= 0.999) {
3188
- out[Y * RW + X] = m >= 0.999 ? 255 : 0;
3189
- continue;
3190
- }
3191
- let fr = 0, fg = 0, fb = 0, fn = 0, br = 0, bg = 0, bb = 0, bn = 0;
3192
- for (let jj = j - 1; jj <= j + 2; jj++) {
3193
- for (let ii = i - 1; ii <= i + 2; ii++) {
3194
- if (ii < 0 || jj < 0 || ii >= rw || jj >= rh) continue;
3195
- const k = ((ry + jj) * ww + rx + ii) * 3;
3196
- if (soft[jj * rw + ii] >= 0.5) {
3197
- fr += rgb[k];
3198
- fg += rgb[k + 1];
3199
- fb += rgb[k + 2];
3200
- fn++;
3201
- } else {
3202
- br += rgb[k];
3203
- bg += rgb[k + 1];
3204
- bb += rgb[k + 2];
3205
- bn++;
3206
- }
3207
- }
3208
- }
3209
- let alpha = m;
3210
- if (fn && bn) {
3211
- fr /= fn;
3212
- fg /= fn;
3213
- fb /= fn;
3214
- br /= bn;
3215
- bg /= bn;
3216
- bb /= bn;
3217
- const dr = fr - br, dg = fg - bg, db = fb - bb;
3218
- const len2 = dr * dr + dg * dg + db * db;
3219
- if (len2 > 400) {
3220
- const p = ((Y0 + Y) * W + X0 + X) * 4;
3221
- const a = data[p + 3] / 255;
3222
- const cr = data[p] * a + 255 * (1 - a) - br;
3223
- const cg = data[p + 1] * a + 255 * (1 - a) - bg;
3224
- const cb = data[p + 2] * a + 255 * (1 - a) - bb;
3225
- const proj = Math.min(1, Math.max(0, (cr * dr + cg * dg + cb * db) / len2));
3226
- alpha = Math.min(1, Math.max(0, proj * 0.75 + m * 0.25));
3227
- } else alpha = Math.min(1, Math.max(0, (m - 0.5) * 2 + 0.5));
3228
- }
3229
- out[Y * RW + X] = Math.round(alpha * 255);
3230
- }
3231
- }
3232
- return selectionFromRegion(W, H, { x: X0, y: Y0, width: RW, height: RH }, out);
3233
- }
3234
-
3235
- // src/image-editor/selection/segment/quick.ts
3236
- var BINS = 16;
3237
- 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);
3238
- function costTable(hist, total) {
3239
- let a = hist;
3240
- for (const stride of [1, BINS, BINS * BINS]) {
3241
- const b = new Float64Array(a.length);
3242
- for (let i = 0; i < a.length; i++) {
3243
- const pos = Math.floor(i / stride) % BINS;
3244
- const lo = pos > 0 ? a[i - stride] : a[i];
3245
- const hi = pos < BINS - 1 ? a[i + stride] : a[i];
3246
- b[i] = (lo + 2 * a[i] + hi) / 4;
3247
- }
3248
- a = b;
3249
- }
3250
- const out = new Float64Array(a.length);
3251
- const floor = 1e-4;
3252
- const denom = Math.max(1, total) * (1 + floor * a.length);
3253
- for (let i = 0; i < a.length; i++) out[i] = -Math.log((a[i] + floor * Math.max(1, total)) / denom);
3254
- return out;
3255
- }
3256
- function strokePixels(points, r, w, h) {
3257
- const mask = new Uint8Array(w * h);
3258
- const list = [];
3259
- const add2 = (g) => {
3260
- if (!mask[g]) {
3261
- mask[g] = 1;
3262
- list.push(g);
3263
- }
3264
- };
3265
- const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
3266
- for (const [a, b] of segs) {
3267
- const x0 = Math.max(0, Math.floor(Math.min(a.x, b.x) - r));
3268
- const x1 = Math.min(w - 1, Math.ceil(Math.max(a.x, b.x) + r));
3269
- const y0 = Math.max(0, Math.floor(Math.min(a.y, b.y) - r));
3270
- const y1 = Math.min(h - 1, Math.ceil(Math.max(a.y, b.y) + r));
3271
- const dx = b.x - a.x;
3272
- const dy = b.y - a.y;
3273
- const len2 = dx * dx + dy * dy;
3274
- for (let y = y0; y <= y1; y++) {
3275
- for (let x = x0; x <= x1; x++) {
3276
- const px2 = x + 0.5 - a.x;
3277
- const py = y + 0.5 - a.y;
3278
- const t = len2 > 0 ? Math.min(1, Math.max(0, (px2 * dx + py * dy) / len2)) : 0;
3279
- if ((px2 - t * dx) ** 2 + (py - t * dy) ** 2 <= r * r) add2(y * w + x);
3280
- }
3281
- }
3282
- }
3283
- if (!list.length) {
3284
- for (const p of points) add2(Math.min(h - 1, Math.max(0, Math.floor(p.y))) * w + Math.min(w - 1, Math.max(0, Math.floor(p.x))));
3285
- }
3286
- return { mask, list };
3287
- }
3288
- function segmentStroke(work, stroke, grown, start) {
3289
- const { width: w, height: h, rgb } = work;
3290
- let roi = start;
3291
- for (; ; ) {
3292
- const { x: rx, y: ry, width: rw, height: rh } = roi;
3293
- const n = rw * rh;
3294
- const fgHist = new Float64Array(BINS ** 3);
3295
- const bgHist = new Float64Array(BINS ** 3);
3296
- let fgN = 0;
3297
- let bgN = 0;
3298
- const atImageEdge = { l: rx === 0, t: ry === 0, r: rx + rw === w, b: ry + rh === h };
3299
- for (let y = 0; y < rh; y++) {
3300
- for (let x = 0; x < rw; x++) {
3301
- const g = (ry + y) * w + (rx + x);
3302
- const bin = binOf(rgb, g);
3303
- bgHist[bin]++;
3304
- bgN++;
3305
- if (stroke.mask[g] || grown?.[g]) {
3306
- fgHist[bin]++;
3307
- fgN++;
3308
- }
3309
- }
3310
- }
3311
- const fgCost = costTable(fgHist, fgN);
3312
- const bgCost = costTable(bgHist, bgN);
3313
- const { right, down } = smoothnessWeights(work, roi);
3314
- const graph = new GridGraph(rw, rh);
3315
- const HARD = 1e9;
3316
- for (let y = 0; y < rh; y++) {
3317
- for (let x = 0; x < rw; x++) {
3318
- const p = y * rw + x;
3319
- const g = (ry + y) * w + rx + x;
3320
- if (stroke.mask[g]) graph.addTerminal(p, HARD, 0);
3321
- else {
3322
- const bin = binOf(rgb, g);
3323
- graph.addTerminal(p, Math.round(bgCost[bin] * UNITS), Math.round(fgCost[bin] * UNITS));
3324
- }
3325
- if (x + 1 < rw) graph.setEdge(p, 0, right[p]);
3326
- if (y + 1 < rh) graph.setEdge(p, 1, down[p]);
3327
- }
3328
- }
3329
- graph.maxflow();
3330
- const label = new Uint8Array(n);
3331
- const stack = [];
3332
- for (const g of stroke.list) {
3333
- const x = g % w - rx;
3334
- const y = Math.floor(g / w) - ry;
3335
- if (x < 0 || y < 0 || x >= rw || y >= rh) continue;
3336
- const p = y * rw + x;
3337
- if (!label[p]) {
3338
- label[p] = 1;
3339
- stack.push(p);
3340
- }
3341
- }
3342
- while (stack.length) {
3343
- const p = stack.pop();
3344
- const x = p % rw;
3345
- for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
3346
- if (q >= 0 && !label[q] && graph.inSource(q)) {
3347
- label[q] = 1;
3348
- stack.push(q);
3349
- }
3350
- }
3351
- }
3352
- let touches = false;
3353
- for (let y = 0; y < rh; y++) {
3354
- for (let x = 0; x < rw; x++) {
3355
- const p = y * rw + x;
3356
- if (!label[p]) continue;
3357
- if (!atImageEdge.l && x === 0 || !atImageEdge.t && y === 0 || !atImageEdge.r && x === rw - 1 || !atImageEdge.b && y === rh - 1) touches = true;
3358
- }
3359
- }
3360
- if (!touches || atImageEdge.l && atImageEdge.t && atImageEdge.r && atImageEdge.b) return { roi, label };
3361
- const nx = Math.max(0, rx - Math.ceil(rw / 2));
3362
- const ny = Math.max(0, ry - Math.ceil(rh / 2));
3363
- 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 };
3364
- if (roi.width * roi.height * 2.5 > w * h) roi = { x: 0, y: 0, width: w, height: h };
3365
- }
3366
- }
3367
- function quickSelectStroke(pixels, stroke, options = {}) {
3368
- if (!stroke.length) throw new Error("A stroke needs at least one point.");
3369
- const work = workImage(pixels);
3370
- const { width: w, height: h, scale } = work;
3371
- const r = Math.max(0.5, (options.size ?? 30) / 2 * scale);
3372
- const pts = stroke.map((p) => ({ x: p.x * scale, y: p.y * scale }));
3373
- const seeds = strokePixels(pts, r, w, h);
3374
- let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
3375
- for (const p of pts) {
3376
- x0 = Math.min(x0, p.x);
3377
- y0 = Math.min(y0, p.y);
3378
- x1 = Math.max(x1, p.x);
3379
- y1 = Math.max(y1, p.y);
3380
- }
3381
- const margin = Math.max(24, 4 * r);
3382
- const rx = Math.max(0, Math.floor(x0 - margin));
3383
- const ry = Math.max(0, Math.floor(y0 - margin));
3384
- const roi = { x: rx, y: ry, width: Math.min(w, Math.ceil(x1 + margin)) - rx, height: Math.min(h, Math.ceil(y1 + margin)) - ry };
3385
- let grown = null;
3386
- const sel = options.grow ? options.selection : null;
3387
- if (sel?.bounds) {
3388
- grown = new Uint8Array(w * h);
3389
- const b = sel.bounds;
3390
- for (let y = Math.max(0, Math.floor(b.y * scale)); y < Math.min(h, Math.ceil((b.y + b.height) * scale)); y++) {
3391
- for (let x = Math.max(0, Math.floor(b.x * scale)); x < Math.min(w, Math.ceil((b.x + b.width) * scale)); x++) {
3392
- 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;
3393
- }
3394
- }
3395
- }
3396
- const { roi: done, label } = segmentStroke(work, seeds, grown, roi);
3397
- return upscaleRegion(work, pixels, done, label, options.enhanceEdge ?? false);
3398
- }
3399
-
3400
- // src/image-editor/selection/segment/grabcut.ts
3401
- var K = 5;
3402
- var ColorGmm = class {
3403
- logWeight = new Float64Array(K);
3404
- mean = new Float64Array(K * 3);
3405
- /** Inverse covariances, row-major 3 × 3. */
3406
- inv = new Float64Array(K * 9);
3407
- /** ln(weight) − ½ ln((2π)³ |Σ|) per component; −∞ for an empty one. */
3408
- logNorm = new Float64Array(K).fill(-Infinity);
3409
- /** Fits the components to the pixels `ids` of `rgb`, each in component `comp[id]`. */
3410
- fit(rgb, ids, comp) {
3411
- const n = new Float64Array(K);
3412
- const sum = new Float64Array(K * 3);
3413
- const prod = new Float64Array(K * 9);
3414
- for (let t = 0; t < ids.length; t++) {
3415
- const i = ids[t];
3416
- const k = comp[i];
3417
- const c = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
3418
- n[k]++;
3419
- for (let a = 0; a < 3; a++) {
3420
- sum[k * 3 + a] += c[a];
3421
- for (let b = 0; b < 3; b++) prod[k * 9 + a * 3 + b] += c[a] * c[b];
3422
- }
3423
- }
3424
- const total = ids.length || 1;
3425
- for (let k = 0; k < K; k++) {
3426
- if (!n[k]) {
3427
- this.logNorm[k] = -Infinity;
3428
- continue;
3429
- }
3430
- const m = [sum[k * 3] / n[k], sum[k * 3 + 1] / n[k], sum[k * 3 + 2] / n[k]];
3431
- const cov = new Float64Array(9);
3432
- for (let a = 0; a < 3; a++) {
3433
- for (let b = 0; b < 3; b++) cov[a * 3 + b] = prod[k * 9 + a * 3 + b] / n[k] - m[a] * m[b];
3434
- cov[a * 4] += 4;
3435
- }
3436
- 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]);
3437
- const d = Math.max(det, 1e-6);
3438
- const inv = [
3439
- (cov[4] * cov[8] - cov[5] * cov[7]) / d,
3440
- (cov[2] * cov[7] - cov[1] * cov[8]) / d,
3441
- (cov[1] * cov[5] - cov[2] * cov[4]) / d,
3442
- (cov[5] * cov[6] - cov[3] * cov[8]) / d,
3443
- (cov[0] * cov[8] - cov[2] * cov[6]) / d,
3444
- (cov[2] * cov[3] - cov[0] * cov[5]) / d,
3445
- (cov[3] * cov[7] - cov[4] * cov[6]) / d,
3446
- (cov[1] * cov[6] - cov[0] * cov[7]) / d,
3447
- (cov[0] * cov[4] - cov[1] * cov[3]) / d
3448
- ];
3449
- this.mean.set(m, k * 3);
3450
- this.inv.set(inv, k * 9);
3451
- this.logWeight[k] = Math.log(n[k] / total);
3452
- this.logNorm[k] = this.logWeight[k] - 0.5 * Math.log(Math.pow(2 * Math.PI, 3) * d);
3453
- }
3454
- }
3455
- /** ln p(colour | component k), up to the shared constant. */
3456
- componentLog(k, r, g, b) {
3457
- const ln = this.logNorm[k];
3458
- if (ln === -Infinity) return -Infinity;
3459
- const dr = r - this.mean[k * 3];
3460
- const dg = g - this.mean[k * 3 + 1];
3461
- const db = b - this.mean[k * 3 + 2];
3462
- const v = this.inv;
3463
- const o = k * 9;
3464
- 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);
3465
- return ln - 0.5 * q;
3466
- }
3467
- /** −ln p(colour) under the mixture (large for colours the model hasn't seen). */
3468
- cost(r, g, b) {
3469
- let best = -Infinity;
3470
- const logs = new Array(K);
3471
- for (let k = 0; k < K; k++) {
3472
- logs[k] = this.componentLog(k, r, g, b);
3473
- if (logs[k] > best) best = logs[k];
3474
- }
3475
- if (best === -Infinity) return 50;
3476
- let s = 0;
3477
- for (let k = 0; k < K; k++) if (logs[k] > -Infinity) s += Math.exp(logs[k] - best);
3478
- return Math.min(50, -(best + Math.log(s)));
3479
- }
3480
- /** The component most likely to have produced the colour. */
3481
- nearest(r, g, b) {
3482
- let best = 0;
3483
- let bestLog = -Infinity;
3484
- for (let k = 0; k < K; k++) {
3485
- const l = this.componentLog(k, r, g, b);
3486
- if (l > bestLog) {
3487
- bestLog = l;
3488
- best = k;
3489
- }
3490
- }
3491
- return best;
3492
- }
3493
- };
3494
- function initialComponents(rgb, ids, comp) {
3495
- if (!ids.length) return;
3496
- const step = Math.max(1, Math.floor(ids.length / 6e3));
3497
- const sample = [];
3498
- for (let t = 0; t < ids.length; t += step) sample.push(ids[t]);
3499
- const centres = [];
3500
- const dist2 = (i, c) => (rgb[i * 3] - c[0]) ** 2 + (rgb[i * 3 + 1] - c[1]) ** 2 + (rgb[i * 3 + 2] - c[2]) ** 2;
3501
- centres.push([rgb[sample[0] * 3], rgb[sample[0] * 3 + 1], rgb[sample[0] * 3 + 2]]);
3502
- const near = sample.map((i) => dist2(i, centres[0]));
3503
- while (centres.length < K) {
3504
- let far = 0;
3505
- for (let t = 1; t < sample.length; t++) if (near[t] > near[far]) far = t;
3506
- const i = sample[far];
3507
- const c = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
3508
- centres.push(c);
3509
- for (let t = 0; t < sample.length; t++) near[t] = Math.min(near[t], dist2(sample[t], c));
3510
- }
3511
- const closest = (i) => {
3512
- let best = 0;
3513
- for (let k = 1; k < K; k++) if (dist2(i, centres[k]) < dist2(i, centres[best])) best = k;
3514
- return best;
3515
- };
3516
- for (let round6 = 0; round6 < 6; round6++) {
3517
- const sum = Array.from({ length: K }, () => [0, 0, 0, 0]);
3518
- for (const i of sample) {
3519
- const s = sum[closest(i)];
3520
- s[0] += rgb[i * 3];
3521
- s[1] += rgb[i * 3 + 1];
3522
- s[2] += rgb[i * 3 + 2];
3523
- s[3]++;
3524
- }
3525
- 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]];
3526
- }
3527
- for (const i of ids) comp[i] = closest(i);
3528
- }
3529
- function grabCutLabels(work, setup, iterations) {
3530
- const { width: w, rgb } = work;
3531
- const { roi, hardBg, hardFg } = setup;
3532
- const { x: rx, y: ry, width: rw, height: rh } = roi;
3533
- const n = rw * rh;
3534
- const label = setup.initial.slice();
3535
- const global = new Int32Array(n);
3536
- for (let y = 0; y < rh; y++) for (let x = 0; x < rw; x++) global[y * rw + x] = (ry + y) * w + rx + x;
3537
- const comp = new Uint8Array(w * work.height);
3538
- const fg = new ColorGmm();
3539
- const bg = new ColorGmm();
3540
- const { right, down } = smoothnessWeights(work, roi);
3541
- const HARD = 1e9;
3542
- for (let iter = 0; iter < iterations; iter++) {
3543
- const fgIds = [];
3544
- const bgIds = [];
3545
- for (let p = 0; p < n; p++) (label[p] ? fgIds : bgIds).push(global[p]);
3546
- if (!fgIds.length) return label;
3547
- if (iter === 0) {
3548
- initialComponents(rgb, fgIds, comp);
3549
- initialComponents(rgb, bgIds, comp);
3550
- } else {
3551
- for (const i of fgIds) comp[i] = fg.nearest(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
3552
- for (const i of bgIds) comp[i] = bg.nearest(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
3553
- }
3554
- fg.fit(rgb, fgIds, comp);
3555
- bg.fit(rgb, bgIds, comp);
3556
- const graph = new GridGraph(rw, rh);
3557
- for (let p = 0; p < n; p++) {
3558
- if (hardBg[p]) graph.addTerminal(p, 0, HARD);
3559
- else if (hardFg?.[p]) graph.addTerminal(p, HARD, 0);
3560
- else {
3561
- const i = global[p] * 3;
3562
- 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));
3563
- }
3564
- if (p % rw + 1 < rw) graph.setEdge(p, 0, right[p]);
3565
- if (p + rw < n) graph.setEdge(p, 1, down[p]);
3566
- }
3567
- graph.maxflow();
3568
- let changed = 0;
3569
- for (let p = 0; p < n; p++) {
3570
- const v = graph.inSource(p) ? 1 : 0;
3571
- if (v !== label[p]) changed++;
3572
- label[p] = v;
3573
- }
3574
- if (changed <= n * 1e-3) break;
3575
- }
3576
- return label;
3577
- }
3578
- function keepMainPieces(label, rw, rh, seed) {
3579
- const n = rw * rh;
3580
- const piece = new Int32Array(n).fill(-1);
3581
- const sizes = [];
3582
- const stack = [];
3583
- for (let s = 0; s < n; s++) {
3584
- if (!label[s] || piece[s] >= 0) continue;
3585
- const id = sizes.length;
3586
- let size = 0;
3587
- piece[s] = id;
3588
- stack.push(s);
3589
- while (stack.length) {
3590
- const p = stack.pop();
3591
- size++;
3592
- const x = p % rw;
3593
- for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
3594
- if (q >= 0 && label[q] && piece[q] < 0) {
3595
- piece[q] = id;
3596
- stack.push(q);
3597
- }
3598
- }
3599
- }
3600
- sizes.push(size);
3601
- }
3602
- if (!sizes.length) return;
3603
- const largest = Math.max(...sizes);
3604
- const keep = (id) => seed !== null && piece[seed] >= 0 ? id === piece[seed] : sizes[id] * 5 >= largest;
3605
- for (let p = 0; p < n; p++) if (label[p] && !keep(piece[p])) label[p] = 0;
3606
- }
3607
- function selectObjectIn(pixels, target, options = {}) {
3608
- const work = workImage(pixels);
3609
- const { width: w, height: h, scale } = work;
3610
- const iterations = Math.max(1, Math.round(options.iterations ?? 5));
3611
- const enhance = options.enhanceEdge ?? false;
3612
- if ("rect" in target) {
3613
- const r = target.rect;
3614
- const bx0 = Math.max(0, Math.floor(r.x * scale));
3615
- const by0 = Math.max(0, Math.floor(r.y * scale));
3616
- const bx1 = Math.min(w, Math.ceil((r.x + r.width) * scale));
3617
- const by1 = Math.min(h, Math.ceil((r.y + r.height) * scale));
3618
- if (bx1 - bx0 < 2 || by1 - by0 < 2) return createSelectionMask(pixels.width, pixels.height);
3619
- const m = Math.max(8, Math.round(0.25 * Math.max(bx1 - bx0, by1 - by0)));
3620
- const rx = Math.max(0, bx0 - m);
3621
- const ry = Math.max(0, by0 - m);
3622
- const roi = { x: rx, y: ry, width: Math.min(w, bx1 + m) - rx, height: Math.min(h, by1 + m) - ry };
3623
- const n = roi.width * roi.height;
3624
- const initial = new Uint8Array(n);
3625
- const hardBg = new Uint8Array(n);
3626
- for (let y = 0; y < roi.height; y++) {
3627
- for (let x = 0; x < roi.width; x++) {
3628
- const gx = rx + x;
3629
- const gy = ry + y;
3630
- const inside = gx >= bx0 && gx < bx1 && gy >= by0 && gy < by1;
3631
- initial[y * roi.width + x] = inside ? 1 : 0;
3632
- hardBg[y * roi.width + x] = inside ? 0 : 1;
3633
- }
3634
- }
3635
- const label = grabCutLabels(work, { roi, initial, hardBg, hardFg: null }, iterations);
3636
- keepMainPieces(label, roi.width, roi.height, null);
3637
- return upscaleRegion(work, pixels, roi, label, enhance);
3638
- }
3639
- const px2 = Math.min(w - 1, Math.max(0, Math.floor(target.point.x * scale)));
3640
- const py = Math.min(h - 1, Math.max(0, Math.floor(target.point.y * scale)));
3641
- let half = Math.max(24, Math.round(0.3 * Math.max(w, h)));
3642
- for (; ; ) {
3643
- const rx = Math.max(0, px2 - half);
3644
- const ry = Math.max(0, py - half);
3645
- const roi = { x: rx, y: ry, width: Math.min(w, px2 + half + 1) - rx, height: Math.min(h, py + half + 1) - ry };
3646
- const whole = roi.width === w && roi.height === h;
3647
- const band = Math.max(4, Math.round(0.15 * half));
3648
- const n = roi.width * roi.height;
3649
- const initial = new Uint8Array(n);
3650
- const hardBg = new Uint8Array(n);
3651
- const hardFg = new Uint8Array(n);
3652
- 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;
3653
- for (let y = 0; y < roi.height; y++) {
3654
- for (let x = 0; x < roi.width; x++) {
3655
- const p = y * roi.width + x;
3656
- if (inBand(x, y)) hardBg[p] = 1;
3657
- else initial[p] = 1;
3658
- if (Math.hypot(rx + x - px2, ry + y - py) <= 1.5) hardFg[p] = 1;
3659
- }
3660
- }
3661
- const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, iterations);
3662
- keepMainPieces(label, roi.width, roi.height, (py - ry) * roi.width + (px2 - rx));
3663
- let touches = false;
3664
- for (let y = 0; y < roi.height && !touches; y++) {
3665
- for (let x = 0; x < roi.width; x++) {
3666
- if (label[y * roi.width + x] && (inBand(x - 1, y) || inBand(x + 1, y) || inBand(x, y - 1) || inBand(x, y + 1))) {
3667
- touches = true;
3668
- break;
3669
- }
3670
- }
3671
- }
3672
- if (!touches || whole) return upscaleRegion(work, pixels, roi, label, enhance);
3673
- half = Math.round(half * 1.6);
3674
- }
3675
- }
3676
-
3677
- // src/image-editor/selection/segment/subject.ts
3678
- function toLab(r, g, b) {
3679
- const lin = (v) => {
3680
- v /= 255;
3681
- return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
3682
- };
3683
- const R = lin(r);
3684
- const G = lin(g);
3685
- const B = lin(b);
3686
- const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
3687
- const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
3688
- const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
3689
- const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
3690
- return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
3691
- }
3692
- function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
3693
- const { width: w, height: h, rgb } = work;
3694
- const n = w * h;
3695
- const lab2 = new Float32Array(n * 3);
3696
- for (let i = 0; i < n; i++) lab2.set(toLab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]), i * 3);
3697
- const S = Math.max(2, Math.round(Math.sqrt(n / target)));
3698
- const seeds = [];
3699
- for (let y = Math.floor(S / 2); y < h; y += S) {
3700
- for (let x = Math.floor(S / 2); x < w; x += S) {
3701
- const i = y * w + x;
3702
- seeds.push([lab2[i * 3], lab2[i * 3 + 1], lab2[i * 3 + 2], x, y]);
3703
- }
3704
- }
3705
- const k = seeds.length;
3706
- const label = new Int32Array(n).fill(-1);
3707
- const best = new Float64Array(n);
3708
- const m2 = (compactness / S) ** 2;
3709
- for (let round6 = 0; round6 < rounds; round6++) {
3710
- best.fill(Infinity);
3711
- for (let c = 0; c < k; c++) {
3712
- const [L, A, B, cx, cy] = seeds[c];
3713
- const x0 = Math.max(0, Math.floor(cx - S));
3714
- const x1 = Math.min(w - 1, Math.ceil(cx + S));
3715
- const y0 = Math.max(0, Math.floor(cy - S));
3716
- const y1 = Math.min(h - 1, Math.ceil(cy + S));
3717
- for (let y = y0; y <= y1; y++) {
3718
- for (let x = x0; x <= x1; x++) {
3719
- const i = y * w + x;
3720
- const d = (lab2[i * 3] - L) ** 2 + (lab2[i * 3 + 1] - A) ** 2 + (lab2[i * 3 + 2] - B) ** 2 + m2 * ((x - cx) ** 2 + (y - cy) ** 2);
3721
- if (d < best[i]) {
3722
- best[i] = d;
3723
- label[i] = c;
3724
- }
3725
- }
3726
- }
3727
- }
3728
- const sum = new Float64Array(k * 6);
3729
- for (let i = 0; i < n; i++) {
3730
- const c = label[i];
3731
- sum[c * 6] += lab2[i * 3];
3732
- sum[c * 6 + 1] += lab2[i * 3 + 1];
3733
- sum[c * 6 + 2] += lab2[i * 3 + 2];
3734
- sum[c * 6 + 3] += i % w;
3735
- sum[c * 6 + 4] += Math.floor(i / w);
3736
- sum[c * 6 + 5]++;
3737
- }
3738
- for (let c = 0; c < k; c++) {
3739
- const s = sum[c * 6 + 5];
3740
- if (s) seeds[c] = [sum[c * 6] / s, sum[c * 6 + 1] / s, sum[c * 6 + 2] / s, sum[c * 6 + 3] / s, sum[c * 6 + 4] / s];
3741
- }
3742
- }
3743
- const remap = new Int32Array(k).fill(-1);
3744
- let count = 0;
3745
- for (let i = 0; i < n; i++) if (remap[label[i]] < 0) remap[label[i]] = count++;
3746
- const meanLab = new Float64Array(count * 3);
3747
- const centre = new Float64Array(count * 2);
3748
- const size = new Int32Array(count);
3749
- for (let i = 0; i < n; i++) {
3750
- const c = label[i] = remap[label[i]];
3751
- meanLab[c * 3] += lab2[i * 3];
3752
- meanLab[c * 3 + 1] += lab2[i * 3 + 1];
3753
- meanLab[c * 3 + 2] += lab2[i * 3 + 2];
3754
- centre[c * 2] += i % w;
3755
- centre[c * 2 + 1] += Math.floor(i / w);
3756
- size[c]++;
3757
- }
3758
- for (let c = 0; c < count; c++) {
3759
- for (let j = 0; j < 3; j++) meanLab[c * 3 + j] /= size[c];
3760
- centre[c * 2] /= size[c];
3761
- centre[c * 2 + 1] /= size[c];
3762
- }
3763
- return { label, count, lab: meanLab, centre, size };
3764
- }
3765
- function superpixelSaliency(sp, width, height) {
3766
- const { count, lab: lab2, centre, size, label } = sp;
3767
- 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]);
3768
- const onBorder = new Uint8Array(count);
3769
- for (let x = 0; x < width; x++) {
3770
- onBorder[label[x]] = 1;
3771
- onBorder[label[(height - 1) * width + x]] = 1;
3772
- }
3773
- for (let y = 0; y < height; y++) {
3774
- onBorder[label[y * width]] = 1;
3775
- onBorder[label[y * width + width - 1]] = 1;
3776
- }
3777
- const border = [];
3778
- for (let c = 0; c < count; c++) if (onBorder[c]) border.push(c);
3779
- const diag = Math.hypot(width, height);
3780
- const unlikeBorder = new Float64Array(count);
3781
- const contrast = new Float64Array(count);
3782
- const centreBias = new Float64Array(count);
3783
- for (let i = 0; i < count; i++) {
3784
- const ds = border.map((j) => dist(i, j)).sort((a, b) => a - b);
3785
- unlikeBorder[i] = ds.length ? ds[Math.floor(ds.length * 0.25)] : 0;
3786
- let s = 0;
3787
- let wsum = 0;
3788
- for (let j = 0; j < count; j++) {
3789
- if (j === i) continue;
3790
- const dx = (centre[i * 2] - centre[j * 2]) / diag;
3791
- const dy = (centre[i * 2 + 1] - centre[j * 2 + 1]) / diag;
3792
- const wgt = size[j] * Math.exp(-(dx * dx + dy * dy) / (2 * 0.25 * 0.25));
3793
- s += wgt * dist(i, j);
3794
- wsum += wgt;
3795
- }
3796
- contrast[i] = wsum ? s / wsum : 0;
3797
- const cx = centre[i * 2] / width - 0.5;
3798
- const cy = centre[i * 2 + 1] / height - 0.5;
3799
- centreBias[i] = 0.4 + 0.6 * Math.exp(-(cx * cx + cy * cy) / (2 * 0.3 * 0.3));
3800
- }
3801
- let farthest = 0;
3802
- for (const v of unlikeBorder) farthest = Math.max(farthest, v);
3803
- if (farthest < 6) return new Float64Array(count);
3804
- const norm = (a) => {
3805
- let max = 0;
3806
- for (const v of a) max = Math.max(max, v);
3807
- if (max > 0) for (let i = 0; i < a.length; i++) a[i] /= max;
3808
- };
3809
- norm(unlikeBorder);
3810
- norm(contrast);
3811
- const out = new Float64Array(count);
3812
- for (let i = 0; i < count; i++) out[i] = unlikeBorder[i] * (0.3 + 0.7 * contrast[i]) * centreBias[i];
3813
- norm(out);
3814
- return out;
3815
- }
3816
- function otsuThreshold(values, weights) {
3817
- const BINS2 = 64;
3818
- const hist = new Float64Array(BINS2);
3819
- let total = 0;
3820
- for (let i = 0; i < values.length; i++) {
3821
- hist[Math.min(BINS2 - 1, Math.floor(values[i] * BINS2))] += weights[i];
3822
- total += weights[i];
3823
- }
3824
- let sumAll = 0;
3825
- for (let b = 0; b < BINS2; b++) sumAll += b * hist[b];
3826
- let wB = 0;
3827
- let sumB = 0;
3828
- let best = 0;
3829
- let bestT = 0.5;
3830
- for (let b = 0; b < BINS2 - 1; b++) {
3831
- wB += hist[b];
3832
- if (!wB) continue;
3833
- const wF = total - wB;
3834
- if (!wF) break;
3835
- sumB += b * hist[b];
3836
- const between = wB * wF * (sumB / wB - (sumAll - sumB) / wF) ** 2;
3837
- if (between > best) {
3838
- best = between;
3839
- bestT = (b + 1) / BINS2;
3840
- }
3841
- }
3842
- return bestT;
3843
- }
3844
- function findSubject(pixels, options = {}) {
3845
- const work = workImage(pixels);
3846
- const { width: w, height: h } = work;
3847
- const sp = slicSuperpixels(work);
3848
- const sal = superpixelSaliency(sp, w, h);
3849
- const t = otsuThreshold(sal, sp.size);
3850
- const n = w * h;
3851
- const initial = new Uint8Array(n);
3852
- const hardBg = new Uint8Array(n);
3853
- const hardFg = new Uint8Array(n);
3854
- const strong = t + 0.6 * (1 - t);
3855
- for (let i = 0; i < n; i++) {
3856
- const s = sal[sp.label[i]];
3857
- if (s >= t) initial[i] = 1;
3858
- if (s >= strong) hardFg[i] = 1;
3859
- }
3860
- for (let x = 0; x < w; x++) {
3861
- for (const y of [0, h - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
3862
- }
3863
- for (let y = 0; y < h; y++) {
3864
- for (const x of [0, w - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
3865
- }
3866
- const empty = (reason) => ({ selection: createSelectionMask(pixels.width, pixels.height), reason });
3867
- let guessed = 0;
3868
- for (let i = 0; i < n; i++) guessed += initial[i];
3869
- if (!guessed || guessed > n * 0.95) return empty("Nothing stands out from the background.");
3870
- const roi = { x: 0, y: 0, width: w, height: h };
3871
- const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, 5);
3872
- keepMainPieces(label, w, h, null);
3873
- let kept = 0;
3874
- for (let i = 0; i < n; i++) kept += label[i];
3875
- if (kept < n * 2e-3) return empty("Nothing stands out from the background.");
3876
- if (kept > n * 0.97) return empty("The whole image looks like one subject; select it with select_all instead.");
3877
- return { selection: upscaleRegion(work, pixels, roi, label, options.enhanceEdge ?? false) };
3878
- }
3879
-
3880
- // src/image-editor/selection/refine.ts
3881
- var DEFAULT_REFINE_EDGE_PARAMS = {
3882
- radius: 0,
3883
- smartRadius: false,
3884
- smooth: 0,
3885
- feather: 0,
3886
- contrast: 0,
3887
- shiftEdge: 0
3888
- };
3889
- var GUIDED_FILTER_EPS = 1e-4;
3890
- var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
3891
- function normalizeRefineEdgeParams(p = {}) {
3892
- return {
3893
- radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
3894
- smartRadius: p.smartRadius === true,
3895
- smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
3896
- feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
3897
- contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
3898
- shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
3899
- };
3900
- }
3901
- var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
3902
- var smoothSigma = (p) => p.smooth / 100 * 4;
3903
- var featherSigma = (p) => p.feather / 2;
3904
- function shiftAmount(p) {
3905
- const px2 = p.shiftEdge / 100 * Math.max(p.radius, 4);
3906
- return Number.isFinite(px2) ? Math.min(64, Math.max(-64, px2)) : 0;
3907
- }
3908
- function refineEdgeHalo(params = {}) {
3909
- const p = normalizeRefineEdgeParams(params);
3910
- const g = gfRadius(p);
3911
- const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
3912
- return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
3913
- }
3914
- function boxMean(src, w, h, r) {
3915
- const tmp = new Float32Array(w * h);
3916
- const pre = new Float64Array(Math.max(w, h) + 1);
3917
- for (let y = 0; y < h; y++) {
3918
- const row = y * w;
3919
- for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
3920
- for (let x = 0; x < w; x++) {
3921
- const lo = Math.max(0, x - r);
3922
- const hi = Math.min(w, x + r + 1);
3923
- tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
3924
- }
3925
- }
3926
- const out = new Float32Array(w * h);
3927
- for (let x = 0; x < w; x++) {
3928
- for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
3929
- for (let y = 0; y < h; y++) {
3930
- const lo = Math.max(0, y - r);
3931
- const hi = Math.min(h, y + r + 1);
3932
- out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
3933
- }
3934
- }
3935
- return out;
3936
- }
3937
- function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
3938
- const n = w * h;
3939
- const [c0, c1, c2] = guide;
3940
- const prod = (a, b2) => {
3941
- const o = new Float32Array(n);
3942
- for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
3943
- return o;
3944
- };
3945
- const m0 = boxMean(c0, w, h, r);
3946
- const m1 = boxMean(c1, w, h, r);
3947
- const m2 = boxMean(c2, w, h, r);
3948
- const mp = boxMean(p, w, h, r);
3949
- const mip0 = boxMean(prod(c0, p), w, h, r);
3950
- const mip1 = boxMean(prod(c1, p), w, h, r);
3951
- const mip2 = boxMean(prod(c2, p), w, h, r);
3952
- const s00 = boxMean(prod(c0, c0), w, h, r);
3953
- const s01 = boxMean(prod(c0, c1), w, h, r);
3954
- const s02 = boxMean(prod(c0, c2), w, h, r);
3955
- const s11 = boxMean(prod(c1, c1), w, h, r);
3956
- const s12 = boxMean(prod(c1, c2), w, h, r);
3957
- const s22 = boxMean(prod(c2, c2), w, h, r);
3958
- const a0 = new Float32Array(n);
3959
- const a1 = new Float32Array(n);
3960
- const a2 = new Float32Array(n);
3961
- const b = new Float32Array(n);
3962
- for (let i = 0; i < n; i++) {
3963
- const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
3964
- const rr = s00[i] - u0 * u0 + eps;
3965
- const rg = s01[i] - u0 * u1;
3966
- const rb = s02[i] - u0 * u2;
3967
- const gg = s11[i] - u1 * u1 + eps;
3968
- const gb = s12[i] - u1 * u2;
3969
- const bb = s22[i] - u2 * u2 + eps;
3970
- const v0 = mip0[i] - u0 * up;
3971
- const v1 = mip1[i] - u1 * up;
3972
- const v2 = mip2[i] - u2 * up;
3973
- const i00 = gg * bb - gb * gb;
3974
- const i01 = gb * rb - rg * bb;
3975
- const i02 = rg * gb - gg * rb;
3976
- const i11 = rr * bb - rb * rb;
3977
- const i12 = rb * rg - rr * gb;
3978
- const i22 = rr * gg - rg * rg;
3979
- const det = rr * i00 + rg * i01 + rb * i02;
3980
- let x0 = 0, x1 = 0, x2 = 0;
3981
- if (Math.abs(det) >= 1e-30) {
3982
- x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
3983
- x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
3984
- x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
3985
- }
3986
- a0[i] = x0;
3987
- a1[i] = x1;
3988
- a2[i] = x2;
3989
- b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
3990
- }
3991
- const ma0 = boxMean(a0, w, h, r);
3992
- const ma1 = boxMean(a1, w, h, r);
3993
- const ma2 = boxMean(a2, w, h, r);
3994
- const mb = boxMean(b, w, h, r);
3995
- const out = new Float32Array(n);
3996
- for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
3997
- return out;
3998
- }
3999
- function edgeWidth(guide, w, h, r) {
4000
- const n = w * h;
4001
- const y = new Float32Array(n);
4002
- for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
4003
- const g = new Float32Array(n);
4004
- for (let yy = 0; yy < h; yy++) {
4005
- for (let xx = 0; xx < w; xx++) {
4006
- const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
4007
- const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
4008
- g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
4009
- }
4010
- }
4011
- const gm = greyMorphology(g, w, h, r, true);
4012
- const hi = greyMorphology(y, w, h, r, true);
4013
- const lo = greyMorphology(y, w, h, r, false);
4014
- const out = new Float32Array(n);
4015
- for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
4016
- return out;
4017
- }
4018
- function refineCoverage(guide, m, w, h, params = {}, band = null) {
4019
- const p = normalizeRefineEdgeParams(params);
4020
- let a = Float32Array.from(m);
4021
- if ((p.radius > 0 || band) && 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 || band) {
4037
- const dist = distanceTransform(boundary, w, h);
4038
- const r = band ? Math.max(4, gfRadius(p)) : 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 edge = p.radius > 0 ? Math.min(1, Math.max(0, reff + 1 - dist[i])) : 0;
4044
- const wgt = band ? Math.max(edge, Math.min(1, Math.max(0, band[i]))) : edge;
4045
- if (wgt > 0) {
4046
- const qi = Math.min(1, Math.max(0, q[i]));
4047
- a[i] = m[i] + (qi - m[i]) * wgt;
4048
- }
4049
- }
4050
- }
4051
- }
4052
- const ss = smoothSigma(p);
4053
- if (ss >= 0.3) {
4054
- a = gaussianBlur(a, w, h, ss, "clamp");
4055
- const gain = 1 + ss;
4056
- for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
4057
- }
4058
- a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
4059
- if (p.contrast > 0) {
4060
- const gain = 1 / (1 - 0.99 * p.contrast / 100);
4061
- for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
4062
- }
4063
- const amount = shiftAmount(p);
4064
- if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
4065
- return a;
4066
- }
4067
- function guidePlanes(pixels, win) {
4068
- const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
4069
- const n = box.width * box.height;
4070
- const r = new Float32Array(n);
4071
- const g = new Float32Array(n);
4072
- const b = new Float32Array(n);
4073
- const d = pixels.data;
4074
- for (let y = 0; y < box.height; y++) {
4075
- for (let x = 0; x < box.width; x++) {
4076
- const i = y * box.width + x;
4077
- const p = ((box.y + y) * pixels.width + box.x + x) * 4;
4078
- r[i] = d[p] / 255;
4079
- g[i] = d[p + 1] / 255;
4080
- b[i] = d[p + 2] / 255;
4081
- }
4082
- }
4083
- return [r, g, b];
4084
- }
4085
- function refineSelection(pixels, mask, params = {}, refineBrush = null) {
4086
- const p = normalizeRefineEdgeParams(params);
4087
- if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
4088
- throw new Error("Refine Edge needs pixels the size of the selection");
4089
- }
4090
- if (!mask.bounds) return mask;
4091
- const brush = refineBrush?.bounds ? refineBrush : null;
4092
- const halo = refineEdgeHalo(p) + (brush ? 2 * Math.max(4, gfRadius(p)) : 0);
4093
- const b = brush ? {
4094
- x: Math.min(mask.bounds.x, brush.bounds.x),
4095
- y: Math.min(mask.bounds.y, brush.bounds.y),
4096
- r: Math.max(mask.bounds.x + mask.bounds.width, brush.bounds.x + brush.bounds.width),
4097
- btm: Math.max(mask.bounds.y + mask.bounds.height, brush.bounds.y + brush.bounds.height)
4098
- } : { x: mask.bounds.x, y: mask.bounds.y, r: mask.bounds.x + mask.bounds.width, btm: mask.bounds.y + mask.bounds.height };
4099
- const x0 = Math.max(0, b.x - halo);
4100
- const y0 = Math.max(0, b.y - halo);
4101
- const x1 = Math.min(mask.width, b.r + halo);
4102
- const y1 = Math.min(mask.height, b.btm + halo);
4103
- const ww = x1 - x0;
4104
- const wh = y1 - y0;
4105
- const m = new Float32Array(ww * wh);
4106
- for (let y = 0; y < wh; y++) {
4107
- const src = (y0 + y) * mask.width + x0;
4108
- for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
4109
- }
4110
- const guide = pixels && (p.radius > 0 || brush) ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
4111
- let band = null;
4112
- if (brush) {
4113
- band = new Float32Array(ww * wh);
4114
- const bd = brush.data;
4115
- for (let y = 0; y < wh; y++) {
4116
- const src = (y0 + y) * mask.width + x0;
4117
- for (let x = 0; x < ww; x++) band[y * ww + x] = bd[src + x] / 255;
4118
- }
4119
- }
4120
- const refined = refineCoverage(guide, m, ww, wh, p, band);
4121
- const out = new Uint8Array(mask.width * mask.height);
4122
- for (let y = 0; y < wh; y++) {
4123
- const dst = (y0 + y) * mask.width + x0;
4124
- for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
4125
- }
4126
- return createSelectionMask(mask.width, mask.height, out);
4127
- }
4128
- function decontaminateColors(rgba, coverage, w, h, amount, radius = 8) {
4129
- const out = Uint8ClampedArray.from(rgba);
4130
- const n = w * h;
4131
- const k = Math.min(1, Math.max(0, amount));
4132
- if (k === 0) return out;
4133
- const r = Math.max(2, Math.round(radius));
4134
- const fgW = new Float32Array(n);
4135
- const bgW = new Float32Array(n);
4136
- for (let i = 0; i < n; i++) {
4137
- fgW[i] = coverage[i] >= 250 ? 1 : 0;
4138
- bgW[i] = coverage[i] <= 5 ? 1 : 0;
4139
- }
4140
- const meanOf = (weights, radiusPx) => {
4141
- const wm = boxMean(weights, w, h, radiusPx);
4142
- const planes = [0, 1, 2].map((c) => {
4143
- const v = new Float32Array(n);
4144
- for (let i = 0; i < n; i++) v[i] = rgba[i * 4 + c] * weights[i];
4145
- const m = boxMean(v, w, h, radiusPx);
4146
- for (let i = 0; i < n; i++) m[i] = wm[i] > 1e-6 ? m[i] / wm[i] : NaN;
4147
- return m;
4148
- });
4149
- return { weight: wm, planes };
4150
- };
4151
- const fg = meanOf(fgW, r);
4152
- const fgWide = meanOf(fgW, r * 3);
4153
- const bg = meanOf(bgW, r);
4154
- for (let i = 0; i < n; i++) {
4155
- const c = coverage[i];
4156
- if (c <= 5 || c >= 250) continue;
4157
- const a = c / 255;
4158
- for (let ch = 0; ch < 3; ch++) {
4159
- const C = rgba[i * 4 + ch];
4160
- const B = bg.planes[ch][i];
4161
- const Fm = Number.isNaN(fg.planes[ch][i]) ? fgWide.planes[ch][i] : fg.planes[ch][i];
4162
- let F;
4163
- if (a >= 0.5 && !Number.isNaN(B)) F = (C - (1 - a) * B) / a;
4164
- else if (!Number.isNaN(Fm)) F = Fm;
4165
- else F = C;
4166
- out[i * 4 + ch] = Math.round(C + k * (Math.min(255, Math.max(0, F)) - C));
4167
- }
4168
- }
4169
- return out;
4170
- }
4171
-
4172
- // src/image-editor/selection/segment/guided.ts
4173
- function workPlanes(work) {
4174
- const n = work.width * work.height;
4175
- const planes = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];
4176
- for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] = work.rgb[i * 3 + c] / 255;
4177
- return planes;
4178
- }
4179
- function workLuma(work) {
4180
- const n = work.width * work.height;
4181
- const out = new Float32Array(n);
4182
- const { rgb } = work;
4183
- 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;
4184
- return out;
4185
- }
4186
- function softMaskToSelection(work, mask) {
4187
- const { sourceWidth: W, sourceHeight: H, width: w, height: h, scale } = work;
4188
- const out = new Uint8Array(W * H);
4189
- const col = new Int32Array(W);
4190
- const fx = new Float32Array(W);
4191
- for (let X = 0; X < W; X++) {
4192
- const u = Math.min(w - 1, Math.max(0, (X + 0.5) * scale - 0.5));
4193
- col[X] = Math.min(w - 2, Math.floor(u));
4194
- fx[X] = u - col[X];
4195
- if (w === 1) {
4196
- col[X] = 0;
4197
- fx[X] = 0;
4198
- }
4199
- }
4200
- for (let Y = 0; Y < H; Y++) {
4201
- const v = Math.min(h - 1, Math.max(0, (Y + 0.5) * scale - 0.5));
4202
- const j = h === 1 ? 0 : Math.min(h - 2, Math.floor(v));
4203
- const fy = h === 1 ? 0 : v - j;
4204
- const r0 = j * w;
4205
- const r1 = h === 1 ? r0 : r0 + w;
4206
- for (let X = 0; X < W; X++) {
4207
- const i = col[X];
4208
- const i1 = w === 1 ? i : i + 1;
4209
- const t = fx[X];
4210
- const m = (mask[r0 + i] * (1 - t) + mask[r0 + i1] * t) * (1 - fy) + (mask[r1 + i] * (1 - t) + mask[r1 + i1] * t) * fy;
4211
- out[Y * W + X] = Math.round(Math.min(1, Math.max(0, m)) * 255);
4212
- }
4213
- }
4214
- return selectionFromRegion(W, H, { x: 0, y: 0, width: W, height: H }, out);
4215
- }
4216
-
4217
- // src/image-editor/selection/segment/focus.ts
4218
- var FOCUS_SIZE = 1024;
4219
- function estimateNoise(luma2, w, h) {
4220
- if (w < 3 || h < 3) return 0;
4221
- let sum = 0;
4222
- for (let y = 1; y < h - 1; y++) {
4223
- for (let x = 1; x < w - 1; x++) {
4224
- const i = y * w + x;
4225
- 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];
4226
- sum += Math.abs(v);
4227
- }
4228
- }
4229
- return Math.sqrt(Math.PI / 2) * sum / (6 * (w - 2) * (h - 2));
4230
- }
4231
- function focusArea(pixels, options = {}) {
4232
- const work = workImage(pixels, FOCUS_SIZE);
4233
- const { width: w, height: h, scale } = work;
4234
- const n = w * h;
4235
- const luma2 = workLuma(work);
4236
- const sigma = estimateNoise(luma2, w, h);
4237
- const noise = Math.min(100, Math.max(0, options.noise ?? 0));
4238
- const floor = 2 * sigma * (1 + noise / 25);
4239
- const detail = new Float32Array(n);
4240
- for (let y = 1; y < h - 1; y++) {
4241
- for (let x = 1; x < w - 1; x++) {
4242
- const i = y * w + x;
4243
- const lap = Math.abs(4 * luma2[i] - luma2[i - 1] - luma2[i + 1] - luma2[i - w] - luma2[i + w]);
4244
- const d = Math.max(0, lap - floor);
4245
- detail[i] = d * d;
4246
- }
4247
- }
4248
- const r = Math.max(2, Math.round(0.012 * Math.max(w, h)));
4249
- const energy = boxMean(detail, w, h, r);
4250
- const sharp = new Float32Array(n);
4251
- for (let i = 0; i < n; i++) sharp[i] = Math.log1p(Math.sqrt(energy[i]) * 400);
4252
- const sorted = Float32Array.from(sharp).sort();
4253
- const top = sorted[Math.floor(n * 0.995)] || 1;
4254
- for (let i = 0; i < n; i++) sharp[i] = Math.min(1, sharp[i] / top);
4255
- const ones = new Float32Array(n).fill(1);
4256
- const otsu = otsuThreshold(sharp, ones);
4257
- const range = Math.min(100, Math.max(0, options.range ?? 50));
4258
- const t = Math.min(0.98, otsu * Math.pow(2, (50 - range) / 25));
4259
- let mask = new Float32Array(n);
4260
- for (let i = 0; i < n; i++) mask[i] = sharp[i] >= t ? 1 : 0;
4261
- const c = 2 * r;
4262
- const grown = boxMean(mask, w, h, c);
4263
- for (let i = 0; i < n; i++) grown[i] = grown[i] > 1e-3 ? 1 : 0;
4264
- const shrunk = boxMean(grown, w, h, c);
4265
- for (let i = 0; i < n; i++) mask[i] = shrunk[i] >= 0.999 ? 1 : 0;
4266
- const pulled = boxMean(mask, w, h, Math.max(1, Math.round(r / 2)));
4267
- for (let i = 0; i < n; i++) mask[i] = pulled[i] >= 0.999 ? 1 : 0;
4268
- const softness = Math.max(0, options.softness ?? 4) * scale;
4269
- if (softness > 0) {
4270
- const fitted = guidedFilterColor(workPlanes(work), mask, w, h, Math.max(1, Math.round(softness * 2)), 1e-3);
4271
- for (let i = 0; i < n; i++) mask[i] = Math.min(1, Math.max(0, fitted[i]));
4272
- }
4273
- return { selection: softMaskToSelection(work, mask), noiseLevel: Math.round(sigma * 255 * 10) / 10 };
4274
- }
4275
-
4276
- // src/image-editor/selection/segment/sky.ts
4277
- var SKY_SIZE = 768;
4278
- function lab(r, g, b) {
4279
- const lin = (v) => {
4280
- v /= 255;
4281
- return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
4282
- };
4283
- const R = lin(r);
4284
- const G = lin(g);
4285
- const B = lin(b);
4286
- const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
4287
- const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
4288
- const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
4289
- const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
4290
- return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
4291
- }
4292
- var sigmoid = (v) => 1 / (1 + Math.exp(-v));
4293
- function findSky(pixels, options = {}) {
4294
- const work = workImage(pixels, SKY_SIZE);
4295
- const { width: w, height: h, rgb, scale } = work;
4296
- const n = w * h;
4297
- const L = new Float32Array(n);
4298
- const A = new Float32Array(n);
4299
- const B = new Float32Array(n);
4300
- for (let i = 0; i < n; i++) {
4301
- const [l, a, b] = lab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
4302
- L[i] = l;
4303
- A[i] = a;
4304
- B[i] = b;
4305
- }
4306
- const luma2 = boxMean(workLuma(work), w, h, 1);
4307
- const grad = new Float32Array(n);
4308
- for (let y = 0; y < h; y++) {
4309
- for (let x = 0; x < w; x++) {
4310
- const i = y * w + x;
4311
- const gx = x + 1 < w ? luma2[i + 1] - luma2[i] : 0;
4312
- const gy = y + 1 < h ? luma2[i + w] - luma2[i] : 0;
4313
- grad[i] = Math.hypot(gx, gy);
4314
- }
4315
- }
4316
- const roughR = Math.max(1, Math.round(6e-3 * Math.max(w, h)));
4317
- const rough = boxMean(grad, w, h, roughR);
4318
- const score = new Float32Array(n);
4319
- for (let i = 0; i < n; i++) {
4320
- const chroma = Math.hypot(A[i], B[i]);
4321
- const blue = sigmoid((-B[i] - 6) / 4) * sigmoid((L[i] - 35) / 6);
4322
- const grey = sigmoid((L[i] - 68) / 5) * sigmoid((18 - chroma) / 4);
4323
- const smooth = Math.exp(-rough[i] / 0.02);
4324
- const y = Math.floor(i / w) / h;
4325
- const high = 1 - 0.35 * y;
4326
- score[i] = Math.max(blue, grey) * smooth * high;
4327
- }
4328
- const threshold = Math.min(100, Math.max(0, options.threshold ?? 50));
4329
- const t = 0.55 - (threshold - 50) * 6e-3;
4330
- const sky = new Uint8Array(n);
4331
- const stack = [];
4332
- const seedRows = Math.max(1, Math.round(h * 0.04));
4333
- for (let y = 0; y < seedRows; y++) {
4334
- for (let x = 0; x < w; x++) {
4335
- const i = y * w + x;
4336
- if (score[i] >= t) {
4337
- sky[i] = 1;
4338
- stack.push(i);
4339
- }
4340
- }
4341
- }
4342
- if (!stack.length) return { selection: createSelectionMask(pixels.width, pixels.height), reason: "No sky was found along the top of the image." };
4343
- const low = t * 0.7;
4344
- while (stack.length) {
4345
- const p = stack.pop();
4346
- const x = p % w;
4347
- for (const q of [x + 1 < w ? p + 1 : -1, x > 0 ? p - 1 : -1, p + w < n ? p + w : -1, p - w]) {
4348
- if (q < 0 || sky[q] || score[q] < low) continue;
4349
- if (Math.hypot(L[q] - L[p], A[q] - A[p], B[q] - B[p]) > 6) continue;
4350
- sky[q] = 1;
4351
- stack.push(q);
4352
- }
4353
- }
4354
- let count = 0;
4355
- const mean = [0, 0, 0];
4356
- for (let i = 0; i < n; i++) {
4357
- if (!sky[i]) continue;
4358
- mean[0] += L[i];
4359
- mean[1] += A[i];
4360
- mean[2] += B[i];
4361
- count++;
4362
- }
4363
- for (let k = 0; k < 3; k++) mean[k] /= count;
4364
- let spread = 0;
4365
- 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;
4366
- const limit = Math.max(8, 2.5 * Math.sqrt(spread / count));
4367
- for (let i = 0; i < n; i++) {
4368
- if (sky[i] || score[i] < t) continue;
4369
- if (Math.hypot(L[i] - mean[0], A[i] - mean[1], B[i] - mean[2]) <= limit) sky[i] = 1;
4370
- }
4371
- let front = [];
4372
- for (let i = 0; i < n; i++) if (sky[i]) front.push(i);
4373
- for (let step = 0; step <= roughR + 1 && front.length; step++) {
4374
- const next = [];
4375
- for (const p of front) {
4376
- const x = p % w;
4377
- for (const q of [x + 1 < w ? p + 1 : -1, x > 0 ? p - 1 : -1, p + w < n ? p + w : -1, p - w]) {
4378
- if (q < 0 || sky[q]) continue;
4379
- if (Math.hypot(L[q] - mean[0], A[q] - mean[1], B[q] - mean[2]) > limit) continue;
4380
- sky[q] = 1;
4381
- next.push(q);
4382
- }
4383
- }
4384
- front = next;
4385
- }
4386
- if (count < n * 5e-3) return { selection: createSelectionMask(pixels.width, pixels.height), reason: "No sky was found along the top of the image." };
4387
- let mask = Float32Array.from(sky);
4388
- const softness = Math.max(0, options.softness ?? 4) * scale;
4389
- if (softness > 0) {
4390
- const fitted = guidedFilterColor(workPlanes(work), mask, w, h, Math.max(1, Math.round(softness * 2)), 1e-3);
4391
- mask = new Float32Array(n);
4392
- for (let i = 0; i < n; i++) mask[i] = Math.min(1, Math.max(0, fitted[i]));
4393
- }
4394
- return { selection: softMaskToSelection(work, mask) };
4395
- }
4396
-
4397
2862
  // src/image-editor/layers/shape-type-ui.ts
4398
2863
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
4399
2864
  kind: "rect",
@@ -4843,123 +3308,6 @@ var TypeToolGesture = class {
4843
3308
  }
4844
3309
  };
4845
3310
 
4846
- // src/image-editor/selection/shapes.ts
4847
- function polygonCoverage(points, width, height, antiAlias = true) {
4848
- const { box, coverage } = polygonCoverageBox(points, width, height, antiAlias);
4849
- if (!box || box.width === width && box.height === height) return box ? coverage : new Float32Array(width * height);
4850
- const m = new Float32Array(width * height);
4851
- 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);
4852
- return m;
4853
- }
4854
- function polygonCoverageBox(points, width, height, antiAlias = true) {
4855
- const none = { box: null, coverage: new Float32Array(0) };
4856
- if (points.length < 3 || width <= 0 || height <= 0) return none;
4857
- let minX = Infinity;
4858
- let minY = Infinity;
4859
- let maxX = -Infinity;
4860
- let maxY = -Infinity;
4861
- for (const p of points) {
4862
- if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
4863
- minX = Math.min(minX, p.x);
4864
- minY = Math.min(minY, p.y);
4865
- maxX = Math.max(maxX, p.x);
4866
- maxY = Math.max(maxY, p.y);
4867
- }
4868
- const bx0 = Math.max(0, Math.floor(minX));
4869
- const by0 = Math.max(0, Math.floor(minY));
4870
- const bx1 = Math.min(width, Math.ceil(maxX));
4871
- const by1 = Math.min(height, Math.ceil(maxY));
4872
- if (!(bx1 > bx0 && by1 > by0)) return none;
4873
- const bw = bx1 - bx0;
4874
- const m = new Float32Array(bw * (by1 - by0));
4875
- const sub2 = antiAlias ? 4 : 1;
4876
- const edges = [];
4877
- for (let i = 0; i < points.length; i++) {
4878
- const a = points[i];
4879
- const b = points[(i + 1) % points.length];
4880
- if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;
4881
- 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 });
4882
- 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 });
4883
- }
4884
- edges.sort((p, q) => p.top - q.top);
4885
- let next = 0;
4886
- let active = [];
4887
- const xs = [];
4888
- for (let py = by0; py < by1; py++) {
4889
- const row = (py - by0) * bw - bx0;
4890
- for (let s = 0; s < sub2; s++) {
4891
- const y = py + (s + 0.5) / sub2;
4892
- while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
4893
- if (active.length) active = active.filter((e) => e.bottom > y);
4894
- if (!active.length) continue;
4895
- xs.length = 0;
4896
- for (const e of active) {
4897
- if (e.top <= y) xs.push(e.ax + (y - e.ay) / (e.by - e.ay) * (e.bx - e.ax));
4898
- }
4899
- xs.sort((a, b) => a - b);
4900
- for (let k = 0; k + 1 < xs.length; k += 2) {
4901
- const x0 = Math.max(0, xs[k]);
4902
- const x1 = Math.min(width, xs[k + 1]);
4903
- if (x1 <= x0) continue;
4904
- const c0 = Math.floor(x0);
4905
- const c1 = Math.min(width, Math.ceil(x1));
4906
- for (let cx = c0; cx < c1; cx++) {
4907
- let cov;
4908
- if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);
4909
- else {
4910
- const c = cx + 0.5;
4911
- cov = c >= x0 && c < x1 ? 1 : 0;
4912
- }
4913
- if (cov > 0) m[row + cx] += cov / sub2;
4914
- }
4915
- }
4916
- }
4917
- }
4918
- for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;
4919
- return { box: { x: bx0, y: by0, width: bw, height: by1 - by0 }, coverage: m };
4920
- }
4921
- function polygonSelection(width, height, points, options = {}) {
4922
- const { box, coverage } = polygonCoverageBox(points, width, height, options.antiAlias ?? true);
4923
- if (!box) return createSelectionMask(width, height);
4924
- const out = new Uint8Array(coverage.length);
4925
- for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
4926
- return selectionFromRegion(width, height, box, out);
4927
- }
4928
- function normalizeRect(rect) {
4929
- const x = rect.width < 0 ? rect.x + rect.width : rect.x;
4930
- const y = rect.height < 0 ? rect.y + rect.height : rect.y;
4931
- return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };
4932
- }
4933
- function rectSelection(width, height, rect, options = {}) {
4934
- const r = normalizeRect(rect);
4935
- const pts = [
4936
- { x: r.x, y: r.y },
4937
- { x: r.x + r.width, y: r.y },
4938
- { x: r.x + r.width, y: r.y + r.height },
4939
- { x: r.x, y: r.y + r.height }
4940
- ];
4941
- return polygonSelection(width, height, pts, options);
4942
- }
4943
- function ellipsePoints(rect) {
4944
- const r = normalizeRect(rect);
4945
- const rx = r.width / 2;
4946
- const ry = r.height / 2;
4947
- const cx = r.x + rx;
4948
- const cy = r.y + ry;
4949
- const rmax = Math.max(rx, ry, 0.5);
4950
- const step = Math.sqrt(0.4 / rmax);
4951
- const n = Math.min(4096, Math.max(16, Math.ceil(2 * Math.PI / step)));
4952
- const pts = [];
4953
- for (let i = 0; i < n; i++) {
4954
- const t = i / n * 2 * Math.PI;
4955
- pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
4956
- }
4957
- return pts;
4958
- }
4959
- function ellipseSelection(width, height, rect, options = {}) {
4960
- return polygonSelection(width, height, ellipsePoints(rect), options);
4961
- }
4962
-
4963
3311
  // src/image-editor/selection/wand.ts
4964
3312
  function checkPixels(p) {
4965
3313
  const n = p.width * p.height;
@@ -5319,78 +3667,6 @@ function segmentsToSelection(width, height, results) {
5319
3667
  return out;
5320
3668
  }
5321
3669
 
5322
- // src/image-editor/selection/refine-edits.ts
5323
- var REFINE_EDIT_KINDS = ["quick-select", "brush", "refine-brush", "lasso", "object"];
5324
- function brushStrokeMask(width, height, points, size, hardness = 1) {
5325
- const r = Math.max(0.5, size / 2);
5326
- const solid = r * Math.min(1, Math.max(0, hardness));
5327
- let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
5328
- for (const p of points) {
5329
- x0 = Math.min(x0, p.x);
5330
- y0 = Math.min(y0, p.y);
5331
- x1 = Math.max(x1, p.x);
5332
- y1 = Math.max(y1, p.y);
5333
- }
5334
- const bx = Math.max(0, Math.floor(x0 - r - 1));
5335
- const by = Math.max(0, Math.floor(y0 - r - 1));
5336
- const bw = Math.min(width, Math.ceil(x1 + r + 1)) - bx;
5337
- const bh = Math.min(height, Math.ceil(y1 + r + 1)) - by;
5338
- if (!(bw > 0 && bh > 0)) return createSelectionMask(width, height);
5339
- const out = new Uint8Array(bw * bh);
5340
- const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
5341
- for (let y = 0; y < bh; y++) {
5342
- for (let x = 0; x < bw; x++) {
5343
- const px2 = bx + x + 0.5;
5344
- const py = by + y + 0.5;
5345
- let d = Infinity;
5346
- for (const [a, b] of segs) {
5347
- const dx = b.x - a.x;
5348
- const dy = b.y - a.y;
5349
- const len2 = dx * dx + dy * dy;
5350
- const t = len2 > 0 ? Math.min(1, Math.max(0, ((px2 - a.x) * dx + (py - a.y) * dy) / len2)) : 0;
5351
- d = Math.min(d, Math.hypot(px2 - a.x - t * dx, py - a.y - t * dy));
5352
- }
5353
- let v = d <= solid ? 1 : solid >= r ? 0 : 1 - (d - solid) / (r - solid);
5354
- v = Math.min(v, Math.min(1, Math.max(0, r + 0.5 - d)));
5355
- out[y * bw + x] = Math.round(Math.max(0, v) * 255);
5356
- }
5357
- }
5358
- return selectionFromRegion(width, height, { x: bx, y: by, width: bw, height: bh }, out);
5359
- }
5360
- function applyRefineEdits(pixels, selection, edits, width, height) {
5361
- let sel = selection?.bounds ? selection : null;
5362
- let band = null;
5363
- for (const e of edits) {
5364
- if (e.kind === "object") {
5365
- if (!pixels) throw new Error("Object Selection needs the image pixels.");
5366
- if (!e.rect === !e.point) throw new Error("An object edit takes rect (a box around the object) or point (on it), not both.");
5367
- const region2 = selectObjectIn(pixels, e.rect ? { rect: e.rect } : { point: e.point });
5368
- sel = combineSelections(sel, region2, e.mode ?? "add");
5369
- continue;
5370
- }
5371
- if (!e.points.length) throw new Error(`A ${e.kind} stroke needs at least one point.`);
5372
- if (e.kind === "lasso") {
5373
- if (e.points.length < 3) throw new Error("A lasso edit needs at least 3 points.");
5374
- sel = combineSelections(sel, polygonSelection(width, height, e.points, {}), e.mode ?? "add");
5375
- continue;
5376
- }
5377
- if (e.kind === "refine-brush") {
5378
- band = combineSelections(band, brushStrokeMask(width, height, e.points, e.size ?? 40), "add");
5379
- continue;
5380
- }
5381
- const mode = e.mode ?? "add";
5382
- let region;
5383
- if (e.kind === "quick-select") {
5384
- if (!pixels) throw new Error("Quick Selection strokes need the image pixels.");
5385
- region = quickSelectStroke(pixels, e.points, { size: e.size, selection: sel, grow: mode === "add" });
5386
- } else {
5387
- region = brushStrokeMask(width, height, e.points, e.size ?? 20, e.hardness ?? 1);
5388
- }
5389
- sel = combineSelections(sel, region, mode);
5390
- }
5391
- return { selection: sel, refineBrush: band };
5392
- }
5393
-
5394
3670
  // src/image-editor/selection/stroke.ts
5395
3671
  function selectionRing(sel, width, location) {
5396
3672
  const half = location === "center" ? width / 2 : width;
@@ -6195,10 +4471,10 @@ function colorModePlanes(doc, mode) {
6195
4471
  }
6196
4472
  const t = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
6197
4473
  return (rgba, n) => {
6198
- const lab2 = new Uint8Array(n * 4);
6199
- t.convert8(rgba, 4, lab2, 4, false);
4474
+ const lab = new Uint8Array(n * 4);
4475
+ t.convert8(rgba, 4, lab, 4, false);
6200
4476
  const planes = [0, 1, 2].map(() => new Uint8Array(n));
6201
- for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] = lab2[i * 4 + c];
4477
+ for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] = lab[i * 4 + c];
6202
4478
  return planes;
6203
4479
  };
6204
4480
  }
@@ -6826,6 +5102,7 @@ var ImageEditorController = class {
6826
5102
  compositor = new LayerCompositor();
6827
5103
  /** The segmentation provider behind `selectSubject` (see `setSegmentationProvider`). */
6828
5104
  segmentation = null;
5105
+ selectionWorker = null;
6829
5106
  /** The effects provider slot (see `setEffectsProvider`). */
6830
5107
  effects = new EffectsSlot();
6831
5108
  /** Renders provider effects: effect smart filter results and effect layers. */
@@ -6836,6 +5113,7 @@ var ImageEditorController = class {
6836
5113
  this.effectsEngine.subscribe(() => this.queueEffectRefresh("incomplete"));
6837
5114
  if (options.effects) this.effects.set(options.effects);
6838
5115
  if (options.segmentation) this.segmentation = options.segmentation;
5116
+ if (options.selectionWorker) this.selectionWorker = new SelectionWorkerClient(options.selectionWorker);
6839
5117
  this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
6840
5118
  if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
6841
5119
  this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
@@ -7025,6 +5303,30 @@ var ImageEditorController = class {
7025
5303
  get segmentationProvider() {
7026
5304
  return this.segmentation;
7027
5305
  }
5306
+ /**
5307
+ * Runs the long selections in a Web Worker made by `factory` (or with `null`, on the page again).
5308
+ * A worker that is cancelled or fails is replaced by a fresh one on the next job.
5309
+ */
5310
+ setSelectionWorker(factory) {
5311
+ this.selectionWorker?.terminate();
5312
+ this.selectionWorker = factory ? new SelectionWorkerClient(factory) : null;
5313
+ }
5314
+ /** Whether the long selections run in a worker. */
5315
+ get hasSelectionWorker() {
5316
+ return this.selectionWorker !== null;
5317
+ }
5318
+ /**
5319
+ * Runs a long selection job on `pixels`: in the worker when there is one, else here (which
5320
+ * blocks the page until it is done). `signal` cancels it (rejecting with an `AbortError`);
5321
+ * `onProgress` hears its stages. The document is not changed.
5322
+ */
5323
+ async runSelectionJob(job, pixels, options = {}) {
5324
+ options.signal?.throwIfAborted();
5325
+ if (this.selectionWorker) return this.selectionWorker.run(job, pixels, options);
5326
+ const result = runSelectionJob(job, pixels, options.onProgress);
5327
+ options.signal?.throwIfAborted();
5328
+ return result;
5329
+ }
7028
5330
  /** Registers (or with `null` removes) the segmentation provider. Not an edit: no undo step. */
7029
5331
  setSegmentationProvider(provider) {
7030
5332
  if (this.segmentation === provider) return;
@@ -10195,11 +8497,22 @@ var ImageEditorController = class {
10195
8497
  const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
10196
8498
  const mode = options.mode ?? "replace";
10197
8499
  let sel = this.state.selection ?? null;
10198
- strokes.forEach((stroke, i) => {
8500
+ for (let i = 0; i < strokes.length; i++) {
10199
8501
  const m = mode === "replace" && i > 0 ? "add" : mode;
10200
- const region = quickSelectStroke(pixels, stroke, { size: options.size, selection: sel, grow: m === "add", enhanceEdge: options.enhanceEdge });
10201
- sel = combineSelections(sel, region, m);
10202
- });
8502
+ const job = {
8503
+ op: "quick",
8504
+ stroke: strokes[i],
8505
+ size: options.size,
8506
+ grow: m === "add",
8507
+ enhanceEdge: options.enhanceEdge,
8508
+ selection: m === "add" && sel?.bounds ? serializeMask(sel) : null
8509
+ };
8510
+ const { mask } = await this.runSelectionJob(job, pixels, {
8511
+ signal: options.signal,
8512
+ onProgress: options.onProgress && ((f, stage) => options.onProgress((i + f) / strokes.length, stage))
8513
+ });
8514
+ sel = combineSelections(sel, mask, m);
8515
+ }
10203
8516
  this.setSelection(sel, "replace");
10204
8517
  return this.state.selection ?? null;
10205
8518
  }
@@ -10268,7 +8581,7 @@ var ImageEditorController = class {
10268
8581
  }
10269
8582
  }
10270
8583
  const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
10271
- const region = selectObjectIn(pixels, target, { enhanceEdge: options.enhanceEdge });
8584
+ const { mask: region } = await this.runSelectionJob({ op: "object", target, enhanceEdge: options.enhanceEdge }, pixels, options);
10272
8585
  this.setSelection(region, options.mode ?? "replace");
10273
8586
  return { selection: this.state.selection ?? null, method: "grabcut" };
10274
8587
  }
@@ -10279,7 +8592,11 @@ var ImageEditorController = class {
10279
8592
  */
10280
8593
  async selectFocusArea(options = {}) {
10281
8594
  const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
10282
- const { selection, noiseLevel } = focusArea(pixels, options);
8595
+ const { mask: selection, noiseLevel = 0 } = await this.runSelectionJob(
8596
+ { op: "focus", options: { range: options.range, noise: options.noise, softness: options.softness } },
8597
+ pixels,
8598
+ options
8599
+ );
10283
8600
  this.setSelection(selection, options.mode ?? "replace");
10284
8601
  return { selection: this.state.selection ?? null, noiseLevel };
10285
8602
  }
@@ -10298,7 +8615,7 @@ var ImageEditorController = class {
10298
8615
  return { selection: this.state.selection ?? null, method: "model" };
10299
8616
  }
10300
8617
  const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
10301
- const { selection, reason } = findSky(pixels, options);
8618
+ const { mask: selection, reason } = await this.runSelectionJob({ op: "sky", options: { threshold: options.threshold, softness: options.softness } }, pixels, options);
10302
8619
  if (!selection.bounds) return { selection: this.state.selection ?? null, method: "classical", note: reason };
10303
8620
  this.setSelection(selection, options.mode ?? "replace");
10304
8621
  return { selection: this.state.selection ?? null, method: "classical" };
@@ -10581,7 +8898,7 @@ var ImageEditorController = class {
10581
8898
  if (!provider) {
10582
8899
  const pixels = await this.requirePixels({});
10583
8900
  options.signal?.throwIfAborted();
10584
- const { selection, reason } = findSubject(pixels, { enhanceEdge: options.enhanceEdge });
8901
+ const { mask: selection, reason } = await this.runSelectionJob({ op: "subject", enhanceEdge: options.enhanceEdge }, pixels, options);
10585
8902
  if (selection.bounds) this.shapeCommand(selection, options);
10586
8903
  const unused = target.trim() ? ` "${target.trim()}" was not used to choose it.` : "";
10587
8904
  return {
@@ -11904,7 +10221,7 @@ var ImageEditorController = class {
11904
10221
  }
11905
10222
  if (src.colorSpace !== "rgb") throw new LayerOperationError("Colours are given as #rrggbb, so the source profile must be RGB.");
11906
10223
  const t = Transform.create(src, dst, { intent: options.intent ?? "relative", bpc: options.bpc ?? true });
11907
- const toLab2 = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
10224
+ const toLab = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
11908
10225
  return colors.map((input) => {
11909
10226
  const m = /^#?([0-9a-f]{6})$/i.exec(input.trim());
11910
10227
  if (!m) throw new LayerOperationError(`Invalid colour "${input}"; use #rrggbb.`);
@@ -11914,9 +10231,9 @@ var ImageEditorController = class {
11914
10231
  t.eval(rgb, out);
11915
10232
  const values = Array.from(out.subarray(0, t.outputs), (v) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4);
11916
10233
  const l = new Float32Array(3);
11917
- toLab2.eval(rgb, l);
11918
- const lab2 = [Math.round(l[0] * 1e4) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];
11919
- const res = { input, values, lab: lab2 };
10234
+ toLab.eval(rgb, l);
10235
+ const lab = [Math.round(l[0] * 1e4) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];
10236
+ const res = { input, values, lab };
11920
10237
  if (dst.colorSpace === "rgb") res.hex = `#${values.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("")}`;
11921
10238
  return res;
11922
10239
  });
@@ -13949,7 +12266,7 @@ function createImageSelectionTools(controller) {
13949
12266
  additionalProperties: false
13950
12267
  },
13951
12268
  annotations: writeAnnotations("Quick Selection"),
13952
- handler: async (args) => {
12269
+ handler: async (args, context) => {
13953
12270
  const strokes = Array.isArray(args.strokes) ? args.strokes.map((s) => Array.isArray(s) ? s.map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) : []) : [];
13954
12271
  if (!strokes.length || strokes.some((s) => !s.length || !s.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)))) {
13955
12272
  throw new Error("strokes needs at least one stroke of { x, y } points in natural image pixels.");
@@ -13958,7 +12275,8 @@ function createImageSelectionTools(controller) {
13958
12275
  size: args.size === void 0 ? void 0 : num2(args.size, 30),
13959
12276
  mode: parseSelectionMode(args.mode),
13960
12277
  sampleAllLayers: args.sampleAllLayers,
13961
- enhanceEdge: args.enhanceEdge
12278
+ enhanceEdge: args.enhanceEdge,
12279
+ signal: context?.signal
13962
12280
  });
13963
12281
  return summary(controller);
13964
12282
  }
@@ -13986,7 +12304,7 @@ function createImageSelectionTools(controller) {
13986
12304
  additionalProperties: false
13987
12305
  },
13988
12306
  annotations: { ...writeAnnotations("Object Selection"), openWorldHint: true },
13989
- handler: async (args) => {
12307
+ handler: async (args, context) => {
13990
12308
  const rect = args.rect;
13991
12309
  const point = args.point;
13992
12310
  if (!rect && !point) throw new Error("Pass rect (a box around the object) or point (a point on it).");
@@ -13995,7 +12313,8 @@ function createImageSelectionTools(controller) {
13995
12313
  mode: parseSelectionMode(args.mode),
13996
12314
  sampleAllLayers: args.sampleAllLayers,
13997
12315
  enhanceEdge: args.enhanceEdge,
13998
- useModel: args.useModel
12316
+ useModel: args.useModel,
12317
+ signal: context?.signal
13999
12318
  };
14000
12319
  const { method } = rect ? await controller.selectObject({ rect: { x: num2(rect.x, NaN), y: num2(rect.y, NaN), width: num2(rect.width, NaN), height: num2(rect.height, NaN) } }, options) : await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
14001
12320
  return { ...summary(controller), method };
@@ -14017,13 +12336,14 @@ function createImageSelectionTools(controller) {
14017
12336
  additionalProperties: false
14018
12337
  },
14019
12338
  annotations: writeAnnotations("Select the focus area", true),
14020
- handler: async (args) => {
12339
+ handler: async (args, context) => {
14021
12340
  const { noiseLevel } = await controller.selectFocusArea({
14022
12341
  range: args.range === void 0 ? void 0 : num2(args.range, 50),
14023
12342
  noise: args.noise === void 0 ? void 0 : num2(args.noise, 0),
14024
12343
  softness: args.softness === void 0 ? void 0 : num2(args.softness, 4),
14025
12344
  mode: parseSelectionMode(args.mode),
14026
- sampleAllLayers: args.sampleAllLayers
12345
+ sampleAllLayers: args.sampleAllLayers,
12346
+ signal: context?.signal
14027
12347
  });
14028
12348
  return { ...summary(controller), noiseLevel };
14029
12349
  }
@@ -14044,13 +12364,14 @@ function createImageSelectionTools(controller) {
14044
12364
  additionalProperties: false
14045
12365
  },
14046
12366
  annotations: { ...writeAnnotations("Select the sky", true), openWorldHint: true },
14047
- handler: async (args) => {
12367
+ handler: async (args, context) => {
14048
12368
  const { note, method } = await controller.selectSky({
14049
12369
  threshold: args.threshold === void 0 ? void 0 : num2(args.threshold, 50),
14050
12370
  softness: args.softness === void 0 ? void 0 : num2(args.softness, 4),
14051
12371
  mode: parseSelectionMode(args.mode),
14052
12372
  sampleAllLayers: args.sampleAllLayers,
14053
- useModel: args.useModel
12373
+ useModel: args.useModel,
12374
+ signal: context?.signal
14054
12375
  });
14055
12376
  return { ...summary(controller), method, ...note ? { note } : {} };
14056
12377
  }
@@ -17062,7 +15383,7 @@ function checkSource(controller, source) {
17062
15383
  }
17063
15384
  return p;
17064
15385
  }
17065
- var SOURCE2 = {
15386
+ var SOURCE = {
17066
15387
  type: "object",
17067
15388
  description: "Source point in document pixels (an Alt-click). Given, it replaces the clone source and the offset starts afresh at this stroke; missing, the current clone source is used (set_clone_source).",
17068
15389
  properties: { x: { type: "number", description: "Source x in px." }, y: { type: "number", description: "Source y in px." } },
@@ -17086,7 +15407,7 @@ function createImageRetouchTools(controller) {
17086
15407
  type: "object",
17087
15408
  properties: {
17088
15409
  points: POINTS,
17089
- source: SOURCE2,
15410
+ source: SOURCE,
17090
15411
  aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
17091
15412
  sample: SAMPLE,
17092
15413
  brush: BRUSH2,
@@ -17106,7 +15427,7 @@ function createImageRetouchTools(controller) {
17106
15427
  type: "object",
17107
15428
  properties: {
17108
15429
  points: POINTS,
17109
- source: SOURCE2,
15430
+ source: SOURCE,
17110
15431
  aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
17111
15432
  sample: SAMPLE,
17112
15433
  brush: BRUSH2,
@@ -19247,6 +17568,9 @@ var SelectAndMaskSession = class {
19247
17568
  note = null;
19248
17569
  /** Bumped each time the preview changes, so a UI can redraw. */
19249
17570
  revision = 0;
17571
+ /** True while the worker computes a preview. */
17572
+ computing = false;
17573
+ dirty = false;
19250
17574
  get settings() {
19251
17575
  return this.settingsValue;
19252
17576
  }
@@ -19303,13 +17627,20 @@ var SelectAndMaskSession = class {
19303
17627
  this.previewPixels = scalePixels(px2, w, h);
19304
17628
  this.baseMask = this.base?.bounds ? s === 1 ? this.base : resizeSelection(this.base, w, h) : null;
19305
17629
  }
19306
- /** Recomputes the preview mask from the base selection, the strokes and the settings. */
17630
+ /**
17631
+ * Recomputes the preview mask from the base selection, the strokes and the settings: at once on
17632
+ * the page, or in the worker, where one job runs at a time and the newest state follows it.
17633
+ */
19307
17634
  recompute() {
19308
17635
  const px2 = this.previewPixels;
19309
17636
  if (!px2) return;
17637
+ this.note = this.settingsValue.refineMode === "object" ? "Object aware refinement asks the segmentation model on OK; the preview refines by colour." : null;
17638
+ if (this.controller.hasSelectionWorker) {
17639
+ void this.recomputeInWorker();
17640
+ return;
17641
+ }
19310
17642
  const { scale } = this.previewSize;
19311
17643
  const edits = [...this.editsList, ...this.live ? [this.live] : []].map((e) => scaleEdit(e, scale));
19312
- this.note = this.settingsValue.refineMode === "object" ? "Object aware refinement asks the segmentation model on OK; the preview refines by colour." : null;
19313
17644
  try {
19314
17645
  const { selection, refineBrush } = applyRefineEdits(px2, this.baseMask, edits, px2.width, px2.height);
19315
17646
  this.previewMask = selection?.bounds ? refineSelection(px2, selection, scaleParams(this.settingsValue.params, scale), refineBrush) : null;
@@ -19320,6 +17651,37 @@ var SelectAndMaskSession = class {
19320
17651
  }
19321
17652
  this.emit();
19322
17653
  }
17654
+ async recomputeInWorker() {
17655
+ if (this.computing) {
17656
+ this.dirty = true;
17657
+ return;
17658
+ }
17659
+ this.computing = true;
17660
+ do {
17661
+ this.dirty = false;
17662
+ const px2 = this.previewPixels;
17663
+ const { scale } = this.previewSize;
17664
+ const edits = [...this.editsList, ...this.live ? [this.live] : []].map((e) => scaleEdit(e, scale));
17665
+ try {
17666
+ const { mask } = await this.controller.runSelectionJob(
17667
+ { op: "refine-preview", base: this.baseMask ? serializeMask(this.baseMask) : null, edits, params: scaleParams(this.settingsValue.params, scale) },
17668
+ px2
17669
+ );
17670
+ this.previewMask = mask.bounds ? mask : null;
17671
+ this.error = null;
17672
+ } catch (err) {
17673
+ this.previewMask = null;
17674
+ this.error = err instanceof Error ? err.message : String(err);
17675
+ }
17676
+ this.emit();
17677
+ } while (this.dirty);
17678
+ this.computing = false;
17679
+ this.emit();
17680
+ }
17681
+ /** Resolves once no preview is being computed (immediately without a worker). */
17682
+ async idle() {
17683
+ while (this.computing) await new Promise((r) => setTimeout(r, 5));
17684
+ }
19323
17685
  /** Coverage 0..1 of the preview at (x, y) in preview pixels (for tests and hover readouts). */
19324
17686
  coverageAt(x, y) {
19325
17687
  const m = this.settingsValue.showOriginal ? this.baseMask : this.previewMask;
@@ -21396,6 +19758,7 @@ export {
21396
19758
  SUPPORTED_ADJUSTMENT_KEYS,
21397
19759
  SelectAndMaskSession,
21398
19760
  SelectionGesture,
19761
+ SelectionWorkerClient,
21399
19762
  ShapeDrawGesture,
21400
19763
  SmudgeTip,
21401
19764
  StrokeCoverage,
@@ -21645,6 +20008,7 @@ export {
21645
20008
  deserializeEffects,
21646
20009
  deserializeLayer,
21647
20010
  deserializeLayers,
20011
+ deserializeMask,
21648
20012
  deserializePatterns,
21649
20013
  deserializeSelection,
21650
20014
  developFilter,
@@ -22066,6 +20430,7 @@ export {
22066
20430
  runLayerFilters,
22067
20431
  runLayerFiltersFloat,
22068
20432
  runLayerMenuAction,
20433
+ runSelectionJob,
22069
20434
  sameCanvasViewport,
22070
20435
  sampleLut,
22071
20436
  sampleToneLut,
@@ -22099,6 +20464,7 @@ export {
22099
20464
  serializeLayer,
22100
20465
  serializeLayers,
22101
20466
  serializeLayersExternal,
20467
+ serializeMask,
22102
20468
  serializePatterns,
22103
20469
  serializeSelection,
22104
20470
  setCloneTransformValue,
@@ -22150,6 +20516,7 @@ export {
22150
20516
  srgbToLinear,
22151
20517
  standardBlendMode,
22152
20518
  startMarchingAnts,
20519
+ startSelectionWorker,
22153
20520
  stepSlotTs,
22154
20521
  strokeGrainCatch,
22155
20522
  styleAt,