@pixerate/editor 0.197.0 → 0.199.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3045,7 +3045,7 @@ var GridGraph = class {
3045
3045
  }
3046
3046
  };
3047
3047
 
3048
- // src/image-editor/selection/segment/quick.ts
3048
+ // src/image-editor/selection/segment/work.ts
3049
3049
  var WORK_SIZE = 512;
3050
3050
  var workCache = /* @__PURE__ */ new WeakMap();
3051
3051
  function workImage(pixels, maxSide = WORK_SIZE) {
@@ -3082,6 +3082,155 @@ function workImage(pixels, maxSide = WORK_SIZE) {
3082
3082
  workCache.set(pixels.data, { maxSide, work });
3083
3083
  return work;
3084
3084
  }
3085
+ var GAMMA = 50;
3086
+ var UNITS = 64;
3087
+ function smoothnessWeights(work, roi) {
3088
+ const { width: w, rgb } = work;
3089
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3090
+ const right = new Float64Array(rw * rh);
3091
+ const down = new Float64Array(rw * rh);
3092
+ let sumSq = 0;
3093
+ let pairs = 0;
3094
+ for (let y = 0; y < rh; y++) {
3095
+ for (let x = 0; x < rw; x++) {
3096
+ const p = y * rw + x;
3097
+ const c = ((ry + y) * w + rx + x) * 3;
3098
+ if (x + 1 < rw) {
3099
+ right[p] = (rgb[c] - rgb[c + 3]) ** 2 + (rgb[c + 1] - rgb[c + 4]) ** 2 + (rgb[c + 2] - rgb[c + 5]) ** 2;
3100
+ sumSq += right[p];
3101
+ pairs++;
3102
+ }
3103
+ if (y + 1 < rh) {
3104
+ const k = c + w * 3;
3105
+ down[p] = (rgb[c] - rgb[k]) ** 2 + (rgb[c + 1] - rgb[k + 1]) ** 2 + (rgb[c + 2] - rgb[k + 2]) ** 2;
3106
+ sumSq += down[p];
3107
+ pairs++;
3108
+ }
3109
+ }
3110
+ }
3111
+ const beta = 1 / (2 * Math.max(1e-6, sumSq / Math.max(1, pairs)));
3112
+ for (let p = 0; p < right.length; p++) {
3113
+ right[p] = Math.round(GAMMA * UNITS * Math.exp(-beta * right[p]));
3114
+ down[p] = Math.round(GAMMA * UNITS * Math.exp(-beta * down[p]));
3115
+ }
3116
+ return { right, down };
3117
+ }
3118
+ function boxBlur(src, w, h, passes) {
3119
+ let a = src;
3120
+ for (let k = 0; k < passes; k++) {
3121
+ const b = new Float32Array(a.length);
3122
+ for (let y = 0; y < h; y++) {
3123
+ for (let x = 0; x < w; x++) {
3124
+ let s = 0;
3125
+ let c = 0;
3126
+ for (let j = Math.max(0, y - 1); j <= Math.min(h - 1, y + 1); j++) {
3127
+ for (let i = Math.max(0, x - 1); i <= Math.min(w - 1, x + 1); i++) {
3128
+ s += a[j * w + i];
3129
+ c++;
3130
+ }
3131
+ }
3132
+ b[y * w + x] = s / c;
3133
+ }
3134
+ }
3135
+ a = b;
3136
+ }
3137
+ return a;
3138
+ }
3139
+ function upscaleRegion(work, pixels, roi, label, enhance) {
3140
+ const { sourceWidth: W, sourceHeight: H, scale, rgb, width: ww } = work;
3141
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3142
+ let soft = Float32Array.from(label);
3143
+ if (enhance) {
3144
+ soft = boxBlur(soft, rw, rh, 2);
3145
+ for (let i = 0; i < soft.length; i++) soft[i] = Math.min(1, Math.max(0, (soft[i] - 0.5) * 3 + 0.5));
3146
+ }
3147
+ const X0 = Math.max(0, Math.floor(rx / scale));
3148
+ const Y0 = Math.max(0, Math.floor(ry / scale));
3149
+ const X1 = Math.min(W, Math.ceil((rx + rw) / scale));
3150
+ const Y1 = Math.min(H, Math.ceil((ry + rh) / scale));
3151
+ const RW = X1 - X0;
3152
+ const RH = Y1 - Y0;
3153
+ const out = new Uint8Array(RW * RH);
3154
+ const at = (x, y) => soft[Math.min(rh - 1, Math.max(0, y)) * rw + Math.min(rw - 1, Math.max(0, x))];
3155
+ const data = pixels.data;
3156
+ const colOf = new Int32Array(RW);
3157
+ const fxOf = new Float32Array(RW);
3158
+ for (let X = 0; X < RW; X++) {
3159
+ const u = (X0 + X + 0.5) * scale - 0.5 - rx;
3160
+ colOf[X] = Math.floor(u);
3161
+ fxOf[X] = u - colOf[X];
3162
+ }
3163
+ const cellState = (i, j) => {
3164
+ const a = at(i, j), b = at(i + 1, j), c = at(i, j + 1), d = at(i + 1, j + 1);
3165
+ if (a === 1 && b === 1 && c === 1 && d === 1) return 1;
3166
+ if (a === 0 && b === 0 && c === 0 && d === 0) return 0;
3167
+ return 2;
3168
+ };
3169
+ for (let Y = 0; Y < RH; Y++) {
3170
+ const v = (Y0 + Y + 0.5) * scale - 0.5 - ry;
3171
+ const j = Math.floor(v);
3172
+ const fy = v - j;
3173
+ for (let X = 0; X < RW; X++) {
3174
+ const i = colOf[X];
3175
+ const state = cellState(i, j);
3176
+ if (state !== 2) {
3177
+ let end = X + 1;
3178
+ while (end < RW && colOf[end] === i) end++;
3179
+ if (state === 1) out.fill(255, Y * RW + X, Y * RW + end);
3180
+ X = end - 1;
3181
+ continue;
3182
+ }
3183
+ const fx = fxOf[X];
3184
+ 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;
3185
+ if (m <= 1e-3 || m >= 0.999) {
3186
+ out[Y * RW + X] = m >= 0.999 ? 255 : 0;
3187
+ continue;
3188
+ }
3189
+ let fr = 0, fg = 0, fb = 0, fn = 0, br = 0, bg = 0, bb = 0, bn = 0;
3190
+ for (let jj = j - 1; jj <= j + 2; jj++) {
3191
+ for (let ii = i - 1; ii <= i + 2; ii++) {
3192
+ if (ii < 0 || jj < 0 || ii >= rw || jj >= rh) continue;
3193
+ const k = ((ry + jj) * ww + rx + ii) * 3;
3194
+ if (soft[jj * rw + ii] >= 0.5) {
3195
+ fr += rgb[k];
3196
+ fg += rgb[k + 1];
3197
+ fb += rgb[k + 2];
3198
+ fn++;
3199
+ } else {
3200
+ br += rgb[k];
3201
+ bg += rgb[k + 1];
3202
+ bb += rgb[k + 2];
3203
+ bn++;
3204
+ }
3205
+ }
3206
+ }
3207
+ let alpha = m;
3208
+ if (fn && bn) {
3209
+ fr /= fn;
3210
+ fg /= fn;
3211
+ fb /= fn;
3212
+ br /= bn;
3213
+ bg /= bn;
3214
+ bb /= bn;
3215
+ const dr = fr - br, dg = fg - bg, db = fb - bb;
3216
+ const len2 = dr * dr + dg * dg + db * db;
3217
+ if (len2 > 400) {
3218
+ const p = ((Y0 + Y) * W + X0 + X) * 4;
3219
+ const a = data[p + 3] / 255;
3220
+ const cr = data[p] * a + 255 * (1 - a) - br;
3221
+ const cg = data[p + 1] * a + 255 * (1 - a) - bg;
3222
+ const cb = data[p + 2] * a + 255 * (1 - a) - bb;
3223
+ const proj = Math.min(1, Math.max(0, (cr * dr + cg * dg + cb * db) / len2));
3224
+ alpha = Math.min(1, Math.max(0, proj * 0.75 + m * 0.25));
3225
+ } else alpha = Math.min(1, Math.max(0, (m - 0.5) * 2 + 0.5));
3226
+ }
3227
+ out[Y * RW + X] = Math.round(alpha * 255);
3228
+ }
3229
+ }
3230
+ return selectionFromRegion(W, H, { x: X0, y: Y0, width: RW, height: RH }, out);
3231
+ }
3232
+
3233
+ // src/image-editor/selection/segment/quick.ts
3085
3234
  var BINS = 16;
3086
3235
  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);
