@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.
- package/dist/image-editor/controller.d.cts +26 -3
- package/dist/image-editor/controller.d.ts +26 -3
- package/dist/image-editor/index.cjs +1157 -567
- package/dist/image-editor/index.js +755 -165
- package/dist/image-editor/selection/gesture.d.cts +3 -1
- package/dist/image-editor/selection/gesture.d.ts +3 -1
- package/dist/image-editor/selection/segment/grabcut.d.cts +63 -0
- package/dist/image-editor/selection/segment/grabcut.d.ts +63 -0
- package/dist/image-editor/selection/segment/index.d.cts +3 -0
- package/dist/image-editor/selection/segment/index.d.ts +3 -0
- package/dist/image-editor/selection/segment/quick.d.cts +0 -19
- package/dist/image-editor/selection/segment/quick.d.ts +0 -19
- package/dist/image-editor/selection/segment/subject.d.cts +44 -0
- package/dist/image-editor/selection/segment/subject.d.ts +44 -0
- package/dist/image-editor/selection/segment/work.d.cts +54 -0
- package/dist/image-editor/selection/segment/work.d.ts +54 -0
- package/dist/image-editor/selection/tool-options.d.cts +5 -1
- package/dist/image-editor/selection/tool-options.d.ts +5 -1
- package/dist/image-editor/selection-tools.d.cts +3 -3
- package/dist/image-editor/selection-tools.d.ts +3 -3
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +2 -1
- package/skills/pixerate-image-selection-masking/SKILL.md +11 -2
|
@@ -3045,7 +3045,7 @@ var GridGraph = class {
|
|
|
3045
3045
|
}
|
|
3046
3046
|
};
|
|
3047
3047
|
|
|
3048
|
-
// src/image-editor/selection/segment/
|
|
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
|
-
|
|
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
|
-
|
|
3193
|
-
if (
|
|
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`
|
|
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.
|
|
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)
|
|
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
|
|
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
|
-
|
|
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:
|
|
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",
|
|
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: "
|
|
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"
|
|
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: "
|
|
14035
|
+
description: "Deprecated: select_subject is in the image editor tool provider.",
|
|
13473
14036
|
skill: "pixerate-image-editor",
|
|
13474
|
-
tools: () =>
|
|
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,
|