@melite/formats 0.0.0-stage → 0.1.4-alpha
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/LICENSE +557 -0
- package/lib/b64.d.ts +2 -0
- package/lib/b64.js +7 -0
- package/lib/bodyPresets.d.ts +40 -0
- package/lib/bodyPresets.js +77 -0
- package/lib/capabilityCards.d.ts +54 -0
- package/lib/capabilityCards.js +127 -0
- package/lib/captionSplitter.d.ts +49 -0
- package/lib/captionSplitter.js +172 -0
- package/lib/cardMutations.d.ts +52 -0
- package/lib/cardMutations.js +67 -0
- package/lib/downloads.d.ts +18 -0
- package/lib/downloads.js +64 -0
- package/lib/extractVideoFrame.d.ts +23 -0
- package/lib/extractVideoFrame.js +114 -0
- package/lib/glbLoad.d.ts +54 -0
- package/lib/glbLoad.js +96 -0
- package/lib/glbSkin.d.ts +14 -0
- package/lib/glbSkin.js +124 -0
- package/lib/handPose.d.ts +46 -0
- package/lib/handPose.js +122 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +26 -0
- package/lib/keyframeExtract.d.ts +46 -0
- package/lib/keyframeExtract.js +89 -0
- package/lib/maskPrep.d.ts +88 -0
- package/lib/maskPrep.js +386 -0
- package/lib/motionConstraints.d.ts +237 -0
- package/lib/motionConstraints.js +369 -0
- package/lib/motionEnvelope.d.ts +56 -0
- package/lib/motionEnvelope.js +59 -0
- package/lib/motionNpz.d.ts +120 -0
- package/lib/motionNpz.js +497 -0
- package/lib/npz.d.ts +24 -0
- package/lib/npz.js +158 -0
- package/lib/openPoseCanvas.d.ts +17 -0
- package/lib/openPoseCanvas.js +90 -0
- package/lib/pngMeta.d.ts +42 -0
- package/lib/pngMeta.js +212 -0
- package/lib/pngMetaInfo.d.ts +14 -0
- package/lib/pngMetaInfo.js +127 -0
- package/lib/poseCodec.d.ts +20 -0
- package/lib/poseCodec.js +61 -0
- package/lib/poseTransforms.d.ts +73 -0
- package/lib/poseTransforms.js +237 -0
- package/lib/poserSkin.d.ts +75 -0
- package/lib/poserSkin.js +126 -0
- package/lib/prompt-tags.json +132 -0
- package/lib/promptTags.d.ts +42 -0
- package/lib/promptTags.js +116 -0
- package/lib/psdExport.d.ts +28 -0
- package/lib/psdExport.js +272 -0
- package/lib/readFile.d.ts +9 -0
- package/lib/readFile.js +16 -0
- package/lib/renderSnapshot.d.ts +34 -0
- package/lib/renderSnapshot.js +55 -0
- package/lib/somaSkin.d.ts +82 -0
- package/lib/somaSkin.js +267 -0
- package/lib/textToPose.d.ts +388 -0
- package/lib/textToPose.js +604 -0
- package/lib/uploadToEstate.d.ts +25 -0
- package/lib/uploadToEstate.js +97 -0
- package/package.json +148 -4
- package/README.md +0 -3
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @melite/formats — pure media codecs + math (npz, skins, masks, presets).
|
|
2
|
+
* Fetch-free by law; three.js types only where geometry demands. */
|
|
3
|
+
export * from "./npz.js";
|
|
4
|
+
export * from "./somaSkin.js";
|
|
5
|
+
export * from "./motionNpz.js";
|
|
6
|
+
export * from "./glbSkin.js";
|
|
7
|
+
export * from "./maskPrep.js";
|
|
8
|
+
export * from "./bodyPresets.js";
|
|
9
|
+
export * from "./poseCodec.js";
|
|
10
|
+
export * from "./motionConstraints.js";
|
|
11
|
+
export * from "./openPoseCanvas.js";
|
|
12
|
+
export * from "./downloads.js";
|
|
13
|
+
export * from "./b64.js";
|
|
14
|
+
export * from "./poseTransforms.js";
|
|
15
|
+
export * from "./psdExport.js";
|
|
16
|
+
export * from "./pngMetaInfo.js";
|
|
17
|
+
export * from "./pngMeta.js";
|
|
18
|
+
export * from "./captionSplitter.js";
|
|
19
|
+
export * from "./promptTags.js";
|
|
20
|
+
export * from "./motionEnvelope.js";
|
|
21
|
+
export * from "./extractVideoFrame.js";
|
|
22
|
+
export * from "./keyframeExtract.js";
|
|
23
|
+
export * from "./handPose.js";
|
|
24
|
+
export * from "./capabilityCards.js";
|
|
25
|
+
export * from "./cardMutations.js";
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @melite/formats — pure media codecs + math (npz, skins, masks, presets).
|
|
2
|
+
* Fetch-free by law; three.js types only where geometry demands. */
|
|
3
|
+
export * from "./npz.js";
|
|
4
|
+
export * from "./somaSkin.js";
|
|
5
|
+
export * from "./motionNpz.js";
|
|
6
|
+
export * from "./glbSkin.js";
|
|
7
|
+
export * from "./maskPrep.js";
|
|
8
|
+
export * from "./bodyPresets.js";
|
|
9
|
+
export * from "./poseCodec.js";
|
|
10
|
+
export * from "./motionConstraints.js";
|
|
11
|
+
export * from "./openPoseCanvas.js";
|
|
12
|
+
export * from "./downloads.js";
|
|
13
|
+
export * from "./b64.js";
|
|
14
|
+
export * from "./poseTransforms.js";
|
|
15
|
+
export * from "./psdExport.js";
|
|
16
|
+
export * from "./pngMetaInfo.js";
|
|
17
|
+
export * from "./pngMeta.js";
|
|
18
|
+
export * from "./captionSplitter.js";
|
|
19
|
+
export * from "./promptTags.js";
|
|
20
|
+
export * from "./motionEnvelope.js";
|
|
21
|
+
export * from "./extractVideoFrame.js";
|
|
22
|
+
export * from "./keyframeExtract.js";
|
|
23
|
+
export * from "./handPose.js";
|
|
24
|
+
export * from "./capabilityCards.js";
|
|
25
|
+
export * from "./cardMutations.js";
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keyframeExtract — pick anchor frames from a SOMA-77 motion trajectory.
|
|
3
|
+
*
|
|
4
|
+
* Used by Pose Studio's "Auto Keyframes" action (Kimodo-TO (g)) to turn a
|
|
5
|
+
* generated motion into a sparse set of keyframe constraints without the
|
|
6
|
+
* user scrubbing and pinning by hand.
|
|
7
|
+
*
|
|
8
|
+
* Two regimes:
|
|
9
|
+
*
|
|
10
|
+
* 1. Multi-prompt motion → the semantic boundaries are already known:
|
|
11
|
+
* each prompt segment's start_frame / end_frame is a natural keyframe
|
|
12
|
+
* (the motion changes intent there). No signal processing needed.
|
|
13
|
+
*
|
|
14
|
+
* 2. Single-prompt motion → run velocity-minima detection on the joint
|
|
15
|
+
* trajectory. Per-frame velocity = total squared displacement of all
|
|
16
|
+
* 77 joints from the previous frame. Local MINIMA of this signal are
|
|
17
|
+
* the holds / extremes (top of a reach, contact frame, pause) — clean,
|
|
18
|
+
* readable anchor poses. Local MAXIMA are mid-motion blur frames and
|
|
19
|
+
* make BAD anchors, so we explicitly look for minima, not peaks.
|
|
20
|
+
*
|
|
21
|
+
* Both paths return frame indices, deduped and sorted, with endpoints and a
|
|
22
|
+
* minimum spacing enforced.
|
|
23
|
+
*/
|
|
24
|
+
import type { PromptSegmentOutput } from "./textToPose.js";
|
|
25
|
+
export interface ExtractOptions {
|
|
26
|
+
/** Minimum frames between extracted keyframes (anti-clustering). */
|
|
27
|
+
minSpacing?: number;
|
|
28
|
+
/** Always include frame 0 and the last frame as anchors. Default true. */
|
|
29
|
+
includeEndpoints?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract keyframe frame indices from a motion's per-frame joint trajectory.
|
|
33
|
+
*
|
|
34
|
+
* @param framesJoints Per-frame joint positions. Shape: [frame][joint][3].
|
|
35
|
+
* (result.motion.frames_soma77_3d from the pose API.)
|
|
36
|
+
* @param segments Optional prompt segments. When present (>1), their
|
|
37
|
+
* boundaries are used directly instead of velocity
|
|
38
|
+
* detection — they ARE the semantic keyframes.
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractKeyframeFrames(framesJoints: number[][][], segments?: PromptSegmentOutput[], opts?: ExtractOptions): number[];
|
|
41
|
+
/**
|
|
42
|
+
* Per-frame velocity (total squared joint displacement). Exposed for the
|
|
43
|
+
* optional velocity-curve visualization in the timeline.
|
|
44
|
+
*/
|
|
45
|
+
export declare function frameVelocities(framesJoints: number[][][]): number[];
|
|
46
|
+
//# sourceMappingURL=keyframeExtract.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract keyframe frame indices from a motion's per-frame joint trajectory.
|
|
3
|
+
*
|
|
4
|
+
* @param framesJoints Per-frame joint positions. Shape: [frame][joint][3].
|
|
5
|
+
* (result.motion.frames_soma77_3d from the pose API.)
|
|
6
|
+
* @param segments Optional prompt segments. When present (>1), their
|
|
7
|
+
* boundaries are used directly instead of velocity
|
|
8
|
+
* detection — they ARE the semantic keyframes.
|
|
9
|
+
*/
|
|
10
|
+
export function extractKeyframeFrames(framesJoints, segments, opts = {}) {
|
|
11
|
+
if (!framesJoints?.length)
|
|
12
|
+
return [];
|
|
13
|
+
const total = framesJoints.length;
|
|
14
|
+
const minSpacing = opts.minSpacing ?? Math.max(2, Math.floor(total / 12));
|
|
15
|
+
const includeEndpoints = opts.includeEndpoints ?? true;
|
|
16
|
+
let raw;
|
|
17
|
+
if (segments && segments.length > 1) {
|
|
18
|
+
// Multi-prompt: segment boundaries are the keyframes.
|
|
19
|
+
raw = new Set();
|
|
20
|
+
for (const seg of segments) {
|
|
21
|
+
raw.add(seg.start_frame);
|
|
22
|
+
raw.add(seg.end_frame);
|
|
23
|
+
}
|
|
24
|
+
// Also include the peak if provided — it's often a strong anchor.
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Single-prompt (or no segments): velocity-minima detection.
|
|
28
|
+
raw = new Set(velocityMinima(framesJoints));
|
|
29
|
+
}
|
|
30
|
+
// Enforce endpoints.
|
|
31
|
+
if (includeEndpoints) {
|
|
32
|
+
raw.add(0);
|
|
33
|
+
raw.add(total - 1);
|
|
34
|
+
}
|
|
35
|
+
// Dedupe + sort + enforce min spacing (greedy — keep first, skip too-close).
|
|
36
|
+
const sorted = Array.from(raw).sort((a, b) => a - b)
|
|
37
|
+
.filter(f => f >= 0 && f < total);
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const f of sorted) {
|
|
40
|
+
if (out.length === 0 || f - out[out.length - 1] >= minSpacing) {
|
|
41
|
+
out.push(f);
|
|
42
|
+
}
|
|
43
|
+
else if (f === total - 1) {
|
|
44
|
+
// Always keep the last frame even if close to the previous — it's
|
|
45
|
+
|
|
46
|
+
// non-empty and f - prev < minSpacing with f === total - 1, so the
|
|
47
|
+
// end-anchor unconditionally replaces its too-close predecessor.
|
|
48
|
+
out[out.length - 1] = f;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Per-frame velocity (total squared joint displacement). Exposed for the
|
|
55
|
+
* optional velocity-curve visualization in the timeline.
|
|
56
|
+
*/
|
|
57
|
+
export function frameVelocities(framesJoints) {
|
|
58
|
+
const v = new Array(framesJoints.length).fill(0);
|
|
59
|
+
for (let f = 1; f < framesJoints.length; f++) {
|
|
60
|
+
const a = framesJoints[f - 1];
|
|
61
|
+
const b = framesJoints[f];
|
|
62
|
+
let sum = 0;
|
|
63
|
+
const n = Math.min(a.length, b.length);
|
|
64
|
+
for (let j = 0; j < n; j++) {
|
|
65
|
+
const aj = a[j], bj = b[j];
|
|
66
|
+
const dx = bj[0] - aj[0];
|
|
67
|
+
const dy = bj[1] - aj[1];
|
|
68
|
+
const dz = bj[2] - aj[2];
|
|
69
|
+
sum += dx * dx + dy * dy + dz * dz;
|
|
70
|
+
}
|
|
71
|
+
v[f] = sum;
|
|
72
|
+
}
|
|
73
|
+
v[0] = v[1] ?? 0;
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
/** Local minima of the velocity curve → holds / extremes = good anchors. */
|
|
77
|
+
function velocityMinima(framesJoints) {
|
|
78
|
+
const v = frameVelocities(framesJoints);
|
|
79
|
+
const minima = [];
|
|
80
|
+
// A frame is a local minimum if it's lower than its neighbors. Use a
|
|
81
|
+
// smoothing window of 2 to avoid jitter-induced false minima.
|
|
82
|
+
for (let f = 2; f < v.length - 2; f++) {
|
|
83
|
+
if (v[f] < v[f - 1] && v[f] < v[f + 1] && v[f] < v[f - 2] && v[f] < v[f + 2]) {
|
|
84
|
+
minima.push(f);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return minima;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=keyframeExtract.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maskPrep — mask preparation (ported from the retired python
|
|
3
|
+
* implementation; the lockstep fixtures are gone — the deterministic
|
|
4
|
+
* loops below are the authority now).
|
|
5
|
+
*
|
|
6
|
+
* Where it runs:
|
|
7
|
+
* • Mask Studio "Prep preview" (view-only — shows the EFFECTIVE mask
|
|
8
|
+
* the Inpaint card will consume; the studio itself writes RAW masks).
|
|
9
|
+
* • Canvas Inpaint Studio (live preview before commit).
|
|
10
|
+
*
|
|
11
|
+
* Numbers are IEEE-754 float64 on both sides; accumulation order is fixed
|
|
12
|
+
* (kernel index ascending) so sequences match Python exactly.
|
|
13
|
+
*
|
|
14
|
+
* NO GPU, no ComfyUI calls — pure geometry, same as the Python module.
|
|
15
|
+
*/
|
|
16
|
+
export interface MaskPrep {
|
|
17
|
+
/** >0 dilate, <0 erode (px, exact Euclidean disk, binarize at 0.5) */
|
|
18
|
+
grow: number;
|
|
19
|
+
/** gaussian falloff radius px (sigma = radius/2). 0 = off */
|
|
20
|
+
feather: number;
|
|
21
|
+
/** re-binarize at >= t AFTER feather. 0 (or negative) = disabled sentinel */
|
|
22
|
+
threshold: number;
|
|
23
|
+
/** 1 - m, applied LAST */
|
|
24
|
+
invert: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function isIdentityPrep(p: MaskPrep): boolean;
|
|
27
|
+
export interface OutpaintSpec {
|
|
28
|
+
top: number;
|
|
29
|
+
right: number;
|
|
30
|
+
bottom: number;
|
|
31
|
+
left: number;
|
|
32
|
+
overlap: number;
|
|
33
|
+
}
|
|
34
|
+
/** Latent alignment: padded canvas dims round up to a multiple of this. */
|
|
35
|
+
export declare const DEFAULT_ALIGNMENT = 8;
|
|
36
|
+
/** Row-major float64 mask, 1.0 = regenerate. Shape: {h, w}. */
|
|
37
|
+
export interface MaskMat {
|
|
38
|
+
w: number;
|
|
39
|
+
h: number;
|
|
40
|
+
data: Float64Array;
|
|
41
|
+
}
|
|
42
|
+
export declare function matFromNested(rows: number[][]): MaskMat;
|
|
43
|
+
export declare function matToNested(m: MaskMat): number[][];
|
|
44
|
+
export declare function cloneMat(m: MaskMat): MaskMat;
|
|
45
|
+
/** Half-width of the disk per row offset dy ∈ [-r, r]: floor(sqrt(r²-dy²)). */
|
|
46
|
+
export declare function diskRowHalfWidths(radius: number): number[];
|
|
47
|
+
/** Dilate/erode the ≥0.5 region by an exact disk. Eroding to empty
|
|
48
|
+
* THROWS (no silent fallback — mirror of the Python hard error). */
|
|
49
|
+
export declare function growMask(m: MaskMat, px: number): MaskMat;
|
|
50
|
+
/** sigma = radius/2, truncated at ±ceil(3σ), renormalized. */
|
|
51
|
+
export declare function gaussianKernel(radius: number): Float64Array;
|
|
52
|
+
/** Gaussian feather (columns then rows — same axis order as Python). */
|
|
53
|
+
export declare function featherMask(m: MaskMat, radius: number): MaskMat;
|
|
54
|
+
export declare function thresholdMask(m: MaskMat, t: number): MaskMat;
|
|
55
|
+
export declare function invertMask(m: MaskMat): MaskMat;
|
|
56
|
+
export declare function coverage(m: MaskMat): number;
|
|
57
|
+
export interface PrepReport {
|
|
58
|
+
applied: string[];
|
|
59
|
+
}
|
|
60
|
+
/** FIXED chain: grow → feather → threshold → invert. */
|
|
61
|
+
export declare function applyPrep(m: MaskMat, prep: MaskPrep): {
|
|
62
|
+
mask: MaskMat;
|
|
63
|
+
report: PrepReport;
|
|
64
|
+
};
|
|
65
|
+
export declare function roundToMultiple(value: number, multiple?: number): number;
|
|
66
|
+
export interface OutpaintGeometry {
|
|
67
|
+
newW: number;
|
|
68
|
+
newH: number;
|
|
69
|
+
offX: number;
|
|
70
|
+
offY: number;
|
|
71
|
+
mask: MaskMat;
|
|
72
|
+
actualRight: number;
|
|
73
|
+
actualBottom: number;
|
|
74
|
+
}
|
|
75
|
+
/** Derive padded-canvas geometry + regenerate mask (border ∪ overlap). */
|
|
76
|
+
export declare function deriveOutpaint(origW: number, origH: number, spec: OutpaintSpec, alignment?: number): OutpaintGeometry;
|
|
77
|
+
/** Overlap seam-kill on an ALREADY-padded canvas (client composite).
|
|
78
|
+
* Original rect = (left, top) .. (w-right, h-bottom). */
|
|
79
|
+
export declare function applyOutpaintOverlap(m: MaskMat, spec: OutpaintSpec): MaskMat;
|
|
80
|
+
/** Place wired 1:1 at (offX, offY) and union (max). */
|
|
81
|
+
export declare function unionMaskAt(canvasMask: MaskMat, wired: MaskMat, offX: number, offY: number): MaskMat;
|
|
82
|
+
/** Extract a mask from ImageData. channel: "alpha" (RGBA canvas carrier)
|
|
83
|
+
* or "red" (white-on-black studio carrier). Values → [0,1] float64. */
|
|
84
|
+
export declare function maskFromImageData(img: ImageData, channel?: "alpha" | "red"): MaskMat;
|
|
85
|
+
/** Composite a mask into ImageData as tinted RGBA (premultiplied by the
|
|
86
|
+
* caller's tint color) — the studio overlay renderer. */
|
|
87
|
+
export declare function tintMaskToImageData(m: MaskMat, img: ImageData, r: number, g: number, b: number, alpha?: number): void;
|
|
88
|
+
//# sourceMappingURL=maskPrep.d.ts.map
|
package/lib/maskPrep.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maskPrep — mask preparation (ported from the retired python
|
|
3
|
+
* implementation; the lockstep fixtures are gone — the deterministic
|
|
4
|
+
* loops below are the authority now).
|
|
5
|
+
*
|
|
6
|
+
* Where it runs:
|
|
7
|
+
* • Mask Studio "Prep preview" (view-only — shows the EFFECTIVE mask
|
|
8
|
+
* the Inpaint card will consume; the studio itself writes RAW masks).
|
|
9
|
+
* • Canvas Inpaint Studio (live preview before commit).
|
|
10
|
+
*
|
|
11
|
+
* Numbers are IEEE-754 float64 on both sides; accumulation order is fixed
|
|
12
|
+
* (kernel index ascending) so sequences match Python exactly.
|
|
13
|
+
*
|
|
14
|
+
* NO GPU, no ComfyUI calls — pure geometry, same as the Python module.
|
|
15
|
+
*/
|
|
16
|
+
export function isIdentityPrep(p) {
|
|
17
|
+
return (p.grow === 0 &&
|
|
18
|
+
p.feather === 0 &&
|
|
19
|
+
p.threshold <= 0 &&
|
|
20
|
+
!p.invert);
|
|
21
|
+
}
|
|
22
|
+
/** Latent alignment: padded canvas dims round up to a multiple of this. */
|
|
23
|
+
export const DEFAULT_ALIGNMENT = 8;
|
|
24
|
+
export function matFromNested(rows) {
|
|
25
|
+
const h = rows.length;
|
|
26
|
+
const w = h > 0 ? rows[0].length : 0;
|
|
27
|
+
const data = new Float64Array(h * w);
|
|
28
|
+
for (let y = 0; y < h; y++) {
|
|
29
|
+
const row = rows[y];
|
|
30
|
+
if (row.length !== w)
|
|
31
|
+
throw new Error("ragged matrix");
|
|
32
|
+
for (let x = 0; x < w; x++)
|
|
33
|
+
data[y * w + x] = row[x];
|
|
34
|
+
}
|
|
35
|
+
return { w, h, data };
|
|
36
|
+
}
|
|
37
|
+
export function matToNested(m) {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (let y = 0; y < m.h; y++) {
|
|
40
|
+
const row = new Array(m.w);
|
|
41
|
+
for (let x = 0; x < m.w; x++)
|
|
42
|
+
row[x] = m.data[y * m.w + x];
|
|
43
|
+
out.push(row);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function cloneMat(m) {
|
|
48
|
+
return { w: m.w, h: m.h, data: new Float64Array(m.data) };
|
|
49
|
+
}
|
|
50
|
+
// ── Exact Euclidean disk ─────────────────────────────────────────────────
|
|
51
|
+
/** Half-width of the disk per row offset dy ∈ [-r, r]: floor(sqrt(r²-dy²)). */
|
|
52
|
+
export function diskRowHalfWidths(radius) {
|
|
53
|
+
const r = radius | 0;
|
|
54
|
+
const out = [];
|
|
55
|
+
for (let dy = -r; dy <= r; dy++) {
|
|
56
|
+
out.push(Math.floor(Math.sqrt(r * r - dy * dy)));
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** Horizontal sliding-window OR (half-width k) via prefix sums, O(h·w). */
|
|
61
|
+
function horizDilateBool(b, w, h, k) {
|
|
62
|
+
if (k <= 0)
|
|
63
|
+
return b.slice();
|
|
64
|
+
const pw = w + 2 * k;
|
|
65
|
+
const padded = new Int32Array(h * pw); // 0-filled = False padding
|
|
66
|
+
for (let y = 0; y < h; y++) {
|
|
67
|
+
for (let x = 0; x < w; x++)
|
|
68
|
+
padded[y * pw + k + x] = b[y * w + x];
|
|
69
|
+
}
|
|
70
|
+
const out = new Uint8Array(h * w);
|
|
71
|
+
for (let y = 0; y < h; y++) {
|
|
72
|
+
// running prefix along the padded row
|
|
73
|
+
let csum = 0;
|
|
74
|
+
const cpre = new Int32Array(pw + 1);
|
|
75
|
+
for (let t = 0; t < pw; t++) {
|
|
76
|
+
csum += padded[y * pw + t];
|
|
77
|
+
cpre[t + 1] = csum;
|
|
78
|
+
}
|
|
79
|
+
for (let x = 0; x < w; x++) {
|
|
80
|
+
// window covers padded [x, x+2k] → cpre[x+2k+1] - cpre[x]
|
|
81
|
+
out[y * w + x] =
|
|
82
|
+
cpre[x + 2 * k + 1] - cpre[x] > 0 ? 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** Exact disk dilation/erosion. Erode = ~dilate(~b); outside behaves as
|
|
88
|
+
* mask material for erosion (lenient edges — GrowMask parity). */
|
|
89
|
+
function diskMorph(b, w, h, radius, erode) {
|
|
90
|
+
const r = radius | 0;
|
|
91
|
+
if (r <= 0)
|
|
92
|
+
return b.slice();
|
|
93
|
+
const src = new Uint8Array(b.length);
|
|
94
|
+
if (erode)
|
|
95
|
+
for (let i = 0; i < b.length; i++)
|
|
96
|
+
src[i] = b[i] ? 0 : 1;
|
|
97
|
+
else
|
|
98
|
+
src.set(b);
|
|
99
|
+
const acc = new Uint8Array(h * w);
|
|
100
|
+
const hw = diskRowHalfWidths(r);
|
|
101
|
+
for (let dy = -r; dy <= r; dy++) {
|
|
102
|
+
const k = hw[dy + r];
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if (k < 0)
|
|
106
|
+
continue;
|
|
107
|
+
const row = horizDilateBool(src, w, h, k);
|
|
108
|
+
if (dy === 0) {
|
|
109
|
+
for (let i = 0; i < acc.length; i++)
|
|
110
|
+
if (row[i])
|
|
111
|
+
acc[i] = 1;
|
|
112
|
+
}
|
|
113
|
+
else if (dy > 0) {
|
|
114
|
+
for (let y = dy; y < h; y++) {
|
|
115
|
+
const ai = y * w, ri = (y - dy) * w;
|
|
116
|
+
for (let x = 0; x < w; x++)
|
|
117
|
+
if (row[ri + x])
|
|
118
|
+
acc[ai + x] = 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
for (let y = 0; y < h + dy; y++) {
|
|
123
|
+
const ai = y * w, ri = (y - dy) * w;
|
|
124
|
+
for (let x = 0; x < w; x++)
|
|
125
|
+
if (row[ri + x])
|
|
126
|
+
acc[ai + x] = 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!erode)
|
|
131
|
+
return acc;
|
|
132
|
+
const out = new Uint8Array(b.length);
|
|
133
|
+
for (let i = 0; i < acc.length; i++)
|
|
134
|
+
out[i] = acc[i] ? 0 : 1;
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
/** Dilate/erode the ≥0.5 region by an exact disk. Eroding to empty
|
|
138
|
+
* THROWS (no silent fallback — mirror of the Python hard error). */
|
|
139
|
+
export function growMask(m, px) {
|
|
140
|
+
px = px | 0;
|
|
141
|
+
if (px === 0)
|
|
142
|
+
return cloneMat(m);
|
|
143
|
+
const b = new Uint8Array(m.w * m.h);
|
|
144
|
+
for (let i = 0; i < b.length; i++)
|
|
145
|
+
b[i] = m.data[i] >= 0.5 ? 1 : 0;
|
|
146
|
+
let outB;
|
|
147
|
+
if (px > 0) {
|
|
148
|
+
outB = diskMorph(b, m.w, m.h, px, false);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
outB = diskMorph(b, m.w, m.h, -px, true);
|
|
152
|
+
let any2 = false;
|
|
153
|
+
for (let i = 0; i < outB.length; i++)
|
|
154
|
+
if (outB[i]) {
|
|
155
|
+
any2 = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
if (!any2) {
|
|
159
|
+
throw new Error(`maskPrep: grow(${px}) eroded the mask to NOTHING — the ` +
|
|
160
|
+
`regenerate region vanished. Reduce the shrink (grow < 0) or ` +
|
|
161
|
+
`paint a larger region. NO SILENT FALLBACK.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const data = new Float64Array(outB.length);
|
|
165
|
+
for (let i = 0; i < outB.length; i++)
|
|
166
|
+
data[i] = outB[i];
|
|
167
|
+
return { w: m.w, h: m.h, data };
|
|
168
|
+
}
|
|
169
|
+
// ── Feather ──────────────────────────────────────────────────────────────
|
|
170
|
+
/** sigma = radius/2, truncated at ±ceil(3σ), renormalized. */
|
|
171
|
+
export function gaussianKernel(radius) {
|
|
172
|
+
const sigma = radius / 2;
|
|
173
|
+
const k = Math.max(1, Math.ceil(3 * sigma));
|
|
174
|
+
const xs = [];
|
|
175
|
+
for (let x = -k; x <= k; x++)
|
|
176
|
+
xs.push(x);
|
|
177
|
+
const g = new Float64Array(xs.length);
|
|
178
|
+
let sum = 0;
|
|
179
|
+
for (let i = 0; i < xs.length; i++) {
|
|
180
|
+
const x = xs[i];
|
|
181
|
+
g[i] = Math.exp(-(x * x) / (2 * sigma * sigma));
|
|
182
|
+
sum += g[i];
|
|
183
|
+
}
|
|
184
|
+
for (let i = 0; i < g.length; i++)
|
|
185
|
+
g[i] = g[i] / sum;
|
|
186
|
+
return g;
|
|
187
|
+
}
|
|
188
|
+
/** 1D convolution along an axis, replicate edges, fixed accumulation
|
|
189
|
+
* order (kernel index ascending) — the parity-critical loop. */
|
|
190
|
+
function convAxisReplicate(m, kernel, axis) {
|
|
191
|
+
const { w, h, data } = m;
|
|
192
|
+
const k = (kernel.length - 1) >> 1;
|
|
193
|
+
const out = new Float64Array(w * h);
|
|
194
|
+
if (axis === 1) {
|
|
195
|
+
for (let y = 0; y < h; y++) {
|
|
196
|
+
const rowIn = new Float64Array(w + 2 * k);
|
|
197
|
+
for (let t = 0; t < k; t++)
|
|
198
|
+
rowIn[t] = data[y * w];
|
|
199
|
+
for (let x = 0; x < w; x++)
|
|
200
|
+
rowIn[k + x] = data[y * w + x];
|
|
201
|
+
for (let t = 0; t < k; t++)
|
|
202
|
+
rowIn[k + w + t] = data[y * w + w - 1];
|
|
203
|
+
for (let x = 0; x < w; x++) {
|
|
204
|
+
let acc = 0;
|
|
205
|
+
for (let i = 0; i < kernel.length; i++) {
|
|
206
|
+
acc += kernel[i] * rowIn[i + x];
|
|
207
|
+
}
|
|
208
|
+
out[y * w + x] = acc;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
for (let x = 0; x < w; x++) {
|
|
214
|
+
const colIn = new Float64Array(h + 2 * k);
|
|
215
|
+
for (let t = 0; t < k; t++)
|
|
216
|
+
colIn[t] = data[x];
|
|
217
|
+
for (let y = 0; y < h; y++)
|
|
218
|
+
colIn[k + y] = data[y * w + x];
|
|
219
|
+
for (let t = 0; t < k; t++)
|
|
220
|
+
colIn[k + h + t] = data[(h - 1) * w + x];
|
|
221
|
+
for (let y = 0; y < h; y++) {
|
|
222
|
+
let acc = 0;
|
|
223
|
+
for (let i = 0; i < kernel.length; i++) {
|
|
224
|
+
acc += kernel[i] * colIn[i + y];
|
|
225
|
+
}
|
|
226
|
+
out[y * w + x] = acc;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { w, h, data: out };
|
|
231
|
+
}
|
|
232
|
+
/** Gaussian feather (columns then rows — same axis order as Python). */
|
|
233
|
+
export function featherMask(m, radius) {
|
|
234
|
+
radius = radius | 0;
|
|
235
|
+
if (radius <= 0)
|
|
236
|
+
return cloneMat(m);
|
|
237
|
+
const kernel = gaussianKernel(radius);
|
|
238
|
+
let out = convAxisReplicate(m, kernel, 1);
|
|
239
|
+
out = convAxisReplicate(out, kernel, 0);
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
// ── Threshold / invert / coverage ────────────────────────────────────────
|
|
243
|
+
export function thresholdMask(m, t) {
|
|
244
|
+
if (t <= 0)
|
|
245
|
+
return cloneMat(m);
|
|
246
|
+
const data = new Float64Array(m.data.length);
|
|
247
|
+
for (let i = 0; i < data.length; i++)
|
|
248
|
+
data[i] = m.data[i] >= t ? 1 : 0;
|
|
249
|
+
return { w: m.w, h: m.h, data };
|
|
250
|
+
}
|
|
251
|
+
export function invertMask(m) {
|
|
252
|
+
const data = new Float64Array(m.data.length);
|
|
253
|
+
for (let i = 0; i < data.length; i++)
|
|
254
|
+
data[i] = 1 - m.data[i];
|
|
255
|
+
return { w: m.w, h: m.h, data };
|
|
256
|
+
}
|
|
257
|
+
export function coverage(m) {
|
|
258
|
+
let s = 0;
|
|
259
|
+
for (let i = 0; i < m.data.length; i++)
|
|
260
|
+
s += m.data[i];
|
|
261
|
+
return s / m.data.length;
|
|
262
|
+
}
|
|
263
|
+
/** FIXED chain: grow → feather → threshold → invert. */
|
|
264
|
+
export function applyPrep(m, prep) {
|
|
265
|
+
let out = cloneMat(m);
|
|
266
|
+
const applied = [];
|
|
267
|
+
if (prep.grow) {
|
|
268
|
+
out = growMask(out, prep.grow);
|
|
269
|
+
applied.push(`grow(${prep.grow}px)`);
|
|
270
|
+
}
|
|
271
|
+
if (prep.feather) {
|
|
272
|
+
out = featherMask(out, prep.feather);
|
|
273
|
+
applied.push(`feather(${prep.feather}px)`);
|
|
274
|
+
}
|
|
275
|
+
if (prep.threshold > 0) {
|
|
276
|
+
out = thresholdMask(out, prep.threshold);
|
|
277
|
+
applied.push(`threshold(${prep.threshold})`);
|
|
278
|
+
}
|
|
279
|
+
if (prep.invert) {
|
|
280
|
+
out = invertMask(out);
|
|
281
|
+
applied.push("invert");
|
|
282
|
+
}
|
|
283
|
+
return { mask: out, report: { applied: applied.length ? applied : ["identity"] } };
|
|
284
|
+
}
|
|
285
|
+
// ── Outpaint ─────────────────────────────────────────────────────────────
|
|
286
|
+
export function roundToMultiple(value, multiple = DEFAULT_ALIGNMENT) {
|
|
287
|
+
if (multiple <= 0)
|
|
288
|
+
throw new Error("alignment multiple must be > 0");
|
|
289
|
+
return (Math.ceil(value / multiple)) * multiple;
|
|
290
|
+
}
|
|
291
|
+
/** Derive padded-canvas geometry + regenerate mask (border ∪ overlap). */
|
|
292
|
+
export function deriveOutpaint(origW, origH, spec, alignment = DEFAULT_ALIGNMENT) {
|
|
293
|
+
if (origW <= 0 || origH <= 0)
|
|
294
|
+
throw new Error("original dims must be positive");
|
|
295
|
+
if (spec.top < 0 || spec.right < 0 || spec.bottom < 0 || spec.left < 0) {
|
|
296
|
+
throw new Error("outpaint pads must be >= 0");
|
|
297
|
+
}
|
|
298
|
+
if (spec.overlap < 0)
|
|
299
|
+
throw new Error("overlap must be >= 0");
|
|
300
|
+
const newW = roundToMultiple(origW + spec.left + spec.right, alignment);
|
|
301
|
+
const newH = roundToMultiple(origH + spec.top + spec.bottom, alignment);
|
|
302
|
+
const offX = spec.left, offY = spec.top;
|
|
303
|
+
const keep = new Uint8Array(newW * newH);
|
|
304
|
+
for (let y = offY; y < offY + origH; y++) {
|
|
305
|
+
for (let x = offX; x < offX + origW; x++)
|
|
306
|
+
keep[y * newW + x] = 1;
|
|
307
|
+
}
|
|
308
|
+
const border = new Uint8Array(keep.length);
|
|
309
|
+
for (let i = 0; i < keep.length; i++)
|
|
310
|
+
border[i] = keep[i] ? 0 : 1;
|
|
311
|
+
const grown = spec.overlap > 0
|
|
312
|
+
? diskMorph(border, newW, newH, spec.overlap, false)
|
|
313
|
+
: border;
|
|
314
|
+
const data = new Float64Array(grown.length);
|
|
315
|
+
for (let i = 0; i < grown.length; i++)
|
|
316
|
+
data[i] = grown[i];
|
|
317
|
+
return {
|
|
318
|
+
newW, newH, offX, offY,
|
|
319
|
+
mask: { w: newW, h: newH, data },
|
|
320
|
+
actualRight: newW - offX - origW,
|
|
321
|
+
actualBottom: newH - offY - origH,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/** Overlap seam-kill on an ALREADY-padded canvas (client composite).
|
|
325
|
+
* Original rect = (left, top) .. (w-right, h-bottom). */
|
|
326
|
+
export function applyOutpaintOverlap(m, spec) {
|
|
327
|
+
const { w, h } = m;
|
|
328
|
+
if (spec.top + spec.bottom > h || spec.left + spec.right > w) {
|
|
329
|
+
throw new Error(`maskPrep: pads (t${spec.top}/r${spec.right}/b${spec.bottom}/` +
|
|
330
|
+
`l${spec.left}) exceed canvas ${w}x${h}`);
|
|
331
|
+
}
|
|
332
|
+
const keep = new Uint8Array(w * h);
|
|
333
|
+
for (let y = spec.top; y < h - spec.bottom; y++) {
|
|
334
|
+
for (let x = spec.left; x < w - spec.right; x++)
|
|
335
|
+
keep[y * w + x] = 1;
|
|
336
|
+
}
|
|
337
|
+
const border = new Uint8Array(w * h);
|
|
338
|
+
for (let i = 0; i < keep.length; i++)
|
|
339
|
+
border[i] = keep[i] ? 0 : 1;
|
|
340
|
+
const grown = spec.overlap > 0
|
|
341
|
+
? diskMorph(border, w, h, spec.overlap, false)
|
|
342
|
+
: border;
|
|
343
|
+
const data = new Float64Array(w * h);
|
|
344
|
+
for (let i = 0; i < data.length; i++) {
|
|
345
|
+
data[i] = m.data[i] > grown[i] ? m.data[i] : grown[i];
|
|
346
|
+
}
|
|
347
|
+
return { w, h, data };
|
|
348
|
+
}
|
|
349
|
+
/** Place wired 1:1 at (offX, offY) and union (max). */
|
|
350
|
+
export function unionMaskAt(canvasMask, wired, offX, offY) {
|
|
351
|
+
const out = cloneMat(canvasMask);
|
|
352
|
+
for (let y = 0; y < wired.h; y++) {
|
|
353
|
+
for (let x = 0; x < wired.w; x++) {
|
|
354
|
+
const v = wired.data[y * wired.w + x];
|
|
355
|
+
const i = (offY + y) * out.w + (offX + x);
|
|
356
|
+
if (v > out.data[i])
|
|
357
|
+
out.data[i] = v;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
// ── ImageData bridge (studio/canvas preview source) ──────────────────────
|
|
363
|
+
/** Extract a mask from ImageData. channel: "alpha" (RGBA canvas carrier)
|
|
364
|
+
* or "red" (white-on-black studio carrier). Values → [0,1] float64. */
|
|
365
|
+
export function maskFromImageData(img, channel = "alpha") {
|
|
366
|
+
const { width: w, height: h, data } = img;
|
|
367
|
+
const out = new Float64Array(w * h);
|
|
368
|
+
const stride = 4; // ImageData is always RGBA
|
|
369
|
+
const off = channel === "alpha" ? 3 : 0;
|
|
370
|
+
for (let i = 0; i < w * h; i++)
|
|
371
|
+
out[i] = data[i * stride + off] / 255;
|
|
372
|
+
return { w, h, data: out };
|
|
373
|
+
}
|
|
374
|
+
/** Composite a mask into ImageData as tinted RGBA (premultiplied by the
|
|
375
|
+
* caller's tint color) — the studio overlay renderer. */
|
|
376
|
+
export function tintMaskToImageData(m, img, r, g, b, alpha = 128) {
|
|
377
|
+
for (let i = 0; i < m.w * m.h; i++) {
|
|
378
|
+
const v = m.data[i];
|
|
379
|
+
const o = i * 4;
|
|
380
|
+
img.data[o] = Math.round(img.data[o] * (1 - v) + r * v);
|
|
381
|
+
img.data[o + 1] = Math.round(img.data[o + 1] * (1 - v) + g * v);
|
|
382
|
+
img.data[o + 2] = Math.round(img.data[o + 2] * (1 - v) + b * v);
|
|
383
|
+
img.data[o + 3] = Math.round(img.data[o + 3] * (1 - v) + alpha * v);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
//# sourceMappingURL=maskPrep.js.map
|