3087
3236
  function costTable(hist, total) {
@@ -3102,8 +3251,6 @@ function costTable(hist, total) {
3102
3251
  for (let i = 0; i < a.length; i++) out[i] = -Math.log((a[i] + floor * Math.max(1, total)) / denom);
3103
3252
  return out;
3104
3253
  }
3105
- var GAMMA = 50;
3106
- var UNITS = 64;
3107
3254
  function strokePixels(points, r, w, h) {
3108
3255
  const mask = new Uint8Array(w * h);
3109
3256
  const list = [];
@@ -3161,23 +3308,7 @@ function segmentStroke(work, stroke, grown, start) {
3161
3308
  }
3162
3309
  const fgCost = costTable(fgHist, fgN);
3163
3310
  const bgCost = costTable(bgHist, bgN);
3164
- let sumSq = 0;
3165
- let pairs = 0;
3166
- for (let y = 0; y < rh; y++) {
3167
- for (let x = 0; x < rw; x++) {
3168
- const g = ((ry + y) * w + rx + x) * 3;
3169
- if (x + 1 < rw) {
3170
- sumSq += (rgb[g] - rgb[g + 3]) ** 2 + (rgb[g + 1] - rgb[g + 4]) ** 2 + (rgb[g + 2] - rgb[g + 5]) ** 2;
3171
- pairs++;
3172
- }
3173
- if (y + 1 < rh) {
3174
- const k = g + w * 3;
3175
- sumSq += (rgb[g] - rgb[k]) ** 2 + (rgb[g + 1] - rgb[k + 1]) ** 2 + (rgb[g + 2] - rgb[k + 2]) ** 2;
3176
- pairs++;
3177
- }
3178
- }
3179
- }
3180
- const beta = 1 / (2 * Math.max(1e-6, sumSq / Math.max(1, pairs)));
3311
+ const { right, down } = smoothnessWeights(work, roi);
3181
3312
  const graph = new GridGraph(rw, rh);
3182
3313
  const HARD = 1e9;
3183
3314
  for (let y = 0; y < rh; y++) {
@@ -3189,16 +3320,8 @@ function segmentStroke(work, stroke, grown, start) {
3189
3320
  const bin = binOf(rgb, g);
3190
3321
  graph.addTerminal(p, Math.round(bgCost[bin] * UNITS), Math.round(fgCost[bin] * UNITS));
3191
3322
  }
3192
- const c = g * 3;
3193
- if (x + 1 < rw) {
3194
- const d = (rgb[c] - rgb[c + 3]) ** 2 + (rgb[c + 1] - rgb[c + 4]) ** 2 + (rgb[c + 2] - rgb[c + 5]) ** 2;
3195
- graph.setEdge(p, 0, Math.round(GAMMA * UNITS * Math.exp(-beta * d)));
3196
- }
3197
- if (y + 1 < rh) {
3198
- const k = c + w * 3;
3199
- const d = (rgb[c] - rgb[k]) ** 2 + (rgb[c + 1] - rgb[k + 1]) ** 2 + (rgb[c + 2] - rgb[k + 2]) ** 2;
3200
- graph.setEdge(p, 1, Math.round(GAMMA * UNITS * Math.exp(-beta * d)));
3201
- }
3323
+ if (x + 1 < rw) graph.setEdge(p, 0, right[p]);
3324
+ if (y + 1 < rh) graph.setEdge(p, 1, down[p]);
3202
3325
  }
3203
3326
  }
3204
3327
  graph.maxflow();
@@ -3239,120 +3362,6 @@ function segmentStroke(work, stroke, grown, start) {
3239
3362
  if (roi.width * roi.height * 2.5 > w * h) roi = { x: 0, y: 0, width: w, height: h };
3240
3363
  }
3241
3364
  }
