rocket-welder-sdk 1.1.43__py3-none-any.whl → 1.1.45__py3-none-any.whl
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.
- rocket_welder_sdk/__init__.py +44 -22
- rocket_welder_sdk/binary_frame_reader.py +222 -0
- rocket_welder_sdk/binary_frame_writer.py +213 -0
- rocket_welder_sdk/confidence.py +206 -0
- rocket_welder_sdk/delta_frame.py +150 -0
- rocket_welder_sdk/graphics/__init__.py +42 -0
- rocket_welder_sdk/graphics/layer_canvas.py +157 -0
- rocket_welder_sdk/graphics/protocol.py +72 -0
- rocket_welder_sdk/graphics/rgb_color.py +109 -0
- rocket_welder_sdk/graphics/stage.py +494 -0
- rocket_welder_sdk/graphics/vector_graphics_encoder.py +575 -0
- rocket_welder_sdk/high_level/__init__.py +8 -1
- rocket_welder_sdk/high_level/client.py +114 -3
- rocket_welder_sdk/high_level/connection_strings.py +88 -15
- rocket_welder_sdk/high_level/frame_sink_factory.py +2 -15
- rocket_welder_sdk/high_level/transport_protocol.py +4 -130
- rocket_welder_sdk/keypoints_protocol.py +520 -55
- rocket_welder_sdk/rocket_welder_client.py +210 -89
- rocket_welder_sdk/segmentation_result.py +387 -2
- rocket_welder_sdk/session_id.py +7 -182
- rocket_welder_sdk/transport/__init__.py +10 -3
- rocket_welder_sdk/transport/frame_sink.py +3 -3
- rocket_welder_sdk/transport/frame_source.py +2 -2
- rocket_welder_sdk/transport/websocket_transport.py +316 -0
- rocket_welder_sdk/varint.py +213 -0
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/METADATA +1 -4
- rocket_welder_sdk-1.1.45.dist-info/RECORD +51 -0
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/WHEEL +1 -1
- rocket_welder_sdk/transport/nng_transport.py +0 -197
- rocket_welder_sdk-1.1.43.dist-info/RECORD +0 -40
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/top_level.txt +0 -0
|
@@ -36,23 +36,38 @@ Features:
|
|
|
36
36
|
- Master/delta frame compression for temporal sequences
|
|
37
37
|
- Varint encoding for efficient integer compression
|
|
38
38
|
- ZigZag encoding for signed deltas
|
|
39
|
-
- Confidence stored as ushort (0-
|
|
39
|
+
- Confidence stored as ushort (0-65535) internally, float (0.0-1.0) in API
|
|
40
40
|
- Explicit little-endian for cross-platform compatibility
|
|
41
41
|
- Default master frame interval: every 300 frames
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
44
46
|
import io
|
|
45
47
|
import json
|
|
46
48
|
import struct
|
|
47
49
|
from abc import ABC, abstractmethod
|
|
48
50
|
from dataclasses import dataclass
|
|
49
|
-
from typing import
|
|
51
|
+
from typing import (
|
|
52
|
+
AsyncIterator,
|
|
53
|
+
BinaryIO,
|
|
54
|
+
Callable,
|
|
55
|
+
Dict,
|
|
56
|
+
Iterator,
|
|
57
|
+
List,
|
|
58
|
+
Optional,
|
|
59
|
+
Sequence,
|
|
60
|
+
Tuple,
|
|
61
|
+
Union,
|
|
62
|
+
)
|
|
50
63
|
|
|
51
64
|
import numpy as np
|
|
52
65
|
import numpy.typing as npt
|
|
53
66
|
from typing_extensions import TypeAlias
|
|
54
67
|
|
|
55
|
-
from .
|
|
68
|
+
from .confidence import Confidence
|
|
69
|
+
from .delta_frame import DeltaFrame
|
|
70
|
+
from .transport import IFrameSink, IFrameSource, StreamFrameSink, StreamFrameSource
|
|
56
71
|
|
|
57
72
|
# Type aliases
|
|
58
73
|
Point = Tuple[int, int]
|
|
@@ -62,9 +77,8 @@ PointArray: TypeAlias = npt.NDArray[np.int32] # Shape: (N, 2)
|
|
|
62
77
|
MASTER_FRAME_TYPE = 0x00
|
|
63
78
|
DELTA_FRAME_TYPE = 0x01
|
|
64
79
|
|
|
65
|
-
# Confidence encoding constants
|
|
66
|
-
|
|
67
|
-
CONFIDENCE_MAX = 10000
|
|
80
|
+
# Confidence encoding constants - matches C# ushort (0-65535)
|
|
81
|
+
CONFIDENCE_MAX = 65535
|
|
68
82
|
|
|
69
83
|
|
|
70
84
|
def _write_varint(stream: BinaryIO, value: int) -> None:
|
|
@@ -111,29 +125,77 @@ def _zigzag_decode(value: int) -> int:
|
|
|
111
125
|
return (value >> 1) ^ -(value & 1)
|
|
112
126
|
|
|
113
127
|
|
|
114
|
-
def _confidence_to_ushort(confidence: float) -> int:
|
|
115
|
-
"""Convert confidence float (0.0-1.0) to ushort (0-
|
|
116
|
-
|
|
128
|
+
def _confidence_to_ushort(confidence: Union[float, Confidence]) -> int:
|
|
129
|
+
"""Convert confidence float (0.0-1.0) or Confidence to ushort (0-65535)."""
|
|
130
|
+
if isinstance(confidence, Confidence):
|
|
131
|
+
return confidence.raw
|
|
132
|
+
return min(max(int(confidence * CONFIDENCE_MAX), 0), CONFIDENCE_MAX)
|
|
117
133
|
|
|
118
134
|
|
|
119
135
|
def _confidence_from_ushort(confidence_ushort: int) -> float:
|
|
120
|
-
"""Convert confidence ushort (0-
|
|
121
|
-
return confidence_ushort /
|
|
136
|
+
"""Convert confidence ushort (0-65535) to float (0.0-1.0)."""
|
|
137
|
+
return confidence_ushort / CONFIDENCE_MAX
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _confidence_to_obj(confidence_ushort: int) -> Confidence:
|
|
141
|
+
"""Convert confidence ushort (0-65535) to Confidence object."""
|
|
142
|
+
return Confidence(raw=confidence_ushort)
|
|
122
143
|
|
|
123
144
|
|
|
124
145
|
@dataclass(frozen=True)
|
|
125
146
|
class KeyPoint:
|
|
126
|
-
"""
|
|
147
|
+
"""
|
|
148
|
+
A single keypoint with position and confidence.
|
|
127
149
|
|
|
128
|
-
|
|
129
|
-
x: int
|
|
130
|
-
y: int
|
|
131
|
-
confidence: float # 0.0 to 1.0
|
|
150
|
+
Matches C# readonly record struct KeyPoint(int Id, Point Position, Confidence Confidence).
|
|
132
151
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
Attributes:
|
|
153
|
+
id: KeyPoint identifier (e.g., 0=nose, 1=left_eye, etc.)
|
|
154
|
+
position: Position of the keypoint in pixel coordinates (x, y).
|
|
155
|
+
confidence: Confidence score (uses full ushort precision 0-65535).
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
id: int
|
|
159
|
+
position: Point
|
|
160
|
+
confidence: Confidence
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def x(self) -> int:
|
|
164
|
+
"""X coordinate of the keypoint position."""
|
|
165
|
+
return self.position[0]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def y(self) -> int:
|
|
169
|
+
"""Y coordinate of the keypoint position."""
|
|
170
|
+
return self.position[1]
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def create(cls, id: int, x: int, y: int, confidence: Union[float, int, Confidence]) -> KeyPoint:
|
|
174
|
+
"""
|
|
175
|
+
Create a keypoint with explicit x, y coordinates.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
id: KeyPoint identifier
|
|
179
|
+
x: X coordinate
|
|
180
|
+
y: Y coordinate
|
|
181
|
+
confidence: Confidence (float 0.0-1.0, raw ushort 0-65535, or Confidence)
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
KeyPoint instance
|
|
185
|
+
"""
|
|
186
|
+
if isinstance(confidence, Confidence):
|
|
187
|
+
conf = confidence
|
|
188
|
+
elif isinstance(confidence, int):
|
|
189
|
+
conf = Confidence(raw=confidence)
|
|
190
|
+
else:
|
|
191
|
+
conf = Confidence.from_float(confidence)
|
|
192
|
+
return cls(id=id, position=(x, y), confidence=conf)
|
|
193
|
+
|
|
194
|
+
# Legacy property for backward compatibility
|
|
195
|
+
@property
|
|
196
|
+
def keypoint_id(self) -> int:
|
|
197
|
+
"""Legacy alias for id (backward compatibility)."""
|
|
198
|
+
return self.id
|
|
137
199
|
|
|
138
200
|
|
|
139
201
|
@dataclass(frozen=True)
|
|
@@ -145,6 +207,195 @@ class KeyPointsDefinition:
|
|
|
145
207
|
points: Dict[str, int] # name -> keypoint_id
|
|
146
208
|
|
|
147
209
|
|
|
210
|
+
@dataclass(frozen=True)
|
|
211
|
+
class KeyPointsFrame:
|
|
212
|
+
"""
|
|
213
|
+
A decoded keypoints frame with absolute keypoint values.
|
|
214
|
+
|
|
215
|
+
Matches C# readonly record struct KeyPointsFrame(ulong FrameId, ReadOnlyMemory<KeyPoint> KeyPoints).
|
|
216
|
+
|
|
217
|
+
Used by Document classes after delta decoding is complete.
|
|
218
|
+
For streaming with delta info, use DeltaFrame[KeyPoint] instead.
|
|
219
|
+
|
|
220
|
+
Attributes:
|
|
221
|
+
frame_id: The frame identifier.
|
|
222
|
+
keypoints: The keypoints in this frame.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
frame_id: int
|
|
226
|
+
keypoints: Sequence[KeyPoint]
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def count(self) -> int:
|
|
230
|
+
"""Number of keypoints in this frame."""
|
|
231
|
+
return len(self.keypoints)
|
|
232
|
+
|
|
233
|
+
def __len__(self) -> int:
|
|
234
|
+
"""Return the number of keypoints."""
|
|
235
|
+
return len(self.keypoints)
|
|
236
|
+
|
|
237
|
+
def __iter__(self) -> Iterator[KeyPoint]:
|
|
238
|
+
"""Iterate over keypoints."""
|
|
239
|
+
return iter(self.keypoints)
|
|
240
|
+
|
|
241
|
+
def __getitem__(self, index: int) -> KeyPoint:
|
|
242
|
+
"""Get keypoint by index."""
|
|
243
|
+
return self.keypoints[index]
|
|
244
|
+
|
|
245
|
+
def find_by_id(self, keypoint_id: int) -> Optional[KeyPoint]:
|
|
246
|
+
"""Find keypoint by ID, or None if not found."""
|
|
247
|
+
for kp in self.keypoints:
|
|
248
|
+
if kp.id == keypoint_id:
|
|
249
|
+
return kp
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class IKeyPointsSource(ABC):
|
|
254
|
+
"""
|
|
255
|
+
Interface for streaming keypoints source.
|
|
256
|
+
|
|
257
|
+
Matches C# IKeyPointsSource interface.
|
|
258
|
+
Returns DeltaFrame<KeyPoint> which includes IsDelta for streaming context.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
@abstractmethod
|
|
262
|
+
def read_frames(self) -> Iterator[DeltaFrame[KeyPoint]]:
|
|
263
|
+
"""
|
|
264
|
+
Stream frames synchronously.
|
|
265
|
+
|
|
266
|
+
Yields:
|
|
267
|
+
DeltaFrame[KeyPoint] with decoded absolute values and IsDelta metadata.
|
|
268
|
+
"""
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
def read_frames_async(self) -> AsyncIterator[DeltaFrame[KeyPoint]]:
|
|
272
|
+
"""
|
|
273
|
+
Stream frames as they arrive from the transport.
|
|
274
|
+
|
|
275
|
+
Supports cancellation and backpressure.
|
|
276
|
+
Returns DeltaFrame with IsDelta indicating master vs delta frame.
|
|
277
|
+
|
|
278
|
+
Yields:
|
|
279
|
+
DeltaFrame[KeyPoint] with decoded absolute values and IsDelta metadata.
|
|
280
|
+
"""
|
|
281
|
+
raise NotImplementedError("Subclass must implement read_frames_async")
|
|
282
|
+
|
|
283
|
+
def close(self) -> None: # noqa: B027
|
|
284
|
+
"""Close the source and release resources."""
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
async def close_async(self) -> None: # noqa: B027
|
|
288
|
+
"""Close the source asynchronously."""
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
def __enter__(self) -> IKeyPointsSource:
|
|
292
|
+
"""Context manager entry."""
|
|
293
|
+
return self
|
|
294
|
+
|
|
295
|
+
def __exit__(self, *args: object) -> None:
|
|
296
|
+
"""Context manager exit."""
|
|
297
|
+
self.close()
|
|
298
|
+
|
|
299
|
+
async def __aenter__(self) -> IKeyPointsSource:
|
|
300
|
+
"""Async context manager entry."""
|
|
301
|
+
return self
|
|
302
|
+
|
|
303
|
+
async def __aexit__(self, *args: object) -> None:
|
|
304
|
+
"""Async context manager exit."""
|
|
305
|
+
await self.close_async()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class KeyPointsSource(IKeyPointsSource):
|
|
309
|
+
"""
|
|
310
|
+
Streaming reader for keypoints.
|
|
311
|
+
|
|
312
|
+
Reads frames from IFrameSource and yields them via Iterator/AsyncIterator.
|
|
313
|
+
Handles master/delta frame decoding automatically using KeyPointsProtocol.
|
|
314
|
+
Returns DeltaFrame[KeyPoint] with decoded absolute values and IsDelta metadata.
|
|
315
|
+
|
|
316
|
+
Matches C# KeyPointsSource class.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def __init__(self, frame_source: IFrameSource) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Create a KeyPointsSource from a frame source.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
frame_source: The underlying frame source (TCP, WebSocket, Stream, etc.)
|
|
325
|
+
"""
|
|
326
|
+
if frame_source is None:
|
|
327
|
+
raise ValueError("frame_source cannot be None")
|
|
328
|
+
self._frame_source = frame_source
|
|
329
|
+
self._previous_frame: Optional[Dict[int, KeyPoint]] = None
|
|
330
|
+
self._closed = False
|
|
331
|
+
|
|
332
|
+
def read_frames(self) -> Iterator[DeltaFrame[KeyPoint]]:
|
|
333
|
+
"""Stream frames synchronously."""
|
|
334
|
+
while not self._closed:
|
|
335
|
+
frame_data = self._frame_source.read_frame()
|
|
336
|
+
if frame_data is None or len(frame_data) == 0:
|
|
337
|
+
break
|
|
338
|
+
frame = self._parse_frame(frame_data)
|
|
339
|
+
yield frame
|
|
340
|
+
|
|
341
|
+
async def read_frames_async(self) -> AsyncIterator[DeltaFrame[KeyPoint]]:
|
|
342
|
+
"""Stream frames as they arrive from the transport asynchronously."""
|
|
343
|
+
while not self._closed:
|
|
344
|
+
frame_data = await self._frame_source.read_frame_async()
|
|
345
|
+
if frame_data is None or len(frame_data) == 0:
|
|
346
|
+
break
|
|
347
|
+
frame = self._parse_frame(frame_data)
|
|
348
|
+
yield frame
|
|
349
|
+
|
|
350
|
+
def _parse_frame(self, data: bytes) -> DeltaFrame[KeyPoint]:
|
|
351
|
+
"""Parse a frame from binary data."""
|
|
352
|
+
result = KeyPointsProtocol.read_with_previous_state(data, self._previous_frame)
|
|
353
|
+
|
|
354
|
+
# Update previous frame state for next delta decoding
|
|
355
|
+
self._previous_frame = {}
|
|
356
|
+
for kp in result.items:
|
|
357
|
+
self._previous_frame[kp.id] = kp
|
|
358
|
+
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
def close(self) -> None:
|
|
362
|
+
"""Close the source and release resources."""
|
|
363
|
+
if self._closed:
|
|
364
|
+
return
|
|
365
|
+
self._closed = True
|
|
366
|
+
self._frame_source.close()
|
|
367
|
+
|
|
368
|
+
async def close_async(self) -> None:
|
|
369
|
+
"""Close the source asynchronously."""
|
|
370
|
+
if self._closed:
|
|
371
|
+
return
|
|
372
|
+
self._closed = True
|
|
373
|
+
await self._frame_source.close_async()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class IKeyPointsSink(ABC):
|
|
377
|
+
"""
|
|
378
|
+
Interface for creating keypoints writers.
|
|
379
|
+
|
|
380
|
+
Matches C# IKeyPointsSink interface.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
@abstractmethod
|
|
384
|
+
def create_writer(self, frame_id: int) -> IKeyPointsWriter:
|
|
385
|
+
"""
|
|
386
|
+
Create a writer for the current frame.
|
|
387
|
+
|
|
388
|
+
Sink decides whether to write master or delta frame.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
frame_id: Unique frame identifier
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
KeyPoints writer for this frame
|
|
395
|
+
"""
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
|
|
148
399
|
class IKeyPointsWriter(ABC):
|
|
149
400
|
"""Interface for writing keypoints data for a single frame."""
|
|
150
401
|
|
|
@@ -163,7 +414,7 @@ class IKeyPointsWriter(ABC):
|
|
|
163
414
|
"""Flush and close the writer."""
|
|
164
415
|
pass
|
|
165
416
|
|
|
166
|
-
def __enter__(self) ->
|
|
417
|
+
def __enter__(self) -> IKeyPointsWriter:
|
|
167
418
|
"""Context manager entry."""
|
|
168
419
|
return self
|
|
169
420
|
|
|
@@ -434,40 +685,6 @@ class KeyPointsSeries:
|
|
|
434
685
|
yield from self.get_keypoint_trajectory(keypoint_id)
|
|
435
686
|
|
|
436
687
|
|
|
437
|
-
class IKeyPointsSink(ABC):
|
|
438
|
-
"""Interface for creating keypoints writers and reading keypoints data."""
|
|
439
|
-
|
|
440
|
-
@abstractmethod
|
|
441
|
-
def create_writer(self, frame_id: int) -> IKeyPointsWriter:
|
|
442
|
-
"""
|
|
443
|
-
Create a writer for the current frame.
|
|
444
|
-
|
|
445
|
-
Sink decides whether to write master or delta frame.
|
|
446
|
-
|
|
447
|
-
Args:
|
|
448
|
-
frame_id: Unique frame identifier
|
|
449
|
-
|
|
450
|
-
Returns:
|
|
451
|
-
KeyPoints writer for this frame
|
|
452
|
-
"""
|
|
453
|
-
pass
|
|
454
|
-
|
|
455
|
-
@staticmethod
|
|
456
|
-
@abstractmethod
|
|
457
|
-
def read(json_definition: str, blob_stream: BinaryIO) -> KeyPointsSeries:
|
|
458
|
-
"""
|
|
459
|
-
Read entire keypoints series into memory for efficient querying.
|
|
460
|
-
|
|
461
|
-
Args:
|
|
462
|
-
json_definition: JSON definition string mapping keypoint names to IDs
|
|
463
|
-
blob_stream: Binary stream containing keypoints data
|
|
464
|
-
|
|
465
|
-
Returns:
|
|
466
|
-
KeyPointsSeries for in-memory queries
|
|
467
|
-
"""
|
|
468
|
-
pass
|
|
469
|
-
|
|
470
|
-
|
|
471
688
|
class KeyPointsSink(IKeyPointsSink):
|
|
472
689
|
"""
|
|
473
690
|
Transport-agnostic keypoints sink with master/delta frame compression.
|
|
@@ -640,3 +857,251 @@ class KeyPointsSink(IKeyPointsSink):
|
|
|
640
857
|
index[frame_id] = frame_keypoints
|
|
641
858
|
|
|
642
859
|
return KeyPointsSeries(version, compute_module_name, points, index)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class KeyPointsProtocol:
|
|
863
|
+
"""
|
|
864
|
+
Static helpers for encoding and decoding keypoints protocol data.
|
|
865
|
+
|
|
866
|
+
Pure protocol logic with no transport or rendering dependencies.
|
|
867
|
+
Matches C# KeyPointsProtocol static class from RocketWelder.SDK.Protocols.
|
|
868
|
+
|
|
869
|
+
Master Frame Format:
|
|
870
|
+
[FrameType: 1 byte (0x00=Master)]
|
|
871
|
+
[FrameId: 8 bytes, little-endian uint64]
|
|
872
|
+
[KeyPointCount: varint]
|
|
873
|
+
[KeyPoints: Id(varint), X(int32 LE), Y(int32 LE), Confidence(uint16 LE)]
|
|
874
|
+
|
|
875
|
+
Delta Frame Format:
|
|
876
|
+
[FrameType: 1 byte (0x01=Delta)]
|
|
877
|
+
[FrameId: 8 bytes, little-endian uint64]
|
|
878
|
+
[KeyPointCount: varint]
|
|
879
|
+
[KeyPoints: Id(varint), DeltaX(zigzag), DeltaY(zigzag), DeltaConfidence(zigzag)]
|
|
880
|
+
"""
|
|
881
|
+
|
|
882
|
+
@staticmethod
|
|
883
|
+
def write_master_frame(frame_id: int, keypoints: Sequence[KeyPoint]) -> bytes:
|
|
884
|
+
"""
|
|
885
|
+
Write a master frame (absolute keypoint positions).
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
frame_id: Frame identifier
|
|
889
|
+
keypoints: List of keypoints with absolute positions
|
|
890
|
+
|
|
891
|
+
Returns:
|
|
892
|
+
Encoded frame bytes
|
|
893
|
+
"""
|
|
894
|
+
buffer = io.BytesIO()
|
|
895
|
+
buffer.write(bytes([MASTER_FRAME_TYPE]))
|
|
896
|
+
buffer.write(struct.pack("<Q", frame_id))
|
|
897
|
+
_write_varint(buffer, len(keypoints))
|
|
898
|
+
|
|
899
|
+
for kp in keypoints:
|
|
900
|
+
_write_varint(buffer, kp.id)
|
|
901
|
+
buffer.write(struct.pack("<i", kp.x))
|
|
902
|
+
buffer.write(struct.pack("<i", kp.y))
|
|
903
|
+
buffer.write(struct.pack("<H", kp.confidence.raw))
|
|
904
|
+
|
|
905
|
+
return buffer.getvalue()
|
|
906
|
+
|
|
907
|
+
@staticmethod
|
|
908
|
+
def write_delta_frame(
|
|
909
|
+
frame_id: int,
|
|
910
|
+
current: Sequence[KeyPoint],
|
|
911
|
+
previous_lookup: Dict[int, KeyPoint],
|
|
912
|
+
) -> bytes:
|
|
913
|
+
"""
|
|
914
|
+
Write a delta frame with variable keypoint counts.
|
|
915
|
+
|
|
916
|
+
KeyPoints are matched by ID using the previous_lookup dictionary.
|
|
917
|
+
New keypoints (not in previous) are written as absolute values (zigzag encoded).
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
frame_id: Frame identifier
|
|
921
|
+
current: Current frame keypoints
|
|
922
|
+
previous_lookup: Previous frame keypoints dictionary for delta calculation
|
|
923
|
+
|
|
924
|
+
Returns:
|
|
925
|
+
Encoded frame bytes
|
|
926
|
+
"""
|
|
927
|
+
buffer = io.BytesIO()
|
|
928
|
+
buffer.write(bytes([DELTA_FRAME_TYPE]))
|
|
929
|
+
buffer.write(struct.pack("<Q", frame_id))
|
|
930
|
+
_write_varint(buffer, len(current))
|
|
931
|
+
|
|
932
|
+
for curr in current:
|
|
933
|
+
_write_varint(buffer, curr.id)
|
|
934
|
+
|
|
935
|
+
if curr.id in previous_lookup:
|
|
936
|
+
prev = previous_lookup[curr.id]
|
|
937
|
+
delta_x = curr.x - prev.x
|
|
938
|
+
delta_y = curr.y - prev.y
|
|
939
|
+
delta_conf = curr.confidence.raw - prev.confidence.raw
|
|
940
|
+
else:
|
|
941
|
+
# New keypoint - write absolute value as zigzag (as if previous was 0)
|
|
942
|
+
delta_x = curr.x
|
|
943
|
+
delta_y = curr.y
|
|
944
|
+
delta_conf = curr.confidence.raw
|
|
945
|
+
|
|
946
|
+
_write_varint(buffer, _zigzag_encode(delta_x))
|
|
947
|
+
_write_varint(buffer, _zigzag_encode(delta_y))
|
|
948
|
+
_write_varint(buffer, _zigzag_encode(delta_conf))
|
|
949
|
+
|
|
950
|
+
return buffer.getvalue()
|
|
951
|
+
|
|
952
|
+
@staticmethod
|
|
953
|
+
def read(data: bytes) -> DeltaFrame[KeyPoint]:
|
|
954
|
+
"""
|
|
955
|
+
Read a keypoints frame (master frame only, no previous state needed).
|
|
956
|
+
|
|
957
|
+
For delta frames, use read_with_previous_state.
|
|
958
|
+
|
|
959
|
+
Args:
|
|
960
|
+
data: Binary frame data
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
DeltaFrame[KeyPoint] with decoded keypoints
|
|
964
|
+
|
|
965
|
+
Raises:
|
|
966
|
+
InvalidOperationError: If called on a delta frame
|
|
967
|
+
"""
|
|
968
|
+
stream = io.BytesIO(data)
|
|
969
|
+
|
|
970
|
+
frame_type = stream.read(1)[0]
|
|
971
|
+
is_delta = frame_type == DELTA_FRAME_TYPE
|
|
972
|
+
frame_id = struct.unpack("<Q", stream.read(8))[0]
|
|
973
|
+
count = _read_varint(stream)
|
|
974
|
+
|
|
975
|
+
if is_delta:
|
|
976
|
+
raise RuntimeError(
|
|
977
|
+
"Cannot read delta frame without previous state. "
|
|
978
|
+
"Use read_with_previous_state instead."
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
keypoints: List[KeyPoint] = []
|
|
982
|
+
for _ in range(count):
|
|
983
|
+
kp_id = _read_varint(stream)
|
|
984
|
+
x = struct.unpack("<i", stream.read(4))[0]
|
|
985
|
+
y = struct.unpack("<i", stream.read(4))[0]
|
|
986
|
+
conf_raw = struct.unpack("<H", stream.read(2))[0]
|
|
987
|
+
keypoints.append(
|
|
988
|
+
KeyPoint(id=kp_id, position=(x, y), confidence=Confidence(raw=conf_raw))
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
return DeltaFrame[KeyPoint](frame_id=frame_id, is_delta=False, items=keypoints)
|
|
992
|
+
|
|
993
|
+
@staticmethod
|
|
994
|
+
def read_with_previous_state(
|
|
995
|
+
data: bytes,
|
|
996
|
+
previous_lookup: Optional[Dict[int, KeyPoint]],
|
|
997
|
+
) -> DeltaFrame[KeyPoint]:
|
|
998
|
+
"""
|
|
999
|
+
Read a keypoints frame with previous state for delta decoding.
|
|
1000
|
+
|
|
1001
|
+
More efficient for streaming scenarios where previous frame is already a dictionary.
|
|
1002
|
+
|
|
1003
|
+
Args:
|
|
1004
|
+
data: Binary frame data
|
|
1005
|
+
previous_lookup: Previous frame keypoints dictionary for delta decoding.
|
|
1006
|
+
Pass None for master frames.
|
|
1007
|
+
|
|
1008
|
+
Returns:
|
|
1009
|
+
DeltaFrame[KeyPoint] with decoded absolute values and IsDelta metadata
|
|
1010
|
+
"""
|
|
1011
|
+
stream = io.BytesIO(data)
|
|
1012
|
+
|
|
1013
|
+
frame_type = stream.read(1)[0]
|
|
1014
|
+
is_delta = frame_type == DELTA_FRAME_TYPE
|
|
1015
|
+
frame_id = struct.unpack("<Q", stream.read(8))[0]
|
|
1016
|
+
count = _read_varint(stream)
|
|
1017
|
+
|
|
1018
|
+
keypoints: List[KeyPoint] = []
|
|
1019
|
+
|
|
1020
|
+
for _ in range(count):
|
|
1021
|
+
kp_id = _read_varint(stream)
|
|
1022
|
+
|
|
1023
|
+
if not is_delta:
|
|
1024
|
+
x = struct.unpack("<i", stream.read(4))[0]
|
|
1025
|
+
y = struct.unpack("<i", stream.read(4))[0]
|
|
1026
|
+
conf_raw = struct.unpack("<H", stream.read(2))[0]
|
|
1027
|
+
keypoints.append(
|
|
1028
|
+
KeyPoint(id=kp_id, position=(x, y), confidence=Confidence(raw=conf_raw))
|
|
1029
|
+
)
|
|
1030
|
+
else:
|
|
1031
|
+
delta_x = _zigzag_decode(_read_varint(stream))
|
|
1032
|
+
delta_y = _zigzag_decode(_read_varint(stream))
|
|
1033
|
+
delta_conf = _zigzag_decode(_read_varint(stream))
|
|
1034
|
+
|
|
1035
|
+
if previous_lookup is not None and kp_id in previous_lookup:
|
|
1036
|
+
prev = previous_lookup[kp_id]
|
|
1037
|
+
x = prev.x + delta_x
|
|
1038
|
+
y = prev.y + delta_y
|
|
1039
|
+
conf_raw = max(0, min(CONFIDENCE_MAX, prev.confidence.raw + delta_conf))
|
|
1040
|
+
else:
|
|
1041
|
+
# New keypoint - delta values are actually absolute
|
|
1042
|
+
x = delta_x
|
|
1043
|
+
y = delta_y
|
|
1044
|
+
conf_raw = max(0, min(CONFIDENCE_MAX, delta_conf))
|
|
1045
|
+
|
|
1046
|
+
keypoints.append(
|
|
1047
|
+
KeyPoint(id=kp_id, position=(x, y), confidence=Confidence(raw=conf_raw))
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
return DeltaFrame[KeyPoint](frame_id=frame_id, is_delta=is_delta, items=keypoints)
|
|
1051
|
+
|
|
1052
|
+
@staticmethod
|
|
1053
|
+
def is_master_frame(data: bytes) -> bool:
|
|
1054
|
+
"""
|
|
1055
|
+
Try to read the frame header to determine if it's a master or delta frame.
|
|
1056
|
+
|
|
1057
|
+
Args:
|
|
1058
|
+
data: Binary frame data
|
|
1059
|
+
|
|
1060
|
+
Returns:
|
|
1061
|
+
True if master frame, False if delta frame
|
|
1062
|
+
"""
|
|
1063
|
+
if len(data) < 1:
|
|
1064
|
+
return False
|
|
1065
|
+
return data[0] == MASTER_FRAME_TYPE
|
|
1066
|
+
|
|
1067
|
+
@staticmethod
|
|
1068
|
+
def should_write_master_frame(frame_id: int, master_interval: int) -> bool:
|
|
1069
|
+
"""
|
|
1070
|
+
Determine if a master frame should be written based on frame interval.
|
|
1071
|
+
|
|
1072
|
+
Args:
|
|
1073
|
+
frame_id: Current frame ID
|
|
1074
|
+
master_interval: Interval between master frames
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
True if master frame should be written
|
|
1078
|
+
"""
|
|
1079
|
+
return frame_id == 0 or (frame_id % master_interval) == 0
|
|
1080
|
+
|
|
1081
|
+
@staticmethod
|
|
1082
|
+
def calculate_master_frame_size(keypoint_count: int) -> int:
|
|
1083
|
+
"""
|
|
1084
|
+
Calculate the maximum buffer size needed for a master frame.
|
|
1085
|
+
|
|
1086
|
+
Args:
|
|
1087
|
+
keypoint_count: Number of keypoints
|
|
1088
|
+
|
|
1089
|
+
Returns:
|
|
1090
|
+
Maximum buffer size in bytes
|
|
1091
|
+
"""
|
|
1092
|
+
# type(1) + frameId(8) + count(varint, max 5) + keypoints(max 15 bytes each)
|
|
1093
|
+
return 1 + 8 + 5 + (keypoint_count * 15)
|
|
1094
|
+
|
|
1095
|
+
@staticmethod
|
|
1096
|
+
def calculate_delta_frame_size(keypoint_count: int) -> int:
|
|
1097
|
+
"""
|
|
1098
|
+
Calculate the maximum buffer size needed for a delta frame.
|
|
1099
|
+
|
|
1100
|
+
Args:
|
|
1101
|
+
keypoint_count: Number of keypoints
|
|
1102
|
+
|
|
1103
|
+
Returns:
|
|
1104
|
+
Maximum buffer size in bytes
|
|
1105
|
+
"""
|
|
1106
|
+
# type(1) + frameId(8) + count(varint, max 5) + keypoints(max 20 bytes each: id + 3 zigzag varints)
|
|
1107
|
+
return 1 + 8 + 5 + (keypoint_count * 20)
|