From e3741a924c1bd03c091535020b9496411ea39054 Mon Sep 17 00:00:00 2001 From: Vladimir Yakunin Date: Wed, 9 Sep 2026 08:32:06 +0000 Subject: [PATCH 1/7] Measure what a frame codec costs the round trip, not only the wire A video codec over the temporal-stack window sends a fraction of the bytes a per-frame JPEG sends. Both the encode and the decode sit on the round trip, so bytes alone do not say whether the codec is faster. This probe replays a recorded episode through the rig-side bound and reports all three per window. On real frames from a curie round, 25 frames, two cameras, 512x288: JPEG q90 919 KiB, encode 22 ms, decode 26 ms; h264 ultrafast crf20 325 KiB, encode 49 ms, decode 16 ms; h264 veryfast crf20 219 KiB, encode 78 ms, decode 22 ms. Ticket: Positronic-Robotics/internal#1168 #refs --- positronic/offboard/frame_codec_cost.py | 174 ++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 positronic/offboard/frame_codec_cost.py diff --git a/positronic/offboard/frame_codec_cost.py b/positronic/offboard/frame_codec_cost.py new file mode 100644 index 000000000..c97002c69 --- /dev/null +++ b/positronic/offboard/frame_codec_cost.py @@ -0,0 +1,174 @@ +"""Measure what one observation window costs per image codec: bytes, encode time, decode time. + +Replays a recorded episode's cameras through the rig-side bound, then encodes each temporal-stack +window two ways — one JPEG per frame, which is what the wire carries today, and one h264 GOP over +the whole window. h264 sends a fraction of the bytes; this reports what that costs in encode and +decode time, both of which sit on the round trip. + +JPEG runs single-threaded through ``encode_jpeg``, the encoder the wire uses. h264 runs with +x264's own frame threading, so the comparison is generous to h264. + +Usage + python -m positronic.offboard.frame_codec_cost --episode .mp4> + python -m positronic.offboard.frame_codec_cost --episode --bound 640x180 --json rows.json +""" + +import argparse +import dataclasses +import io +import json +import pathlib +import statistics +import time + +import av +import numpy as np +from PIL import Image as PilImage + +from positronic.utils.serialization import encode_jpeg, unpack + +JPEG = 'jpeg q90' + + +@dataclasses.dataclass(frozen=True) +class H264: + """One x264 setting, named the way the report names it.""" + + preset: str + crf: int + + @property + def name(self) -> str: + return f'h264 {self.preset} crf{self.crf}' + + +@dataclasses.dataclass(frozen=True) +class Cost: + """What one window of one camera cost under one codec.""" + + codec: str + camera: str + window: int + kib: float + encode_ms: float + decode_ms: float + + +def bounded_frames(mp4: pathlib.Path, width: int, height: int, rate_hz: float) -> list[np.ndarray]: + """Every frame the stack samples, scaled the way ``RestrictImageSize`` scales it.""" + with av.open(str(mp4), 'r') as container: + recorded_rate = container.streams.video[0].average_rate + if recorded_rate is None: + raise ValueError(f'{mp4} declares no frame rate, so the sampled frames cannot be chosen') + step = max(1, round(float(recorded_rate) / rate_hz)) + frames = [] + for index, frame in enumerate(container.decode(video=0)): + if index % step: + continue + image = frame.to_ndarray(format='rgb24') + source_height, source_width = image.shape[:2] + scale = min(1.0, width / source_width, height / source_height) + if scale < 1.0: + size = (int(source_width * scale), int(source_height * scale)) + image = np.array(PilImage.fromarray(image).resize(size, PilImage.Resampling.BILINEAR)) + frames.append(image) + return frames + + +def jpeg_cost(window: np.ndarray) -> tuple[int, float, float]: + """Bytes, encode ms and decode ms for one window as one JPEG per frame.""" + start = time.perf_counter() + marker = encode_jpeg(window) + encode_ms = 1000 * (time.perf_counter() - start) + + start = time.perf_counter() + unpack(marker) + decode_ms = 1000 * (time.perf_counter() - start) + return sum(len(buf) for buf in marker[b'frames']), encode_ms, decode_ms + + +def h264_cost(window: np.ndarray, codec: H264) -> tuple[int, float, float]: + """Bytes, encode ms and decode ms for one window as a single h264 GOP.""" + height, width = window.shape[1:3] + buffer = io.BytesIO() + start = time.perf_counter() + with av.open(buffer, 'w', format='mp4') as container: + stream = container.add_stream('libx264', rate=15) + stream.width, stream.height, stream.pix_fmt = width, height, 'yuv420p' + # One self-contained GOP with no lookahead: a request carries its own window and waits on it. + stream.options = {'preset': codec.preset, 'crf': str(codec.crf), 'tune': 'zerolatency', 'g': str(len(window))} + for frame in window: + container.mux(stream.encode(av.VideoFrame.from_ndarray(frame, format='rgb24'))) + container.mux(stream.encode(None)) + encode_ms = 1000 * (time.perf_counter() - start) + payload = buffer.getvalue() + + start = time.perf_counter() + with av.open(io.BytesIO(payload), 'r') as container: + for frame in container.decode(video=0): + frame.to_ndarray(format='rgb24') + return len(payload), encode_ms, 1000 * (time.perf_counter() - start) + + +def costs(frames: list[np.ndarray], camera: str, depth: int, codecs: list[H264], limit: int) -> list[Cost]: + """One row per codec per window, over the windows the episode holds.""" + starts = list(range(0, len(frames) - depth + 1))[: limit or None] + rows = [] + for window_index, start in enumerate(starts): + window = np.stack(frames[start : start + depth]) + size, encode_ms, decode_ms = jpeg_cost(window) + rows.append(Cost(JPEG, camera, window_index, size / 1024, encode_ms, decode_ms)) + for codec in codecs: + size, encode_ms, decode_ms = h264_cost(window, codec) + rows.append(Cost(codec.name, camera, window_index, size / 1024, encode_ms, decode_ms)) + return rows + + +def report(rows: list[Cost], depth: int) -> None: + """Median cost per codec, per camera and summed over the cameras one request carries.""" + names = list(dict.fromkeys(row.codec for row in rows)) + cameras = list(dict.fromkeys(row.camera for row in rows)) + windows = {row.window for row in rows} + print(f'\n{depth} frames, {len(windows)} windows, {len(cameras)} cameras\n') + print(f'{"codec":>22} {"camera":>16} {"KiB":>8} {"encode ms":>10} {"decode ms":>10}') + for name in names: + of_codec = [row for row in rows if row.codec == name] + for camera in cameras + ['every camera']: + group = of_codec if camera == 'every camera' else [row for row in of_codec if row.camera == camera] + per_window = [[row for row in group if row.window == window] for window in sorted(windows)] + print( + f'{name:>22} {camera:>16} ' + f'{statistics.median(sum(r.kib for r in w) for w in per_window):8.0f} ' + f'{statistics.median(sum(r.encode_ms for r in w) for w in per_window):10.1f} ' + f'{statistics.median(sum(r.decode_ms for r in w) for w in per_window):10.1f}' + ) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--episode', required=True, type=pathlib.Path, help='directory holding .mp4') + parser.add_argument('--cameras', default='image.exterior,image.wrist') + parser.add_argument('--frames', type=int, default=25, help='temporal-stack depth') + parser.add_argument('--rate', type=float, default=15.0, help='stack sampling rate, Hz') + parser.add_argument('--bound', default='1024x288', help='WxH rig-side bound') + parser.add_argument('--x264', default='ultrafast:20,veryfast:20', help='comma-separated preset:crf') + parser.add_argument('--windows', type=int, default=100, help='windows per camera, 0 for every one') + parser.add_argument('--json', type=pathlib.Path, help='write every row here') + args = parser.parse_args() + + width, height = (int(side) for side in args.bound.lower().split('x')) + codecs = [H264(spec.split(':')[0], int(spec.split(':')[1])) for spec in args.x264.split(',')] + rows: list[Cost] = [] + for camera in args.cameras.split(','): + frames = bounded_frames(args.episode / f'{camera}.mp4', width, height, args.rate) + print(f'{camera}: {len(frames)} sampled frames at {frames[0].shape[1]}x{frames[0].shape[0]}') + rows += costs(frames, camera, args.frames, codecs, args.windows) + + report(rows, args.frames) + if args.json: + args.json.write_text(json.dumps([dataclasses.asdict(row) for row in rows])) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From c37aa6c7fcccb7b2aba4ac21cc75680920daf16f Mon Sep 17 00:00:00 2001 From: Vladimir Yakunin Date: Wed, 9 Sep 2026 08:38:30 +0000 Subject: [PATCH 2/7] Give every codec in the probe one shape, and name the wire's frames key Each codec now answers `cost(window)` and carries its own name, so the report loops one list instead of calling JPEG inline beside a list of h264 variants. Adding a codec is appending to that list. `serialization` keeps the per-frame JPEG buffers under `b'frames'`, and a second module now reads a marker, so the key becomes a public constant rather than a literal each side spells for itself. Ticket: Positronic-Robotics/internal#1168 #refs --- positronic/offboard/frame_codec_cost.py | 91 +++++++++++++------------ positronic/utils/serialization.py | 6 +- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/positronic/offboard/frame_codec_cost.py b/positronic/offboard/frame_codec_cost.py index c97002c69..95af5ea8c 100644 --- a/positronic/offboard/frame_codec_cost.py +++ b/positronic/offboard/frame_codec_cost.py @@ -25,14 +25,30 @@ import numpy as np from PIL import Image as PilImage -from positronic.utils.serialization import encode_jpeg, unpack +from positronic.utils.serialization import FRAMES, encode_jpeg, unpack -JPEG = 'jpeg q90' + +@dataclasses.dataclass(frozen=True) +class Jpeg: + """One JPEG per frame, at the quality the wire itself encodes at.""" + + name = 'jpeg' + + def cost(self, window: np.ndarray) -> tuple[int, float, float]: + """Bytes, encode ms and decode ms for one window.""" + start = time.perf_counter() + marker = encode_jpeg(window) + encode_ms = 1000 * (time.perf_counter() - start) + + start = time.perf_counter() + unpack(marker) + decode_ms = 1000 * (time.perf_counter() - start) + return sum(len(buf) for buf in marker[FRAMES]), encode_ms, decode_ms @dataclasses.dataclass(frozen=True) class H264: - """One x264 setting, named the way the report names it.""" + """One x264 setting, over the whole window as a single GOP.""" preset: str crf: int @@ -41,6 +57,31 @@ class H264: def name(self) -> str: return f'h264 {self.preset} crf{self.crf}' + def cost(self, window: np.ndarray) -> tuple[int, float, float]: + """Bytes, encode ms and decode ms for one window.""" + height, width = window.shape[1:3] + buffer = io.BytesIO() + start = time.perf_counter() + with av.open(buffer, 'w', format='mp4') as container: + stream = container.add_stream('libx264', rate=15) + stream.width, stream.height, stream.pix_fmt = width, height, 'yuv420p' + # One self-contained GOP with no lookahead: a request carries its own window and waits on it. + stream.options = {'preset': self.preset, 'crf': str(self.crf), 'tune': 'zerolatency', 'g': str(len(window))} + for frame in window: + container.mux(stream.encode(av.VideoFrame.from_ndarray(frame, format='rgb24'))) + container.mux(stream.encode(None)) + encode_ms = 1000 * (time.perf_counter() - start) + payload = buffer.getvalue() + + start = time.perf_counter() + with av.open(io.BytesIO(payload), 'r') as container: + for frame in container.decode(video=0): + frame.to_ndarray(format='rgb24') + return len(payload), encode_ms, 1000 * (time.perf_counter() - start) + + +Codec = Jpeg | H264 + @dataclasses.dataclass(frozen=True) class Cost: @@ -75,51 +116,14 @@ def bounded_frames(mp4: pathlib.Path, width: int, height: int, rate_hz: float) - return frames -def jpeg_cost(window: np.ndarray) -> tuple[int, float, float]: - """Bytes, encode ms and decode ms for one window as one JPEG per frame.""" - start = time.perf_counter() - marker = encode_jpeg(window) - encode_ms = 1000 * (time.perf_counter() - start) - - start = time.perf_counter() - unpack(marker) - decode_ms = 1000 * (time.perf_counter() - start) - return sum(len(buf) for buf in marker[b'frames']), encode_ms, decode_ms - - -def h264_cost(window: np.ndarray, codec: H264) -> tuple[int, float, float]: - """Bytes, encode ms and decode ms for one window as a single h264 GOP.""" - height, width = window.shape[1:3] - buffer = io.BytesIO() - start = time.perf_counter() - with av.open(buffer, 'w', format='mp4') as container: - stream = container.add_stream('libx264', rate=15) - stream.width, stream.height, stream.pix_fmt = width, height, 'yuv420p' - # One self-contained GOP with no lookahead: a request carries its own window and waits on it. - stream.options = {'preset': codec.preset, 'crf': str(codec.crf), 'tune': 'zerolatency', 'g': str(len(window))} - for frame in window: - container.mux(stream.encode(av.VideoFrame.from_ndarray(frame, format='rgb24'))) - container.mux(stream.encode(None)) - encode_ms = 1000 * (time.perf_counter() - start) - payload = buffer.getvalue() - - start = time.perf_counter() - with av.open(io.BytesIO(payload), 'r') as container: - for frame in container.decode(video=0): - frame.to_ndarray(format='rgb24') - return len(payload), encode_ms, 1000 * (time.perf_counter() - start) - - -def costs(frames: list[np.ndarray], camera: str, depth: int, codecs: list[H264], limit: int) -> list[Cost]: +def costs(frames: list[np.ndarray], camera: str, depth: int, codecs: list[Codec], limit: int) -> list[Cost]: """One row per codec per window, over the windows the episode holds.""" starts = list(range(0, len(frames) - depth + 1))[: limit or None] rows = [] for window_index, start in enumerate(starts): window = np.stack(frames[start : start + depth]) - size, encode_ms, decode_ms = jpeg_cost(window) - rows.append(Cost(JPEG, camera, window_index, size / 1024, encode_ms, decode_ms)) for codec in codecs: - size, encode_ms, decode_ms = h264_cost(window, codec) + size, encode_ms, decode_ms = codec.cost(window) rows.append(Cost(codec.name, camera, window_index, size / 1024, encode_ms, decode_ms)) return rows @@ -157,7 +161,8 @@ def main() -> int: args = parser.parse_args() width, height = (int(side) for side in args.bound.lower().split('x')) - codecs = [H264(spec.split(':')[0], int(spec.split(':')[1])) for spec in args.x264.split(',')] + codecs: list[Codec] = [Jpeg()] + codecs += [H264(spec.split(':')[0], int(spec.split(':')[1])) for spec in args.x264.split(',')] rows: list[Cost] = [] for camera in args.cameras.split(','): frames = bounded_frames(args.episode / f'{camera}.mp4', width, height, args.rate) diff --git a/positronic/utils/serialization.py b/positronic/utils/serialization.py index 09948c22a..f5f4f5f2b 100644 --- a/positronic/utils/serialization.py +++ b/positronic/utils/serialization.py @@ -26,7 +26,7 @@ _DATA = b'data' _DTYPE = b'dtype' _SHAPE = b'shape' -_FRAMES = b'frames' +FRAMES = b'frames' # the wire's own name for the per-frame JPEGs; a reader of a marker needs it _NDIM = b'ndim' # JPEG quality for images on the wire. A single HD frame — and especially a (T, H, W, 3) stack — is many @@ -46,12 +46,12 @@ def encode_jpeg(image: np.ndarray) -> dict[bytes, Any]: buf = io.BytesIO() PilImage.fromarray(np.ascontiguousarray(frame, dtype=np.uint8)).save(buf, format='JPEG', quality=_JPEG_QUALITY) bufs.append(buf.getvalue()) - return {_JPEG: True, _FRAMES: bufs, _NDIM: int(image.ndim)} + return {_JPEG: True, FRAMES: bufs, _NDIM: int(image.ndim)} def _decode_jpeg(marker: dict) -> np.ndarray: """Inverse of ``encode_jpeg``: decode per-frame JPEGs and restore the original shape.""" - frames = np.stack([np.asarray(PilImage.open(io.BytesIO(buf))) for buf in marker[_FRAMES]]) + frames = np.stack([np.asarray(PilImage.open(io.BytesIO(buf))) for buf in marker[FRAMES]]) return frames if marker[_NDIM] == 4 else frames[0] From 831bd037730580e06d1f00f02d167e3155421c27 Mon Sep 17 00:00:00 2001 From: Vladimir Yakunin Date: Wed, 9 Sep 2026 08:54:15 +0000 Subject: [PATCH 3/7] State what the frames key is, not why it is public Ticket: Positronic-Robotics/internal#1168 #refs --- positronic/utils/serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/positronic/utils/serialization.py b/positronic/utils/serialization.py index f5f4f5f2b..e7552dd8f 100644 --- a/positronic/utils/serialization.py +++ b/positronic/utils/serialization.py @@ -26,7 +26,7 @@ _DATA = b'data' _DTYPE = b'dtype' _SHAPE = b'shape' -FRAMES = b'frames' # the wire's own name for the per-frame JPEGs; a reader of a marker needs it +FRAMES = b'frames' # the wire's own name for the per-frame JPEGs _NDIM = b'ndim' # JPEG quality for images on the wire. A single HD frame — and especially a (T, H, W, 3) stack — is many From fe391f856d18ce47d2ec4f0e6a523400f57d6c33 Mon Sep 17 00:00:00 2001 From: Vladimir Yakunin Date: Wed, 9 Sep 2026 08:55:54 +0000 Subject: [PATCH 4/7] Bound the probe's frames through the rig's own codec `RestrictImageSize` is what bounds an image before it reaches the wire, and the probe re-derived its scaling instead of calling it. The two cannot then be shown to agree, which is the one property a measurement of the wire needs. Byte counts are unchanged, which says they did agree. Ticket: Positronic-Robotics/internal#1168 #refs --- positronic/offboard/frame_codec_cost.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/positronic/offboard/frame_codec_cost.py b/positronic/offboard/frame_codec_cost.py index 95af5ea8c..c9da714bc 100644 --- a/positronic/offboard/frame_codec_cost.py +++ b/positronic/offboard/frame_codec_cost.py @@ -23,8 +23,8 @@ import av import numpy as np -from PIL import Image as PilImage +from positronic.policy.codec import RestrictImageSize from positronic.utils.serialization import FRAMES, encode_jpeg, unpack @@ -95,8 +95,9 @@ class Cost: decode_ms: float -def bounded_frames(mp4: pathlib.Path, width: int, height: int, rate_hz: float) -> list[np.ndarray]: - """Every frame the stack samples, scaled the way ``RestrictImageSize`` scales it.""" +def bounded_frames(mp4: pathlib.Path, bound: RestrictImageSize, rate_hz: float) -> list[np.ndarray]: + """Every frame the stack samples, through the rig's own bound.""" + key = 'image' with av.open(str(mp4), 'r') as container: recorded_rate = container.streams.video[0].average_rate if recorded_rate is None: @@ -106,13 +107,7 @@ def bounded_frames(mp4: pathlib.Path, width: int, height: int, rate_hz: float) - for index, frame in enumerate(container.decode(video=0)): if index % step: continue - image = frame.to_ndarray(format='rgb24') - source_height, source_width = image.shape[:2] - scale = min(1.0, width / source_width, height / source_height) - if scale < 1.0: - size = (int(source_width * scale), int(source_height * scale)) - image = np.array(PilImage.fromarray(image).resize(size, PilImage.Resampling.BILINEAR)) - frames.append(image) + frames.append(bound.encode({key: frame.to_ndarray(format='rgb24')})[key]) return frames @@ -160,12 +155,12 @@ def main() -> int: parser.add_argument('--json', type=pathlib.Path, help='write every row here') args = parser.parse_args() - width, height = (int(side) for side in args.bound.lower().split('x')) + bound = RestrictImageSize(*(int(side) for side in args.bound.lower().split('x'))) codecs: list[Codec] = [Jpeg()] codecs += [H264(spec.split(':')[0], int(spec.split(':')[1])) for spec in args.x264.split(',')] rows: list[Cost] = [] for camera in args.cameras.split(','): - frames = bounded_frames(args.episode / f'{camera}.mp4', width, height, args.rate) + frames = bounded_frames(args.episode / f'{camera}.mp4', bound, args.rate) print(f'{camera}: {len(frames)} sampled frames at {frames[0].shape[1]}x{frames[0].shape[0]}') rows += costs(frames, camera, args.frames, codecs, args.windows) From 4b0b8f3f863278280ea02c32e8f43bdd94fa14a9 Mon Sep 17 00:00:00 2001 From: Vladimir Yakunin Date: Wed, 9 Sep 2026 09:03:08 +0000 Subject: [PATCH 5/7] Build the report's rows without a sentinel camera The per-camera rows and the summed row were one loop over the cameras plus the string 'every camera', which two lines had to spell the same way. They are now two calls to one row builder. Ticket: Positronic-Robotics/internal#1168 #refs --- positronic/offboard/frame_codec_cost.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/positronic/offboard/frame_codec_cost.py b/positronic/offboard/frame_codec_cost.py index c9da714bc..a39ce786a 100644 --- a/positronic/offboard/frame_codec_cost.py +++ b/positronic/offboard/frame_codec_cost.py @@ -123,24 +123,29 @@ def costs(frames: list[np.ndarray], camera: str, depth: int, codecs: list[Codec] return rows +def _row(label: str, group: list[Cost], windows: list[int]) -> str: + """One printed line: the median over windows of what the group cost in each window.""" + per_window = [[row for row in group if row.window == window] for window in windows] + return ( + f'{label:>16} ' + f'{statistics.median(sum(row.kib for row in w) for w in per_window):8.0f} ' + f'{statistics.median(sum(row.encode_ms for row in w) for w in per_window):10.1f} ' + f'{statistics.median(sum(row.decode_ms for row in w) for w in per_window):10.1f}' + ) + + def report(rows: list[Cost], depth: int) -> None: """Median cost per codec, per camera and summed over the cameras one request carries.""" names = list(dict.fromkeys(row.codec for row in rows)) cameras = list(dict.fromkeys(row.camera for row in rows)) - windows = {row.window for row in rows} + windows = sorted({row.window for row in rows}) print(f'\n{depth} frames, {len(windows)} windows, {len(cameras)} cameras\n') print(f'{"codec":>22} {"camera":>16} {"KiB":>8} {"encode ms":>10} {"decode ms":>10}') for name in names: of_codec = [row for row in rows if row.codec == name] - for camera in cameras + ['every camera']: - group = of_codec if camera == 'every camera' else [row for row in of_codec if row.camera == camera] - per_window = [[row for row in group if row.window == window] for window in sorted(windows)] - print( - f'{name:>22} {camera:>16} ' - f'{statistics.median(sum(r.kib for r in w) for w in per_window):8.0f} ' - f'{statistics.median(sum(r.encode_ms for r in w) for w in per_window):10.1f} ' - f'{statistics.median(sum(r.decode_ms for r in w) for w in per_window):10.1f}' - ) + for camera in cameras: + print(f'{name:>22} {_row(camera, [row for row in of_codec if row.camera == camera], windows)}') + print(f'{name:>22} {_row("every camera", of_codec, windows)}') def main() -> int: From 97c14a531a13b9ccd1cd592b4338de70fd04e1d3 Mon Sep 17 00:00:00 2001 From: Vladimir Yakunin Date: Wed, 9 Sep 2026 09:06:37 +0000 Subject: [PATCH 6/7] Keep the module docstring to what a caller acts on Ticket: Positronic-Robotics/internal#1168 #refs --- positronic/offboard/frame_codec_cost.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/positronic/offboard/frame_codec_cost.py b/positronic/offboard/frame_codec_cost.py index a39ce786a..80372d585 100644 --- a/positronic/offboard/frame_codec_cost.py +++ b/positronic/offboard/frame_codec_cost.py @@ -1,12 +1,8 @@ """Measure what one observation window costs per image codec: bytes, encode time, decode time. Replays a recorded episode's cameras through the rig-side bound, then encodes each temporal-stack -window two ways — one JPEG per frame, which is what the wire carries today, and one h264 GOP over -the whole window. h264 sends a fraction of the bytes; this reports what that costs in encode and -decode time, both of which sit on the round trip. - -JPEG runs single-threaded through ``encode_jpeg``, the encoder the wire uses. h264 runs with -x264's own frame threading, so the comparison is generous to h264. +window as one JPEG per frame, which is what the wire carries, and as one h264 GOP. JPEG encodes +single-threaded through ``encode_jpeg``; h264 encodes with x264's own frame threading. Usage python -m positronic.offboard.frame_codec_cost --episode .mp4> From 3aa0af2eb26b7cd4e33894f4982d46cca5c8fcc5 Mon Sep 17 00:00:00 2001 From: Vladimir Yakunin Date: Wed, 9 Sep 2026 12:36:19 +0000 Subject: [PATCH 7/7] Say what the wire carries without the cleft Ticket: Positronic-Robotics/internal#1168 #refs --- positronic/offboard/frame_codec_cost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/positronic/offboard/frame_codec_cost.py b/positronic/offboard/frame_codec_cost.py index 80372d585..21350711f 100644 --- a/positronic/offboard/frame_codec_cost.py +++ b/positronic/offboard/frame_codec_cost.py @@ -1,7 +1,7 @@ """Measure what one observation window costs per image codec: bytes, encode time, decode time. Replays a recorded episode's cameras through the rig-side bound, then encodes each temporal-stack -window as one JPEG per frame, which is what the wire carries, and as one h264 GOP. JPEG encodes +window as one JPEG per frame, which the wire carries, and as one h264 GOP. JPEG encodes single-threaded through ``encode_jpeg``; h264 encodes with x264's own frame threading. Usage