@pixerate/editor 0.197.0 → 0.198.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,283 @@ 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
+
3389
3675
  // src/image-editor/layers/shape-type-ui.ts
3390
3676
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
3391
3677
  kind: "rect",
@@ -9355,6 +9641,28 @@ var ImageEditorController = class {
9355
9641
  this.setSelection(sel, "replace");
9356
9642
  return this.state.selection ?? null;
9357
9643
  }
9644
+ /**
9645
+ * Object Selection: the object inside `target.rect` (natural pixels; what is outside the box is
9646
+ * background) or under `target.point`, found by GrabCut (colour models of object and background
9647
+ * plus a graph cut along edges), combined with the selection by `mode` as one undo step.
9648
+ * Resolves to the resulting selection.
9649
+ */
9650
+ async selectObject(target, options = {}) {
9651
+ const { width, height } = this.naturalSize;
9652
+ if ("rect" in target) {
9653
+ const r = target.rect;
9654
+ if (!(r.width > 0 && r.height > 0)) throw new Error("The box needs a positive width and height.");
9655
+ if (r.x >= width || r.y >= height || r.x + r.width <= 0 || r.y + r.height <= 0) {
9656
+ throw new Error(`The box lies outside the image (0..${width}, 0..${height}); draw it around the object.`);
9657
+ }
9658
+ } else if (!(target.point.x >= 0 && target.point.y >= 0 && target.point.x < width && target.point.y < height)) {
9659
+ throw new Error(`(${target.point.x}, ${target.point.y}) is outside the image (0..${width - 1}, 0..${height - 1}); click on the object.`);
9660
+ }
9661
+ const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
9662
+ const region = selectObjectIn(pixels, target, { enhanceEdge: options.enhanceEdge });
9663
+ this.setSelection(region, options.mode ?? "replace");
9664
+ return this.state.selection ?? null;
9665
+ }
9358
9666
  /**
9359
9667
  * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
9360
9668
  * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)
@@ -12242,7 +12550,7 @@ var DEPTH_MASK_CONTROLS = [
12242
12550
  // src/image-editor/playback.ts
12243
12551
  var IMAGE_PLAYBACK_KIND = "layer";
12244
12552
  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)$/;
12553
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
12246
12554
  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
12555
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
12248
12556
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -12365,6 +12673,7 @@ var SELECTION_TOOLS = [
12365
12673
  { id: "polygon", label: "Polygonal Lasso" },
12366
12674
  { id: "magnetic", label: "Magnetic Lasso" },
12367
12675
  { id: "quick", label: "Quick Selection" },
12676
+ { id: "object", label: "Object Selection" },
12368
12677
  { id: "range", label: "Color Range" },
12369
12678
  { id: "type", label: "Type Mask" },
12370
12679
  { id: "type-vertical", label: "Vertical Type Mask" }
@@ -12405,6 +12714,8 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
12405
12714
  quickSize: 30,
12406
12715
  quickSampleAll: true,
12407
12716
  quickEnhance: false,
12717
+ objectSampleAll: true,
12718
+ objectEnhance: false,
12408
12719
  typeText: "Type",
12409
12720
  typeSize: 72,
12410
12721
  typeFamily: "sans-serif"
@@ -12840,6 +13151,39 @@ function createImageSelectionTools(controller) {
12840
13151
  return summary(controller);
12841
13152
  }
12842
13153
  };
13154
+ const selectObjectTool = {
13155
+ name: "select_object",
13156
+ title: "Object Selection",
13157
+ 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.",
13158
+ inputSchema: {
13159
+ type: "object",
13160
+ properties: {
13161
+ rect: {
13162
+ type: "object",
13163
+ properties: rectProperties,
13164
+ required: ["x", "y", "width", "height"],
13165
+ additionalProperties: false,
13166
+ description: "A box around the object (x, y, width, height in natural image pixels). Pass this or point."
13167
+ },
13168
+ point: { ...pointSchema, description: "A point on the object (natural image pixels). Pass this or rect." },
13169
+ mode: modeSchema,
13170
+ sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
13171
+ enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false)." }
13172
+ },
13173
+ additionalProperties: false
13174
+ },
13175
+ annotations: writeAnnotations("Object Selection"),
13176
+ handler: async (args) => {
13177
+ const rect = args.rect;
13178
+ const point = args.point;
13179
+ if (!rect && !point) throw new Error("Pass rect (a box around the object) or point (a point on it).");
13180
+ if (rect && point) throw new Error("Pass rect or point, not both.");
13181
+ const options = { mode: parseSelectionMode(args.mode), sampleAllLayers: args.sampleAllLayers, enhanceEdge: args.enhanceEdge };
13182
+ 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);
13183
+ else await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
13184
+ return summary(controller);
13185
+ }
13186
+ };
12843
13187
  const magicWandTool = {
12844
13188
  name: "magic_wand",
12845
13189
  title: "Magic wand",
@@ -13393,6 +13737,7 @@ function createImageSelectionTools(controller) {
13393
13737
  selectEllipse,
13394
13738
  selectPolygonTool,
13395
13739
  quickSelectTool,
13740
+ selectObjectTool,
13396
13741
  lineTool,
13397
13742
  magicWandTool,
13398
13743
  colorRangeTool,
@@ -14784,7 +15129,7 @@ var SelectionGesture = class {
14784
15129
  get active() {
14785
15130
  return this.start !== null || this.polygon !== null || this.magnetic !== null;
14786
15131
  }
14787
- /** Resolves once a Quick Selection stroke has been applied. */
15132
+ /** Resolves once a Quick Selection stroke or an Object Selection has been applied. */
14788
15133
  get settled() {
14789
15134
  return this.pending;
14790
15135
  }