3242
- function boxBlur(src, w, h, passes) {
3243
- let a = src;
3244
- for (let k = 0; k < passes; k++) {
3245
- const b = new Float32Array(a.length);
3246
- for (let y = 0; y < h; y++) {
3247
- for (let x = 0; x < w; x++) {
3248
- let s = 0;
3249
- let c = 0;
3250
- for (let j = Math.max(0, y - 1); j <= Math.min(h - 1, y + 1); j++) {
3251
- for (let i = Math.max(0, x - 1); i <= Math.min(w - 1, x + 1); i++) {
3252
- s += a[j * w + i];
3253
- c++;
3254
- }
3255
- }
3256
- b[y * w + x] = s / c;
3257
- }
3258
- }
3259
- a = b;
3260
- }
3261
- return a;
3262
- }
3263
- function upscaleRegion(work, pixels, roi, label, enhance) {
3264
- const { sourceWidth: W, sourceHeight: H, scale, rgb, width: ww } = work;
3265
- const { x: rx, y: ry, width: rw, height: rh } = roi;
3266
- let soft = Float32Array.from(label);
3267
- if (enhance) {
3268
- soft = boxBlur(soft, rw, rh, 2);
3269
- for (let i = 0; i < soft.length; i++) soft[i] = Math.min(1, Math.max(0, (soft[i] - 0.5) * 3 + 0.5));
3270
- }
3271
- const X0 = Math.max(0, Math.floor(rx / scale));
3272
- const Y0 = Math.max(0, Math.floor(ry / scale));
3273
- const X1 = Math.min(W, Math.ceil((rx + rw) / scale));
3274
- const Y1 = Math.min(H, Math.ceil((ry + rh) / scale));
3275
- const RW = X1 - X0;
3276
- const RH = Y1 - Y0;
3277
- const out = new Uint8Array(RW * RH);
3278
- const at = (x, y) => soft[Math.min(rh - 1, Math.max(0, y)) * rw + Math.min(rw - 1, Math.max(0, x))];
3279
- const data = pixels.data;
3280
- const colOf = new Int32Array(RW);
3281
- const fxOf = new Float32Array(RW);
3282
- for (let X = 0; X < RW; X++) {
3283
- const u = (X0 + X + 0.5) * scale - 0.5 - rx;
3284
- colOf[X] = Math.floor(u);
3285
- fxOf[X] = u - colOf[X];
3286
- }
3287
- const cellState = (i, j) => {
3288
- const a = at(i, j), b = at(i + 1, j), c = at(i, j + 1), d = at(i + 1, j + 1);
3289
- if (a === 1 && b === 1 && c === 1 && d === 1) return 1;
3290
- if (a === 0 && b === 0 && c === 0 && d === 0) return 0;
3291
- return 2;
3292
- };
3293
- for (let Y = 0; Y < RH; Y++) {
3294
- const v = (Y0 + Y + 0.5) * scale - 0.5 - ry;
3295
- const j = Math.floor(v);
3296
- const fy = v - j;
3297
- for (let X = 0; X < RW; X++) {
3298
- const i = colOf[X];
3299
- const state = cellState(i, j);
3300
- if (state !== 2) {
3301
- let end = X + 1;
3302
- while (end < RW && colOf[end] === i) end++;
3303
- if (state === 1) out.fill(255, Y * RW + X, Y * RW + end);
3304
- X = end - 1;
3305
- continue;
3306
- }
3307
- const fx = fxOf[X];
3308
- 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;
3309
- if (m <= 1e-3 || m >= 0.999) {
3310
- out[Y * RW + X] = m >= 0.999 ? 255 : 0;
3311
- continue;
3312
- }
3313
- let fr = 0, fg = 0, fb = 0, fn = 0, br = 0, bg = 0, bb = 0, bn = 0;
3314
- for (let jj = j - 1; jj <= j + 2; jj++) {
3315
- for (let ii = i - 1; ii <= i + 2; ii++) {
3316
- if (ii < 0 || jj < 0 || ii >= rw || jj >= rh) continue;
3317
- const k = ((ry + jj) * ww + rx + ii) * 3;
3318
- if (soft[jj * rw + ii] >= 0.5) {
3319
- fr += rgb[k];
3320
- fg += rgb[k + 1];
3321
- fb += rgb[k + 2];
3322
- fn++;
3323
- } else {
3324
- br += rgb[k];
3325
- bg += rgb[k + 1];
3326
- bb += rgb[k + 2];
3327
- bn++;
3328
- }
3329
- }
3330
- }
3331
- let alpha = m;
3332
- if (fn && bn) {
3333
- fr /= fn;
3334
- fg /= fn;
3335
- fb /= fn;
3336
- br /= bn;
3337
- bg /= bn;
3338
- bb /= bn;
3339
- const dr = fr - br, dg = fg - bg, db = fb - bb;
3340
- const len2 = dr * dr + dg * dg + db * db;
3341
- if (len2 > 400) {
3342
- const p = ((Y0 + Y) * W + X0 + X) * 4;
3343
- const a = data[p + 3] / 255;
3344
- const cr = data[p] * a + 255 * (1 - a) - br;
3345
- const cg = data[p + 1] * a + 255 * (1 - a) - bg;
3346
- const cb = data[p + 2] * a + 255 * (1 - a) - bb;
3347
- const proj = Math.min(1, Math.max(0, (cr * dr + cg * dg + cb * db) / len2));
3348
- alpha = Math.min(1, Math.max(0, proj * 0.75 + m * 0.25));
3349
- } else alpha = Math.min(1, Math.max(0, (m - 0.5) * 2 + 0.5));
3350
- }
3351
- out[Y * RW + X] = Math.round(alpha * 255);
3352
- }
3353
- }
3354
- return selectionFromRegion(W, H, { x: X0, y: Y0, width: RW, height: RH }, out);
3355
- }
3356
3365
  function quickSelectStroke(pixels, stroke, options = {}) {
3357
3366
  if (!stroke.length) throw new Error("A stroke needs at least one point.");
3358
3367
  const work = workImage(pixels);
@@ -3386,6 +3395,486 @@ function quickSelectStroke(pixels, stroke, options = {}) {
3386
3395
  return upscaleRegion(work, pixels, done, label, options.enhanceEdge ?? false);
3387
3396
  }
3388
3397
 
3398
+ // src/image-editor/selection/segment/grabcut.ts
3399
+ var K = 5;
3400
+ var ColorGmm = class {
3401
+ logWeight = new Float64Array(K);
3402
+ mean = new Float64Array(K * 3);
3403
+ /** Inverse covariances, row-major 3 × 3. */
3404
+ inv = new Float64Array(K * 9);
3405
+ /** ln(weight) − ½ ln((2π)³ |Σ|) per component; −∞ for an empty one. */
3406
+ logNorm = new Float64Array(K).fill(-Infinity);
3407
+ /** Fits the components to the pixels `ids` of `rgb`, each in component `comp[id]`. */
3408
+ fit(rgb, ids, comp) {
3409
+ const n = new Float64Array(K);
3410
+ const sum = new Float64Array(K * 3);
3411
+ const prod = new Float64Array(K * 9);
3412
+ for (let t = 0; t < ids.length; t++) {
3413
+ const i = ids[t];
3414
+ const k = comp[i];
3415
+ const c = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
3416
+ n[k]++;
3417
+ for (let a = 0; a < 3; a++) {
3418
+ sum[k * 3 + a] += c[a];
3419
+ for (let b = 0; b < 3; b++) prod[k * 9 + a * 3 + b] += c[a] * c[b];
3420
+ }
3421
+ }
3422
+ const total = ids.length || 1;
3423
+ for (let k = 0; k < K; k++) {
3424
+ if (!n[k]) {
3425
+ this.logNorm[k] = -Infinity;
3426
+ continue;
3427
+ }
3428
+ const m = [sum[k * 3] / n[k], sum[k * 3 + 1] / n[k], sum[k * 3 + 2] / n[k]];
3429
+ const cov = new Float64Array(9);
3430
+ for (let a = 0; a < 3; a++) {
3431
+ for (let b = 0; b < 3; b++) cov[a * 3 + b] = prod[k * 9 + a * 3 + b] / n[k] - m[a] * m[b];
3432
+ cov[a * 4] += 4;
3433
+ }
3434
+ 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]);
3435
+ const d = Math.max(det, 1e-6);
3436
+ const inv = [
3437
+ (cov[4] * cov[8] - cov[5] * cov[7]) / d,
3438
+ (cov[2] * cov[7] - cov[1] * cov[8]) / d,
3439
+ (cov[1] * cov[5] - cov[2] * cov[4]) / d,
3440
+ (cov[5] * cov[6] - cov[3] * cov[8]) / d,
3441
+ (cov[0] * cov[8] - cov[2] * cov[6]) / d,
3442
+ (cov[2] * cov[3] - cov[0] * cov[5]) / d,
3443
+ (cov[3] * cov[7] - cov[4] * cov[6]) / d,
3444
+ (cov[1] * cov[6] - cov[0] * cov[7]) / d,
3445
+ (cov[0] * cov[4] - cov[1] * cov[3]) / d
3446
+ ];
3447
+ this.mean.set(m, k * 3);
3448
+ this.inv.set(inv, k * 9);
3449
+ this.logWeight[k] = Math.log(n[k] / total);
3450
+ this.logNorm[k] = this.logWeight[k] - 0.5 * Math.log(Math.pow(2 * Math.PI, 3) * d);
3451
+ }
3452
+ }
3453
+ /** ln p(colour | component k), up to the shared constant. */
3454
+ componentLog(k, r, g, b) {
3455
+ const ln = this.logNorm[k];
3456
+ if (ln === -Infinity) return -Infinity;
3457
+ const dr = r - this.mean[k * 3];
3458
+ const dg = g - this.mean[k * 3 + 1];
3459
+ const db = b - this.mean[k * 3 + 2];
3460
+ const v = this.inv;
3461
+ const o = k * 9;
3462
+ 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);
3463
+ return ln - 0.5 * q;
3464
+ }
3465
+ /** −ln p(colour) under the mixture (large for colours the model hasn't seen). */
3466
+ cost(r, g, b) {
3467
+ let best = -Infinity;
3468
+ const logs = new Array(K);
3469
+ for (let k = 0; k < K; k++) {
3470
+ logs[k] = this.componentLog(k, r, g, b);
3471
+ if (logs[k] > best) best = logs[k];
3472
+ }
3473
+ if (best === -Infinity) return 50;
3474
+ let s = 0;
3475
+ for (let k = 0; k < K; k++) if (logs[k] > -Infinity) s += Math.exp(logs[k] - best);
3476
+ return Math.min(50, -(best + Math.log(s)));
3477
+ }
3478
+ /** The component most likely to have produced the colour. */
3479
+ nearest(r, g, b) {
3480
+ let best = 0;
3481
+ let bestLog = -Infinity;
3482
+ for (let k = 0; k < K; k++) {
3483
+ const l = this.componentLog(k, r, g, b);
3484
+ if (l > bestLog) {
3485
+ bestLog = l;
3486
+ best = k;
3487
+ }
3488
+ }
3489
+ return best;
3490
+ }
3491
+ };
3492
+ function initialComponents(rgb, ids, comp) {
3493
+ if (!ids.length) return;
3494
+ const step = Math.max(1, Math.floor(ids.length / 6e3));
3495
+ const sample = [];
3496
+ for (let t = 0; t < ids.length; t += step) sample.push(ids[t]);
3497
+ const centres = [];
3498
+ const dist2 = (i, c) => (rgb[i * 3] - c[0]) ** 2 + (rgb[i * 3 + 1] - c[1]) ** 2 + (rgb[i * 3 + 2] - c[2]) ** 2;
3499
+ centres.push([rgb[sample[0] * 3], rgb[sample[0] * 3 + 1], rgb[sample[0] * 3 + 2]]);
3500
+ const near = sample.map((i) => dist2(i, centres[0]));
3501
+ while (centres.length < K) {
3502
+ let far = 0;
3503
+ for (let t = 1; t < sample.length; t++) if (near[t] > near[far]) far = t;
3504
+ const i = sample[far];
3505
+ const c = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
3506
+ centres.push(c);
3507
+ for (let t = 0; t < sample.length; t++) near[t] = Math.min(near[t], dist2(sample[t], c));
3508
+ }
3509
+ const closest = (i) => {
3510
+ let best = 0;
3511
+ for (let k = 1; k < K; k++) if (dist2(i, centres[k]) < dist2(i, centres[best])) best = k;
3512
+ return best;
3513
+ };
3514
+ for (let round6 = 0; round6 < 6; round6++) {
3515
+ const sum = Array.from({ length: K }, () => [0, 0, 0, 0]);
3516
+ for (const i of sample) {
3517
+ const s = sum[closest(i)];
3518
+ s[0] += rgb[i * 3];
3519
+ s[1] += rgb[i * 3 + 1];
3520
+ s[2] += rgb[i * 3 + 2];
3521
+ s[3]++;
3522
+ }
3523
+ 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]];
3524
+ }
3525
+ for (const i of ids) comp[i] = closest(i);
3526
+ }
3527
+ function grabCutLabels(work, setup, iterations) {
3528
+ const { width: w, rgb } = work;
3529
+ const { roi, hardBg, hardFg } = setup;
3530
+ const { x: rx, y: ry, width: rw, height: rh } = roi;
3531
+ const n = rw * rh;
3532
+ const label = setup.initial.slice();
3533
+ const global = new Int32Array(n);
3534
+ for (let y = 0; y < rh; y++) for (let x = 0; x < rw; x++) global[y * rw + x] = (ry + y) * w + rx + x;
3535
+ const comp = new Uint8Array(w * work.height);
3536
+ const fg = new ColorGmm();
3537
+ const bg = new ColorGmm();
3538
+ const { right, down } = smoothnessWeights(work, roi);
3539
+ const HARD = 1e9;
3540
+ for (let iter = 0; iter < iterations; iter++) {
3541
+ const fgIds = [];
3542
+ const bgIds = [];
3543
+ for (let p = 0; p < n; p++) (label[p] ? fgIds : bgIds).push(global[p]);
3544
+ if (!fgIds.length) return label;
3545
+ if (iter === 0) {
3546
+ initialComponents(rgb, fgIds, comp);
3547
+ initialComponents(rgb, bgIds, comp);
3548
+ } else {
3549
+ for (const i of fgIds) comp[i] = fg.nearest(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
3550
+ for (const i of bgIds) comp[i] = bg.nearest(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
3551
+ }
3552
+ fg.fit(rgb, fgIds, comp);
3553
+ bg.fit(rgb, bgIds, comp);
3554
+ const graph = new GridGraph(rw, rh);
3555
+ for (let p = 0; p < n; p++) {
3556
+ if (hardBg[p]) graph.addTerminal(p, 0, HARD);
3557
+ else if (hardFg?.[p]) graph.addTerminal(p, HARD, 0);
3558
+ else {
3559
+ const i = global[p] * 3;
3560
+ 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));
3561
+ }
3562
+ if (p % rw + 1 < rw) graph.setEdge(p, 0, right[p]);
3563
+ if (p + rw < n) graph.setEdge(p, 1, down[p]);
3564
+ }
3565
+ graph.maxflow();
3566
+ let changed = 0;
3567
+ for (let p = 0; p < n; p++) {
3568
+ const v = graph.inSource(p) ? 1 : 0;
3569
+ if (v !== label[p]) changed++;
3570
+ label[p] = v;
3571
+ }
3572
+ if (changed <= n * 1e-3) break;
3573
+ }
3574
+ return label;
3575
+ }
3576
+ function keepMainPieces(label, rw, rh, seed) {
3577
+ const n = rw * rh;
3578
+ const piece = new Int32Array(n).fill(-1);
3579
+ const sizes = [];
3580
+ const stack = [];
3581
+ for (let s = 0; s < n; s++) {
3582
+ if (!label[s] || piece[s] >= 0) continue;
3583
+ const id = sizes.length;
3584
+ let size = 0;
3585
+ piece[s] = id;
3586
+ stack.push(s);
3587
+ while (stack.length) {
3588
+ const p = stack.pop();
3589
+ size++;
3590
+ const x = p % rw;
3591
+ for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
3592
+ if (q >= 0 && label[q] && piece[q] < 0) {
3593
+ piece[q] = id;
3594
+ stack.push(q);
3595
+ }
3596
+ }
3597
+ }
3598
+ sizes.push(size);
3599
+ }
3600
+ if (!sizes.length) return;
3601
+ const largest = Math.max(...sizes);
3602
+ const keep = (id) => seed !== null && piece[seed] >= 0 ? id === piece[seed] : sizes[id] * 5 >= largest;
3603
+ for (let p = 0; p < n; p++) if (label[p] && !keep(piece[p])) label[p] = 0;
3604
+ }
3605
+ function selectObjectIn(pixels, target, options = {}) {
3606
+ const work = workImage(pixels);
3607
+ const { width: w, height: h, scale } = work;
3608
+ const iterations = Math.max(1, Math.round(options.iterations ?? 5));
3609
+ const enhance = options.enhanceEdge ?? false;
3610
+ if ("rect" in target) {
3611
+ const r = target.rect;
3612
+ const bx0 = Math.max(0, Math.floor(r.x * scale));
3613
+ const by0 = Math.max(0, Math.floor(r.y * scale));
3614
+ const bx1 = Math.min(w, Math.ceil((r.x + r.width) * scale));
3615
+ const by1 = Math.min(h, Math.ceil((r.y + r.height) * scale));
3616
+ if (bx1 - bx0 < 2 || by1 - by0 < 2) return createSelectionMask(pixels.width, pixels.height);
3617
+ const m = Math.max(8, Math.round(0.25 * Math.max(bx1 - bx0, by1 - by0)));
3618
+ const rx = Math.max(0, bx0 - m);
3619
+ const ry = Math.max(0, by0 - m);
3620
+ const roi = { x: rx, y: ry, width: Math.min(w, bx1 + m) - rx, height: Math.min(h, by1 + m) - ry };
3621
+ const n = roi.width * roi.height;
3622
+ const initial = new Uint8Array(n);
3623
+ const hardBg = new Uint8Array(n);
3624
+ for (let y = 0; y < roi.height; y++) {
3625
+ for (let x = 0; x < roi.width; x++) {
3626
+ const gx = rx + x;
3627
+ const gy = ry + y;
3628
+ const inside = gx >= bx0 && gx < bx1 && gy >= by0 && gy < by1;
3629
+ initial[y * roi.width + x] = inside ? 1 : 0;
3630
+ hardBg[y * roi.width + x] = inside ? 0 : 1;
3631
+ }
3632
+ }
3633
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg: null }, iterations);
3634
+ keepMainPieces(label, roi.width, roi.height, null);
3635
+ return upscaleRegion(work, pixels, roi, label, enhance);
3636
+ }
3637
+ const px2 = Math.min(w - 1, Math.max(0, Math.floor(target.point.x * scale)));
3638
+ const py = Math.min(h - 1, Math.max(0, Math.floor(target.point.y * scale)));
3639
+ let half = Math.max(24, Math.round(0.3 * Math.max(w, h)));
3640
+ for (; ; ) {
3641
+ const rx = Math.max(0, px2 - half);
3642
+ const ry = Math.max(0, py - half);
3643
+ const roi = { x: rx, y: ry, width: Math.min(w, px2 + half + 1) - rx, height: Math.min(h, py + half + 1) - ry };
3644
+ const whole = roi.width === w && roi.height === h;
3645
+ const band = Math.max(4, Math.round(0.15 * half));
3646
+ const n = roi.width * roi.height;
3647
+ const initial = new Uint8Array(n);
3648
+ const hardBg = new Uint8Array(n);
3649
+ const hardFg = new Uint8Array(n);
3650
+ 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;
3651
+ for (let y = 0; y < roi.height; y++) {
3652
+ for (let x = 0; x < roi.width; x++) {
3653
+ const p = y * roi.width + x;
3654
+ if (inBand(x, y)) hardBg[p] = 1;
3655
+ else initial[p] = 1;
3656
+ if (Math.hypot(rx + x - px2, ry + y - py) <= 1.5) hardFg[p] = 1;
3657
+ }
3658
+ }
3659
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, iterations);
3660
+ keepMainPieces(label, roi.width, roi.height, (py - ry) * roi.width + (px2 - rx));
3661
+ let touches = false;
3662
+ for (let y = 0; y < roi.height && !touches; y++) {
3663
+ for (let x = 0; x < roi.width; x++) {
3664
+ if (label[y * roi.width + x] && (inBand(x - 1, y) || inBand(x + 1, y) || inBand(x, y - 1) || inBand(x, y + 1))) {
3665
+ touches = true;
3666
+ break;
3667
+ }
3668
+ }
3669
+ }
3670
+ if (!touches || whole) return upscaleRegion(work, pixels, roi, label, enhance);
3671
+ half = Math.round(half * 1.6);
3672
+ }
3673
+ }
3674
+
3675
+ // src/image-editor/selection/segment/subject.ts
3676
+ function toLab(r, g, b) {
3677
+ const lin = (v) => {
3678
+ v /= 255;
3679
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
3680
+ };
3681
+ const R = lin(r);
3682
+ const G = lin(g);
3683
+ const B = lin(b);
3684
+ const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
3685
+ const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
3686
+ const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
3687
+ const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
3688
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
3689
+ }
3690
+ function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
3691
+ const { width: w, height: h, rgb } = work;
3692
+ const n = w * h;
3693
+ const lab = new Float32Array(n * 3);
3694
+ for (let i = 0; i < n; i++) lab.set(toLab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]), i * 3);
3695
+ const S = Math.max(2, Math.round(Math.sqrt(n / target)));
3696
+ const seeds = [];
3697
+ for (let y = Math.floor(S / 2); y < h; y += S) {
3698
+ for (let x = Math.floor(S / 2); x < w; x += S) {
3699
+ const i = y * w + x;
3700
+ seeds.push([lab[i * 3], lab[i * 3 + 1], lab[i * 3 + 2], x, y]);
3701
+ }
3702
+ }
3703
+ const k = seeds.length;
3704
+ const label = new Int32Array(n).fill(-1);
3705
+ const best = new Float64Array(n);
3706
+ const m2 = (compactness / S) ** 2;
3707
+ for (let round6 = 0; round6 < rounds; round6++) {
3708
+ best.fill(Infinity);
3709
+ for (let c = 0; c < k; c++) {
3710
+ const [L, A, B, cx, cy] = seeds[c];
3711
+ const x0 = Math.max(0, Math.floor(cx - S));
3712
+ const x1 = Math.min(w - 1, Math.ceil(cx + S));
3713
+ const y0 = Math.max(0, Math.floor(cy - S));
3714
+ const y1 = Math.min(h - 1, Math.ceil(cy + S));
3715
+ for (let y = y0; y <= y1; y++) {
3716
+ for (let x = x0; x <= x1; x++) {
3717
+ const i = y * w + x;
3718
+ const d = (lab[i * 3] - L) ** 2 + (lab[i * 3 + 1] - A) ** 2 + (lab[i * 3 + 2] - B) ** 2 + m2 * ((x - cx) ** 2 + (y - cy) ** 2);
3719
+ if (d < best[i]) {
3720
+ best[i] = d;
3721
+ label[i] = c;
3722
+ }
3723
+ }
3724
+ }
3725
+ }
3726
+ const sum = new Float64Array(k * 6);
3727
+ for (let i = 0; i < n; i++) {
3728
+ const c = label[i];
3729
+ sum[c * 6] += lab[i * 3];
3730
+ sum[c * 6 + 1] += lab[i * 3 + 1];
3731
+ sum[c * 6 + 2] += lab[i * 3 + 2];
3732
+ sum[c * 6 + 3] += i % w;
3733
+ sum[c * 6 + 4] += Math.floor(i / w);
3734
+ sum[c * 6 + 5]++;
3735
+ }
3736
+ for (let c = 0; c < k; c++) {
3737
+ const s = sum[c * 6 + 5];
3738
+ if (s) seeds[c] = [sum[c * 6] / s, sum[c * 6 + 1] / s, sum[c * 6 + 2] / s, sum[c * 6 + 3] / s, sum[c * 6 + 4] / s];
3739
+ }
3740
+ }
3741
+ const remap = new Int32Array(k).fill(-1);
3742
+ let count = 0;
3743
+ for (let i = 0; i < n; i++) if (remap[label[i]] < 0) remap[label[i]] = count++;
3744
+ const meanLab = new Float64Array(count * 3);
3745
+ const centre = new Float64Array(count * 2);
3746
+ const size = new Int32Array(count);
3747
+ for (let i = 0; i < n; i++) {
3748
+ const c = label[i] = remap[label[i]];
3749
+ meanLab[c * 3] += lab[i * 3];
3750
+ meanLab[c * 3 + 1] += lab[i * 3 + 1];
3751
+ meanLab[c * 3 + 2] += lab[i * 3 + 2];
3752
+ centre[c * 2] += i % w;
3753
+ centre[c * 2 + 1] += Math.floor(i / w);
3754
+ size[c]++;
3755
+ }
3756
+ for (let c = 0; c < count; c++) {
3757
+ for (let j = 0; j < 3; j++) meanLab[c * 3 + j] /= size[c];
3758
+ centre[c * 2] /= size[c];
3759
+ centre[c * 2 + 1] /= size[c];
3760
+ }
3761
+ return { label, count, lab: meanLab, centre, size };
3762
+ }
3763
+ function superpixelSaliency(sp, width, height) {
3764
+ const { count, lab, centre, size, label } = sp;
3765
+ const dist = (a, b) => Math.hypot(lab[a * 3] - lab[b * 3], lab[a * 3 + 1] - lab[b * 3 + 1], lab[a * 3 + 2] - lab[b * 3 + 2]);
3766
+ const onBorder = new Uint8Array(count);
3767
+ for (let x = 0; x < width; x++) {
3768
+ onBorder[label[x]] = 1;
3769
+ onBorder[label[(height - 1) * width + x]] = 1;
3770
+ }
3771
+ for (let y = 0; y < height; y++) {
3772
+ onBorder[label[y * width]] = 1;
3773
+ onBorder[label[y * width + width - 1]] = 1;
3774
+ }
3775
+ const border = [];
3776
+ for (let c = 0; c < count; c++) if (onBorder[c]) border.push(c);
3777
+ const diag = Math.hypot(width, height);
3778
+ const unlikeBorder = new Float64Array(count);
3779
+ const contrast = new Float64Array(count);
3780
+ const centreBias = new Float64Array(count);
3781
+ for (let i = 0; i < count; i++) {
3782
+ const ds = border.map((j) => dist(i, j)).sort((a, b) => a - b);
3783
+ unlikeBorder[i] = ds.length ? ds[Math.floor(ds.length * 0.25)] : 0;
3784
+ let s = 0;
3785
+ let wsum = 0;
3786
+ for (let j = 0; j < count; j++) {
3787
+ if (j === i) continue;
3788
+ const dx = (centre[i * 2] - centre[j * 2]) / diag;
3789
+ const dy = (centre[i * 2 + 1] - centre[j * 2 + 1]) / diag;
3790
+ const wgt = size[j] * Math.exp(-(dx * dx + dy * dy) / (2 * 0.25 * 0.25));
3791
+ s += wgt * dist(i, j);
3792
+ wsum += wgt;
3793
+ }
3794
+ contrast[i] = wsum ? s / wsum : 0;
3795
+ const cx = centre[i * 2] / width - 0.5;
3796
+ const cy = centre[i * 2 + 1] / height - 0.5;
3797
+ centreBias[i] = 0.4 + 0.6 * Math.exp(-(cx * cx + cy * cy) / (2 * 0.3 * 0.3));
3798
+ }
3799
+ let farthest = 0;
3800
+ for (const v of unlikeBorder) farthest = Math.max(farthest, v);
3801
+ if (farthest < 6) return new Float64Array(count);
3802
+ const norm = (a) => {
3803
+ let max = 0;
3804
+ for (const v of a) max = Math.max(max, v);
3805
+ if (max > 0) for (let i = 0; i < a.length; i++) a[i] /= max;
3806
+ };
3807
+ norm(unlikeBorder);
3808
+ norm(contrast);
3809
+ const out = new Float64Array(count);
3810
+ for (let i = 0; i < count; i++) out[i] = unlikeBorder[i] * (0.3 + 0.7 * contrast[i]) * centreBias[i];
3811
+ norm(out);
3812
+ return out;
3813
+ }
3814
+ function otsuThreshold(values, weights) {
3815
+ const BINS2 = 64;
3816
+ const hist = new Float64Array(BINS2);
3817
+ let total = 0;
3818
+ for (let i = 0; i < values.length; i++) {
3819
+ hist[Math.min(BINS2 - 1, Math.floor(values[i] * BINS2))] += weights[i];
3820
+ total += weights[i];
3821
+ }
3822
+ let sumAll = 0;
3823
+ for (let b = 0; b < BINS2; b++) sumAll += b * hist[b];
3824
+ let wB = 0;
3825
+ let sumB = 0;
3826
+ let best = 0;
3827
+ let bestT = 0.5;
3828
+ for (let b = 0; b < BINS2 - 1; b++) {
3829
+ wB += hist[b];
3830
+ if (!wB) continue;
3831
+ const wF = total - wB;
3832
+ if (!wF) break;
3833
+ sumB += b * hist[b];
3834
+ const between = wB * wF * (sumB / wB - (sumAll - sumB) / wF) ** 2;
3835
+ if (between > best) {
3836
+ best = between;
3837
+ bestT = (b + 1) / BINS2;
3838
+ }
3839
+ }
3840
+ return bestT;
3841
+ }
3842
+ function findSubject(pixels, options = {}) {
3843
+ const work = workImage(pixels);
3844
+ const { width: w, height: h } = work;
3845
+ const sp = slicSuperpixels(work);
3846
+ const sal = superpixelSaliency(sp, w, h);
3847
+ const t = otsuThreshold(sal, sp.size);
3848
+ const n = w * h;
3849
+ const initial = new Uint8Array(n);
3850
+ const hardBg = new Uint8Array(n);
3851
+ const hardFg = new Uint8Array(n);
3852
+ const strong = t + 0.6 * (1 - t);
3853
+ for (let i = 0; i < n; i++) {
3854
+ const s = sal[sp.label[i]];
3855
+ if (s >= t) initial[i] = 1;
3856
+ if (s >= strong) hardFg[i] = 1;
3857
+ }
3858
+ for (let x = 0; x < w; x++) {
3859
+ for (const y of [0, h - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
3860
+ }
3861
+ for (let y = 0; y < h; y++) {
3862
+ for (const x of [0, w - 1]) if (sal[sp.label[y * w + x]] < t * 0.5) hardBg[y * w + x] = 1;
3863
+ }
3864
+ const empty = (reason) => ({ selection: createSelectionMask(pixels.width, pixels.height), reason });
3865
+ let guessed = 0;
3866
+ for (let i = 0; i < n; i++) guessed += initial[i];
3867
+ if (!guessed || guessed > n * 0.95) return empty("Nothing stands out from the background.");
3868
+ const roi = { x: 0, y: 0, width: w, height: h };
3869
+ const label = grabCutLabels(work, { roi, initial, hardBg, hardFg }, 5);
3870
+ keepMainPieces(label, w, h, null);
3871
+ let kept = 0;
3872
+ for (let i = 0; i < n; i++) kept += label[i];
3873
+ if (kept < n * 2e-3) return empty("Nothing stands out from the background.");
3874
+ if (kept > n * 0.97) return empty("The whole image looks like one subject; select it with select_all instead.");
3875
+ return { selection: upscaleRegion(work, pixels, roi, label, options.enhanceEdge ?? false) };
3876
+ }
3877
+
3389
3878
  // src/image-editor/layers/shape-type-ui.ts
3390
3879
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
3391
3880
  kind: "rect",
@@ -6173,7 +6662,7 @@ var ImageEditorController = class {
6173
6662
  this.state.layers = next;
6174
6663
  this.notify();
6175
6664
  }
6176
- /** The registered segmentation provider, or `null` (then `selectSubject` is unavailable). */
6665
+ /** The registered segmentation provider, or `null` (then `selectSubject` finds the most salient subject without a model). */
6177
6666
  get segmentationProvider() {
6178
6667
  return this.segmentation;
6179
6668
  }
@@ -9355,6 +9844,28 @@ var ImageEditorController = class {
9355
9844
  this.setSelection(sel, "replace");
9356
9845
  return this.state.selection ?? null;
9357
9846
  }
9847
+ /**
9848
+ * Object Selection: the object inside `target.rect` (natural pixels; what is outside the box is
9849
+ * background) or under `target.point`, found by GrabCut (colour models of object and background
9850
+ * plus a graph cut along edges), combined with the selection by `mode` as one undo step.
9851
+ * Resolves to the resulting selection.
9852
+ */
9853
+ async selectObject(target, options = {}) {
9854
+ const { width, height } = this.naturalSize;
9855
+ if ("rect" in target) {
9856
+ const r = target.rect;
9857
+ if (!(r.width > 0 && r.height > 0)) throw new Error("The box needs a positive width and height.");
9858
+ if (r.x >= width || r.y >= height || r.x + r.width <= 0 || r.y + r.height <= 0) {
9859
+ throw new Error(`The box lies outside the image (0..${width}, 0..${height}); draw it around the object.`);
9860
+ }
9861
+ } else if (!(target.point.x >= 0 && target.point.y >= 0 && target.point.x < width && target.point.y < height)) {
9862
+ throw new Error(`(${target.point.x}, ${target.point.y}) is outside the image (0..${width - 1}, 0..${height - 1}); click on the object.`);
9863
+ }
9864
+ const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
9865
+ const region = selectObjectIn(pixels, target, { enhanceEdge: options.enhanceEdge });
9866
+ this.setSelection(region, options.mode ?? "replace");
9867
+ return this.state.selection ?? null;
9868
+ }
9358
9869
  /**
9359
9870
  * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
9360
9871
  * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)
@@ -9523,11 +10034,24 @@ var ImageEditorController = class {
9523
10034
  * Select › Subject: asks the segmentation provider for the objects matching `target` ("the
9524
10035
  * dog", "the sky") in the flattened image and selects their shapes, combined with the
9525
10036
  * selection by `mode`, as one undo step. Returns what was found (labels and bounds in natural
9526
- * pixels); an empty list leaves the selection as it was. Throws without a provider.
10037
+ * pixels); an empty list leaves the selection as it was. Without a provider, the subject that
10038
+ * stands out most is found classically (superpixels, saliency, Otsu, GrabCut): `target` can't
10039
+ * choose it, and `method: 'saliency'` and a `note` say so.
9527
10040
  */
9528
- async selectSubject(target, options = {}) {
10041
+ async selectSubject(target = "", options = {}) {
9529
10042
  const provider = this.segmentation;
9530
- if (!provider) throw new Error("Select Subject needs a segmentation provider (setSegmentationProvider).");
10043
+ if (!provider) {
10044
+ const pixels = await this.requirePixels({});
10045
+ options.signal?.throwIfAborted();
10046
+ const { selection, reason } = findSubject(pixels, { enhanceEdge: options.enhanceEdge });
10047
+ if (selection.bounds) this.shapeCommand(selection, options);
10048
+ const unused = target.trim() ? ` "${target.trim()}" was not used to choose it.` : "";
10049
+ return {
10050
+ segments: selection.bounds ? [{ label: "subject", bounds: selection.bounds }] : [],
10051
+ method: "saliency",
10052
+ note: reason ?? `No segmentation model is set, so the subject that stands out most was selected.${unused}`
10053
+ };
10054
+ }
9531
10055
  const description = target.trim();
9532
10056
  if (!description) throw new Error("Describe the subject to select.");
9533
10057
  const { width, height } = this.naturalSize;
@@ -9553,7 +10077,7 @@ var ImageEditorController = class {
9553
10077
  options.signal?.throwIfAborted();
9554
10078
  }
9555
10079
  if (mask) this.shapeCommand(mask, options);
9556
- return { segments: segments.filter((s) => s.bounds) };
10080
+ return { segments: segments.filter((s) => s.bounds), method: "model" };
9557
10081
  }
9558
10082
  /** Quick Mask view (tints the unselected area). View state: no undo step. */
9559
10083
  setQuickMask(on) {
@@ -10848,7 +11372,7 @@ var ImageEditorController = class {
10848
11372
  }
10849
11373
  if (src.colorSpace !== "rgb") throw new LayerOperationError("Colours are given as #rrggbb, so the source profile must be RGB.");
10850
11374
  const t = Transform.create(src, dst, { intent: options.intent ?? "relative", bpc: options.bpc ?? true });
10851
- const toLab = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
11375
+ const toLab2 = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
10852
11376
  return colors.map((input) => {
10853
11377
  const m = /^#?([0-9a-f]{6})$/i.exec(input.trim());
10854
11378
  if (!m) throw new LayerOperationError(`Invalid colour "${input}"; use #rrggbb.`);
@@ -10858,7 +11382,7 @@ var ImageEditorController = class {
10858
11382
  t.eval(rgb, out);
10859
11383
  const values = Array.from(out.subarray(0, t.outputs), (v) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4);
10860
11384
  const l = new Float32Array(3);
10861
- toLab.eval(rgb, l);
11385
+ toLab2.eval(rgb, l);
10862
11386
  const lab = [Math.round(l[0] * 1e4) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];
10863
11387
  const res = { input, values, lab };
10864
11388
  if (dst.colorSpace === "rgb") res.hex = `#${values.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("")}`;
@@ -12242,7 +12766,7 @@ var DEPTH_MASK_CONTROLS = [
12242
12766
  // src/image-editor/playback.ts
12243
12767
  var IMAGE_PLAYBACK_KIND = "layer";
12244
12768
  var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
12245
- var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
12769
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
12246
12770
  var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|set_text_orientation|convert_text_to_shape)$/;
12247
12771
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
12248
12772
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -12365,6 +12889,7 @@ var SELECTION_TOOLS = [
12365
12889
  { id: "polygon", label: "Polygonal Lasso" },
12366
12890
  { id: "magnetic", label: "Magnetic Lasso" },
12367
12891
  { id: "quick", label: "Quick Selection" },
12892
+ { id: "object", label: "Object Selection" },
12368
12893
  { id: "range", label: "Color Range" },
12369
12894
  { id: "type", label: "Type Mask" },
12370
12895
  { id: "type-vertical", label: "Vertical Type Mask" }
@@ -12405,6 +12930,8 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
12405
12930
  quickSize: 30,
12406
12931
  quickSampleAll: true,
12407
12932
  quickEnhance: false,
12933
+ objectSampleAll: true,
12934
+ objectEnhance: false,
12408
12935
  typeText: "Type",
12409
12936
  typeSize: 72,
12410
12937
  typeFamily: "sans-serif"
@@ -12840,6 +13367,39 @@ function createImageSelectionTools(controller) {
12840
13367
  return summary(controller);
12841
13368
  }
12842
13369
  };
13370
+ const selectObjectTool = {
13371
+ name: "select_object",
13372
+ title: "Object Selection",
13373
+ description: "Object Selection: selects the object inside rect (a box drawn loosely around it, natural image pixels; what lies outside the box is treated as background) or the object under point, found from the colours of object and background and the edges between them (GrabCut). Combined with the selection by mode. A box is more reliable than a point: make it a little larger than the object on every side. Check with get_selection and capture_view, then fix with quick_select strokes or select_object with mode add / subtract. One undoable step. Returns the selection.",
13374
+ inputSchema: {
13375
+ type: "object",
13376
+ properties: {
13377
+ rect: {
13378
+ type: "object",
13379
+ properties: rectProperties,
13380
+ required: ["x", "y", "width", "height"],
13381
+ additionalProperties: false,
13382
+ description: "A box around the object (x, y, width, height in natural image pixels). Pass this or point."
13383
+ },
13384
+ point: { ...pointSchema, description: "A point on the object (natural image pixels). Pass this or rect." },
13385
+ mode: modeSchema,
13386
+ sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
13387
+ enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false)." }
13388
+ },
13389
+ additionalProperties: false
13390
+ },
13391
+ annotations: writeAnnotations("Object Selection"),
13392
+ handler: async (args) => {
13393
+ const rect = args.rect;
13394
+ const point = args.point;
13395
+ if (!rect && !point) throw new Error("Pass rect (a box around the object) or point (a point on it).");
13396
+ if (rect && point) throw new Error("Pass rect or point, not both.");
13397
+ const options = { mode: parseSelectionMode(args.mode), sampleAllLayers: args.sampleAllLayers, enhanceEdge: args.enhanceEdge };
13398
+ if (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);
13399
+ else await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
13400
+ return summary(controller);
13401
+ }
13402
+ };
12843
13403
  const magicWandTool = {
12844
13404
  name: "magic_wand",
12845
13405
  title: "Magic wand",
@@ -13393,6 +13953,8 @@ function createImageSelectionTools(controller) {
13393
13953
  selectEllipse,
13394
13954
  selectPolygonTool,
13395
13955
  quickSelectTool,
13956
+ selectObjectTool,
13957
+ createImageSubjectSelectionTool(controller),
13396
13958
  lineTool,
13397
13959
  magicWandTool,
13398
13960
  colorRangeTool,
@@ -13429,49 +13991,50 @@ function createImageSubjectSelectionTool(controller) {
13429
13991
  return {
13430
13992
  name: "select_subject",
13431
13993
  title: "Select a subject",
13432
- description: 'Select \u203A Subject: an AI segmentation model finds the objects matching target (a description such as "the dog", "the house", "the sky") in the flattened image and selects their outlines, combined with the selection by mode. Use it to cut a subject out by its contour (then layer_via_copy for a layer of just that subject); describe one object precisely to get only that one. One undoable step. Returns the selection and the objects found (label, bounds); none found leaves the selection unchanged.',
13994
+ description: 'Select \u203A Subject: selects the main subject of the flattened image, combined with the selection by mode. With a segmentation model set by the host, target (a description such as "the dog" or "the house") chooses which objects; describe one object precisely to get only that one. Without a model, the subject that stands out most from the background is found classically (superpixel saliency and GrabCut): target cannot choose it, and method "saliency" and a note say so. Then check it with capture_view and fix it with quick_select or select_object. One undoable step. Returns the selection, the objects found (label, bounds), method ("model" or "saliency") and any note; none found leaves the selection unchanged.',
13433
13995
  inputSchema: {
13434
13996
  type: "object",
13435
13997
  properties: {
13436
- target: { type: "string", minLength: 1, description: "What to select, in words." },
13998
+ target: { type: "string", description: "What to select, in words. Needed with a model; ignored without one." },
13437
13999
  maxSegments: {
13438
14000
  type: "integer",
13439
14001
  minimum: 1,
13440
14002
  maximum: 32,
13441
- description: "Select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
14003
+ description: "With a model: select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
13442
14004
  },
13443
14005
  mode: modeSchema,
13444
- feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
14006
+ feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." },
14007
+ enhanceEdge: { type: "boolean", description: "Without a model: smoother, less blocky edges (Auto-Enhance; default false)." }
13445
14008
  },
13446
- required: ["target"],
13447
14009
  additionalProperties: false
13448
14010
  },
13449
14011
  annotations: { ...writeAnnotations("Select a subject"), openWorldHint: true },
13450
- handler: async ({ target, maxSegments, mode, feather }, context) => {
13451
- if (typeof target !== "string" || !target.trim()) throw new Error("target must describe what to select.");
14012
+ handler: async ({ target, maxSegments, mode, feather, enhanceEdge }, context) => {
14013
+ if (target !== void 0 && typeof target !== "string") throw new Error("target must be a description in words.");
14014
+ if (controller.segmentationProvider && !String(target ?? "").trim()) throw new Error("target must describe what to select (a model is set, and it selects by description).");
13452
14015
  const perform = context?.perform;
13453
- const { segments } = await controller.selectSubject(target, {
14016
+ const { segments, method, note } = await controller.selectSubject(String(target ?? ""), {
13454
14017
  mode: parseSelectionMode(mode),
13455
14018
  feather: num2(feather, 0),
13456
14019
  maxSegments: typeof maxSegments === "number" ? Math.max(1, Math.round(maxSegments)) : void 0,
14020
+ enhanceEdge: enhanceEdge === true,
13457
14021
  signal: context?.signal,
13458
14022
  // Under playback the cursor traces what the model found, as a lasso, before it is selected.
13459
14023
  beforeApply: perform ? (outlines) => perform(planOutlineTrace(outlines)) : void 0
13460
14024
  });
13461
- return { ...summary(controller), found: segments };
14025
+ return { ...summary(controller), found: segments, method, ...note ? { note } : {} };
13462
14026
  }
13463
14027
  };
13464
14028
  }
13465
14029
  function createImageSegmentationToolProvider(controller, options = {}) {
13466
- const tool = createImageSubjectSelectionTool(controller);
13467
14030
  return withImagePlayback(
13468
14031
  {
13469
14032
  id: options.id ?? "image-segmentation",
13470
14033
  namespace: options.namespace,
13471
14034
  title: "Image segmentation",
13472
- description: "Select \u203A Subject: select objects in the image by description through an AI segmentation model.",
14035
+ description: "Deprecated: select_subject is in the image editor tool provider.",
13473
14036
  skill: "pixerate-image-editor",
13474
- tools: () => controller.segmentationProvider ? [tool] : []
14037
+ tools: () => []
13475
14038
  },
13476
14039
  options.playbackSurface
13477
14040
  );
@@ -14784,7 +15347,7 @@ var SelectionGesture = class {
14784
15347
  get active() {
14785
15348
  return this.start !== null || this.polygon !== null || this.magnetic !== null;
14786
15349
  }
14787
- /** Resolves once a Quick Selection stroke has been applied. */
15350
+ /** Resolves once a Quick Selection stroke or an Object Selection has been applied. */
14788
15351
  get settled() {
14789
15352
  return this.pending;
14790
15353
  }
@@ -14954,6 +15517,20 @@ var SelectionGesture = class {
14954
15517
  if (point) this.move(point);
14955
15518
  const options = this.getOptions();
14956
15519
  const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
15520
+ if (options.tool === "object") {
15521
+ const rect2 = normalizeRect({ x: this.start.x, y: this.start.y, width: this.current.x - this.start.x, height: this.current.y - this.start.y });
15522
+ this.start = null;
15523
+ this.current = null;
15524
+ this.points = [];
15525
+ const target = rect2.width >= 4 && rect2.height >= 4 ? { rect: rect2 } : { point: { x: rect2.x + rect2.width / 2, y: rect2.y + rect2.height / 2 } };
15526
+ this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
15527
+ () => void 0,
15528
+ (err) => {
15529
+ this.error = err instanceof Error ? err.message : String(err);
15530
+ }
15531
+ );
15532
+ return false;
15533
+ }
14957
15534
  if (options.tool === "quick") {
14958
15535
  const stroke = this.points;
14959
15536
  this.start = null;
@@ -19600,6 +20177,7 @@ export {
19600
20177
  COLOR_MODE_OPTIONS,
19601
20178
  COLOR_RANGE_PRESETS,
19602
20179
  COLOR_RANGE_PRESET_LABELS,
20180
+ ColorGmm,
19603
20181
  CoverageMap,
19604
20182
  D50,
19605
20183
  D65,
@@ -19652,6 +20230,7 @@ export {
19652
20230
  FREE_TRANSFORM_MODE_OPTIONS,
19653
20231
  FreeTransformSession,
19654
20232
  FreeTransformTool,
20233
+ GAMMA,
19655
20234
  GRAIN_PATTERNS,
19656
20235
  GRAIN_TILE,
19657
20236
  GUIDED_FILTER_EPS,
@@ -19810,6 +20389,7 @@ export {
19810
20389
  TransformTooLargeError,
19811
20390
  TransformedPixelSource,
19812
20391
  TypeToolGesture,
20392
+ UNITS,
19813
20393
  U_MAX,
19814
20394
  VIBRANCE_GREY,
19815
20395
  W9,
@@ -19893,6 +20473,7 @@ export {
19893
20473
  blenderDab,
19894
20474
  blurFilter,
19895
20475
  borderCoverage,
20476
+ boxBlur,
19896
20477
  boxBlurPixels,
19897
20478
  boxFrameHit,
19898
20479
  boxFrameOverlay,
@@ -20087,6 +20668,7 @@ export {
20087
20668
  findAdjustmentControl,
20088
20669
  findLayer,
20089
20670
  findLayerControl,
20671
+ findSubject,
20090
20672
  findWebGpu,
20091
20673
  finishTile,
20092
20674
  fitParagraphRuns,
@@ -20108,6 +20690,7 @@ export {
20108
20690
  getViewportMetrics,
20109
20691
  getViewportProjection,
20110
20692
  globalImageCache,
20693
+ grabCutLabels,
20111
20694
  gradientDabColor,
20112
20695
  grainCatch,
20113
20696
  grainHeights,
@@ -20199,6 +20782,7 @@ export {
20199
20782
  isTransformAnchor,
20200
20783
  isValidWarp,
20201
20784
  keepDeepSamples,
20785
+ keepMainPieces,
20202
20786
  kelvinToTemperature,
20203
20787
  keysWeight,
20204
20788
  latticeViscosity,
@@ -20311,6 +20895,7 @@ export {
20311
20895
  offsetVectorMask,
20312
20896
  openMaskFile,
20313
20897
  openPsdFile,
20898
+ otsuThreshold,
20314
20899
  packRgba,
20315
20900
  packWetParams,
20316
20901
  paintGradientMappingFor,
@@ -20454,6 +21039,7 @@ export {
20454
21039
  segmentToSelection,
20455
21040
  segmentationOutlines,
20456
21041
  segmentsToSelection,
21042
+ selectObjectIn,
20457
21043
  selectSimilar,
20458
21044
  selectionArea,
20459
21045
  selectionFromCoverage,
@@ -20499,10 +21085,12 @@ export {
20499
21085
  shiftCoverage,
20500
21086
  simplifyPixelPath,
20501
21087
  skinToneRange,
21088
+ slicSuperpixels,
20502
21089
  smartFilterRows,
20503
21090
  smartObjectLayers,
20504
21091
  smoothCoverage,
20505
21092
  smoothPoints,
21093
+ smoothnessWeights,
20506
21094
  smudgeEffect,
20507
21095
  snapPolygonToEdges,
20508
21096
  snapTo45,
@@ -20528,6 +21116,7 @@ export {
20528
21116
  styleAt,
20529
21117
  subpathsToSvgPath,
20530
21118
  sumHistograms,
21119
+ superpixelSaliency,
20531
21120
  surfaceBytes,
20532
21121
  surfaceDepth,
20533
21122
  surfaceFromDeepPixels,
@@ -20589,6 +21178,7 @@ export {
20589
21178
  updateShapeLayer,
20590
21179
  updateSmartFilter,
20591
21180
  updateTypeLayer,
21181
+ upscaleRegion,
20592
21182
  userErrorMessage,
20593
21183
  uvToXy,
20594
21184
  validateEffectGradient,