@meta-sam/graphics 0.1.5

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.
@@ -0,0 +1,230 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
3
+ */
4
+ import { SegmentationResourceLimitError } from './errors.js';
5
+ /**
6
+ * A contour this small is a lone pixel or a two-pixel sliver, whose polygon is
7
+ * already smaller than the stroke. Smoothing would shrink it further — the
8
+ * single-pixel diamond loses a quarter of its radius — so it keeps its
9
+ * straight segments and stays a visible mark.
10
+ */
11
+ const SMALL_CONTOUR_VERTICES = 4;
12
+ /**
13
+ * Doubled lattice coordinate to SVG user units. Vertices sit on half pixels
14
+ * and their midpoints on quarter pixels, so one decimal is exact for a vertex
15
+ * and rounds a midpoint by at most 0.05 source pixels — two orders of
16
+ * magnitude under the stroke width, and it keeps the path string short.
17
+ */
18
+ function coordinate(doubled) {
19
+ return String(Math.round(doubled * 5) / 10);
20
+ }
21
+ /**
22
+ * Marching-squares cell cases. A cell spans four pixel centers, so its corners
23
+ * are the centers of pixels (cx, cy), (cx + 1, cy), (cx + 1, cy + 1) and
24
+ * (cx, cy + 1); the grid runs from cx = -1 so that the border of the raster is
25
+ * covered by cells whose outer corners are empty.
26
+ */
27
+ const TOP_LEFT = 1;
28
+ const TOP_RIGHT = 2;
29
+ const BOTTOM_RIGHT = 4;
30
+ const BOTTOM_LEFT = 8;
31
+ /**
32
+ * Crossing vertices, in doubled coordinates so every lattice point is an
33
+ * integer: a crossing sits at the midpoint of the cell edge it cuts, which is
34
+ * always half a pixel from one pixel center and half a pixel from the next.
35
+ */
36
+ function topVertex(cx, cy, stride) {
37
+ return (2 * cy + 1) * stride + (2 * cx + 2);
38
+ }
39
+ function rightVertex(cx, cy, stride) {
40
+ return (2 * cy + 2) * stride + (2 * cx + 3);
41
+ }
42
+ function bottomVertex(cx, cy, stride) {
43
+ return (2 * cy + 3) * stride + (2 * cx + 2);
44
+ }
45
+ function leftVertex(cx, cy, stride) {
46
+ return (2 * cy + 2) * stride + (2 * cx + 1);
47
+ }
48
+ /**
49
+ * Emits a decimated polygon as straight segments, the geometry the tracer
50
+ * produced before any smoothing.
51
+ */
52
+ function polyline(vx, vy) {
53
+ let part = `M${coordinate(vx[0])} ${coordinate(vy[0])}`;
54
+ for (let index = 1; index < vx.length; index += 1) {
55
+ part += `L${coordinate(vx[index])} ${coordinate(vy[index])}`;
56
+ }
57
+ return `${part}Z`;
58
+ }
59
+ /**
60
+ * Emits a closed polygon as a uniform quadratic B-spline: the curve runs
61
+ * through the midpoint of every edge and takes each vertex as the control
62
+ * point between two midpoints, so `M m₀ Q p₁ m₁ … Q p₀ m₀ Z`.
63
+ *
64
+ * The curve therefore never passes through a vertex: a 1-pixel step becomes a
65
+ * curve instead of a corner, while the middle of a long edge is a knot the
66
+ * curve interpolates with the edge's own tangent, so decimated straight runs
67
+ * stay straight. A vertex is pulled toward the chord of its two midpoints by
68
+ * an eighth of the difference of its edge vectors, which for the 45° bevel
69
+ * marching squares puts on a right-angle corner is under half a source pixel.
70
+ */
71
+ function spline(vx, vy) {
72
+ const total = vx.length;
73
+ const midX = (index) => (vx[index] + vx[(index + 1) % total]) / 2;
74
+ const midY = (index) => (vy[index] + vy[(index + 1) % total]) / 2;
75
+ const startX = coordinate(midX(0));
76
+ const startY = coordinate(midY(0));
77
+ let part = `M${startX} ${startY}`;
78
+ for (let index = 1; index < total; index += 1) {
79
+ part += `Q${coordinate(vx[index])} ${coordinate(vy[index])} ${coordinate(midX(index))} ${coordinate(midY(index))}`;
80
+ }
81
+ return `${part}Q${coordinate(vx[0])} ${coordinate(vy[0])} ${startX} ${startY}Z`;
82
+ }
83
+ /**
84
+ * Traces the contour of a binary raster as closed polygons with marching
85
+ * squares, then smooths each polygon.
86
+ *
87
+ * Contour vertices are the midpoints of the edges between neighbouring pixel
88
+ * centers, so a boundary that runs straight follows the pixel edge exactly
89
+ * while a corner or a diagonal is cut at 45° instead of stepping. Every
90
+ * segment is emitted with the filled region on its right, which makes outer
91
+ * contours wind opposite to the holes they enclose; the caller fills the
92
+ * result with `evenodd` and strokes the same path.
93
+ *
94
+ * The two saddle cases (a filled diagonal pair) are both resolved as a filled
95
+ * center, matching the eight-connected reading of the raster. That choice is
96
+ * what makes every crossing the endpoint of exactly one segment, so chaining
97
+ * the segments is a walk rather than a search.
98
+ *
99
+ * A polygon is then decimated — a vertex the contour passes straight through
100
+ * is dropped, collapsing a straight run to its two endpoints — and emitted as
101
+ * a quadratic B-spline through the edge midpoints, which turns the 1-pixel
102
+ * staircase of a native-resolution mask into a smooth boundary. Contours of at
103
+ * most `SMALL_CONTOUR_VERTICES` vertices keep their straight segments.
104
+ */
105
+ export function traceContour(raster, width, height, limit, options = {}) {
106
+ const smooth = options.smooth !== false;
107
+ const stride = 2 * width + 1;
108
+ /** Start vertex to end vertex; each vertex starts at most one segment. */
109
+ const next = new Map();
110
+ for (let cy = -1; cy < height; cy += 1) {
111
+ const topRow = cy >= 0 ? cy * width : -1;
112
+ const bottomRow = cy + 1 < height ? (cy + 1) * width : -1;
113
+ let topLeft = 0;
114
+ let bottomLeft = 0;
115
+ for (let cx = -1; cx < width; cx += 1) {
116
+ const column = cx + 1;
117
+ const inside = column < width;
118
+ const topRight = topRow >= 0 && inside ? raster[topRow + column] : 0;
119
+ const bottomRight = bottomRow >= 0 && inside ? raster[bottomRow + column] : 0;
120
+ const corners = (topLeft === 1 ? TOP_LEFT : 0) |
121
+ (topRight === 1 ? TOP_RIGHT : 0) |
122
+ (bottomRight === 1 ? BOTTOM_RIGHT : 0) |
123
+ (bottomLeft === 1 ? BOTTOM_LEFT : 0);
124
+ topLeft = topRight;
125
+ bottomLeft = bottomRight;
126
+ if (corners === 0 || corners === 15)
127
+ continue;
128
+ switch (corners) {
129
+ case TOP_LEFT:
130
+ next.set(topVertex(cx, cy, stride), leftVertex(cx, cy, stride));
131
+ break;
132
+ case TOP_RIGHT:
133
+ next.set(rightVertex(cx, cy, stride), topVertex(cx, cy, stride));
134
+ break;
135
+ case BOTTOM_RIGHT:
136
+ next.set(bottomVertex(cx, cy, stride), rightVertex(cx, cy, stride));
137
+ break;
138
+ case BOTTOM_LEFT:
139
+ next.set(leftVertex(cx, cy, stride), bottomVertex(cx, cy, stride));
140
+ break;
141
+ case TOP_LEFT | TOP_RIGHT:
142
+ next.set(rightVertex(cx, cy, stride), leftVertex(cx, cy, stride));
143
+ break;
144
+ case TOP_RIGHT | BOTTOM_RIGHT:
145
+ next.set(bottomVertex(cx, cy, stride), topVertex(cx, cy, stride));
146
+ break;
147
+ case BOTTOM_RIGHT | BOTTOM_LEFT:
148
+ next.set(leftVertex(cx, cy, stride), rightVertex(cx, cy, stride));
149
+ break;
150
+ case TOP_LEFT | BOTTOM_LEFT:
151
+ next.set(topVertex(cx, cy, stride), bottomVertex(cx, cy, stride));
152
+ break;
153
+ case TOP_RIGHT | BOTTOM_RIGHT | BOTTOM_LEFT:
154
+ next.set(leftVertex(cx, cy, stride), topVertex(cx, cy, stride));
155
+ break;
156
+ case TOP_LEFT | BOTTOM_RIGHT | BOTTOM_LEFT:
157
+ next.set(topVertex(cx, cy, stride), rightVertex(cx, cy, stride));
158
+ break;
159
+ case TOP_LEFT | TOP_RIGHT | BOTTOM_LEFT:
160
+ next.set(rightVertex(cx, cy, stride), bottomVertex(cx, cy, stride));
161
+ break;
162
+ case TOP_LEFT | TOP_RIGHT | BOTTOM_RIGHT:
163
+ next.set(bottomVertex(cx, cy, stride), leftVertex(cx, cy, stride));
164
+ break;
165
+ case TOP_LEFT | BOTTOM_RIGHT:
166
+ next.set(topVertex(cx, cy, stride), rightVertex(cx, cy, stride));
167
+ next.set(bottomVertex(cx, cy, stride), leftVertex(cx, cy, stride));
168
+ break;
169
+ default:
170
+ // TOP_RIGHT | BOTTOM_LEFT, the other saddle.
171
+ next.set(leftVertex(cx, cy, stride), topVertex(cx, cy, stride));
172
+ next.set(rightVertex(cx, cy, stride), bottomVertex(cx, cy, stride));
173
+ break;
174
+ }
175
+ }
176
+ }
177
+ const parts = [];
178
+ let complexity = 0;
179
+ const points = [];
180
+ const vx = [];
181
+ const vy = [];
182
+ while (next.size > 0) {
183
+ const start = next.keys().next().value;
184
+ points.length = 0;
185
+ let vertex = start;
186
+ for (;;) {
187
+ const following = next.get(vertex);
188
+ if (following === undefined)
189
+ break;
190
+ next.delete(vertex);
191
+ points.push(vertex);
192
+ vertex = following;
193
+ if (vertex === start)
194
+ break;
195
+ }
196
+ const total = points.length;
197
+ if (total < 3)
198
+ continue;
199
+ vx.length = 0;
200
+ vy.length = 0;
201
+ for (let index = 0; index < total; index += 1) {
202
+ const point = points[index];
203
+ const previous = points[(index + total - 1) % total];
204
+ const following = points[(index + 1) % total];
205
+ const x = point % stride;
206
+ const y = (point - x) / stride;
207
+ const previousX = previous % stride;
208
+ const followingX = following % stride;
209
+ // Drop a vertex the contour passes straight through, so a run of cells
210
+ // along one pixel edge — or one 45° diagonal — collapses to its two
211
+ // endpoints. Every step of the walk is one cell wide, so equal steps in
212
+ // and out are exactly the collinear case.
213
+ if (followingX - x === x - previousX &&
214
+ (following - followingX) / stride - y === y - (previous - previousX) / stride) {
215
+ continue;
216
+ }
217
+ vx.push(x);
218
+ vy.push(y);
219
+ }
220
+ if (vx.length < 3)
221
+ continue;
222
+ const part = smooth && vx.length > SMALL_CONTOUR_VERTICES ? spline(vx, vy) : polyline(vx, vy);
223
+ complexity += part.length;
224
+ if (complexity > limit) {
225
+ throw new SegmentationResourceLimitError('maxPathComplexity');
226
+ }
227
+ parts.push(part);
228
+ }
229
+ return { d: parts.join(''), complexity };
230
+ }
@@ -0,0 +1,24 @@
1
+ export declare class SegmentationGraphicsError extends Error {
2
+ readonly code: string;
3
+ constructor(message: string, code: string, options?: ErrorOptions);
4
+ }
5
+ export declare class UnsupportedMaskEncodingError extends SegmentationGraphicsError {
6
+ readonly encoding: string;
7
+ constructor(encoding: string);
8
+ }
9
+ export declare class InvalidMaskPayloadError extends SegmentationGraphicsError {
10
+ constructor(message: string);
11
+ }
12
+ export declare class SegmentationResourceLimitError extends SegmentationGraphicsError {
13
+ readonly limit: string;
14
+ constructor(limit: string);
15
+ }
16
+ export declare class InvalidRenderOptionsError extends SegmentationGraphicsError {
17
+ constructor(message: string);
18
+ }
19
+ export declare class RendererDisposedError extends SegmentationGraphicsError {
20
+ constructor();
21
+ }
22
+ export declare class Path2DUnavailableError extends SegmentationGraphicsError {
23
+ constructor();
24
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
3
+ */
4
+ export class SegmentationGraphicsError extends Error {
5
+ code;
6
+ constructor(message, code, options) {
7
+ super(message, options);
8
+ this.code = code;
9
+ this.name = new.target.name;
10
+ }
11
+ }
12
+ export class UnsupportedMaskEncodingError extends SegmentationGraphicsError {
13
+ encoding;
14
+ constructor(encoding) {
15
+ super(`Unsupported complete mask encoding: ${encoding}.`, 'unsupported_encoding');
16
+ this.encoding = encoding;
17
+ }
18
+ }
19
+ export class InvalidMaskPayloadError extends SegmentationGraphicsError {
20
+ constructor(message) {
21
+ super(message, 'invalid_mask_payload');
22
+ }
23
+ }
24
+ export class SegmentationResourceLimitError extends SegmentationGraphicsError {
25
+ limit;
26
+ constructor(limit) {
27
+ super(`Segmentation rendering exceeded the ${limit} limit.`, 'resource_limit');
28
+ this.limit = limit;
29
+ }
30
+ }
31
+ export class InvalidRenderOptionsError extends SegmentationGraphicsError {
32
+ constructor(message) {
33
+ super(message, 'invalid_render_options');
34
+ }
35
+ }
36
+ export class RendererDisposedError extends SegmentationGraphicsError {
37
+ constructor() {
38
+ super('The segmentation renderer has been disposed.', 'renderer_disposed');
39
+ }
40
+ }
41
+ export class Path2DUnavailableError extends SegmentationGraphicsError {
42
+ constructor() {
43
+ super('Path2D is unavailable in this environment.', 'path2d_unavailable');
44
+ }
45
+ }
@@ -0,0 +1,3 @@
1
+ export { InvalidMaskPayloadError, InvalidRenderOptionsError, Path2DUnavailableError, RendererDisposedError, SegmentationGraphicsError, SegmentationResourceLimitError, UnsupportedMaskEncodingError, } from './errors.js';
2
+ export { SegmentationRenderer, objectColor } from './renderer.js';
3
+ export type { ImageRenderOptions, MaskOutlineOptions, Rectangle, SegmentationCanvasContext, SegmentationRendererOptions, SegmentationRenderOptions, SegmentationUpdateOptions, VideoFrameCompositionContext, VideoFrameCompositionOptions, VideoFrameFit, VideoRenderOptions, } from './renderer.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
3
+ */
4
+ export { InvalidMaskPayloadError, InvalidRenderOptionsError, Path2DUnavailableError, RendererDisposedError, SegmentationGraphicsError, SegmentationResourceLimitError, UnsupportedMaskEncodingError, } from './errors.js';
5
+ export { SegmentationRenderer, objectColor } from './renderer.js';
@@ -0,0 +1,111 @@
1
+ import { type SegmentationResult, type SegmentationSnapshot } from '@meta-sam/parser';
2
+ export interface Rectangle {
3
+ readonly x: number;
4
+ readonly y: number;
5
+ readonly width: number;
6
+ readonly height: number;
7
+ }
8
+ export type SegmentationCanvasContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
9
+ export type VideoFrameFit = 'contain' | 'cover' | 'fill';
10
+ /**
11
+ * Structural subset of a Canvas media-player render context.
12
+ *
13
+ * This deliberately does not import @meta-sam/video, so graphics and video remain
14
+ * independently usable without a dependency cycle.
15
+ */
16
+ export interface VideoFrameCompositionContext {
17
+ readonly frame: CanvasImageSource & {
18
+ readonly width: number;
19
+ readonly height: number;
20
+ };
21
+ readonly frameIndex: number;
22
+ readonly canvas: {
23
+ readonly width: number;
24
+ readonly height: number;
25
+ };
26
+ readonly ctx: SegmentationCanvasContext;
27
+ /** Optional bare-frame surface supplied by @meta-sam/video for deadline fallback. */
28
+ readonly fallbackCanvas?: {
29
+ readonly width: number;
30
+ readonly height: number;
31
+ };
32
+ readonly fallbackCtx?: SegmentationCanvasContext;
33
+ readonly signal: AbortSignal;
34
+ }
35
+ export interface VideoFrameCompositionOptions {
36
+ /** Fit the decoded frame into the logical canvas. Defaults to contain. */
37
+ readonly fit?: VideoFrameFit;
38
+ /** Logical-to-backing-store scale. Defaults to 1 and may be read per render. */
39
+ readonly devicePixelRatio?: number | (() => number);
40
+ /** Object IDs, not mask identities, to omit from this composition. */
41
+ readonly hiddenIds?: ReadonlySet<string> | readonly string[];
42
+ }
43
+ export interface MaskOutlineOptions {
44
+ /**
45
+ * Contour width in source pixels — the same space as mask rasters and box
46
+ * edges, so it scales with the source-to-target transform. Defaults to
47
+ * `0.003 × min(source.width, source.height)`.
48
+ */
49
+ readonly width?: number;
50
+ /** Contour opacity. Defaults to 0.8. */
51
+ readonly opacity?: number;
52
+ }
53
+ export interface SegmentationRendererOptions {
54
+ /** Mask fill opacity. Defaults to 0.35. */
55
+ readonly maskFillOpacity?: number;
56
+ /**
57
+ * Stroke the mask contour in the object color on top of the translucent
58
+ * fill. Defaults to enabled; pass `false` for fill only.
59
+ */
60
+ readonly maskOutline?: boolean | MaskOutlineOptions;
61
+ /** Traced paths kept in the LRU cache. */
62
+ readonly maxCachedPaths?: number;
63
+ /** Total traced-path characters kept in the LRU cache. */
64
+ readonly maxCachedComplexity?: number;
65
+ readonly maxRecords?: number;
66
+ readonly maxMasks?: number;
67
+ readonly maxBoxes?: number;
68
+ readonly maxMaskArea?: number;
69
+ readonly maxMaskPayloadLength?: number;
70
+ /** Traced characters a single mask may produce before it fails. */
71
+ readonly maxPathComplexity?: number;
72
+ /**
73
+ * Bookkeeping the retained state may hold across every known frame. Masks are
74
+ * retained as identity plus a reference to the parser's own payload and are
75
+ * traced lazily, so this counts per-mask bookkeeping and box geometry rather
76
+ * than payload or traced-path characters.
77
+ */
78
+ readonly maxRetainedComplexity?: number;
79
+ }
80
+ export interface SegmentationUpdateOptions {
81
+ readonly reset?: boolean;
82
+ }
83
+ interface RenderOptionsBase {
84
+ readonly source: Rectangle;
85
+ readonly target: Rectangle;
86
+ readonly hiddenIds?: ReadonlySet<string> | readonly string[];
87
+ }
88
+ export interface ImageRenderOptions extends RenderOptionsBase {
89
+ readonly media: 'image';
90
+ }
91
+ export interface VideoRenderOptions extends RenderOptionsBase {
92
+ readonly media: 'video';
93
+ readonly frameIndex: number;
94
+ }
95
+ export type SegmentationRenderOptions = ImageRenderOptions | VideoRenderOptions;
96
+ type SegmentationView = SegmentationResult | SegmentationSnapshot;
97
+ /**
98
+ * The fill and stroke color the renderer assigns to an object identifier.
99
+ * Exposed so legends and inspectors can match the composited overlay exactly.
100
+ */
101
+ export declare function objectColor(objectId: string): string;
102
+ export declare class SegmentationRenderer {
103
+ #private;
104
+ constructor(options?: SegmentationRendererOptions);
105
+ update(result: SegmentationView, options?: SegmentationUpdateOptions): Promise<void>;
106
+ renderVideoFrame(composition: VideoFrameCompositionContext, options?: VideoFrameCompositionOptions): boolean;
107
+ render(context: SegmentationCanvasContext, options: SegmentationRenderOptions): void;
108
+ clear(): void;
109
+ dispose(): void;
110
+ }
111
+ export {};