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.
Files changed (31) hide show
  1. rocket_welder_sdk/__init__.py +44 -22
  2. rocket_welder_sdk/binary_frame_reader.py +222 -0
  3. rocket_welder_sdk/binary_frame_writer.py +213 -0
  4. rocket_welder_sdk/confidence.py +206 -0
  5. rocket_welder_sdk/delta_frame.py +150 -0
  6. rocket_welder_sdk/graphics/__init__.py +42 -0
  7. rocket_welder_sdk/graphics/layer_canvas.py +157 -0
  8. rocket_welder_sdk/graphics/protocol.py +72 -0
  9. rocket_welder_sdk/graphics/rgb_color.py +109 -0
  10. rocket_welder_sdk/graphics/stage.py +494 -0
  11. rocket_welder_sdk/graphics/vector_graphics_encoder.py +575 -0
  12. rocket_welder_sdk/high_level/__init__.py +8 -1
  13. rocket_welder_sdk/high_level/client.py +114 -3
  14. rocket_welder_sdk/high_level/connection_strings.py +88 -15
  15. rocket_welder_sdk/high_level/frame_sink_factory.py +2 -15
  16. rocket_welder_sdk/high_level/transport_protocol.py +4 -130
  17. rocket_welder_sdk/keypoints_protocol.py +520 -55
  18. rocket_welder_sdk/rocket_welder_client.py +210 -89
  19. rocket_welder_sdk/segmentation_result.py +387 -2
  20. rocket_welder_sdk/session_id.py +7 -182
  21. rocket_welder_sdk/transport/__init__.py +10 -3
  22. rocket_welder_sdk/transport/frame_sink.py +3 -3
  23. rocket_welder_sdk/transport/frame_source.py +2 -2
  24. rocket_welder_sdk/transport/websocket_transport.py +316 -0
  25. rocket_welder_sdk/varint.py +213 -0
  26. {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/METADATA +1 -4
  27. rocket_welder_sdk-1.1.45.dist-info/RECORD +51 -0
  28. {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/WHEEL +1 -1
  29. rocket_welder_sdk/transport/nng_transport.py +0 -197
  30. rocket_welder_sdk-1.1.43.dist-info/RECORD +0 -40
  31. {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-10000) internally, float (0.0-1.0) in API
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 BinaryIO, Callable, Dict, Iterator, List, Optional, Tuple
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 .transport import IFrameSink, StreamFrameSink, StreamFrameSource
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
- CONFIDENCE_SCALE = 10000.0
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-10000)."""
116
- return min(max(int(confidence * CONFIDENCE_SCALE), 0), CONFIDENCE_MAX)
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-10000) to float (0.0-1.0)."""
121
- return confidence_ushort / CONFIDENCE_SCALE
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
- """A single keypoint with position and confidence."""
147
+ """
148
+ A single keypoint with position and confidence.
127
149
 
128
- keypoint_id: int
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
- def __post_init__(self) -> None:
134
- """Validate keypoint data."""
135
- if not 0.0 <= self.confidence <= 1.0:
136
- raise ValueError(f"Confidence must be in [0.0, 1.0], got {self.confidence}")
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) -> "IKeyPointsWriter":
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)