@pixerate/editor 0.196.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.
- package/dist/image-editor/controller.d.cts +35 -0
- package/dist/image-editor/controller.d.ts +35 -0
- package/dist/image-editor/index.cjs +1450 -406
- package/dist/image-editor/index.js +1050 -6
- package/dist/image-editor/selection/gesture.d.cts +8 -0
- package/dist/image-editor/selection/gesture.d.ts +8 -0
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/selection/segment/grabcut.d.cts +45 -0
- package/dist/image-editor/selection/segment/grabcut.d.ts +45 -0
- package/dist/image-editor/selection/segment/index.d.cts +4 -0
- package/dist/image-editor/selection/segment/index.d.ts +4 -0
- package/dist/image-editor/selection/segment/maxflow.d.cts +30 -0
- package/dist/image-editor/selection/segment/maxflow.d.ts +30 -0
- package/dist/image-editor/selection/segment/quick.d.cts +28 -0
- package/dist/image-editor/selection/segment/quick.d.ts +28 -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 +11 -1
- package/dist/image-editor/selection/tool-options.d.ts +11 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +7 -1
|
@@ -2803,6 +2803,875 @@ function magneticAnchorSpacing(frequency = 57) {
|
|
|
2803
2803
|
return 8 + (100 - f);
|
|
2804
2804
|
}
|
|
2805
2805
|
|
|
2806
|
+
// src/image-editor/selection/segment/maxflow.ts
|
|
2807
|
+
var TERMINAL = -2;
|
|
2808
|
+
var NONE = -1;
|
|
2809
|
+
var FREE = 0;
|
|
2810
|
+
var SOURCE = 1;
|
|
2811
|
+
var SINK = 2;
|
|
2812
|
+
var GridGraph = class {
|
|
2813
|
+
width;
|
|
2814
|
+
height;
|
|
2815
|
+
/** Residual capacity of the edge leaving node `p` in direction `d`, at `p * 4 + d`. */
|
|
2816
|
+
cap;
|
|
2817
|
+
/** Net terminal capacity: above 0 to the source, below 0 to the sink. */
|
|
2818
|
+
tr;
|
|
2819
|
+
tree;
|
|
2820
|
+
constructor(width, height) {
|
|
2821
|
+
this.width = width;
|
|
2822
|
+
this.height = height;
|
|
2823
|
+
const n = width * height;
|
|
2824
|
+
this.cap = new Float64Array(n * 4);
|
|
2825
|
+
this.tr = new Float64Array(n);
|
|
2826
|
+
this.tree = new Uint8Array(n);
|
|
2827
|
+
}
|
|
2828
|
+
/** Sets the capacity of the edge between `p` and its neighbour in direction `d`, both ways. */
|
|
2829
|
+
setEdge(p, d, capacity) {
|
|
2830
|
+
const q = d === 0 ? p + 1 : p + this.width;
|
|
2831
|
+
this.cap[p * 4 + d] = capacity;
|
|
2832
|
+
this.cap[q * 4 + (d ^ 2)] = capacity;
|
|
2833
|
+
}
|
|
2834
|
+
/** Adds `source` (cost of putting `p` on the sink side) and `sink` (the other way) to `p`. */
|
|
2835
|
+
addTerminal(p, source, sink) {
|
|
2836
|
+
this.tr[p] += source - sink;
|
|
2837
|
+
}
|
|
2838
|
+
/** Whether `p` is on the source side of the minimum cut (after {@link maxflow}). */
|
|
2839
|
+
inSource(p) {
|
|
2840
|
+
return this.tree[p] === SOURCE;
|
|
2841
|
+
}
|
|
2842
|
+
/** Runs max-flow and returns the flow (the cut's cost, less the constant terminal parts). */
|
|
2843
|
+
maxflow() {
|
|
2844
|
+
const { width: w, height: h, cap, tr, tree } = this;
|
|
2845
|
+
const n = w * h;
|
|
2846
|
+
const nb = (p, d) => {
|
|
2847
|
+
const x = p % w;
|
|
2848
|
+
if (d === 0) return x < w - 1 ? p + 1 : -1;
|
|
2849
|
+
if (d === 1) return p + w < n ? p + w : -1;
|
|
2850
|
+
if (d === 2) return x > 0 ? p - 1 : -1;
|
|
2851
|
+
return p - w >= 0 ? p - w : -1;
|
|
2852
|
+
};
|
|
2853
|
+
const parent = new Int8Array(n).fill(NONE);
|
|
2854
|
+
const ts = new Int32Array(n);
|
|
2855
|
+
const dist = new Int32Array(n);
|
|
2856
|
+
const queue = new Int32Array(n + 1);
|
|
2857
|
+
const queued = new Uint8Array(n);
|
|
2858
|
+
let qHead = 0;
|
|
2859
|
+
let qTail = 0;
|
|
2860
|
+
const activate = (p) => {
|
|
2861
|
+
if (queued[p]) return;
|
|
2862
|
+
queued[p] = 1;
|
|
2863
|
+
queue[qTail] = p;
|
|
2864
|
+
qTail = qTail === n ? 0 : qTail + 1;
|
|
2865
|
+
};
|
|
2866
|
+
const nextActive = () => {
|
|
2867
|
+
while (qHead !== qTail) {
|
|
2868
|
+
const p = queue[qHead];
|
|
2869
|
+
qHead = qHead === n ? 0 : qHead + 1;
|
|
2870
|
+
queued[p] = 0;
|
|
2871
|
+
if (tree[p] !== FREE) return p;
|
|
2872
|
+
}
|
|
2873
|
+
return -1;
|
|
2874
|
+
};
|
|
2875
|
+
let orphans = new Int32Array(64);
|
|
2876
|
+
let orphanCount = 0;
|
|
2877
|
+
const orphan = (p) => {
|
|
2878
|
+
parent[p] = NONE;
|
|
2879
|
+
if (orphanCount === orphans.length) {
|
|
2880
|
+
const grown = new Int32Array(orphans.length * 2);
|
|
2881
|
+
grown.set(orphans);
|
|
2882
|
+
orphans = grown;
|
|
2883
|
+
}
|
|
2884
|
+
orphans[orphanCount++] = p;
|
|
2885
|
+
};
|
|
2886
|
+
tree.fill(FREE);
|
|
2887
|
+
for (let p = 0; p < n; p++) {
|
|
2888
|
+
if (tr[p] > 0) tree[p] = SOURCE;
|
|
2889
|
+
else if (tr[p] < 0) tree[p] = SINK;
|
|
2890
|
+
else continue;
|
|
2891
|
+
parent[p] = TERMINAL;
|
|
2892
|
+
dist[p] = 1;
|
|
2893
|
+
activate(p);
|
|
2894
|
+
}
|
|
2895
|
+
let flow = 0;
|
|
2896
|
+
let time = 0;
|
|
2897
|
+
let current = -1;
|
|
2898
|
+
for (; ; ) {
|
|
2899
|
+
if (current < 0 || tree[current] === FREE) {
|
|
2900
|
+
current = nextActive();
|
|
2901
|
+
if (current < 0) break;
|
|
2902
|
+
}
|
|
2903
|
+
const p = current;
|
|
2904
|
+
const side = tree[p];
|
|
2905
|
+
let s = -1;
|
|
2906
|
+
let t = -1;
|
|
2907
|
+
let dir = 0;
|
|
2908
|
+
for (let d = 0; d < 4; d++) {
|
|
2909
|
+
const q = nb(p, d);
|
|
2910
|
+
if (q < 0) continue;
|
|
2911
|
+
if (side === SOURCE ? cap[p * 4 + d] <= 0 : cap[q * 4 + (d ^ 2)] <= 0) continue;
|
|
2912
|
+
if (tree[q] === FREE) {
|
|
2913
|
+
tree[q] = side;
|
|
2914
|
+
parent[q] = d ^ 2;
|
|
2915
|
+
ts[q] = ts[p];
|
|
2916
|
+
dist[q] = dist[p] + 1;
|
|
2917
|
+
activate(q);
|
|
2918
|
+
} else if (tree[q] !== side) {
|
|
2919
|
+
if (side === SOURCE) {
|
|
2920
|
+
s = p;
|
|
2921
|
+
t = q;
|
|
2922
|
+
dir = d;
|
|
2923
|
+
} else {
|
|
2924
|
+
s = q;
|
|
2925
|
+
t = p;
|
|
2926
|
+
dir = d ^ 2;
|
|
2927
|
+
}
|
|
2928
|
+
break;
|
|
2929
|
+
} else if (ts[q] <= ts[p] && dist[q] > dist[p]) {
|
|
2930
|
+
parent[q] = d ^ 2;
|
|
2931
|
+
ts[q] = ts[p];
|
|
2932
|
+
dist[q] = dist[p] + 1;
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
if (s < 0) {
|
|
2936
|
+
current = -1;
|
|
2937
|
+
continue;
|
|
2938
|
+
}
|
|
2939
|
+
time++;
|
|
2940
|
+
let f = cap[s * 4 + dir];
|
|
2941
|
+
for (let x = s; ; ) {
|
|
2942
|
+
const a = parent[x];
|
|
2943
|
+
if (a === TERMINAL) {
|
|
2944
|
+
f = Math.min(f, tr[x]);
|
|
2945
|
+
break;
|
|
2946
|
+
}
|
|
2947
|
+
const up = nb(x, a);
|
|
2948
|
+
f = Math.min(f, cap[up * 4 + (a ^ 2)]);
|
|
2949
|
+
x = up;
|
|
2950
|
+
}
|
|
2951
|
+
for (let x = t; ; ) {
|
|
2952
|
+
const a = parent[x];
|
|
2953
|
+
if (a === TERMINAL) {
|
|
2954
|
+
f = Math.min(f, -tr[x]);
|
|
2955
|
+
break;
|
|
2956
|
+
}
|
|
2957
|
+
f = Math.min(f, cap[x * 4 + a]);
|
|
2958
|
+
x = nb(x, a);
|
|
2959
|
+
}
|
|
2960
|
+
cap[s * 4 + dir] -= f;
|
|
2961
|
+
cap[t * 4 + (dir ^ 2)] += f;
|
|
2962
|
+
for (let x = s; ; ) {
|
|
2963
|
+
const a = parent[x];
|
|
2964
|
+
if (a === TERMINAL) {
|
|
2965
|
+
tr[x] -= f;
|
|
2966
|
+
if (tr[x] <= 0) orphan(x);
|
|
2967
|
+
break;
|
|
2968
|
+
}
|
|
2969
|
+
const up = nb(x, a);
|
|
2970
|
+
cap[up * 4 + (a ^ 2)] -= f;
|
|
2971
|
+
cap[x * 4 + a] += f;
|
|
2972
|
+
if (cap[up * 4 + (a ^ 2)] <= 0) orphan(x);
|
|
2973
|
+
x = up;
|
|
2974
|
+
}
|
|
2975
|
+
for (let x = t; ; ) {
|
|
2976
|
+
const a = parent[x];
|
|
2977
|
+
if (a === TERMINAL) {
|
|
2978
|
+
tr[x] += f;
|
|
2979
|
+
if (tr[x] >= 0) orphan(x);
|
|
2980
|
+
break;
|
|
2981
|
+
}
|
|
2982
|
+
const up = nb(x, a);
|
|
2983
|
+
cap[x * 4 + a] -= f;
|
|
2984
|
+
cap[up * 4 + (a ^ 2)] += f;
|
|
2985
|
+
if (cap[x * 4 + a] <= 0) orphan(x);
|
|
2986
|
+
x = up;
|
|
2987
|
+
}
|
|
2988
|
+
flow += f;
|
|
2989
|
+
while (orphanCount) {
|
|
2990
|
+
const o = orphans[--orphanCount];
|
|
2991
|
+
const oSide = tree[o];
|
|
2992
|
+
let best = NONE;
|
|
2993
|
+
let bestDist = Infinity;
|
|
2994
|
+
for (let d = 0; d < 4; d++) {
|
|
2995
|
+
const q = nb(o, d);
|
|
2996
|
+
if (q < 0 || tree[q] !== oSide) continue;
|
|
2997
|
+
if (oSide === SOURCE ? cap[q * 4 + (d ^ 2)] <= 0 : cap[o * 4 + d] <= 0) continue;
|
|
2998
|
+
let k = 0;
|
|
2999
|
+
let x = q;
|
|
3000
|
+
for (; ; ) {
|
|
3001
|
+
if (ts[x] === time) {
|
|
3002
|
+
k += dist[x];
|
|
3003
|
+
break;
|
|
3004
|
+
}
|
|
3005
|
+
const a = parent[x];
|
|
3006
|
+
k++;
|
|
3007
|
+
if (a === TERMINAL) {
|
|
3008
|
+
ts[x] = time;
|
|
3009
|
+
dist[x] = 1;
|
|
3010
|
+
break;
|
|
3011
|
+
}
|
|
3012
|
+
if (a === NONE) {
|
|
3013
|
+
k = Infinity;
|
|
3014
|
+
break;
|
|
3015
|
+
}
|
|
3016
|
+
x = nb(x, a);
|
|
3017
|
+
}
|
|
3018
|
+
if (k === Infinity) continue;
|
|
3019
|
+
if (k < bestDist) {
|
|
3020
|
+
bestDist = k;
|
|
3021
|
+
best = d;
|
|
3022
|
+
}
|
|
3023
|
+
for (let y = q, dd = k; ts[y] !== time; y = nb(y, parent[y]), dd--) {
|
|
3024
|
+
ts[y] = time;
|
|
3025
|
+
dist[y] = dd;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
if (best !== NONE) {
|
|
3029
|
+
parent[o] = best;
|
|
3030
|
+
ts[o] = time;
|
|
3031
|
+
dist[o] = bestDist + 1;
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
for (let d = 0; d < 4; d++) {
|
|
3035
|
+
const q = nb(o, d);
|
|
3036
|
+
if (q < 0 || tree[q] !== oSide) continue;
|
|
3037
|
+
if (oSide === SOURCE ? cap[q * 4 + (d ^ 2)] > 0 : cap[o * 4 + d] > 0) activate(q);
|
|
3038
|
+
const a = parent[q];
|
|
3039
|
+
if (a !== TERMINAL && a !== NONE && nb(q, a) === o) orphan(q);
|
|
3040
|
+
}
|
|
3041
|
+
tree[o] = FREE;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
return flow;
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
|
|
3048
|
+
// src/image-editor/selection/segment/work.ts
|
|
3049
|
+
var WORK_SIZE = 512;
|
|
3050
|
+
var workCache = /* @__PURE__ */ new WeakMap();
|
|
3051
|
+
function workImage(pixels, maxSide = WORK_SIZE) {
|
|
3052
|
+
const cached = workCache.get(pixels.data);
|
|
3053
|
+
if (cached && cached.maxSide === maxSide && cached.work.sourceWidth === pixels.width && cached.work.sourceHeight === pixels.height) return cached.work;
|
|
3054
|
+
const { width: W, height: H, data } = pixels;
|
|
3055
|
+
const scale = Math.min(1, maxSide / Math.max(W, H));
|
|
3056
|
+
const w = Math.max(1, Math.round(W * scale));
|
|
3057
|
+
const h = Math.max(1, Math.round(H * scale));
|
|
3058
|
+
const sum = new Float64Array(w * h * 3);
|
|
3059
|
+
const count = new Float64Array(w * h);
|
|
3060
|
+
const xs = new Int32Array(W);
|
|
3061
|
+
for (let x = 0; x < W; x++) xs[x] = Math.min(w - 1, Math.floor(x * w / W));
|
|
3062
|
+
for (let y = 0; y < H; y++) {
|
|
3063
|
+
const row = Math.min(h - 1, Math.floor(y * h / H)) * w;
|
|
3064
|
+
for (let x = 0, p = y * W * 4; x < W; x++, p += 4) {
|
|
3065
|
+
const o = row + xs[x];
|
|
3066
|
+
const a = data[p + 3] / 255;
|
|
3067
|
+
const k = o * 3;
|
|
3068
|
+
sum[k] += data[p] * a + 255 * (1 - a);
|
|
3069
|
+
sum[k + 1] += data[p + 1] * a + 255 * (1 - a);
|
|
3070
|
+
sum[k + 2] += data[p + 2] * a + 255 * (1 - a);
|
|
3071
|
+
count[o]++;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
const rgb = new Float32Array(w * h * 3);
|
|
3075
|
+
for (let i = 0; i < w * h; i++) {
|
|
3076
|
+
const c = count[i] || 1;
|
|
3077
|
+
rgb[i * 3] = sum[i * 3] / c;
|
|
3078
|
+
rgb[i * 3 + 1] = sum[i * 3 + 1] / c;
|
|
3079
|
+
rgb[i * 3 + 2] = sum[i * 3 + 2] / c;
|
|
3080
|
+
}
|
|
3081
|
+
const work = { sourceWidth: W, sourceHeight: H, width: w, height: h, scale: w / W, rgb };
|
|
3082
|
+
workCache.set(pixels.data, { maxSide, work });
|
|
3083
|
+
return work;
|
|
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
|
|
3234
|
+
var BINS = 16;
|
|
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);
|
|
3236
|
+
function costTable(hist, total) {
|
|
3237
|
+
let a = hist;
|
|
3238
|
+
for (const stride of [1, BINS, BINS * BINS]) {
|
|
3239
|
+
const b = new Float64Array(a.length);
|
|
3240
|
+
for (let i = 0; i < a.length; i++) {
|
|
3241
|
+
const pos = Math.floor(i / stride) % BINS;
|
|
3242
|
+
const lo = pos > 0 ? a[i - stride] : a[i];
|
|
3243
|
+
const hi = pos < BINS - 1 ? a[i + stride] : a[i];
|
|
3244
|
+
b[i] = (lo + 2 * a[i] + hi) / 4;
|
|
3245
|
+
}
|
|
3246
|
+
a = b;
|
|
3247
|
+
}
|
|
3248
|
+
const out = new Float64Array(a.length);
|
|
3249
|
+
const floor = 1e-4;
|
|
3250
|
+
const denom = Math.max(1, total) * (1 + floor * a.length);
|
|
3251
|
+
for (let i = 0; i < a.length; i++) out[i] = -Math.log((a[i] + floor * Math.max(1, total)) / denom);
|
|
3252
|
+
return out;
|
|
3253
|
+
}
|
|
3254
|
+
function strokePixels(points, r, w, h) {
|
|
3255
|
+
const mask = new Uint8Array(w * h);
|
|
3256
|
+
const list = [];
|
|
3257
|
+
const add2 = (g) => {
|
|
3258
|
+
if (!mask[g]) {
|
|
3259
|
+
mask[g] = 1;
|
|
3260
|
+
list.push(g);
|
|
3261
|
+
}
|
|
3262
|
+
};
|
|
3263
|
+
const segs = points.length === 1 ? [[points[0], points[0]]] : points.slice(1).map((p, i) => [points[i], p]);
|
|
3264
|
+
for (const [a, b] of segs) {
|
|
3265
|
+
const x0 = Math.max(0, Math.floor(Math.min(a.x, b.x) - r));
|
|
3266
|
+
const x1 = Math.min(w - 1, Math.ceil(Math.max(a.x, b.x) + r));
|
|
3267
|
+
const y0 = Math.max(0, Math.floor(Math.min(a.y, b.y) - r));
|
|
3268
|
+
const y1 = Math.min(h - 1, Math.ceil(Math.max(a.y, b.y) + r));
|
|
3269
|
+
const dx = b.x - a.x;
|
|
3270
|
+
const dy = b.y - a.y;
|
|
3271
|
+
const len2 = dx * dx + dy * dy;
|
|
3272
|
+
for (let y = y0; y <= y1; y++) {
|
|
3273
|
+
for (let x = x0; x <= x1; x++) {
|
|
3274
|
+
const px2 = x + 0.5 - a.x;
|
|
3275
|
+
const py = y + 0.5 - a.y;
|
|
3276
|
+
const t = len2 > 0 ? Math.min(1, Math.max(0, (px2 * dx + py * dy) / len2)) : 0;
|
|
3277
|
+
if ((px2 - t * dx) ** 2 + (py - t * dy) ** 2 <= r * r) add2(y * w + x);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
if (!list.length) {
|
|
3282
|
+
for (const p of points) add2(Math.min(h - 1, Math.max(0, Math.floor(p.y))) * w + Math.min(w - 1, Math.max(0, Math.floor(p.x))));
|
|
3283
|
+
}
|
|
3284
|
+
return { mask, list };
|
|
3285
|
+
}
|
|
3286
|
+
function segmentStroke(work, stroke, grown, start) {
|
|
3287
|
+
const { width: w, height: h, rgb } = work;
|
|
3288
|
+
let roi = start;
|
|
3289
|
+
for (; ; ) {
|
|
3290
|
+
const { x: rx, y: ry, width: rw, height: rh } = roi;
|
|
3291
|
+
const n = rw * rh;
|
|
3292
|
+
const fgHist = new Float64Array(BINS ** 3);
|
|
3293
|
+
const bgHist = new Float64Array(BINS ** 3);
|
|
3294
|
+
let fgN = 0;
|
|
3295
|
+
let bgN = 0;
|
|
3296
|
+
const atImageEdge = { l: rx === 0, t: ry === 0, r: rx + rw === w, b: ry + rh === h };
|
|
3297
|
+
for (let y = 0; y < rh; y++) {
|
|
3298
|
+
for (let x = 0; x < rw; x++) {
|
|
3299
|
+
const g = (ry + y) * w + (rx + x);
|
|
3300
|
+
const bin = binOf(rgb, g);
|
|
3301
|
+
bgHist[bin]++;
|
|
3302
|
+
bgN++;
|
|
3303
|
+
if (stroke.mask[g] || grown?.[g]) {
|
|
3304
|
+
fgHist[bin]++;
|
|
3305
|
+
fgN++;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
const fgCost = costTable(fgHist, fgN);
|
|
3310
|
+
const bgCost = costTable(bgHist, bgN);
|
|
3311
|
+
const { right, down } = smoothnessWeights(work, roi);
|
|
3312
|
+
const graph = new GridGraph(rw, rh);
|
|
3313
|
+
const HARD = 1e9;
|
|
3314
|
+
for (let y = 0; y < rh; y++) {
|
|
3315
|
+
for (let x = 0; x < rw; x++) {
|
|
3316
|
+
const p = y * rw + x;
|
|
3317
|
+
const g = (ry + y) * w + rx + x;
|
|
3318
|
+
if (stroke.mask[g]) graph.addTerminal(p, HARD, 0);
|
|
3319
|
+
else {
|
|
3320
|
+
const bin = binOf(rgb, g);
|
|
3321
|
+
graph.addTerminal(p, Math.round(bgCost[bin] * UNITS), Math.round(fgCost[bin] * UNITS));
|
|
3322
|
+
}
|
|
3323
|
+
if (x + 1 < rw) graph.setEdge(p, 0, right[p]);
|
|
3324
|
+
if (y + 1 < rh) graph.setEdge(p, 1, down[p]);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
graph.maxflow();
|
|
3328
|
+
const label = new Uint8Array(n);
|
|
3329
|
+
const stack = [];
|
|
3330
|
+
for (const g of stroke.list) {
|
|
3331
|
+
const x = g % w - rx;
|
|
3332
|
+
const y = Math.floor(g / w) - ry;
|
|
3333
|
+
if (x < 0 || y < 0 || x >= rw || y >= rh) continue;
|
|
3334
|
+
const p = y * rw + x;
|
|
3335
|
+
if (!label[p]) {
|
|
3336
|
+
label[p] = 1;
|
|
3337
|
+
stack.push(p);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
while (stack.length) {
|
|
3341
|
+
const p = stack.pop();
|
|
3342
|
+
const x = p % rw;
|
|
3343
|
+
for (const q of [x + 1 < rw ? p + 1 : -1, x > 0 ? p - 1 : -1, p + rw < n ? p + rw : -1, p - rw]) {
|
|
3344
|
+
if (q >= 0 && !label[q] && graph.inSource(q)) {
|
|
3345
|
+
label[q] = 1;
|
|
3346
|
+
stack.push(q);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
let touches = false;
|
|
3351
|
+
for (let y = 0; y < rh; y++) {
|
|
3352
|
+
for (let x = 0; x < rw; x++) {
|
|
3353
|
+
const p = y * rw + x;
|
|
3354
|
+
if (!label[p]) continue;
|
|
3355
|
+
if (!atImageEdge.l && x === 0 || !atImageEdge.t && y === 0 || !atImageEdge.r && x === rw - 1 || !atImageEdge.b && y === rh - 1) touches = true;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
if (!touches || atImageEdge.l && atImageEdge.t && atImageEdge.r && atImageEdge.b) return { roi, label };
|
|
3359
|
+
const nx = Math.max(0, rx - Math.ceil(rw / 2));
|
|
3360
|
+
const ny = Math.max(0, ry - Math.ceil(rh / 2));
|
|
3361
|
+
roi = { x: nx, y: ny, width: Math.min(w, rx + rw + Math.ceil(rw / 2)) - nx, height: Math.min(h, ry + rh + Math.ceil(rh / 2)) - ny };
|
|
3362
|
+
if (roi.width * roi.height * 2.5 > w * h) roi = { x: 0, y: 0, width: w, height: h };
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function quickSelectStroke(pixels, stroke, options = {}) {
|
|
3366
|
+
if (!stroke.length) throw new Error("A stroke needs at least one point.");
|
|
3367
|
+
const work = workImage(pixels);
|
|
3368
|
+
const { width: w, height: h, scale } = work;
|
|
3369
|
+
const r = Math.max(0.5, (options.size ?? 30) / 2 * scale);
|
|
3370
|
+
const pts = stroke.map((p) => ({ x: p.x * scale, y: p.y * scale }));
|
|
3371
|
+
const seeds = strokePixels(pts, r, w, h);
|
|
3372
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
3373
|
+
for (const p of pts) {
|
|
3374
|
+
x0 = Math.min(x0, p.x);
|
|
3375
|
+
y0 = Math.min(y0, p.y);
|
|
3376
|
+
x1 = Math.max(x1, p.x);
|
|
3377
|
+
y1 = Math.max(y1, p.y);
|
|
3378
|
+
}
|
|
3379
|
+
const margin = Math.max(24, 4 * r);
|
|
3380
|
+
const rx = Math.max(0, Math.floor(x0 - margin));
|
|
3381
|
+
const ry = Math.max(0, Math.floor(y0 - margin));
|
|
3382
|
+
const roi = { x: rx, y: ry, width: Math.min(w, Math.ceil(x1 + margin)) - rx, height: Math.min(h, Math.ceil(y1 + margin)) - ry };
|
|
3383
|
+
let grown = null;
|
|
3384
|
+
const sel = options.grow ? options.selection : null;
|
|
3385
|
+
if (sel?.bounds) {
|
|
3386
|
+
grown = new Uint8Array(w * h);
|
|
3387
|
+
const b = sel.bounds;
|
|
3388
|
+
for (let y = Math.max(0, Math.floor(b.y * scale)); y < Math.min(h, Math.ceil((b.y + b.height) * scale)); y++) {
|
|
3389
|
+
for (let x = Math.max(0, Math.floor(b.x * scale)); x < Math.min(w, Math.ceil((b.x + b.width) * scale)); x++) {
|
|
3390
|
+
if (selectionValueAt(sel, Math.min(sel.width - 1, Math.floor((x + 0.5) / scale)), Math.min(sel.height - 1, Math.floor((y + 0.5) / scale))) >= 0.5) grown[y * w + x] = 1;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
const { roi: done, label } = segmentStroke(work, seeds, grown, roi);
|
|
3395
|
+
return upscaleRegion(work, pixels, done, label, options.enhanceEdge ?? false);
|
|
3396
|
+
}
|
|
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
|
+
|
|
2806
3675
|
// src/image-editor/layers/shape-type-ui.ts
|
|
2807
3676
|
var DEFAULT_SHAPE_TOOL_OPTIONS = {
|
|
2808
3677
|
kind: "rect",
|
|
@@ -8747,6 +9616,53 @@ var ImageEditorController = class {
|
|
|
8747
9616
|
const pixels = await this.requirePixels(options);
|
|
8748
9617
|
return snapPolygonToEdges(pixels, points, options);
|
|
8749
9618
|
}
|
|
9619
|
+
/**
|
|
9620
|
+
* Quick Selection: each stroke (the brush's path in natural pixels) grows into the region it is
|
|
9621
|
+
* painted on, out to that region's edges, and is combined with the selection by `mode`; with
|
|
9622
|
+
* `replace`, the first stroke replaces it and the rest add, as the brush does. All strokes are
|
|
9623
|
+
* one undo step. Resolves to the resulting selection.
|
|
9624
|
+
*/
|
|
9625
|
+
async quickSelect(strokes, options = {}) {
|
|
9626
|
+
if (!strokes.length || strokes.some((s) => !s.length)) throw new Error("Pass at least one stroke, each with at least one point.");
|
|
9627
|
+
const { width, height } = this.naturalSize;
|
|
9628
|
+
for (const s of strokes) {
|
|
9629
|
+
if (!s.some((p) => p.x >= 0 && p.y >= 0 && p.x < width && p.y < height)) {
|
|
9630
|
+
throw new Error(`A stroke lies entirely outside the image (0..${width - 1}, 0..${height - 1}); paint over the area to select.`);
|
|
9631
|
+
}
|
|
9632
|
+
}
|
|
9633
|
+
const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
|
|
9634
|
+
const mode = options.mode ?? "replace";
|
|
9635
|
+
let sel = this.state.selection ?? null;
|
|
9636
|
+
strokes.forEach((stroke, i) => {
|
|
9637
|
+
const m = mode === "replace" && i > 0 ? "add" : mode;
|
|
9638
|
+
const region = quickSelectStroke(pixels, stroke, { size: options.size, selection: sel, grow: m === "add", enhanceEdge: options.enhanceEdge });
|
|
9639
|
+
sel = combineSelections(sel, region, m);
|
|
9640
|
+
});
|
|
9641
|
+
this.setSelection(sel, "replace");
|
|
9642
|
+
return this.state.selection ?? null;
|
|
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
|
+
}
|
|
8750
9666
|
/**
|
|
8751
9667
|
* Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
|
|
8752
9668
|
* or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)
|
|
@@ -11634,7 +12550,7 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
11634
12550
|
// src/image-editor/playback.ts
|
|
11635
12551
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
11636
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)$/;
|
|
11637
|
-
var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|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)$/;
|
|
11638
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)$/;
|
|
11639
12555
|
var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
11640
12556
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
@@ -11756,6 +12672,8 @@ var SELECTION_TOOLS = [
|
|
|
11756
12672
|
{ id: "lasso", label: "Lasso" },
|
|
11757
12673
|
{ id: "polygon", label: "Polygonal Lasso" },
|
|
11758
12674
|
{ id: "magnetic", label: "Magnetic Lasso" },
|
|
12675
|
+
{ id: "quick", label: "Quick Selection" },
|
|
12676
|
+
{ id: "object", label: "Object Selection" },
|
|
11759
12677
|
{ id: "range", label: "Color Range" },
|
|
11760
12678
|
{ id: "type", label: "Type Mask" },
|
|
11761
12679
|
{ id: "type-vertical", label: "Vertical Type Mask" }
|
|
@@ -11793,6 +12711,11 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
|
|
|
11793
12711
|
magneticWidth: 10,
|
|
11794
12712
|
magneticContrast: 10,
|
|
11795
12713
|
magneticFrequency: 57,
|
|
12714
|
+
quickSize: 30,
|
|
12715
|
+
quickSampleAll: true,
|
|
12716
|
+
quickEnhance: false,
|
|
12717
|
+
objectSampleAll: true,
|
|
12718
|
+
objectEnhance: false,
|
|
11796
12719
|
typeText: "Type",
|
|
11797
12720
|
typeSize: 72,
|
|
11798
12721
|
typeFamily: "sans-serif"
|
|
@@ -12192,6 +13115,75 @@ function createImageSelectionTools(controller) {
|
|
|
12192
13115
|
return { ...summary(controller), outlinePoints: outline.length };
|
|
12193
13116
|
}
|
|
12194
13117
|
};
|
|
13118
|
+
const quickSelectTool = {
|
|
13119
|
+
name: "quick_select",
|
|
13120
|
+
title: "Quick Selection",
|
|
13121
|
+
description: "Quick Selection: paints brush strokes (paths in natural image pixels) that each grow into the region they are painted on, out to its edges (a graph cut on colour and edges), combined with the selection by mode. With replace (default) the first stroke replaces the selection and later ones add, as the brush does; use mode subtract to take an area back out. Best for a subject or area that differs from its surroundings: paint inside it, away from the edge, then check with get_selection and capture_view and add or subtract strokes where it leaked or fell short. All strokes are one undoable step. Returns the selection.",
|
|
13122
|
+
inputSchema: {
|
|
13123
|
+
type: "object",
|
|
13124
|
+
properties: {
|
|
13125
|
+
strokes: {
|
|
13126
|
+
type: "array",
|
|
13127
|
+
minItems: 1,
|
|
13128
|
+
items: { type: "array", minItems: 1, items: pointSchema, description: "One stroke: the brush path, in order (one point is a dab)." },
|
|
13129
|
+
description: "Brush strokes in natural image pixels, applied in order."
|
|
13130
|
+
},
|
|
13131
|
+
size: { type: "number", minimum: 1, maximum: 2500, description: "Brush diameter in natural image pixels (default 30). Smaller for a narrow area, larger for a broad one." },
|
|
13132
|
+
mode: modeSchema,
|
|
13133
|
+
sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." },
|
|
13134
|
+
enhanceEdge: { type: "boolean", description: "Smoother, less blocky edges (Auto-Enhance; default false). Use it before masking a smooth-edged subject." }
|
|
13135
|
+
},
|
|
13136
|
+
required: ["strokes"],
|
|
13137
|
+
additionalProperties: false
|
|
13138
|
+
},
|
|
13139
|
+
annotations: writeAnnotations("Quick Selection"),
|
|
13140
|
+
handler: async (args) => {
|
|
13141
|
+
const strokes = Array.isArray(args.strokes) ? args.strokes.map((s) => Array.isArray(s) ? s.map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) : []) : [];
|
|
13142
|
+
if (!strokes.length || strokes.some((s) => !s.length || !s.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)))) {
|
|
13143
|
+
throw new Error("strokes needs at least one stroke of { x, y } points in natural image pixels.");
|
|
13144
|
+
}
|
|
13145
|
+
await controller.quickSelect(strokes, {
|
|
13146
|
+
size: args.size === void 0 ? void 0 : num2(args.size, 30),
|
|
13147
|
+
mode: parseSelectionMode(args.mode),
|
|
13148
|
+
sampleAllLayers: args.sampleAllLayers,
|
|
13149
|
+
enhanceEdge: args.enhanceEdge
|
|
13150
|
+
});
|
|
13151
|
+
return summary(controller);
|
|
13152
|
+
}
|
|
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
|
+
};
|
|
12195
13187
|
const magicWandTool = {
|
|
12196
13188
|
name: "magic_wand",
|
|
12197
13189
|
title: "Magic wand",
|
|
@@ -12744,6 +13736,8 @@ function createImageSelectionTools(controller) {
|
|
|
12744
13736
|
selectRect,
|
|
12745
13737
|
selectEllipse,
|
|
12746
13738
|
selectPolygonTool,
|
|
13739
|
+
quickSelectTool,
|
|
13740
|
+
selectObjectTool,
|
|
12747
13741
|
lineTool,
|
|
12748
13742
|
magicWandTool,
|
|
12749
13743
|
colorRangeTool,
|
|
@@ -14131,9 +15125,14 @@ var SelectionGesture = class {
|
|
|
14131
15125
|
closeRadius = 8;
|
|
14132
15126
|
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
14133
15127
|
error = null;
|
|
15128
|
+
pending = Promise.resolve();
|
|
14134
15129
|
get active() {
|
|
14135
15130
|
return this.start !== null || this.polygon !== null || this.magnetic !== null;
|
|
14136
15131
|
}
|
|
15132
|
+
/** Resolves once a Quick Selection stroke or an Object Selection has been applied. */
|
|
15133
|
+
get settled() {
|
|
15134
|
+
return this.pending;
|
|
15135
|
+
}
|
|
14137
15136
|
/** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
|
|
14138
15137
|
get magneticAnchors() {
|
|
14139
15138
|
const m = this.magnetic;
|
|
@@ -14145,7 +15144,7 @@ var SelectionGesture = class {
|
|
|
14145
15144
|
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
14146
15145
|
if (!this.start || !this.current) return null;
|
|
14147
15146
|
const { tool } = this.getOptions();
|
|
14148
|
-
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
15147
|
+
if (tool === "lasso" || tool === "quick") return this.points.length > 1 ? this.points.slice() : null;
|
|
14149
15148
|
const rect = normalizeRect({
|
|
14150
15149
|
x: this.start.x,
|
|
14151
15150
|
y: this.start.y,
|
|
@@ -14279,7 +15278,8 @@ var SelectionGesture = class {
|
|
|
14279
15278
|
}
|
|
14280
15279
|
if (!this.start) return false;
|
|
14281
15280
|
this.current = point;
|
|
14282
|
-
|
|
15281
|
+
const drawn = this.getOptions().tool;
|
|
15282
|
+
if (drawn === "lasso" || drawn === "quick") {
|
|
14283
15283
|
const last = this.points[this.points.length - 1];
|
|
14284
15284
|
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
14285
15285
|
this.points.push(point);
|
|
@@ -14299,6 +15299,39 @@ var SelectionGesture = class {
|
|
|
14299
15299
|
if (point) this.move(point);
|
|
14300
15300
|
const options = this.getOptions();
|
|
14301
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
|
+
}
|
|
15316
|
+
if (options.tool === "quick") {
|
|
15317
|
+
const stroke = this.points;
|
|
15318
|
+
this.start = null;
|
|
15319
|
+
this.current = null;
|
|
15320
|
+
this.points = [];
|
|
15321
|
+
const strokeMode = mode === "replace" && this.controller.getSelection() ? "add" : mode;
|
|
15322
|
+
this.pending = this.controller.quickSelect([stroke], {
|
|
15323
|
+
size: options.quickSize ?? 30,
|
|
15324
|
+
mode: strokeMode,
|
|
15325
|
+
sampleAllLayers: options.quickSampleAll ?? true,
|
|
15326
|
+
enhanceEdge: options.quickEnhance ?? false
|
|
15327
|
+
}).then(
|
|
15328
|
+
() => void 0,
|
|
15329
|
+
(err) => {
|
|
15330
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
15331
|
+
}
|
|
15332
|
+
);
|
|
15333
|
+
return false;
|
|
15334
|
+
}
|
|
14302
15335
|
const path = this.draftPath;
|
|
14303
15336
|
const rect = normalizeRect({
|
|
14304
15337
|
x: this.start.x,
|
|
@@ -15154,7 +16187,7 @@ function checkSource(controller, source) {
|
|
|
15154
16187
|
}
|
|
15155
16188
|
return p;
|
|
15156
16189
|
}
|
|
15157
|
-
var
|
|
16190
|
+
var SOURCE2 = {
|
|
15158
16191
|
type: "object",
|
|
15159
16192
|
description: "Source point in document pixels (an Alt-click). Given, it replaces the clone source and the offset starts afresh at this stroke; missing, the current clone source is used (set_clone_source).",
|
|
15160
16193
|
properties: { x: { type: "number", description: "Source x in px." }, y: { type: "number", description: "Source y in px." } },
|
|
@@ -15178,7 +16211,7 @@ function createImageRetouchTools(controller) {
|
|
|
15178
16211
|
type: "object",
|
|
15179
16212
|
properties: {
|
|
15180
16213
|
points: POINTS,
|
|
15181
|
-
source:
|
|
16214
|
+
source: SOURCE2,
|
|
15182
16215
|
aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
|
|
15183
16216
|
sample: SAMPLE,
|
|
15184
16217
|
brush: BRUSH2,
|
|
@@ -15198,7 +16231,7 @@ function createImageRetouchTools(controller) {
|
|
|
15198
16231
|
type: "object",
|
|
15199
16232
|
properties: {
|
|
15200
16233
|
points: POINTS,
|
|
15201
|
-
source:
|
|
16234
|
+
source: SOURCE2,
|
|
15202
16235
|
aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
|
|
15203
16236
|
sample: SAMPLE,
|
|
15204
16237
|
brush: BRUSH2,
|
|
@@ -18926,6 +19959,7 @@ export {
|
|
|
18926
19959
|
COLOR_MODE_OPTIONS,
|
|
18927
19960
|
COLOR_RANGE_PRESETS,
|
|
18928
19961
|
COLOR_RANGE_PRESET_LABELS,
|
|
19962
|
+
ColorGmm,
|
|
18929
19963
|
CoverageMap,
|
|
18930
19964
|
D50,
|
|
18931
19965
|
D65,
|
|
@@ -18978,9 +20012,11 @@ export {
|
|
|
18978
20012
|
FREE_TRANSFORM_MODE_OPTIONS,
|
|
18979
20013
|
FreeTransformSession,
|
|
18980
20014
|
FreeTransformTool,
|
|
20015
|
+
GAMMA,
|
|
18981
20016
|
GRAIN_PATTERNS,
|
|
18982
20017
|
GRAIN_TILE,
|
|
18983
20018
|
GUIDED_FILTER_EPS,
|
|
20019
|
+
GridGraph,
|
|
18984
20020
|
HIDE,
|
|
18985
20021
|
HISTOGRAM_BINS,
|
|
18986
20022
|
HUE_RANGE_LABELS,
|
|
@@ -19135,6 +20171,7 @@ export {
|
|
|
19135
20171
|
TransformTooLargeError,
|
|
19136
20172
|
TransformedPixelSource,
|
|
19137
20173
|
TypeToolGesture,
|
|
20174
|
+
UNITS,
|
|
19138
20175
|
U_MAX,
|
|
19139
20176
|
VIBRANCE_GREY,
|
|
19140
20177
|
W9,
|
|
@@ -19149,6 +20186,7 @@ export {
|
|
|
19149
20186
|
WGSL_SHADE,
|
|
19150
20187
|
WGSL_STEP,
|
|
19151
20188
|
WORKING_SPACE_OPTIONS,
|
|
20189
|
+
WORK_SIZE,
|
|
19152
20190
|
WetGpuEngine,
|
|
19153
20191
|
WetMedium,
|
|
19154
20192
|
WetWorkerPool,
|
|
@@ -19217,6 +20255,7 @@ export {
|
|
|
19217
20255
|
blenderDab,
|
|
19218
20256
|
blurFilter,
|
|
19219
20257
|
borderCoverage,
|
|
20258
|
+
boxBlur,
|
|
19220
20259
|
boxBlurPixels,
|
|
19221
20260
|
boxFrameHit,
|
|
19222
20261
|
boxFrameOverlay,
|
|
@@ -19699,6 +20738,7 @@ export {
|
|
|
19699
20738
|
put255,
|
|
19700
20739
|
quadFrameOverlay,
|
|
19701
20740
|
quickMaskPixels,
|
|
20741
|
+
quickSelectStroke,
|
|
19702
20742
|
rand01,
|
|
19703
20743
|
randSigned,
|
|
19704
20744
|
rangeLightness,
|
|
@@ -19777,6 +20817,7 @@ export {
|
|
|
19777
20817
|
segmentToSelection,
|
|
19778
20818
|
segmentationOutlines,
|
|
19779
20819
|
segmentsToSelection,
|
|
20820
|
+
selectObjectIn,
|
|
19780
20821
|
selectSimilar,
|
|
19781
20822
|
selectionArea,
|
|
19782
20823
|
selectionFromCoverage,
|
|
@@ -19826,6 +20867,7 @@ export {
|
|
|
19826
20867
|
smartObjectLayers,
|
|
19827
20868
|
smoothCoverage,
|
|
19828
20869
|
smoothPoints,
|
|
20870
|
+
smoothnessWeights,
|
|
19829
20871
|
smudgeEffect,
|
|
19830
20872
|
snapPolygonToEdges,
|
|
19831
20873
|
snapTo45,
|
|
@@ -19912,6 +20954,7 @@ export {
|
|
|
19912
20954
|
updateShapeLayer,
|
|
19913
20955
|
updateSmartFilter,
|
|
19914
20956
|
updateTypeLayer,
|
|
20957
|
+
upscaleRegion,
|
|
19915
20958
|
userErrorMessage,
|
|
19916
20959
|
uvToXy,
|
|
19917
20960
|
validateEffectGradient,
|
|
@@ -19941,6 +20984,7 @@ export {
|
|
|
19941
20984
|
withFilterMaskSurface,
|
|
19942
20985
|
withImagePlayback,
|
|
19943
20986
|
withNewIds,
|
|
20987
|
+
workImage,
|
|
19944
20988
|
writeAdjustmentBlock,
|
|
19945
20989
|
writeDeepRegion,
|
|
19946
20990
|
writeLfx2,
|