@meta-sam/graphics 0.1.9 → 0.1.11
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/README.md +23 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/renderer.d.ts +19 -0
- package/dist/renderer.js +89 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -272,9 +272,21 @@ are configurable:
|
|
|
272
272
|
- Boxes use the same color at `globalAlpha = 1` and a source-coordinate line width
|
|
273
273
|
of `2 / max(abs(scaleX), abs(scaleY))`.
|
|
274
274
|
- Masks render before boxes.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
- With `boxLabels: true`, each visible box gets a label at its top-left corner
|
|
276
|
+
after all boxes are drawn. The text is
|
|
277
|
+
`formatBoxLabel(boxLabel, objectId, confidence)`: the render's trimmed
|
|
278
|
+
`boxLabel` option, the box's `objectId`, then its parser `confidence` with
|
|
279
|
+
three decimals in parentheses, as `pillow 3 (0.945)`. The label and the
|
|
280
|
+
confidence appear only when present, so a box shows `pillow 3` without a
|
|
281
|
+
confidence and `3 (0.945)` without a label. The text is white 11px on a 16px-high fill in the object color.
|
|
282
|
+
Labels are sized in target CSS pixels, so they do not scale with the media. A
|
|
283
|
+
label sits above its box when the target has room and inside the box's top
|
|
284
|
+
edge otherwise, and it shifts left to stay inside the target. Hidden objects
|
|
285
|
+
and boxes from other video frames get no label. Labels are off by default.
|
|
286
|
+
Pass `boxLabel` to `render()` or `renderVideoFrame()`; it must be a string.
|
|
287
|
+
|
|
288
|
+
The renderer does not mutate records. Object colors, contour geometry, box and
|
|
289
|
+
label styling, draw order, and source transforms are not configurable.
|
|
278
290
|
|
|
279
291
|
## Lifecycle and transactional behavior
|
|
280
292
|
|
|
@@ -316,6 +328,7 @@ import {
|
|
|
316
328
|
const options: SegmentationRendererOptions = {
|
|
317
329
|
maskFillOpacity: 0.5,
|
|
318
330
|
maskOutline: { width: 2, opacity: 0.9 },
|
|
331
|
+
boxLabels: true,
|
|
319
332
|
maxCachedPaths: 128,
|
|
320
333
|
maxCachedComplexity: 250_000,
|
|
321
334
|
maxRecords: 20_000,
|
|
@@ -335,6 +348,7 @@ const renderer = new SegmentationRenderer(options);
|
|
|
335
348
|
| `maskOutline` | `true` |
|
|
336
349
|
| `maskOutline.width` | `0.003 × min(source.width, source.height)` |
|
|
337
350
|
| `maskOutline.opacity` | `0.8` |
|
|
351
|
+
| `boxLabels` | `false` |
|
|
338
352
|
| `maxCachedPaths` | `128` |
|
|
339
353
|
| `maxCachedComplexity` | `250_000` |
|
|
340
354
|
| `maxRecords` | `20_000` |
|
|
@@ -348,7 +362,8 @@ const renderer = new SegmentationRenderer(options);
|
|
|
348
362
|
`maskFillOpacity` and `maskOutline.opacity` must be finite numbers in the inclusive
|
|
349
363
|
range from `0` through `1`. An outline with opacity `0` remains enabled and is still
|
|
350
364
|
stroked. `maskOutline` also accepts `true`, `false`, or `{ width }`; width is in source
|
|
351
|
-
pixels and must be finite and greater than zero.
|
|
365
|
+
pixels and must be finite and greater than zero. `boxLabels` must be a
|
|
366
|
+
boolean. Every resource override must be a
|
|
352
367
|
positive safe integer. Invalid constructor options throw `TypeError`. Constructor
|
|
353
368
|
settings are resolved once, so later mutation of an options object has no effect.
|
|
354
369
|
Cache limits evict least-recently-used paths. Other resource limits reject an update
|
|
@@ -364,6 +379,8 @@ Only the package root is public; deep imports are not supported.
|
|
|
364
379
|
| -------------------------------- | ----------------------------------------------------------- |
|
|
365
380
|
| `SegmentationRenderer` | Retain parser views and render masks and boxes. |
|
|
366
381
|
| `objectColor` | The color assigned to an object ID, for legends and labels. |
|
|
382
|
+
| `formatBoxLabel` | The text of one box label, for matching legends. |
|
|
383
|
+
| `formatConfidence` | A confidence as box labels show it, for matching legends. |
|
|
367
384
|
| `SegmentationGraphicsError` | Base class for package-specific errors. |
|
|
368
385
|
| `UnsupportedMaskEncodingError` | Reject a mask encoding other than `lossless` or `one_bit`. |
|
|
369
386
|
| `InvalidMaskPayloadError` | Reject invalid mask data or a conflicting mask revision. |
|
|
@@ -399,6 +416,8 @@ interface SegmentationRendererOptions {
|
|
|
399
416
|
/** Mask fill opacity. Defaults to 0.35. */
|
|
400
417
|
readonly maskFillOpacity?: number;
|
|
401
418
|
readonly maskOutline?: boolean | MaskOutlineOptions;
|
|
419
|
+
/** Draw "<boxLabel> <objectId> (<confidence>)" box labels. Defaults to false. */
|
|
420
|
+
readonly boxLabels?: boolean;
|
|
402
421
|
// Resource limit options are unchanged.
|
|
403
422
|
}
|
|
404
423
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { InvalidMaskPayloadError, InvalidRenderOptionsError, Path2DUnavailableError, RendererDisposedError, SegmentationGraphicsError, SegmentationResourceLimitError, UnsupportedMaskEncodingError, } from './errors.js';
|
|
2
|
-
export { SegmentationRenderer, objectColor } from './renderer.js';
|
|
2
|
+
export { SegmentationRenderer, formatBoxLabel, formatConfidence, objectColor, } from './renderer.js';
|
|
3
3
|
export type { ImageRenderOptions, MaskOutlineOptions, Rectangle, SegmentationCanvasContext, SegmentationRendererOptions, SegmentationRenderOptions, SegmentationUpdateOptions, VideoFrameCompositionContext, VideoFrameCompositionOptions, VideoFrameFit, VideoRenderOptions, } from './renderer.js';
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
|
|
3
3
|
*/
|
|
4
4
|
export { InvalidMaskPayloadError, InvalidRenderOptionsError, Path2DUnavailableError, RendererDisposedError, SegmentationGraphicsError, SegmentationResourceLimitError, UnsupportedMaskEncodingError, } from './errors.js';
|
|
5
|
-
export { SegmentationRenderer, objectColor } from './renderer.js';
|
|
5
|
+
export { SegmentationRenderer, formatBoxLabel, formatConfidence, objectColor, } from './renderer.js';
|
package/dist/renderer.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface VideoFrameCompositionOptions {
|
|
|
39
39
|
readonly devicePixelRatio?: number | (() => number);
|
|
40
40
|
/** Object IDs, not mask identities, to omit from this composition. */
|
|
41
41
|
readonly hiddenIds?: ReadonlySet<string> | readonly string[];
|
|
42
|
+
/** Text shown before the object ID in box labels; see `boxLabels`. */
|
|
43
|
+
readonly boxLabel?: string;
|
|
42
44
|
}
|
|
43
45
|
export interface MaskOutlineOptions {
|
|
44
46
|
/**
|
|
@@ -58,6 +60,13 @@ export interface SegmentationRendererOptions {
|
|
|
58
60
|
* fill. Defaults to enabled; pass `false` for fill only.
|
|
59
61
|
*/
|
|
60
62
|
readonly maskOutline?: boolean | MaskOutlineOptions;
|
|
63
|
+
/**
|
|
64
|
+
* Draw a label at each box's top-left corner: the render's `boxLabel`, the
|
|
65
|
+
* box's object ID, then its parser `confidence` in parentheses, as
|
|
66
|
+
* `pillow 3 (0.945)`. The label and the confidence appear only when present.
|
|
67
|
+
* Defaults to `false`.
|
|
68
|
+
*/
|
|
69
|
+
readonly boxLabels?: boolean;
|
|
61
70
|
/** Traced paths kept in the LRU cache. */
|
|
62
71
|
readonly maxCachedPaths?: number;
|
|
63
72
|
/** Total traced-path characters kept in the LRU cache. */
|
|
@@ -84,6 +93,8 @@ interface RenderOptionsBase {
|
|
|
84
93
|
readonly source: Rectangle;
|
|
85
94
|
readonly target: Rectangle;
|
|
86
95
|
readonly hiddenIds?: ReadonlySet<string> | readonly string[];
|
|
96
|
+
/** Text shown before the object ID in box labels; see `boxLabels`. */
|
|
97
|
+
readonly boxLabel?: string;
|
|
87
98
|
}
|
|
88
99
|
export interface ImageRenderOptions extends RenderOptionsBase {
|
|
89
100
|
readonly media: 'image';
|
|
@@ -99,6 +110,14 @@ type SegmentationView = SegmentationResult | SegmentationSnapshot;
|
|
|
99
110
|
* Exposed so legends and inspectors can match the composited overlay exactly.
|
|
100
111
|
*/
|
|
101
112
|
export declare function objectColor(objectId: string): string;
|
|
113
|
+
/** A confidence as box labels show it: the value with three decimals. */
|
|
114
|
+
export declare function formatConfidence(confidence: number): string;
|
|
115
|
+
/**
|
|
116
|
+
* The text of one box label: the trimmed label, the object ID, then the
|
|
117
|
+
* confidence in parentheses, as `pillow 3 (0.945)`. A missing or blank label
|
|
118
|
+
* and a missing confidence are left out, so the object ID is always shown.
|
|
119
|
+
*/
|
|
120
|
+
export declare function formatBoxLabel(label: string | undefined, objectId: string, confidence: number | undefined): string;
|
|
102
121
|
export declare class SegmentationRenderer {
|
|
103
122
|
#private;
|
|
104
123
|
constructor(options?: SegmentationRendererOptions);
|
package/dist/renderer.js
CHANGED
|
@@ -179,6 +179,32 @@ const DEFAULT_MASK_OUTLINE_OPACITY = 0.8;
|
|
|
179
179
|
const MASK_OUTLINE_WIDTH_RATIO = 0.003;
|
|
180
180
|
/** Contour width in target CSS pixels when the source size is unusable. */
|
|
181
181
|
const FALLBACK_MASK_OUTLINE_WIDTH = 1.5;
|
|
182
|
+
/** Box label geometry in target CSS pixels. */
|
|
183
|
+
const BOX_LABEL_HEIGHT = 16;
|
|
184
|
+
const BOX_LABEL_PADDING = 4;
|
|
185
|
+
const BOX_LABEL_FONT = '600 11px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
|
|
186
|
+
const BOX_LABEL_TEXT_COLOR = '#ffffff';
|
|
187
|
+
/** A confidence as box labels show it: the value with three decimals. */
|
|
188
|
+
export function formatConfidence(confidence) {
|
|
189
|
+
return confidence.toFixed(3);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The text of one box label: the trimmed label, the object ID, then the
|
|
193
|
+
* confidence in parentheses, as `pillow 3 (0.945)`. A missing or blank label
|
|
194
|
+
* and a missing confidence are left out, so the object ID is always shown.
|
|
195
|
+
*/
|
|
196
|
+
export function formatBoxLabel(label, objectId, confidence) {
|
|
197
|
+
return [
|
|
198
|
+
...(label === undefined || label.trim().length === 0 ? [] : [label.trim()]),
|
|
199
|
+
objectId,
|
|
200
|
+
...(confidence === undefined ? [] : [`(${formatConfidence(confidence)})`]),
|
|
201
|
+
].join(' ');
|
|
202
|
+
}
|
|
203
|
+
function validateBoxLabel(label) {
|
|
204
|
+
if (label !== undefined && typeof label !== 'string') {
|
|
205
|
+
throw new InvalidRenderOptionsError('boxLabel must be a string.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
182
208
|
function resolveMaskOutline(option) {
|
|
183
209
|
if (option === false) {
|
|
184
210
|
return { enabled: false, width: null, opacity: DEFAULT_MASK_OUTLINE_OPACITY };
|
|
@@ -206,6 +232,7 @@ export class SegmentationRenderer {
|
|
|
206
232
|
#limits;
|
|
207
233
|
#maskFillOpacity;
|
|
208
234
|
#outline;
|
|
235
|
+
#boxLabels;
|
|
209
236
|
#cache = new Map();
|
|
210
237
|
#cacheComplexity = 0;
|
|
211
238
|
#state;
|
|
@@ -215,6 +242,10 @@ export class SegmentationRenderer {
|
|
|
215
242
|
constructor(options = {}) {
|
|
216
243
|
this.#maskFillOpacity = unitInterval(options.maskFillOpacity, DEFAULT_MASK_FILL_OPACITY, 'maskFillOpacity');
|
|
217
244
|
this.#outline = resolveMaskOutline(options.maskOutline);
|
|
245
|
+
if (options.boxLabels !== undefined && typeof options.boxLabels !== 'boolean') {
|
|
246
|
+
throw new TypeError('boxLabels must be a boolean.');
|
|
247
|
+
}
|
|
248
|
+
this.#boxLabels = options.boxLabels === true;
|
|
218
249
|
this.#limits = {
|
|
219
250
|
maxCachedPaths: positiveInteger(options.maxCachedPaths, 128, 'maxCachedPaths'),
|
|
220
251
|
maxCachedComplexity: positiveInteger(options.maxCachedComplexity, 250_000, 'maxCachedComplexity'),
|
|
@@ -313,6 +344,7 @@ export class SegmentationRenderer {
|
|
|
313
344
|
source,
|
|
314
345
|
target,
|
|
315
346
|
...(options.hiddenIds === undefined ? {} : { hiddenIds: options.hiddenIds }),
|
|
347
|
+
...(options.boxLabel === undefined ? {} : { boxLabel: options.boxLabel }),
|
|
316
348
|
});
|
|
317
349
|
}
|
|
318
350
|
finally {
|
|
@@ -327,6 +359,7 @@ export class SegmentationRenderer {
|
|
|
327
359
|
validateRectangle(options.target, 'target');
|
|
328
360
|
if (options.media === 'video')
|
|
329
361
|
validateFrame(options.frameIndex);
|
|
362
|
+
validateBoxLabel(options.boxLabel);
|
|
330
363
|
const state = this.#state;
|
|
331
364
|
if (state === undefined)
|
|
332
365
|
return;
|
|
@@ -400,6 +433,54 @@ export class SegmentationRenderer {
|
|
|
400
433
|
2 / Math.max(Math.abs(scaleX), Math.abs(scaleY), Number.MIN_VALUE);
|
|
401
434
|
context.strokeRect(box.left, box.top, box.right - box.left, box.bottom - box.top);
|
|
402
435
|
}
|
|
436
|
+
if (this.#boxLabels) {
|
|
437
|
+
this.#paintBoxLabels(context, state, options, hidden, {
|
|
438
|
+
scaleX,
|
|
439
|
+
scaleY,
|
|
440
|
+
offsetX,
|
|
441
|
+
offsetY,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
context.restore();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Draws each visible box's label in target CSS pixels, so the label keeps
|
|
451
|
+
* one size whatever the media scale. The label sits on the box's top
|
|
452
|
+
* edge, above the box when the target has room and inside it otherwise, and
|
|
453
|
+
* it is shifted left when it would extend past the target's right edge.
|
|
454
|
+
*/
|
|
455
|
+
#paintBoxLabels(context, state, options, hidden, transform) {
|
|
456
|
+
const { scaleX, scaleY, offsetX, offsetY } = transform;
|
|
457
|
+
const { target } = options;
|
|
458
|
+
context.save();
|
|
459
|
+
try {
|
|
460
|
+
// Undo the source-to-target transform; any device-pixel scale stays.
|
|
461
|
+
context.transform(1 / scaleX, 0, 0, 1 / scaleY, -offsetX / scaleX, -offsetY / scaleY);
|
|
462
|
+
context.globalAlpha = 1;
|
|
463
|
+
context.font = BOX_LABEL_FONT;
|
|
464
|
+
context.textAlign = 'left';
|
|
465
|
+
context.textBaseline = 'middle';
|
|
466
|
+
for (const box of state.boxes.values()) {
|
|
467
|
+
if (hidden.has(box.objectId) ||
|
|
468
|
+
(options.media === 'video' &&
|
|
469
|
+
box.frameIndex !== undefined &&
|
|
470
|
+
box.frameIndex !== options.frameIndex)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const text = formatBoxLabel(options.boxLabel, box.objectId, box.confidence);
|
|
474
|
+
const width = context.measureText(text).width + 2 * BOX_LABEL_PADDING;
|
|
475
|
+
const left = offsetX + box.left * scaleX;
|
|
476
|
+
const top = offsetY + box.top * scaleY;
|
|
477
|
+
const x = Math.max(target.x, Math.min(left, target.x + target.width - width));
|
|
478
|
+
const y = top - BOX_LABEL_HEIGHT >= target.y ? top - BOX_LABEL_HEIGHT : top;
|
|
479
|
+
context.fillStyle = colorFor(box.objectId);
|
|
480
|
+
context.fillRect(x, y, width, BOX_LABEL_HEIGHT);
|
|
481
|
+
context.fillStyle = BOX_LABEL_TEXT_COLOR;
|
|
482
|
+
context.fillText(text, x + BOX_LABEL_PADDING, y + BOX_LABEL_HEIGHT / 2);
|
|
483
|
+
}
|
|
403
484
|
}
|
|
404
485
|
finally {
|
|
405
486
|
context.restore();
|
|
@@ -471,6 +552,12 @@ export class SegmentationRenderer {
|
|
|
471
552
|
height < 0) {
|
|
472
553
|
throw new InvalidRenderOptionsError('Box coordinates are invalid.');
|
|
473
554
|
}
|
|
555
|
+
if (record.confidence !== undefined &&
|
|
556
|
+
(!Number.isFinite(record.confidence) ||
|
|
557
|
+
record.confidence < 0 ||
|
|
558
|
+
record.confidence > 1)) {
|
|
559
|
+
throw new InvalidRenderOptionsError('Box confidence must be a number from 0 through 1.');
|
|
560
|
+
}
|
|
474
561
|
const identity = boxIdentity(record);
|
|
475
562
|
boxes.set(identity, {
|
|
476
563
|
identity,
|
|
@@ -482,6 +569,7 @@ export class SegmentationRenderer {
|
|
|
482
569
|
top: record.top,
|
|
483
570
|
right: record.right,
|
|
484
571
|
bottom: record.bottom,
|
|
572
|
+
...(record.confidence === undefined ? {} : { confidence: record.confidence }),
|
|
485
573
|
});
|
|
486
574
|
}
|
|
487
575
|
}
|
|
@@ -496,6 +584,7 @@ export class SegmentationRenderer {
|
|
|
496
584
|
box.top,
|
|
497
585
|
box.right,
|
|
498
586
|
box.bottom,
|
|
587
|
+
box.confidence ?? null,
|
|
499
588
|
]).length;
|
|
500
589
|
if (complexity > this.#limits.maxRetainedComplexity) {
|
|
501
590
|
throw new SegmentationResourceLimitError('maxRetainedComplexity');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meta-sam/graphics",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Retained Canvas 2D renderer for SAM 3 segmentation",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://dev.meta.ai/",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": "^20.17.0 || >=22.9.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@meta-sam/parser": "0.0.
|
|
36
|
+
"@meta-sam/parser": "0.0.13"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsc -b",
|