Skip to content

Commit 77adf0b

Browse files
AmitMYAmit Moryossefclaude
authored
EmptyPoseBody: zero-allocation reads + fix v0.1 frame count via BytesIOReader (#234)
* EmptyPoseBody: zero-allocation reads, fix v0.1 frame count via BytesIOReader unpack_empty_tensor now returns a zero-stride broadcast view instead of np.empty, keeping shape and dtype without allocating shape-sized memory. Adding a shape test exposed a pre-existing bug: BytesIOReader.bytes_left() only counted already-buffered bytes, so v0.1 files (which infer frame count from bytes_left) read 0 frames through any BytesIOReader path (EmptyPoseBody, start_frame/start_time on file objects). bytes_left() now reports the stream's remaining bytes, matching BufferReader semantics, while expect_to_read uses the new buffered_bytes_left() for chunk fetching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Comment the seek dance in BytesIOReader.bytes_left Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Amit Moryossef <amitmoryossef@Amits-MacBook-Pro.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7640301 commit 77adf0b

3 files changed

Lines changed: 43 additions & 3 deletions

File tree

src/python/pose_format/utils/reader.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ def unpack_f(self, s_format: str):
9292
return self.unpack(getattr(ConstStructs, s_format))
9393

9494
def unpack_empty_tensor(self, s: struct.Struct, shape: Tuple):
95-
arr = np.empty(shape, s.format)
95+
# Zero-stride view: keeps the shape without allocating shape-sized memory
96+
arr = np.broadcast_to(np.array(0, dtype=s.format), shape)
9697
self.advance(s, int(np.prod(shape)))
9798
return arr
9899

@@ -220,6 +221,17 @@ def skip(self, s: struct.Struct, times=1):
220221
self.read_skipped += s.size * times
221222
super().skip(s, times)
222223

224+
def bytes_left(self):
225+
# Unlike BufferReader, the buffer only holds what was already read from the file,
226+
# so count the file's remaining bytes as well (v0.1 infers frame count from this)
227+
current = self.reader.tell() # remember the position, since seeking moves it
228+
file_size = self.reader.seek(0, 2) # seek 0 bytes from the end (SEEK_END): reads nothing, returns the file size
229+
self.reader.seek(current) # restore the position so read_chunk continues from the right place
230+
return file_size - self.read_offset
231+
232+
def buffered_bytes_left(self):
233+
return len(self.buffer) - self.read_offset + self.read_skipped
234+
223235
def read_chunk(self, chunk_size: int):
224236
if self.read_offset > self.reader.tell():
225237
self.reader.seek(self.read_offset, 0) # 0 means absolute seek
@@ -230,8 +242,8 @@ def read_chunk(self, chunk_size: int):
230242
raise EOFError("End of file reached")
231243

232244
def expect_to_read(self, n: int):
233-
if self.bytes_left() < n:
234-
self.read_chunk(n - self.bytes_left())
245+
if self.buffered_bytes_left() < n:
246+
self.read_chunk(n - self.buffered_bytes_left())
235247

236248

237249
if __name__ == "__main__":

src/python/pose_format/utils/reader_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ def test_unpack_numpy_writeable(self):
7575

7676
arr -= 0.1
7777

78+
def test_unpack_empty_tensor(self):
79+
""" Test that unpack_empty_tensor keeps shape and dtype, and advances the buffer"""
80+
buffer = struct.pack("<ffff", 1., 2.5, 3.5, 4.5)
81+
reader = BufferReader(buffer)
82+
83+
arr = reader.unpack_empty_tensor(ConstStructs.float, (2, 2))
84+
85+
self.assertEqual(arr.shape, (2, 2))
86+
self.assertEqual(arr.dtype, np.float32)
87+
self.assertEqual(reader.bytes_left(), 0, msg="Buffer should advance as if the data was read")
88+
7889
def test_unpack_torch(self):
7990
""" Test that unpack_torch returns the correct value"""
8091
buffer = struct.pack("<ffff", 1., 2.5, 3.5, 4.5)

src/python/tests/pose_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from pose_format.numpy.pose_body import NumPyPoseBody
1111
from pose_format.pose import Pose
12+
from pose_format.pose_body import EmptyPoseBody
1213
from pose_format.pose_header import (PoseHeader, PoseHeaderComponent,
1314
PoseHeaderDimensions)
1415

@@ -323,6 +324,22 @@ def test_pose_remove_components(self):
323324
self.assertEqual(c_copy.name, c_orig.name) # with the same name
324325
self.assertEqual(c_copy.points, c_orig.points) # and the same points
325326

327+
def test_read_empty_pose_body_shape_matches_numpy_pose_body(self):
328+
data_dir = Path(__file__).parent / "data"
329+
for pose_file in sorted(data_dir.glob("*.pose")):
330+
with self.subTest(pose_file=pose_file.name):
331+
with open(pose_file, 'rb') as f:
332+
numpy_pose = Pose.read(f, pose_body=NumPyPoseBody)
333+
if numpy_pose.header.version == 0: # EmptyPoseBody does not support the legacy v0.0 format
334+
continue
335+
with open(pose_file, 'rb') as f:
336+
empty_pose = Pose.read(f, pose_body=EmptyPoseBody)
337+
338+
self.assertEqual(empty_pose.body.data.shape, numpy_pose.body.data.shape)
339+
self.assertEqual(empty_pose.body.data.dtype, numpy_pose.body.data.dtype)
340+
self.assertEqual(empty_pose.body.confidence.shape, numpy_pose.body.confidence.shape)
341+
self.assertEqual(empty_pose.body.fps, numpy_pose.body.fps)
342+
326343
def test_pose_bbox(self):
327344
data_dir = Path(__file__).parent / "data"
328345
with open(data_dir / 'mediapipe.pose', 'rb') as f:

0 commit comments

Comments
 (0)