editmamei 0.20.0 → 0.22.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/NOTICES.md +17 -1
- package/README.md +8 -7
- package/dist/api/extendscript/_helpers.js +1 -1
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/cli/config.js +3 -3
- package/dist/cli/install.js +2 -2
- package/dist/core/server.js +7 -7
- package/dist/core/settings.js +2 -2
- package/dist/core/tool-groups.js +91 -82
- package/dist/core/tool-tiers.js +91 -82
- package/dist/detection/landmark-detection-client.js +8 -0
- package/dist/detection/mesh-eye-correction.js +126 -0
- package/dist/detection/sam-segmenter.js +83 -0
- package/dist/index.js +2 -0
- package/dist/license/entitlement.js +52 -0
- package/dist/modules/ce/index.js +4 -4
- package/dist/perception/grounding-anchors.js +37 -0
- package/dist/perception/grounding-corners.js +93 -0
- package/dist/perception/grounding-edge-trace.js +90 -0
- package/dist/perception/grounding-extrema.js +41 -0
- package/dist/perception/grounding-gate.js +154 -0
- package/dist/perception/grounding-geometry.js +132 -0
- package/dist/perception/grounding-grid.js +34 -0
- package/dist/perception/grounding-landmarks.js +40 -0
- package/dist/perception/grounding-locate.js +341 -0
- package/dist/perception/grounding-resolver.js +202 -0
- package/dist/perception/grounding-review-crop.js +85 -0
- package/dist/perception/grounding-warp.js +182 -0
- package/dist/perception/sky-mask-transfer.js +50 -18
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +3 -3
- package/dist/tools/brush-tools.js +111 -23
- package/dist/tools/channel-compose-tools.js +241 -0
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/diagnostics-tools.js +1 -1
- package/dist/tools/document-tools.js +6 -6
- package/dist/tools/filter-tools.js +476 -19
- package/dist/tools/group-tools.js +10 -10
- package/dist/tools/guide-tools.js +4 -4
- package/dist/tools/history-tools.js +4 -4
- package/dist/tools/image-placement-tools.js +1 -1
- package/dist/tools/image-tools.js +62 -19
- package/dist/tools/inspect-tools.js +2 -2
- package/dist/tools/layer-ordering-tools.js +1 -1
- package/dist/tools/layer-properties-tools.js +42 -23
- package/dist/tools/layer-tools.js +10 -10
- package/dist/tools/layer-transform-tools.js +80 -41
- package/dist/tools/overview-tools.js +42 -42
- package/dist/tools/path-tools.js +53 -10
- package/dist/tools/portrait-tools.js +3 -3
- package/dist/tools/preview-tools.js +7 -82
- package/dist/tools/retouch-tools.js +93 -25
- package/dist/tools/scene-tools.js +13 -13
- package/dist/tools/selection-tools.js +319 -40
- package/dist/tools/shape-tools.js +216 -0
- package/dist/tools/text-tools.js +1 -1
- package/dist/tools/transform-canvas-tools.js +2 -2
- package/dist/tools/vector-mask-tools.js +23 -8
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/preview/coordinate-grid.js +0 -250
- package/dist/tools/object-tools.js +0 -193
- package/dist/tools/text-on-object-tools.js +0 -225
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { LANDMARK_GROUPS, LANDMARK_COUNT } from './landmark-spec.js';
|
|
2
|
+
const EYE_RIGID_GROUPS = ['leftEye', 'rightEye', 'leftEyebrow', 'rightEyebrow', 'nose'];
|
|
3
|
+
const EYE_RIGID_INDICES = new Set();
|
|
4
|
+
for (const g of EYE_RIGID_GROUPS)
|
|
5
|
+
for (const i of LANDMARK_GROUPS[g])
|
|
6
|
+
EYE_RIGID_INDICES.add(i);
|
|
7
|
+
const centroid = (pts) => ({
|
|
8
|
+
x: pts.reduce((s, p) => s + p.x, 0) / pts.length,
|
|
9
|
+
y: pts.reduce((s, p) => s + p.y, 0) / pts.length,
|
|
10
|
+
});
|
|
11
|
+
const bbox = (pts) => ({
|
|
12
|
+
x0: Math.min(...pts.map((p) => p.x)),
|
|
13
|
+
y0: Math.min(...pts.map((p) => p.y)),
|
|
14
|
+
x1: Math.max(...pts.map((p) => p.x)),
|
|
15
|
+
y1: Math.max(...pts.map((p) => p.y)),
|
|
16
|
+
});
|
|
17
|
+
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
18
|
+
function makeCtx(img) {
|
|
19
|
+
const { width: W, height: H, data } = img;
|
|
20
|
+
const luma = (x, y) => {
|
|
21
|
+
const i = (y * W + x) * 4;
|
|
22
|
+
return 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
|
23
|
+
};
|
|
24
|
+
const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
25
|
+
const blur = (x, y, r) => {
|
|
26
|
+
let s = 0, n = 0;
|
|
27
|
+
for (let dy = -r; dy <= r; dy++)
|
|
28
|
+
for (let dx = -r; dx <= r; dx++) {
|
|
29
|
+
s += luma(clamp(x + dx, 0, W - 1), clamp(y + dy, 0, H - 1));
|
|
30
|
+
n++;
|
|
31
|
+
}
|
|
32
|
+
return s / n;
|
|
33
|
+
};
|
|
34
|
+
return { W, H, luma, clamp, blur };
|
|
35
|
+
}
|
|
36
|
+
function findEyeCenter(ctx, center, ring) {
|
|
37
|
+
const { W, H, luma, clamp, blur } = ctx;
|
|
38
|
+
const rw = ring.x1 - ring.x0, rh = ring.y1 - ring.y0;
|
|
39
|
+
const x0 = clamp(Math.round(center.x - 0.55 * rw), 0, W - 1);
|
|
40
|
+
const x1 = clamp(Math.round(center.x + 0.55 * rw), 0, W - 1);
|
|
41
|
+
const y0 = clamp(Math.round(center.y - 0.5 * rh), 0, H - 1);
|
|
42
|
+
const y1 = clamp(Math.round(center.y + 2.0 * rh), 0, H - 1);
|
|
43
|
+
const ww = x1 - x0 + 1, wh = y1 - y0 + 1;
|
|
44
|
+
if (ww < 5 || wh < 5)
|
|
45
|
+
return { x: center.x, y: center.y, atEdge: false };
|
|
46
|
+
const L = new Float32Array(ww * wh);
|
|
47
|
+
for (let y = 0; y < wh; y++)
|
|
48
|
+
for (let x = 0; x < ww; x++)
|
|
49
|
+
L[y * ww + x] = luma(x0 + x, y0 + y);
|
|
50
|
+
const edges = [];
|
|
51
|
+
const gg = [];
|
|
52
|
+
let magSum = 0;
|
|
53
|
+
for (let y = 1; y < wh - 1; y++)
|
|
54
|
+
for (let x = 1; x < ww - 1; x++) {
|
|
55
|
+
const gx = (L[y * ww + x + 1] - L[y * ww + x - 1]) / 2;
|
|
56
|
+
const gy = (L[(y + 1) * ww + x] - L[(y - 1) * ww + x]) / 2;
|
|
57
|
+
const m = Math.hypot(gx, gy);
|
|
58
|
+
gg.push({ m, x, y, gx, gy });
|
|
59
|
+
magSum += m;
|
|
60
|
+
}
|
|
61
|
+
const mean = magSum / (gg.length || 1);
|
|
62
|
+
let v = 0;
|
|
63
|
+
for (const g of gg)
|
|
64
|
+
v += (g.m - mean) ** 2;
|
|
65
|
+
const std = Math.sqrt(v / (gg.length || 1));
|
|
66
|
+
const thr = mean + 0.3 * std;
|
|
67
|
+
for (const g of gg)
|
|
68
|
+
if (g.m > thr)
|
|
69
|
+
edges.push({ x: g.x, y: g.y, gx: g.gx / g.m, gy: g.gy / g.m });
|
|
70
|
+
let best = -Infinity, bx = Math.round(center.x - x0), by = Math.round(center.y - y0);
|
|
71
|
+
for (let cy = 2; cy < wh - 2; cy += 2)
|
|
72
|
+
for (let cx = 2; cx < ww - 2; cx += 2) {
|
|
73
|
+
let s = 0;
|
|
74
|
+
for (const e of edges) {
|
|
75
|
+
const dx = e.x - cx, dy = e.y - cy, l = Math.hypot(dx, dy);
|
|
76
|
+
if (l < 3)
|
|
77
|
+
continue;
|
|
78
|
+
const dot = (dx / l) * e.gx + (dy / l) * e.gy;
|
|
79
|
+
if (dot > 0)
|
|
80
|
+
s += dot * dot;
|
|
81
|
+
}
|
|
82
|
+
s *= 255 - blur(x0 + cx, y0 + cy, 3);
|
|
83
|
+
if (s > best) {
|
|
84
|
+
best = s;
|
|
85
|
+
bx = cx;
|
|
86
|
+
by = cy;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const atEdge = bx <= 3 || bx >= ww - 4 || by >= wh - 4;
|
|
90
|
+
return { x: x0 + bx, y: y0 + by, atEdge };
|
|
91
|
+
}
|
|
92
|
+
function similarity(m1, m2, t1, t2) {
|
|
93
|
+
const dmx = m2.x - m1.x, dmy = m2.y - m1.y, dtx = t2.x - t1.x, dty = t2.y - t1.y;
|
|
94
|
+
const s = Math.hypot(dtx, dty) / (Math.hypot(dmx, dmy) || 1);
|
|
95
|
+
const th = Math.atan2(dty, dtx) - Math.atan2(dmy, dmx);
|
|
96
|
+
const c = Math.cos(th) * s, sn = Math.sin(th) * s;
|
|
97
|
+
return (p) => ({
|
|
98
|
+
x: t1.x + c * (p.x - m1.x) - sn * (p.y - m1.y),
|
|
99
|
+
y: t1.y + sn * (p.x - m1.x) + c * (p.y - m1.y),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function correctMeshEyes(img, points) {
|
|
103
|
+
if (!points || points.length < LANDMARK_COUNT)
|
|
104
|
+
return { points, correction: null };
|
|
105
|
+
if (!img || !img.data || img.data.length < img.width * img.height * 4) {
|
|
106
|
+
return { points, correction: null };
|
|
107
|
+
}
|
|
108
|
+
const ctx = makeCtx(img);
|
|
109
|
+
const leftEye = LANDMARK_GROUPS.leftEye.map((i) => points[i]);
|
|
110
|
+
const rightEye = LANDMARK_GROUPS.rightEye.map((i) => points[i]);
|
|
111
|
+
const eyeLm = centroid(leftEye), eyeRm = centroid(rightEye);
|
|
112
|
+
if (dist(eyeLm, eyeRm) < 4)
|
|
113
|
+
return { points, correction: null };
|
|
114
|
+
const eyeLc = findEyeCenter(ctx, eyeLm, bbox(leftEye));
|
|
115
|
+
const eyeRc = findEyeCenter(ctx, eyeRm, bbox(rightEye));
|
|
116
|
+
const T = similarity(eyeLm, eyeRm, eyeLc, eyeRc);
|
|
117
|
+
const corrected = points.map((p, i) => (EYE_RIGID_INDICES.has(i) ? { ...T(p), z: p.z } : p));
|
|
118
|
+
return {
|
|
119
|
+
points: corrected,
|
|
120
|
+
correction: {
|
|
121
|
+
drift_left: Math.round(dist(eyeLc, eyeLm)),
|
|
122
|
+
drift_right: Math.round(dist(eyeRc, eyeRm)),
|
|
123
|
+
low_confidence: eyeLc.atEdge || eyeRc.atEdge,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { loadModel, resolveModelPath, ort } from './runtime.js';
|
|
2
|
+
const MODEL_ENCODER = 'pro/mobile_sam_encoder.onnx';
|
|
3
|
+
const MODEL_DECODER = 'pro/mobile_sam_decoder.onnx';
|
|
4
|
+
const SAM_INPUT = 1024;
|
|
5
|
+
export function boxToPromptCoords(box, imgW, imgH) {
|
|
6
|
+
const s = SAM_INPUT / Math.max(imgW, imgH);
|
|
7
|
+
const [x1, y1, x2, y2] = box;
|
|
8
|
+
return {
|
|
9
|
+
coords: Float32Array.from([x1 * s, y1 * s, x2 * s, y2 * s]),
|
|
10
|
+
labels: Float32Array.from([2, 3]),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function pointsToPromptCoords(fg, bg, imgW, imgH) {
|
|
14
|
+
const s = SAM_INPUT / Math.max(imgW, imgH);
|
|
15
|
+
const pts = [...fg, ...bg, { x: 0, y: 0 }];
|
|
16
|
+
const labels = [...fg.map(() => 1), ...bg.map(() => 0), -1];
|
|
17
|
+
const coords = new Float32Array(pts.length * 2);
|
|
18
|
+
pts.forEach((p, i) => {
|
|
19
|
+
coords[i * 2] = p.x * s;
|
|
20
|
+
coords[i * 2 + 1] = p.y * s;
|
|
21
|
+
});
|
|
22
|
+
return { coords, labels: Float32Array.from(labels) };
|
|
23
|
+
}
|
|
24
|
+
export function toEncoderInput(img) {
|
|
25
|
+
const { width: w, height: h, data } = img;
|
|
26
|
+
const out = new Float32Array(h * w * 3);
|
|
27
|
+
for (let p = 0; p < h * w; p++) {
|
|
28
|
+
out[p * 3] = data[p * 4];
|
|
29
|
+
out[p * 3 + 1] = data[p * 4 + 1];
|
|
30
|
+
out[p * 3 + 2] = data[p * 4 + 2];
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
export function thresholdMask(maskData, n) {
|
|
35
|
+
const mask = new Uint8Array(n);
|
|
36
|
+
for (let i = 0; i < n; i++)
|
|
37
|
+
mask[i] = maskData[i] > 0 ? 1 : 0;
|
|
38
|
+
return mask;
|
|
39
|
+
}
|
|
40
|
+
export class OnnxSamSegmenter {
|
|
41
|
+
async encodeImage(img) {
|
|
42
|
+
const session = await loadModel(resolveModelPath(MODEL_ENCODER));
|
|
43
|
+
const input = new ort.Tensor('float32', toEncoderInput(img), [img.height, img.width, 3]);
|
|
44
|
+
const out = await session.run({ input_image: input });
|
|
45
|
+
return out.image_embeddings;
|
|
46
|
+
}
|
|
47
|
+
async runDecoder(embedding, coords, labels, imgW, imgH) {
|
|
48
|
+
const session = await loadModel(resolveModelPath(MODEL_DECODER));
|
|
49
|
+
const nPts = labels.length;
|
|
50
|
+
const out = await session.run({
|
|
51
|
+
image_embeddings: embedding,
|
|
52
|
+
point_coords: new ort.Tensor('float32', coords, [1, nPts, 2]),
|
|
53
|
+
point_labels: new ort.Tensor('float32', labels, [1, nPts]),
|
|
54
|
+
mask_input: new ort.Tensor('float32', new Float32Array(256 * 256), [1, 1, 256, 256]),
|
|
55
|
+
has_mask_input: new ort.Tensor('float32', Float32Array.from([0]), [1]),
|
|
56
|
+
orig_im_size: new ort.Tensor('float32', Float32Array.from([imgH, imgW]), [2]),
|
|
57
|
+
});
|
|
58
|
+
const masks = out.masks;
|
|
59
|
+
const [, , mh, mw] = masks.dims;
|
|
60
|
+
return {
|
|
61
|
+
mask: thresholdMask(masks.data, mh * mw),
|
|
62
|
+
width: mw,
|
|
63
|
+
height: mh,
|
|
64
|
+
iou: out.iou_predictions.data[0] ?? 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async segmentBox(embedding, box, imgW, imgH) {
|
|
68
|
+
const { coords, labels } = boxToPromptCoords(box, imgW, imgH);
|
|
69
|
+
return this.runDecoder(embedding, coords, labels, imgW, imgH);
|
|
70
|
+
}
|
|
71
|
+
async segmentPointsAt(embedding, fg, bg, imgW, imgH) {
|
|
72
|
+
const { coords, labels } = pointsToPromptCoords(fg, bg, imgW, imgH);
|
|
73
|
+
return this.runDecoder(embedding, coords, labels, imgW, imgH);
|
|
74
|
+
}
|
|
75
|
+
async segment(img, box) {
|
|
76
|
+
const embedding = await this.encodeImage(img);
|
|
77
|
+
return this.segmentBox(embedding, box, img.width, img.height);
|
|
78
|
+
}
|
|
79
|
+
async segmentPoints(img, fg, bg = []) {
|
|
80
|
+
const embedding = await this.encodeImage(img);
|
|
81
|
+
return this.segmentPointsAt(embedding, fg, bg, img.width, img.height);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
import { EditmameiServer } from './core/server.js';
|
|
5
5
|
import { routeCli } from './cli/router.js';
|
|
6
6
|
import { maybeActivateFromEnv } from './license/env-activation.js';
|
|
7
|
+
import { refreshIfStale } from './license/entitlement.js';
|
|
7
8
|
import { Logger } from './utils/logger.js';
|
|
8
9
|
const logger = new Logger('Main');
|
|
9
10
|
function installProcessHandlers() {
|
|
@@ -37,6 +38,7 @@ async function main() {
|
|
|
37
38
|
try {
|
|
38
39
|
logger.info('Starting Editmamei...');
|
|
39
40
|
await maybeActivateFromEnv();
|
|
41
|
+
await refreshIfStale();
|
|
40
42
|
const server = new EditmameiServer();
|
|
41
43
|
await server.start();
|
|
42
44
|
let shuttingDown = false;
|
|
@@ -2,7 +2,11 @@ import { resolvePolarConfig } from './config.js';
|
|
|
2
2
|
import { computeDeviceHash } from './device-hash.js';
|
|
3
3
|
import { readLicense, writeLicense, clearLicense, } from './store.js';
|
|
4
4
|
import { PolarLicenseClient, PolarLicenseError } from './polar-client.js';
|
|
5
|
+
import { Logger } from '../utils/logger.js';
|
|
6
|
+
const logger = new Logger('License');
|
|
5
7
|
export const GRACE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
8
|
+
export const REFRESH_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
export const EXPIRED_REFRESH_TIMEOUT_MS = 5_000;
|
|
6
10
|
export function evaluateEntitlement(rec, now) {
|
|
7
11
|
if (!rec)
|
|
8
12
|
return { entitled: false, reason: 'no-license' };
|
|
@@ -59,6 +63,54 @@ export async function activate(key, ops = {}) {
|
|
|
59
63
|
writeLicense(rec, ops);
|
|
60
64
|
return rec;
|
|
61
65
|
}
|
|
66
|
+
export async function refreshIfStale(ops = {}) {
|
|
67
|
+
const env = ops.env ?? process.env;
|
|
68
|
+
if (env.VITEST !== undefined || env.NODE_ENV === 'test')
|
|
69
|
+
return;
|
|
70
|
+
const rec = readLicense(ops);
|
|
71
|
+
if (!rec)
|
|
72
|
+
return;
|
|
73
|
+
if (rec.status === 'revoked' || rec.status === 'disabled')
|
|
74
|
+
return;
|
|
75
|
+
const now = (ops.now ?? Date.now)();
|
|
76
|
+
if (rec.expires_at !== null && Date.parse(rec.expires_at) <= now)
|
|
77
|
+
return;
|
|
78
|
+
const last = Date.parse(rec.last_validated_at);
|
|
79
|
+
const age = Number.isFinite(last) ? now - last : Infinity;
|
|
80
|
+
if (age <= REFRESH_AFTER_MS)
|
|
81
|
+
return;
|
|
82
|
+
if (age <= GRACE_MS) {
|
|
83
|
+
refresh(ops).catch((err) => {
|
|
84
|
+
logger.warn(`Background license refresh failed (grace covers offline use): ` +
|
|
85
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const timeoutMs = ops.expiredRefreshTimeoutMs ?? EXPIRED_REFRESH_TIMEOUT_MS;
|
|
90
|
+
const attempt = refresh(ops);
|
|
91
|
+
attempt.catch(() => { });
|
|
92
|
+
let timer;
|
|
93
|
+
try {
|
|
94
|
+
const winner = await Promise.race([
|
|
95
|
+
attempt,
|
|
96
|
+
new Promise((resolve) => {
|
|
97
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
if (winner === 'timeout') {
|
|
102
|
+
logger.warn('License re-validation timed out past the grace window — continuing as Community; the next boot retries.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
logger.warn(`License re-validation failed past the grace window (continuing as Community): ` +
|
|
107
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (timer)
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
62
114
|
export async function refresh(ops = {}) {
|
|
63
115
|
const rec = readLicense(ops);
|
|
64
116
|
if (!rec)
|
package/dist/modules/ce/index.js
CHANGED
|
@@ -23,12 +23,12 @@ import { createBrushTools } from '../../tools/brush-tools.js';
|
|
|
23
23
|
import { createTransformCanvasTools } from '../../tools/transform-canvas-tools.js';
|
|
24
24
|
import { createGuideTools } from '../../tools/guide-tools.js';
|
|
25
25
|
import { createDetectionTools } from '../../tools/detection-tools.js';
|
|
26
|
-
import { createObjectTools } from '../../tools/object-tools.js';
|
|
27
26
|
import { createPortraitTools } from '../../tools/portrait-tools.js';
|
|
28
|
-
import { createTextOnObjectTools } from '../../tools/text-on-object-tools.js';
|
|
29
27
|
import { createSceneTools } from '../../tools/scene-tools.js';
|
|
30
28
|
import { createPathTools } from '../../tools/path-tools.js';
|
|
31
29
|
import { createVectorMaskTools } from '../../tools/vector-mask-tools.js';
|
|
30
|
+
import { createChannelComposeTools } from '../../tools/channel-compose-tools.js';
|
|
31
|
+
import { createShapeTools } from '../../tools/shape-tools.js';
|
|
32
32
|
const ceFactories = [
|
|
33
33
|
createDocumentTools,
|
|
34
34
|
createLayerTools,
|
|
@@ -52,11 +52,11 @@ const ceFactories = [
|
|
|
52
52
|
createTransformCanvasTools,
|
|
53
53
|
createGuideTools,
|
|
54
54
|
createDetectionTools,
|
|
55
|
-
createObjectTools,
|
|
56
55
|
createPortraitTools,
|
|
57
|
-
createTextOnObjectTools,
|
|
58
56
|
createPathTools,
|
|
59
57
|
createVectorMaskTools,
|
|
58
|
+
createChannelComposeTools,
|
|
59
|
+
createShapeTools,
|
|
60
60
|
];
|
|
61
61
|
export const ceModule = {
|
|
62
62
|
manifest: { id: 'ce', name: 'Editmamei Community Tools', abi: KERNEL_ABI },
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function bboxToBox(b) {
|
|
2
|
+
return { left: b[0], top: b[1], right: b[2], bottom: b[3] };
|
|
3
|
+
}
|
|
4
|
+
const cx = (b) => (b[0] + b[2]) / 2;
|
|
5
|
+
const cy = (b) => (b[1] + b[3]) / 2;
|
|
6
|
+
const area = (b) => (b[2] - b[0]) * (b[3] - b[1]);
|
|
7
|
+
function orderBy(items, pick) {
|
|
8
|
+
const cmp = {
|
|
9
|
+
leftmost: (a, b) => cx(a.bbox) - cx(b.bbox),
|
|
10
|
+
rightmost: (a, b) => cx(b.bbox) - cx(a.bbox),
|
|
11
|
+
topmost: (a, b) => cy(a.bbox) - cy(b.bbox),
|
|
12
|
+
bottommost: (a, b) => cy(b.bbox) - cy(a.bbox),
|
|
13
|
+
largest: (a, b) => area(b.bbox) - area(a.bbox),
|
|
14
|
+
smallest: (a, b) => area(a.bbox) - area(b.bbox),
|
|
15
|
+
};
|
|
16
|
+
return pick === 'confidence' ? items : [...items].sort(cmp[pick]);
|
|
17
|
+
}
|
|
18
|
+
export function localizeAnchor(det, sel) {
|
|
19
|
+
const candidates = sel.kind === 'face'
|
|
20
|
+
? (det.faces ?? []).map((f) => ({ bbox: f.bbox }))
|
|
21
|
+
: (det.objects ?? []).filter((o) => o.label === sel.label).map((o) => ({ bbox: o.bbox }));
|
|
22
|
+
const ordered = orderBy(candidates, sel.pick ?? 'confidence');
|
|
23
|
+
const c = ordered[sel.instance ?? 0];
|
|
24
|
+
return c ? { kind: 'box', box: bboxToBox(c.bbox) } : null;
|
|
25
|
+
}
|
|
26
|
+
export function localizeAnchors(det, selectors) {
|
|
27
|
+
const anchors = {};
|
|
28
|
+
const missing = [];
|
|
29
|
+
for (const [id, sel] of Object.entries(selectors)) {
|
|
30
|
+
const prim = localizeAnchor(det, sel);
|
|
31
|
+
if (prim)
|
|
32
|
+
anchors[id] = prim;
|
|
33
|
+
else
|
|
34
|
+
missing.push(id);
|
|
35
|
+
}
|
|
36
|
+
return { anchors, missing };
|
|
37
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { dist } from './grounding-geometry.js';
|
|
2
|
+
const clampi = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
3
|
+
export function findCorners(img, region, opts = {}) {
|
|
4
|
+
const k = opts.k ?? 0.05;
|
|
5
|
+
const wr = Math.max(1, opts.windowRadius ?? 2);
|
|
6
|
+
const nms = Math.max(1, opts.nmsRadius ?? 3);
|
|
7
|
+
const relT = opts.relThreshold ?? 0.05;
|
|
8
|
+
const cap = opts.max ?? 32;
|
|
9
|
+
const rx0 = clampi(Math.round(region.left), 0, img.width - 1);
|
|
10
|
+
const ry0 = clampi(Math.round(region.top), 0, img.height - 1);
|
|
11
|
+
const rx1 = clampi(Math.round(region.right), 0, img.width - 1);
|
|
12
|
+
const ry1 = clampi(Math.round(region.bottom), 0, img.height - 1);
|
|
13
|
+
const rw = rx1 - rx0 + 1, rh = ry1 - ry0 + 1;
|
|
14
|
+
if (rw < 3 || rh < 3)
|
|
15
|
+
return [];
|
|
16
|
+
const lum = new Float32Array(rw * rh);
|
|
17
|
+
for (let y = 0; y < rh; y++)
|
|
18
|
+
for (let x = 0; x < rw; x++) {
|
|
19
|
+
const i = ((ry0 + y) * img.width + (rx0 + x)) * 4;
|
|
20
|
+
lum[y * rw + x] = 0.299 * img.data[i] + 0.587 * img.data[i + 1] + 0.114 * img.data[i + 2];
|
|
21
|
+
}
|
|
22
|
+
const L = (x, y) => lum[clampi(y, 0, rh - 1) * rw + clampi(x, 0, rw - 1)];
|
|
23
|
+
const Ix = new Float32Array(rw * rh), Iy = new Float32Array(rw * rh);
|
|
24
|
+
for (let y = 0; y < rh; y++)
|
|
25
|
+
for (let x = 0; x < rw; x++) {
|
|
26
|
+
Ix[y * rw + x] =
|
|
27
|
+
-L(x - 1, y - 1) -
|
|
28
|
+
2 * L(x - 1, y) -
|
|
29
|
+
L(x - 1, y + 1) +
|
|
30
|
+
L(x + 1, y - 1) +
|
|
31
|
+
2 * L(x + 1, y) +
|
|
32
|
+
L(x + 1, y + 1);
|
|
33
|
+
Iy[y * rw + x] =
|
|
34
|
+
-L(x - 1, y - 1) -
|
|
35
|
+
2 * L(x, y - 1) -
|
|
36
|
+
L(x + 1, y - 1) +
|
|
37
|
+
L(x - 1, y + 1) +
|
|
38
|
+
2 * L(x, y + 1) +
|
|
39
|
+
L(x + 1, y + 1);
|
|
40
|
+
}
|
|
41
|
+
const R = new Float32Array(rw * rh);
|
|
42
|
+
let maxR = 0;
|
|
43
|
+
for (let y = 0; y < rh; y++)
|
|
44
|
+
for (let x = 0; x < rw; x++) {
|
|
45
|
+
let a = 0, b = 0, c = 0;
|
|
46
|
+
for (let dy = -wr; dy <= wr; dy++)
|
|
47
|
+
for (let dx = -wr; dx <= wr; dx++) {
|
|
48
|
+
const xx = clampi(x + dx, 0, rw - 1), yy = clampi(y + dy, 0, rh - 1);
|
|
49
|
+
const gx = Ix[yy * rw + xx], gy = Iy[yy * rw + xx];
|
|
50
|
+
a += gx * gx;
|
|
51
|
+
b += gx * gy;
|
|
52
|
+
c += gy * gy;
|
|
53
|
+
}
|
|
54
|
+
const r = a * c - b * b - k * (a + c) * (a + c);
|
|
55
|
+
R[y * rw + x] = r;
|
|
56
|
+
if (r > maxR)
|
|
57
|
+
maxR = r;
|
|
58
|
+
}
|
|
59
|
+
if (maxR <= 0)
|
|
60
|
+
return [];
|
|
61
|
+
const threshold = relT * maxR;
|
|
62
|
+
const corners = [];
|
|
63
|
+
for (let y = 0; y < rh; y++)
|
|
64
|
+
for (let x = 0; x < rw; x++) {
|
|
65
|
+
const r = R[y * rw + x];
|
|
66
|
+
if (r < threshold)
|
|
67
|
+
continue;
|
|
68
|
+
let isMax = true;
|
|
69
|
+
for (let dy = -nms; dy <= nms && isMax; dy++)
|
|
70
|
+
for (let dx = -nms; dx <= nms; dx++) {
|
|
71
|
+
const xx = x + dx, yy = y + dy;
|
|
72
|
+
if (xx < 0 || yy < 0 || xx >= rw || yy >= rh)
|
|
73
|
+
continue;
|
|
74
|
+
if (R[yy * rw + xx] > r) {
|
|
75
|
+
isMax = false;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (isMax)
|
|
80
|
+
corners.push({ point: { x: rx0 + x, y: ry0 + y }, response: r });
|
|
81
|
+
}
|
|
82
|
+
corners.sort((p, q) => q.response - p.response);
|
|
83
|
+
return corners.slice(0, cap);
|
|
84
|
+
}
|
|
85
|
+
export function cornerAnchor(img, region, opts = {}) {
|
|
86
|
+
const corners = findCorners(img, region, opts);
|
|
87
|
+
if (corners.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
const chosen = opts.near
|
|
90
|
+
? corners.reduce((best, c) => dist(c.point, opts.near) < dist(best.point, opts.near) ? c : best)
|
|
91
|
+
: corners[0];
|
|
92
|
+
return { kind: 'point', point: chosen.point };
|
|
93
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const lumaAt = (img, x, y) => {
|
|
2
|
+
const i = (y * img.width + x) * 4;
|
|
3
|
+
return 0.299 * img.data[i] + 0.587 * img.data[i + 1] + 0.114 * img.data[i + 2];
|
|
4
|
+
};
|
|
5
|
+
function bandDeltaCol(img, x, y, band) {
|
|
6
|
+
let a = 0, b = 0, na = 0, nb = 0;
|
|
7
|
+
for (let k = 1; k <= band; k++) {
|
|
8
|
+
if (y - k >= 0) {
|
|
9
|
+
a += lumaAt(img, x, y - k);
|
|
10
|
+
na++;
|
|
11
|
+
}
|
|
12
|
+
if (y + k < img.height) {
|
|
13
|
+
b += lumaAt(img, x, y + k);
|
|
14
|
+
nb++;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (na === 0 || nb === 0)
|
|
18
|
+
return 0;
|
|
19
|
+
return Math.abs(a / na - b / nb);
|
|
20
|
+
}
|
|
21
|
+
function bandDeltaRow(img, x, y, band) {
|
|
22
|
+
let a = 0, b = 0, na = 0, nb = 0;
|
|
23
|
+
for (let k = 1; k <= band; k++) {
|
|
24
|
+
if (x - k >= 0) {
|
|
25
|
+
a += lumaAt(img, x - k, y);
|
|
26
|
+
na++;
|
|
27
|
+
}
|
|
28
|
+
if (x + k < img.width) {
|
|
29
|
+
b += lumaAt(img, x + k, y);
|
|
30
|
+
nb++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (na === 0 || nb === 0)
|
|
34
|
+
return 0;
|
|
35
|
+
return Math.abs(a / na - b / nb);
|
|
36
|
+
}
|
|
37
|
+
const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
38
|
+
export function traceEdge(img, opts) {
|
|
39
|
+
const r = opts.region;
|
|
40
|
+
const left = clamp(Math.round(r.left), 0, img.width - 1);
|
|
41
|
+
const right = clamp(Math.round(r.right), 0, img.width - 1);
|
|
42
|
+
const top = clamp(Math.round(r.top), 0, img.height - 1);
|
|
43
|
+
const bottom = clamp(Math.round(r.bottom), 0, img.height - 1);
|
|
44
|
+
const samples = Math.max(2, opts.samples ?? 24);
|
|
45
|
+
const polyline = [];
|
|
46
|
+
const confidences = [];
|
|
47
|
+
if (opts.orientation === 'horizontal-edge') {
|
|
48
|
+
const band = opts.band ?? Math.max(3, Math.round((bottom - top) / 20));
|
|
49
|
+
for (let i = 0; i < samples; i++) {
|
|
50
|
+
const x = Math.round(left + ((i + 0.5) / samples) * (right - left));
|
|
51
|
+
let bestY = -1, best = -1;
|
|
52
|
+
for (let y = top + band; y <= bottom - band; y++) {
|
|
53
|
+
const d = bandDeltaCol(img, x, y, band);
|
|
54
|
+
if (d > best) {
|
|
55
|
+
best = d;
|
|
56
|
+
bestY = y;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (bestY < 0)
|
|
60
|
+
continue;
|
|
61
|
+
polyline.push({ x, y: bestY });
|
|
62
|
+
confidences.push(Math.round(best));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const band = opts.band ?? Math.max(3, Math.round((right - left) / 20));
|
|
67
|
+
for (let i = 0; i < samples; i++) {
|
|
68
|
+
const y = Math.round(top + ((i + 0.5) / samples) * (bottom - top));
|
|
69
|
+
let bestX = -1, best = -1;
|
|
70
|
+
for (let x = left + band; x <= right - band; x++) {
|
|
71
|
+
const d = bandDeltaRow(img, x, y, band);
|
|
72
|
+
if (d > best) {
|
|
73
|
+
best = d;
|
|
74
|
+
bestX = x;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (bestX < 0)
|
|
78
|
+
continue;
|
|
79
|
+
polyline.push({ x: bestX, y });
|
|
80
|
+
confidences.push(Math.round(best));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const meanConfidence = confidences.length
|
|
84
|
+
? confidences.reduce((s, v) => s + v, 0) / confidences.length
|
|
85
|
+
: 0;
|
|
86
|
+
return { polyline, confidences, meanConfidence };
|
|
87
|
+
}
|
|
88
|
+
export function edgeAnchor(edge) {
|
|
89
|
+
return { kind: 'polyline', polyline: edge.polyline };
|
|
90
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const clampi = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
2
|
+
function windowMean(img, x, y, w, measure) {
|
|
3
|
+
let sum = 0, n = 0;
|
|
4
|
+
for (let dy = -w; dy <= w; dy++) {
|
|
5
|
+
for (let dx = -w; dx <= w; dx++) {
|
|
6
|
+
const px = clampi(x + dx, 0, img.width - 1), py = clampi(y + dy, 0, img.height - 1);
|
|
7
|
+
const i = (py * img.width + px) * 4;
|
|
8
|
+
const r = img.data[i], g = img.data[i + 1], b = img.data[i + 2];
|
|
9
|
+
const v = measure === 'most-saturated'
|
|
10
|
+
? Math.max(r, g, b) - Math.min(r, g, b)
|
|
11
|
+
: 0.299 * r + 0.587 * g + 0.114 * b;
|
|
12
|
+
sum += v;
|
|
13
|
+
n++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return sum / n;
|
|
17
|
+
}
|
|
18
|
+
export function findExtremum(img, region, measure, opts = {}) {
|
|
19
|
+
const stride = Math.max(1, opts.stride ?? 2);
|
|
20
|
+
const w = Math.max(0, opts.window ?? 1);
|
|
21
|
+
const left = clampi(Math.round(region.left), 0, img.width - 1);
|
|
22
|
+
const right = clampi(Math.round(region.right), 0, img.width - 1);
|
|
23
|
+
const top = clampi(Math.round(region.top), 0, img.height - 1);
|
|
24
|
+
const bottom = clampi(Math.round(region.bottom), 0, img.height - 1);
|
|
25
|
+
const minimize = measure === 'darkest';
|
|
26
|
+
let best = minimize ? Infinity : -Infinity;
|
|
27
|
+
let bestPt = { x: left, y: top };
|
|
28
|
+
for (let y = top; y <= bottom; y += stride) {
|
|
29
|
+
for (let x = left; x <= right; x += stride) {
|
|
30
|
+
const v = windowMean(img, x, y, w, measure);
|
|
31
|
+
if (minimize ? v < best : v > best) {
|
|
32
|
+
best = v;
|
|
33
|
+
bestPt = { x, y };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { point: bestPt, value: Math.round(best) };
|
|
38
|
+
}
|
|
39
|
+
export function extremumAnchor(img, region, measure, opts) {
|
|
40
|
+
return { kind: 'point', point: findExtremum(img, region, measure, opts).point };
|
|
41
|
+
}
|