@@ -14954,6 +15299,20 @@ var SelectionGesture = class {
14954
15299
  if (point) this.move(point);
14955
15300
  const options = this.getOptions();
14956
15301
  const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
15302
+ if (options.tool === "object") {
15303
+ 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 });
15304
+ this.start = null;
15305
+ this.current = null;
15306
+ this.points = [];
15307
+ const target = rect2.width >= 4 && rect2.height >= 4 ? { rect: rect2 } : { point: { x: rect2.x + rect2.width / 2, y: rect2.y + rect2.height / 2 } };
15308
+ this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
15309
+ () => void 0,
15310
+ (err) => {
15311
+ this.error = err instanceof Error ? err.message : String(err);
15312
+ }
15313
+ );
15314
+ return false;
15315
+ }
14957
15316
  if (options.tool === "quick") {
14958
15317
  const stroke = this.points;
14959
15318
  this.start = null;
@@ -19600,6 +19959,7 @@ export {
19600
19959
  COLOR_MODE_OPTIONS,
19601
19960
  COLOR_RANGE_PRESETS,
19602
19961
  COLOR_RANGE_PRESET_LABELS,
19962
+ ColorGmm,
19603
19963
  CoverageMap,
19604
19964
  D50,
19605
19965
  D65,
@@ -19652,6 +20012,7 @@ export {
19652
20012
  FREE_TRANSFORM_MODE_OPTIONS,
19653
20013
  FreeTransformSession,
19654
20014
  FreeTransformTool,
20015
+ GAMMA,
19655
20016
  GRAIN_PATTERNS,
19656
20017
  GRAIN_TILE,
19657
20018
  GUIDED_FILTER_EPS,
@@ -19810,6 +20171,7 @@ export {
19810
20171
  TransformTooLargeError,
19811
20172
  TransformedPixelSource,
19812
20173
  TypeToolGesture,
20174
+ UNITS,
19813
20175
  U_MAX,
19814
20176
  VIBRANCE_GREY,
19815
20177
  W9,
@@ -19893,6 +20255,7 @@ export {
19893
20255
  blenderDab,
19894
20256
  blurFilter,
19895
20257
  borderCoverage,
20258
+ boxBlur,
19896
20259
  boxBlurPixels,
19897
20260
  boxFrameHit,
19898
20261
  boxFrameOverlay,
@@ -20454,6 +20817,7 @@ export {
20454
20817
  segmentToSelection,
20455
20818
  segmentationOutlines,
20456
20819
  segmentsToSelection,
20820
+ selectObjectIn,
20457
20821
  selectSimilar,
20458
20822
  selectionArea,
20459
20823
  selectionFromCoverage,
@@ -20503,6 +20867,7 @@ export {
20503
20867
  smartObjectLayers,
20504
20868
  smoothCoverage,
20505
20869
  smoothPoints,
20870
+ smoothnessWeights,
20506
20871
  smudgeEffect,
20507
20872
  snapPolygonToEdges,
20508
20873
  snapTo45,
@@ -20589,6 +20954,7 @@ export {
20589
20954
  updateShapeLayer,
20590
20955
  updateSmartFilter,
20591
20956
  updateTypeLayer,
20957
+ upscaleRegion,
20592
20958
  userErrorMessage,
20593
20959
  uvToXy,
20594
20960
  validateEffectGradient,