meta-sam-parser 0.0.2__tar.gz → 0.0.3__tar.gz
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.
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/PKG-INFO +49 -19
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/README.md +48 -18
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/pyproject.toml +1 -1
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/src/meta_sam_parser/__init__.py +2 -2
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/src/meta_sam_parser/_segmentation.py +28 -189
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/src/meta_sam_parser/_types.py +13 -19
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/LICENSE +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/src/meta_sam_parser/_errors.py +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/src/meta_sam_parser/_mask_codec.py +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/src/meta_sam_parser/_mask_conversion.py +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/src/meta_sam_parser/_stream.py +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.3}/src/meta_sam_parser/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-sam-parser
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Native Python parsing primitives for the SAM 3 segmentation protocol
|
|
5
5
|
Project-URL: Homepage, https://github.com/meta-models/meta-sam
|
|
6
6
|
Project-URL: Repository, https://github.com/meta-models/meta-sam
|
|
@@ -106,24 +106,35 @@ python -m pip install .
|
|
|
106
106
|
|
|
107
107
|
Import supported APIs only from the package root. Deep imports are unsupported;
|
|
108
108
|
underscore-prefixed modules are implementation details and may change without notice.
|
|
109
|
-
Format factories are zero-argument and create isolated parser state
|
|
109
|
+
Format factories are zero-argument and create isolated parser state. The example
|
|
110
|
+
feeds the parser one frame of SAM 3.1 output exactly as the API emits it - one line
|
|
111
|
+
per frame, `<Nf>` then comma-separated `id<|box;...|><|mask;...|>` records:
|
|
110
112
|
|
|
111
113
|
```python
|
|
112
114
|
from meta_sam_parser import CompletedOutcome, video_segmentation_format
|
|
113
115
|
|
|
116
|
+
output_text = (
|
|
117
|
+
"<0f>0<|box;x1=211;y1=228;x2=270;y2=254;w=320;h=334|>"
|
|
118
|
+
"<|mask;x=0;y=0;data=27,60,~!!!!M!0c[0o91w?q1!pIH4pPRVp2B3'7`e.ioeAf6-k/#Xd8%dX9x(|>"
|
|
119
|
+
",1<|box;x1=155;y1=228;x2=202;y2=254;w=320;h=334|>"
|
|
120
|
+
"<|mask;x=0;y=0;data=27,48,~!!!!J!0c[q=Pj_zs=*4C4(/./x#:/`S_GnD`=o3?X{emCgO$y@|>\n"
|
|
121
|
+
)
|
|
122
|
+
|
|
114
123
|
format_ = video_segmentation_format()
|
|
115
124
|
parser = format_.create_parser()
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
):
|
|
126
|
+
# Chunks may split anywhere, including inside a mask payload.
|
|
127
|
+
split = output_text.index(",1<|box")
|
|
128
|
+
for snapshot in parser.push(output_text[:split]):
|
|
121
129
|
print(snapshot.revision, len(snapshot.records))
|
|
130
|
+
for snapshot in parser.push(output_text[split:]):
|
|
131
|
+
print(snapshot.revision, len(snapshot.records)) # 1 4
|
|
122
132
|
|
|
123
133
|
finished = parser.finish(CompletedOutcome())
|
|
124
134
|
for snapshot in finished.events:
|
|
125
135
|
print(snapshot.revision, len(snapshot.records))
|
|
126
136
|
result = finished.result
|
|
137
|
+
# Two box records and two mask records, object IDs "0" and "1", frame 0.
|
|
127
138
|
```
|
|
128
139
|
|
|
129
140
|
`push()` accepts arbitrarily split text chunks and returns zero or one cumulative
|
|
@@ -228,10 +239,10 @@ All public values are frozen, slotted dataclasses. Observable collections are
|
|
|
228
239
|
tuples. The package root exports:
|
|
229
240
|
|
|
230
241
|
- Geometry, masks, and aliases: `FrameReference`, `SegmentationMaskBounds`,
|
|
231
|
-
`SegmentationMask`, `SegmentationMaskIdentity`, `SegmentationMedia`,
|
|
242
|
+
`SegmentationMask`, `SegmentationMaskEncoding`, `SegmentationMaskIdentity`, `SegmentationMedia`,
|
|
232
243
|
`DiagnosticSeverity`, and `IncompleteReason`.
|
|
233
|
-
- Records: `SegmentationTextRecord`, `
|
|
234
|
-
`
|
|
244
|
+
- Records: `SegmentationTextRecord`, `SegmentationBoxRecord`,
|
|
245
|
+
`SegmentationMaskRecord`, and `SegmentationRecord`.
|
|
235
246
|
- Views: `SegmentationDiagnostic`, `ImageSegmentationSnapshot`,
|
|
236
247
|
`VideoSegmentationSnapshot`, `SegmentationSnapshot`,
|
|
237
248
|
`ImageSegmentationResult`, `VideoSegmentationResult`, and
|
|
@@ -258,15 +269,31 @@ the next revision.
|
|
|
258
269
|
|
|
259
270
|
## Parsing behavior
|
|
260
271
|
|
|
261
|
-
|
|
272
|
+
SAM 3.1 returns segmentation as special-token text in one `output_text` lane, one
|
|
273
|
+
line per frame:
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
<Nf>id<|box;x1=..;y1=..;x2=..;y2=..;w=<frameW>;h=<frameH>|><|mask;x=0;y=0;data=<H>,<W>,<enc>payload|>,id<|box;...|><|mask;...|>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`<Nf>` is the zero-based frame index; frames without a visible object emit no
|
|
280
|
+
line, so indices can skip. Each comma-separated record is a bare integer object
|
|
281
|
+
id - stable for an object across the frames of one response and not a dense
|
|
282
|
+
sequence - followed by one box and one mask. The parser retains the id as a string
|
|
283
|
+
in `object_id`. Box corners and the `w`/`h` frame size are source pixels; the
|
|
284
|
+
inclusive wire `x2`/`y2` become half-open `right`/`bottom`. The mask tuple is
|
|
285
|
+
`height,width,payload`, where the payload's first character selects the encoding:
|
|
286
|
+
`~` for `lossless` (the API default) or `!` for `one_bit`. The payload is base85,
|
|
287
|
+
not base64: after the marker its digits are printable ASCII `!` through `{` minus
|
|
288
|
+
the wire delimiters `" \ , ; < > |`, so it contains `*`, `$`, brackets, and
|
|
289
|
+
backticks, a base64 regex will not match it, and only the first character after
|
|
290
|
+
`H,W,` is the marker (`!` is also digit zero). Pass it through unchanged. Each record becomes one `SegmentationBoxRecord` followed by
|
|
291
|
+
one `SegmentationMaskRecord` whose `bounds` is the same half-open box. Image streams require frame zero and omit the frame from
|
|
292
|
+
normalized records; video records retain `frame.frame_index`. Every mask is
|
|
293
|
+
strictly decoded before insertion; an empty lane is a valid completed response
|
|
294
|
+
with no records.
|
|
295
|
+
The grammar is specified in the
|
|
262
296
|
[SAM 3 protocol](https://github.com/meta-models/meta-sam/blob/main/protocol/sam3.md).
|
|
263
|
-
The parser separately accepts compact box-first SAM API output as production
|
|
264
|
-
compatibility input and normalizes it to the same records. Its object token is an
|
|
265
|
-
ASCII-decimal ID retained as a string, and IDs may be multi-digit or non-contiguous.
|
|
266
|
-
The compact input's inclusive `x2`/`y2` coordinates become half-open `right`/`bottom`
|
|
267
|
-
bounds. Image API
|
|
268
|
-
records require frame zero and omit it from normalized records; video records retain
|
|
269
|
-
frame indices. Every mask is strictly decoded before insertion.
|
|
270
297
|
|
|
271
298
|
Plain text remains an ordered text record. Malformed structured-looking lines
|
|
272
299
|
produce diagnostics and parsing continues. `raw_output` preserves every input
|
|
@@ -359,8 +386,11 @@ by the `release PyPI distribution` workflow
|
|
|
359
386
|
1. Bump `version` in `pyproject.toml`, run `node scripts/sync-compatibility` from
|
|
360
387
|
the repository root so the compatibility matrix records the new version, and
|
|
361
388
|
merge that change to `main`.
|
|
362
|
-
2. Push the tag `meta-sam-parser@<version>` at that commit
|
|
363
|
-
tag that does not match the manifest version
|
|
389
|
+
2. Push the tag `meta-sam-parser@<version>` at that merge commit on `main`. The
|
|
390
|
+
workflow refuses a tag that does not match the manifest version, a tag whose
|
|
391
|
+
commit is not on `main`, and a tag older than the latest `main` commit touching
|
|
392
|
+
`python/`, `conformance/`, or `protocol/`, so a tag created before later fixes
|
|
393
|
+
merged cannot publish stale source.
|
|
364
394
|
3. The workflow builds the reproducible wheel and sdist, runs the artifact and
|
|
365
395
|
clean-consumer audits and the cross-language conformance suite, and then waits
|
|
366
396
|
for approval in the `pypi` GitHub environment before uploading through PyPI
|
|
@@ -17,24 +17,35 @@ python -m pip install .
|
|
|
17
17
|
|
|
18
18
|
Import supported APIs only from the package root. Deep imports are unsupported;
|
|
19
19
|
underscore-prefixed modules are implementation details and may change without notice.
|
|
20
|
-
Format factories are zero-argument and create isolated parser state
|
|
20
|
+
Format factories are zero-argument and create isolated parser state. The example
|
|
21
|
+
feeds the parser one frame of SAM 3.1 output exactly as the API emits it - one line
|
|
22
|
+
per frame, `<Nf>` then comma-separated `id<|box;...|><|mask;...|>` records:
|
|
21
23
|
|
|
22
24
|
```python
|
|
23
25
|
from meta_sam_parser import CompletedOutcome, video_segmentation_format
|
|
24
26
|
|
|
27
|
+
output_text = (
|
|
28
|
+
"<0f>0<|box;x1=211;y1=228;x2=270;y2=254;w=320;h=334|>"
|
|
29
|
+
"<|mask;x=0;y=0;data=27,60,~!!!!M!0c[0o91w?q1!pIH4pPRVp2B3'7`e.ioeAf6-k/#Xd8%dX9x(|>"
|
|
30
|
+
",1<|box;x1=155;y1=228;x2=202;y2=254;w=320;h=334|>"
|
|
31
|
+
"<|mask;x=0;y=0;data=27,48,~!!!!J!0c[q=Pj_zs=*4C4(/./x#:/`S_GnD`=o3?X{emCgO$y@|>\n"
|
|
32
|
+
)
|
|
33
|
+
|
|
25
34
|
format_ = video_segmentation_format()
|
|
26
35
|
parser = format_.create_parser()
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
):
|
|
37
|
+
# Chunks may split anywhere, including inside a mask payload.
|
|
38
|
+
split = output_text.index(",1<|box")
|
|
39
|
+
for snapshot in parser.push(output_text[:split]):
|
|
32
40
|
print(snapshot.revision, len(snapshot.records))
|
|
41
|
+
for snapshot in parser.push(output_text[split:]):
|
|
42
|
+
print(snapshot.revision, len(snapshot.records)) # 1 4
|
|
33
43
|
|
|
34
44
|
finished = parser.finish(CompletedOutcome())
|
|
35
45
|
for snapshot in finished.events:
|
|
36
46
|
print(snapshot.revision, len(snapshot.records))
|
|
37
47
|
result = finished.result
|
|
48
|
+
# Two box records and two mask records, object IDs "0" and "1", frame 0.
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
`push()` accepts arbitrarily split text chunks and returns zero or one cumulative
|
|
@@ -139,10 +150,10 @@ All public values are frozen, slotted dataclasses. Observable collections are
|
|
|
139
150
|
tuples. The package root exports:
|
|
140
151
|
|
|
141
152
|
- Geometry, masks, and aliases: `FrameReference`, `SegmentationMaskBounds`,
|
|
142
|
-
`SegmentationMask`, `SegmentationMaskIdentity`, `SegmentationMedia`,
|
|
153
|
+
`SegmentationMask`, `SegmentationMaskEncoding`, `SegmentationMaskIdentity`, `SegmentationMedia`,
|
|
143
154
|
`DiagnosticSeverity`, and `IncompleteReason`.
|
|
144
|
-
- Records: `SegmentationTextRecord`, `
|
|
145
|
-
`
|
|
155
|
+
- Records: `SegmentationTextRecord`, `SegmentationBoxRecord`,
|
|
156
|
+
`SegmentationMaskRecord`, and `SegmentationRecord`.
|
|
146
157
|
- Views: `SegmentationDiagnostic`, `ImageSegmentationSnapshot`,
|
|
147
158
|
`VideoSegmentationSnapshot`, `SegmentationSnapshot`,
|
|
148
159
|
`ImageSegmentationResult`, `VideoSegmentationResult`, and
|
|
@@ -169,15 +180,31 @@ the next revision.
|
|
|
169
180
|
|
|
170
181
|
## Parsing behavior
|
|
171
182
|
|
|
172
|
-
|
|
183
|
+
SAM 3.1 returns segmentation as special-token text in one `output_text` lane, one
|
|
184
|
+
line per frame:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
<Nf>id<|box;x1=..;y1=..;x2=..;y2=..;w=<frameW>;h=<frameH>|><|mask;x=0;y=0;data=<H>,<W>,<enc>payload|>,id<|box;...|><|mask;...|>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`<Nf>` is the zero-based frame index; frames without a visible object emit no
|
|
191
|
+
line, so indices can skip. Each comma-separated record is a bare integer object
|
|
192
|
+
id - stable for an object across the frames of one response and not a dense
|
|
193
|
+
sequence - followed by one box and one mask. The parser retains the id as a string
|
|
194
|
+
in `object_id`. Box corners and the `w`/`h` frame size are source pixels; the
|
|
195
|
+
inclusive wire `x2`/`y2` become half-open `right`/`bottom`. The mask tuple is
|
|
196
|
+
`height,width,payload`, where the payload's first character selects the encoding:
|
|
197
|
+
`~` for `lossless` (the API default) or `!` for `one_bit`. The payload is base85,
|
|
198
|
+
not base64: after the marker its digits are printable ASCII `!` through `{` minus
|
|
199
|
+
the wire delimiters `" \ , ; < > |`, so it contains `*`, `$`, brackets, and
|
|
200
|
+
backticks, a base64 regex will not match it, and only the first character after
|
|
201
|
+
`H,W,` is the marker (`!` is also digit zero). Pass it through unchanged. Each record becomes one `SegmentationBoxRecord` followed by
|
|
202
|
+
one `SegmentationMaskRecord` whose `bounds` is the same half-open box. Image streams require frame zero and omit the frame from
|
|
203
|
+
normalized records; video records retain `frame.frame_index`. Every mask is
|
|
204
|
+
strictly decoded before insertion; an empty lane is a valid completed response
|
|
205
|
+
with no records.
|
|
206
|
+
The grammar is specified in the
|
|
173
207
|
[SAM 3 protocol](https://github.com/meta-models/meta-sam/blob/main/protocol/sam3.md).
|
|
174
|
-
The parser separately accepts compact box-first SAM API output as production
|
|
175
|
-
compatibility input and normalizes it to the same records. Its object token is an
|
|
176
|
-
ASCII-decimal ID retained as a string, and IDs may be multi-digit or non-contiguous.
|
|
177
|
-
The compact input's inclusive `x2`/`y2` coordinates become half-open `right`/`bottom`
|
|
178
|
-
bounds. Image API
|
|
179
|
-
records require frame zero and omit it from normalized records; video records retain
|
|
180
|
-
frame indices. Every mask is strictly decoded before insertion.
|
|
181
208
|
|
|
182
209
|
Plain text remains an ordered text record. Malformed structured-looking lines
|
|
183
210
|
produce diagnostics and parsing continues. `raw_output` preserves every input
|
|
@@ -270,8 +297,11 @@ by the `release PyPI distribution` workflow
|
|
|
270
297
|
1. Bump `version` in `pyproject.toml`, run `node scripts/sync-compatibility` from
|
|
271
298
|
the repository root so the compatibility matrix records the new version, and
|
|
272
299
|
merge that change to `main`.
|
|
273
|
-
2. Push the tag `meta-sam-parser@<version>` at that commit
|
|
274
|
-
tag that does not match the manifest version
|
|
300
|
+
2. Push the tag `meta-sam-parser@<version>` at that merge commit on `main`. The
|
|
301
|
+
workflow refuses a tag that does not match the manifest version, a tag whose
|
|
302
|
+
commit is not on `main`, and a tag older than the latest `main` commit touching
|
|
303
|
+
`python/`, `conformance/`, or `protocol/`, so a tag created before later fixes
|
|
304
|
+
merged cannot publish stale source.
|
|
275
305
|
3. The workflow builds the reproducible wheel and sdist, runs the artifact and
|
|
276
306
|
clean-consumer audits and the cross-language conformance suite, and then waits
|
|
277
307
|
for approval in the `pypi` GitHub environment before uploading through PyPI
|
|
@@ -36,10 +36,10 @@ from ._types import (
|
|
|
36
36
|
SegmentationDiagnostic,
|
|
37
37
|
SegmentationMask,
|
|
38
38
|
SegmentationMaskBounds,
|
|
39
|
+
SegmentationMaskEncoding,
|
|
39
40
|
SegmentationMaskIdentity,
|
|
40
41
|
SegmentationMaskRecord,
|
|
41
42
|
SegmentationMedia,
|
|
42
|
-
SegmentationPointRecord,
|
|
43
43
|
SegmentationRecord,
|
|
44
44
|
SegmentationResult,
|
|
45
45
|
SegmentationSnapshot,
|
|
@@ -80,10 +80,10 @@ __all__ = [
|
|
|
80
80
|
"SegmentationDiagnostic",
|
|
81
81
|
"SegmentationMask",
|
|
82
82
|
"SegmentationMaskBounds",
|
|
83
|
+
"SegmentationMaskEncoding",
|
|
83
84
|
"SegmentationMaskIdentity",
|
|
84
85
|
"SegmentationMaskRecord",
|
|
85
86
|
"SegmentationMedia",
|
|
86
|
-
"SegmentationPointRecord",
|
|
87
87
|
"SegmentationRecord",
|
|
88
88
|
"SegmentationResult",
|
|
89
89
|
"SegmentationSnapshot",
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import math
|
|
6
5
|
import re
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from typing import cast
|
|
@@ -23,7 +22,6 @@ from ._types import (
|
|
|
23
22
|
SegmentationMaskIdentity,
|
|
24
23
|
SegmentationMaskRecord,
|
|
25
24
|
SegmentationMedia,
|
|
26
|
-
SegmentationPointRecord,
|
|
27
25
|
SegmentationRecord,
|
|
28
26
|
SegmentationResult,
|
|
29
27
|
SegmentationSnapshot,
|
|
@@ -40,65 +38,28 @@ _JS_WHITESPACE = (
|
|
|
40
38
|
_JS_WHITESPACE_CLASS = (
|
|
41
39
|
r"\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
42
40
|
)
|
|
43
|
-
_JS_WHITESPACE_PATTERN = rf"[{_JS_WHITESPACE_CLASS}]"
|
|
44
|
-
_JS_NON_WHITESPACE_PATTERN = rf"[^{_JS_WHITESPACE_CLASS}]"
|
|
45
41
|
_JS_DOT_PATTERN = r"[^\n\r\u2028\u2029]"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
rf"object=([A-Za-z0-9._-]+){_JS_WHITESPACE_PATTERN}+"
|
|
49
|
-
rf"(point|box|mask)=({_JS_DOT_PATTERN}+)$"
|
|
50
|
-
)
|
|
51
|
-
_COMPACT_API_HEADER_PATTERN = re.compile(rf"^<([0-9]+)f>({_JS_DOT_PATTERN}*)$")
|
|
52
|
-
_COMPACT_API_OBJECT_PATTERN = re.compile(
|
|
42
|
+
_API_HEADER_PATTERN = re.compile(rf"^<([0-9]+)f>({_JS_DOT_PATTERN}*)$")
|
|
43
|
+
_API_RECORD_PATTERN = re.compile(
|
|
53
44
|
r"^(?:,)?([0-9]+)"
|
|
54
45
|
r"<\|box;x1=(-?[0-9]+);y1=(-?[0-9]+);x2=(-?[0-9]+);"
|
|
55
46
|
r"y2=(-?[0-9]+);w=([0-9]+);h=([0-9]+)\|>"
|
|
56
47
|
r"<\|mask;x=0;y=0;data=([0-9]+),([0-9]+),([!~][^|]+)\|>"
|
|
57
48
|
)
|
|
58
|
-
_POINT_PATTERN = re.compile(r"^\(([^,]+),([^,]+)\)$")
|
|
59
|
-
_BOX_PATTERN = re.compile(r"^\(([^,]+),([^,]+),([^,]+),([^,]+)\)$")
|
|
60
|
-
_MASK_PATTERN = re.compile(
|
|
61
|
-
rf"^([^;{_JS_WHITESPACE_CLASS}]+);"
|
|
62
|
-
rf"size=([0-9]+)x([0-9]+);data=({_JS_NON_WHITESPACE_PATTERN}+)$"
|
|
63
|
-
)
|
|
64
|
-
_DECIMAL_NUMBER_PATTERN = re.compile(
|
|
65
|
-
r"^[+-]?(?:(?:[0-9]+(?:\.[0-9]*)?)|(?:\.[0-9]+))"
|
|
66
|
-
r"(?:[eE][+-]?[0-9]+)?$"
|
|
67
|
-
)
|
|
68
|
-
_HEXADECIMAL_NUMBER_PATTERN = re.compile(r"^0[xX][0-9A-Fa-f]+$")
|
|
69
|
-
_BINARY_NUMBER_PATTERN = re.compile(r"^0[bB][01]+$")
|
|
70
|
-
_OCTAL_NUMBER_PATTERN = re.compile(r"^0[oO][0-7]+$")
|
|
71
49
|
|
|
72
50
|
|
|
73
51
|
def _trim(value: str) -> str:
|
|
74
52
|
return value.strip(_JS_WHITESPACE)
|
|
75
53
|
|
|
76
54
|
|
|
77
|
-
def _javascript_number(value: str) -> float | None:
|
|
78
|
-
normalized = _trim(value)
|
|
79
|
-
if not normalized:
|
|
80
|
-
return None
|
|
81
|
-
try:
|
|
82
|
-
if _HEXADECIMAL_NUMBER_PATTERN.fullmatch(normalized):
|
|
83
|
-
parsed = float(int(normalized[2:], 16))
|
|
84
|
-
elif _BINARY_NUMBER_PATTERN.fullmatch(normalized):
|
|
85
|
-
parsed = float(int(normalized[2:], 2))
|
|
86
|
-
elif _OCTAL_NUMBER_PATTERN.fullmatch(normalized):
|
|
87
|
-
parsed = float(int(normalized[2:], 8))
|
|
88
|
-
elif _DECIMAL_NUMBER_PATTERN.fullmatch(normalized):
|
|
89
|
-
parsed = float(normalized)
|
|
90
|
-
else:
|
|
91
|
-
return None
|
|
92
|
-
except (OverflowError, ValueError):
|
|
93
|
-
return None
|
|
94
|
-
return parsed if math.isfinite(parsed) else None
|
|
95
|
-
|
|
96
|
-
|
|
97
55
|
def _integer_text(value: str) -> int | None:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
56
|
+
"""Parse a decimal integer token the way JavaScript's ``Number`` observes it.
|
|
57
|
+
|
|
58
|
+
Callers only pass regex captures of ``-?[0-9]+``; values beyond the
|
|
59
|
+
JavaScript safe-integer range are rejected so both parsers agree.
|
|
60
|
+
"""
|
|
61
|
+
parsed = int(value)
|
|
62
|
+
return parsed if abs(parsed) <= _MAXIMUM_SAFE_INTEGER else None
|
|
102
63
|
|
|
103
64
|
|
|
104
65
|
class _SegmentationParser:
|
|
@@ -180,50 +141,15 @@ class _SegmentationParser:
|
|
|
180
141
|
return False
|
|
181
142
|
|
|
182
143
|
if line.startswith("<"):
|
|
183
|
-
accepted = self.
|
|
144
|
+
accepted = self._accept_api_line(line, raw)
|
|
184
145
|
if accepted is not None:
|
|
185
146
|
return accepted
|
|
186
147
|
|
|
187
|
-
|
|
188
|
-
if match is None:
|
|
189
|
-
if line.startswith("[frame=") or line.startswith("object="):
|
|
190
|
-
self._diagnose("malformed_record", "Malformed structured record.", raw)
|
|
191
|
-
else:
|
|
192
|
-
self._add_record(
|
|
193
|
-
SegmentationTextRecord(order=len(self._records), text=raw)
|
|
194
|
-
)
|
|
195
|
-
return True
|
|
196
|
-
|
|
197
|
-
frame_index = None if match.group(1) is None else _integer_text(match.group(1))
|
|
198
|
-
if match.group(1) is not None and (frame_index is None or frame_index < 0):
|
|
199
|
-
self._diagnose(
|
|
200
|
-
"invalid_frame",
|
|
201
|
-
"Frame references must be non-negative safe integers.",
|
|
202
|
-
raw,
|
|
203
|
-
)
|
|
204
|
-
return True
|
|
205
|
-
if self._media == "image" and frame_index is not None:
|
|
206
|
-
self._diagnose(
|
|
207
|
-
"unexpected_frame",
|
|
208
|
-
"Image segmentation records cannot include a frame reference.",
|
|
209
|
-
raw,
|
|
210
|
-
)
|
|
211
|
-
return True
|
|
212
|
-
|
|
213
|
-
object_id = match.group(2)
|
|
214
|
-
kind = match.group(3)
|
|
215
|
-
value = match.group(4)
|
|
216
|
-
frame = None if frame_index is None else FrameReference(frame_index)
|
|
217
|
-
if kind == "point":
|
|
218
|
-
self._accept_point(object_id, frame, value, raw)
|
|
219
|
-
elif kind == "box":
|
|
220
|
-
self._accept_box(object_id, frame, value, raw)
|
|
221
|
-
else:
|
|
222
|
-
self._accept_mask(object_id, frame, value, raw)
|
|
148
|
+
self._add_record(SegmentationTextRecord(order=len(self._records), text=raw))
|
|
223
149
|
return True
|
|
224
150
|
|
|
225
|
-
def
|
|
226
|
-
header =
|
|
151
|
+
def _accept_api_line(self, line: str, raw: str) -> bool | None:
|
|
152
|
+
header = _API_HEADER_PATTERN.fullmatch(line)
|
|
227
153
|
if header is None:
|
|
228
154
|
return None
|
|
229
155
|
frame_index = _integer_text(header.group(1))
|
|
@@ -247,17 +173,17 @@ class _SegmentationParser:
|
|
|
247
173
|
if not remainder:
|
|
248
174
|
self._diagnose(
|
|
249
175
|
"malformed_record",
|
|
250
|
-
"Malformed
|
|
176
|
+
"Malformed SAM API object record.",
|
|
251
177
|
raw,
|
|
252
178
|
)
|
|
253
179
|
return True
|
|
254
180
|
accepted = False
|
|
255
181
|
while remainder:
|
|
256
|
-
match =
|
|
182
|
+
match = _API_RECORD_PATTERN.match(remainder)
|
|
257
183
|
if match is None:
|
|
258
184
|
self._diagnose(
|
|
259
185
|
"malformed_record",
|
|
260
|
-
"Malformed
|
|
186
|
+
"Malformed SAM API object record.",
|
|
261
187
|
raw,
|
|
262
188
|
)
|
|
263
189
|
return True
|
|
@@ -267,7 +193,7 @@ class _SegmentationParser:
|
|
|
267
193
|
)
|
|
268
194
|
if any(value is None for value in coordinate_values):
|
|
269
195
|
self._diagnose(
|
|
270
|
-
"invalid_box", "
|
|
196
|
+
"invalid_box", "SAM API box coordinates are invalid.", raw
|
|
271
197
|
)
|
|
272
198
|
return True
|
|
273
199
|
(
|
|
@@ -289,7 +215,7 @@ class _SegmentationParser:
|
|
|
289
215
|
or inclusive_bottom >= source_height
|
|
290
216
|
):
|
|
291
217
|
self._diagnose(
|
|
292
|
-
"invalid_box", "
|
|
218
|
+
"invalid_box", "SAM API box coordinates are invalid.", raw
|
|
293
219
|
)
|
|
294
220
|
return True
|
|
295
221
|
bounds = SegmentationMaskBounds(
|
|
@@ -309,14 +235,16 @@ class _SegmentationParser:
|
|
|
309
235
|
bottom=inclusive_bottom + 1,
|
|
310
236
|
)
|
|
311
237
|
)
|
|
312
|
-
mask_height = match.group(8)
|
|
313
|
-
mask_width = match.group(9)
|
|
314
238
|
payload = match.group(10)
|
|
315
|
-
encoding = "lossless" if payload.startswith("~") else "one_bit"
|
|
316
239
|
self._accept_mask(
|
|
317
240
|
object_id,
|
|
318
241
|
frame,
|
|
319
|
-
|
|
242
|
+
SegmentationMask(
|
|
243
|
+
encoding="lossless" if payload.startswith("~") else "one_bit",
|
|
244
|
+
payload=payload,
|
|
245
|
+
width=int(match.group(9)),
|
|
246
|
+
height=int(match.group(8)),
|
|
247
|
+
),
|
|
320
248
|
raw,
|
|
321
249
|
bounds,
|
|
322
250
|
)
|
|
@@ -324,109 +252,20 @@ class _SegmentationParser:
|
|
|
324
252
|
remainder = remainder[match.end() :]
|
|
325
253
|
return accepted
|
|
326
254
|
|
|
327
|
-
def _accept_point(
|
|
328
|
-
self,
|
|
329
|
-
object_id: str,
|
|
330
|
-
frame: FrameReference | None,
|
|
331
|
-
value: str,
|
|
332
|
-
raw: str,
|
|
333
|
-
) -> None:
|
|
334
|
-
match = _POINT_PATTERN.fullmatch(_trim(value))
|
|
335
|
-
x = None if match is None else _javascript_number(match.group(1))
|
|
336
|
-
y = None if match is None else _javascript_number(match.group(2))
|
|
337
|
-
if x is None or y is None:
|
|
338
|
-
self._diagnose(
|
|
339
|
-
"malformed_point", "Point coordinates must be finite numbers.", raw
|
|
340
|
-
)
|
|
341
|
-
return
|
|
342
|
-
self._add_record(
|
|
343
|
-
SegmentationPointRecord(
|
|
344
|
-
order=len(self._records),
|
|
345
|
-
object_id=object_id,
|
|
346
|
-
frame=frame,
|
|
347
|
-
x=x,
|
|
348
|
-
y=y,
|
|
349
|
-
)
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
def _accept_box(
|
|
353
|
-
self,
|
|
354
|
-
object_id: str,
|
|
355
|
-
frame: FrameReference | None,
|
|
356
|
-
value: str,
|
|
357
|
-
raw: str,
|
|
358
|
-
) -> None:
|
|
359
|
-
match = _BOX_PATTERN.fullmatch(_trim(value))
|
|
360
|
-
coordinates = (
|
|
361
|
-
None
|
|
362
|
-
if match is None
|
|
363
|
-
else tuple(_javascript_number(item) for item in match.group(1, 2, 3, 4))
|
|
364
|
-
)
|
|
365
|
-
if coordinates is None or any(item is None for item in coordinates):
|
|
366
|
-
self._diagnose(
|
|
367
|
-
"malformed_box", "Box coordinates must be finite numbers.", raw
|
|
368
|
-
)
|
|
369
|
-
return
|
|
370
|
-
left, top, right, bottom = cast(tuple[float, float, float, float], coordinates)
|
|
371
|
-
if right < left or bottom < top:
|
|
372
|
-
self._diagnose("invalid_box", "Box coordinates must be ordered.", raw)
|
|
373
|
-
return
|
|
374
|
-
self._add_record(
|
|
375
|
-
SegmentationBoxRecord(
|
|
376
|
-
order=len(self._records),
|
|
377
|
-
object_id=object_id,
|
|
378
|
-
frame=frame,
|
|
379
|
-
left=left,
|
|
380
|
-
top=top,
|
|
381
|
-
right=right,
|
|
382
|
-
bottom=bottom,
|
|
383
|
-
)
|
|
384
|
-
)
|
|
385
|
-
|
|
386
255
|
def _accept_mask(
|
|
387
256
|
self,
|
|
388
257
|
object_id: str,
|
|
389
258
|
frame: FrameReference | None,
|
|
390
|
-
|
|
259
|
+
mask: SegmentationMask,
|
|
391
260
|
raw: str,
|
|
392
|
-
bounds: SegmentationMaskBounds
|
|
261
|
+
bounds: SegmentationMaskBounds,
|
|
393
262
|
) -> None:
|
|
394
|
-
|
|
395
|
-
if
|
|
396
|
-
self._diagnose(
|
|
397
|
-
"malformed_mask",
|
|
398
|
-
"Mask records require encoding, size, and one complete payload.",
|
|
399
|
-
raw,
|
|
400
|
-
)
|
|
401
|
-
return
|
|
402
|
-
encoding = match.group(1)
|
|
403
|
-
width_number = _javascript_number(match.group(2))
|
|
404
|
-
height_number = _javascript_number(match.group(3))
|
|
405
|
-
width = int(width_number) if width_number is not None else 0
|
|
406
|
-
height = int(height_number) if height_number is not None else 0
|
|
407
|
-
payload = match.group(4)
|
|
408
|
-
if encoding not in ("one_bit", "lossless"):
|
|
409
|
-
self._diagnose(
|
|
410
|
-
"unsupported_mask_encoding",
|
|
411
|
-
f"Unsupported complete mask encoding: {encoding}.",
|
|
412
|
-
raw,
|
|
413
|
-
)
|
|
414
|
-
return
|
|
415
|
-
area = float(width) * float(height)
|
|
416
|
-
if (
|
|
417
|
-
width <= 0
|
|
418
|
-
or height <= 0
|
|
419
|
-
or not math.isfinite(area)
|
|
420
|
-
or not area.is_integer()
|
|
421
|
-
or abs(area) > _MAXIMUM_SAFE_INTEGER
|
|
422
|
-
):
|
|
263
|
+
area = mask.width * mask.height
|
|
264
|
+
if mask.width <= 0 or mask.height <= 0 or area > _MAXIMUM_SAFE_INTEGER:
|
|
423
265
|
self._diagnose(
|
|
424
266
|
"invalid_mask_size", "Mask dimensions must be positive.", raw
|
|
425
267
|
)
|
|
426
268
|
return
|
|
427
|
-
mask = SegmentationMask(
|
|
428
|
-
encoding=encoding, payload=payload, width=width, height=height
|
|
429
|
-
)
|
|
430
269
|
try:
|
|
431
270
|
decode_mask_to_raster(mask)
|
|
432
271
|
except InvalidSegmentationMaskError as error:
|
|
@@ -48,11 +48,20 @@ class SegmentationMaskBounds:
|
|
|
48
48
|
bottom: float
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
SegmentationMaskEncoding: TypeAlias = Literal["lossless", "one_bit"]
|
|
52
|
+
"""The wire encodings of a mask payload: ``~`` selects lossless, ``!`` one_bit."""
|
|
53
|
+
|
|
54
|
+
|
|
51
55
|
@dataclass(frozen=True, slots=True)
|
|
52
56
|
class SegmentationMask:
|
|
53
|
-
"""One complete
|
|
57
|
+
"""One complete mask exactly as the SAM API emitted it.
|
|
58
|
+
|
|
59
|
+
``payload`` is the base85 text after the encoding marker and is never
|
|
60
|
+
partial; ``width`` and ``height`` are the raster's own dimensions, not the
|
|
61
|
+
frame's.
|
|
62
|
+
"""
|
|
54
63
|
|
|
55
|
-
encoding:
|
|
64
|
+
encoding: SegmentationMaskEncoding
|
|
56
65
|
payload: str
|
|
57
66
|
width: int
|
|
58
67
|
height: int
|
|
@@ -76,18 +85,6 @@ class SegmentationTextRecord:
|
|
|
76
85
|
text: str
|
|
77
86
|
|
|
78
87
|
|
|
79
|
-
@dataclass(frozen=True, slots=True)
|
|
80
|
-
class SegmentationPointRecord:
|
|
81
|
-
"""A point associated with one object and optional frame."""
|
|
82
|
-
|
|
83
|
-
kind: Literal["point"] = field(default="point", init=False)
|
|
84
|
-
order: int
|
|
85
|
-
object_id: str
|
|
86
|
-
x: float
|
|
87
|
-
y: float
|
|
88
|
-
frame: FrameReference | None = None
|
|
89
|
-
|
|
90
|
-
|
|
91
88
|
@dataclass(frozen=True, slots=True)
|
|
92
89
|
class SegmentationBoxRecord:
|
|
93
90
|
"""A half-open or explicitly supplied box in source-media coordinates."""
|
|
@@ -112,15 +109,12 @@ class SegmentationMaskRecord:
|
|
|
112
109
|
identity: SegmentationMaskIdentity
|
|
113
110
|
revision: int
|
|
114
111
|
mask: SegmentationMask
|
|
112
|
+
bounds: SegmentationMaskBounds
|
|
115
113
|
frame: FrameReference | None = None
|
|
116
|
-
bounds: SegmentationMaskBounds | None = None
|
|
117
114
|
|
|
118
115
|
|
|
119
116
|
SegmentationRecord: TypeAlias = (
|
|
120
|
-
SegmentationTextRecord
|
|
121
|
-
| SegmentationPointRecord
|
|
122
|
-
| SegmentationBoxRecord
|
|
123
|
-
| SegmentationMaskRecord
|
|
117
|
+
SegmentationTextRecord | SegmentationBoxRecord | SegmentationMaskRecord
|
|
124
118
|
)
|
|
125
119
|
|
|
126
120
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|