embodichain.data_pipeline.depth_video#
Compressed depth sidecar storage for LeRobot datasets on Python 3.10–3.12.
This package implements issue #424 Path A: an EmbodiChain-owned depth writer
that stores camera depth as gray12le/HEVC sidecar videos alongside a
LeRobot dataset without modifying the installed LeRobot package.
Depth quantization math is vendored from lerobot v0.6.0 so that sidecar videos remain binary-compatible with the official reader.
Overview#
Compressed depth-sidecar storage for LeRobot datasets on Python 3.10–3.12.
The package stores camera depth as gray12le/HEVC sidecar videos alongside
a LeRobot dataset without modifying the installed LeRobot package. Depth
quantization math is vendored from lerobot v0.6.0 so that sidecar videos stay
binary-compatible with the official reader.
The main entry points are DepthVideoWriter and
DepthSidecarManager for writing, DepthVideoReader for
reading, and load_depth_dataset() / load_depth_meta() for loading
depth data alongside a LeRobot dataset. Behavior is configured through
DepthVideoCfg; codec selection goes through detect_depth_encoder()
and resolve_depth_vcodec().
Classes
|
Configuration for the compressed depth sidecar writer. |
|
Streaming writer that encodes one depth episode to a single MP4. |
|
Owns the depth sidecar videos and metadata for one dataset recording. |
|
Decode and dequantize a single depth sidecar MP4. |
|
Index of all depth sidecar videos for a dataset. |
|
Raised when no suitable depth video codec is available. |
Functions
|
Return an available depth video encoder name, or |
|
Return an available depth video encoder name, raising on failure. |
|
Quantize depth to 12-bit codes ( |
|
Inverse of |
|
Load a LeRobot dataset together with its depth sidecar library. |
|
Load and return the |
Configuration#
Configuration for compressed depth sidecar storage.
Classes:
Configuration for the compressed depth sidecar writer. |
- class embodichain.data_pipeline.depth_video.cfg.DepthVideoCfg[source]#
Bases:
objectConfiguration for the compressed depth sidecar writer.
Depth is quantized to 12-bit codes and encoded as a single-channel
gray12levideo. Withlossless=True(default) the 12-bit codes are preserved bit-exactly by HEVC; the only error is the configurable float32 -> 12-bit quantization step.- enable#
If False, depth is stored as numeric LeRobot features (PR #422).
- vcodec#
Video codec for depth. Defaults to
"libx265"(HEVC), which supports 12-bit grayscale losslessly on typical FFmpeg builds.
- lossless#
If True, encode with HEVC lossless mode so 12-bit codes are bit-exact. If False, use
crffor lossy encoding.
- crf#
Constant rate factor for lossy mode (ignored when
lossless=True). Lower is higher quality; 0 is lossless for libx265.
- depth_min#
Depth (metres) mapped to quantum 0.
- depth_max#
Depth (metres) mapped to quantum
DEPTH_QMAX.
- shift#
Pre-log offset (metres) for numerical stability near zero.
- use_log#
Logarithmic (True) or linear (False) quantization.
- pix_fmt#
Pixel format for the depth video.
gray12lecarries the 12-bit codes in a single channel.
- input_unit#
Unit of the incoming depth arrays (
"auto"infers from dtype: float -> metres, int -> millimetres).
- output_unit#
Unit returned by the reader (
"m"or"mm").
- keep_numeric_fallback#
If True, also keep depth as a numeric LeRobot feature (exact raw values, ~2x depth storage). If False, depth lives only in the sidecar videos.
Methods:
__init__([enable, vcodec, lossless, crf, ...])copy(**kwargs)Return a new object replacing specified fields with new values.
replace(**kwargs)Return a new object replacing specified fields with new values.
to_dict()Convert an object into dictionary recursively.
validate([prefix])Check the validity of configclass object.
Attributes:
- __init__(enable=<factory>, vcodec=<factory>, lossless=<factory>, crf=<factory>, depth_min=<factory>, depth_max=<factory>, shift=<factory>, use_log=<factory>, pix_fmt=<factory>, input_unit=<factory>, output_unit=<factory>, keep_numeric_fallback=<factory>)#
- copy(**kwargs)#
Return a new object replacing specified fields with new values.
This is especially useful for frozen classes. Example usage:
@configclass(frozen=True) class C: x: int y: int c = C(1, 2) c1 = c.replace(x=3) assert c1.x == 3 and c1.y == 2
- Parameters:
obj (
object) – The object to replace.**kwargs – The fields to replace and their new values.
- Return type:
object- Returns:
The new object.
-
crf:
int#
-
depth_max:
float#
-
depth_min:
float#
-
enable:
bool#
-
input_unit:
Literal['auto','m','mm']#
-
keep_numeric_fallback:
bool#
-
lossless:
bool#
-
output_unit:
Literal['m','mm']#
-
pix_fmt:
str#
- replace(**kwargs)#
Return a new object replacing specified fields with new values.
This is especially useful for frozen classes. Example usage:
@configclass(frozen=True) class C: x: int y: int c = C(1, 2) c1 = c.replace(x=3) assert c1.x == 3 and c1.y == 2
- Parameters:
obj (
object) – The object to replace.**kwargs – The fields to replace and their new values.
- Return type:
object- Returns:
The new object.
-
shift:
float#
- to_dict()#
Convert an object into dictionary recursively.
Note
Ignores all names starting with “__” (i.e. built-in methods).
- Parameters:
obj (
object) – An instance of a class to convert.- Raises:
ValueError – When input argument is not an object.
- Return type:
dict[str,Any]- Returns:
Converted dictionary mapping.
-
use_log:
bool#
- validate(prefix='')#
Check the validity of configclass object.
This function checks if the object is a valid configclass object. A valid configclass object contains no MISSING entries.
- Parameters:
obj (
object) – The object to check.prefix (
str) – The prefix to add to the missing fields. Defaults to ‘’.
- Return type:
list[str]- Returns:
A list of missing fields.
- Raises:
TypeError – When the object is not a valid configuration object.
-
vcodec:
str#
Codec & Quantization#
Codec availability detection for depth video encoding.
Adapted from lerobot v0.6.0 pyav_utils.py (Apache-2.0). Probes the bundled
FFmpeg build through PyAV so the writer can pick a working encoder and degrade
gracefully (to numeric Parquet depth, PR #422) when HEVC is unavailable.
Exceptions:
Raised when no suitable depth video codec is available. |
Functions:
|
Return an available depth video encoder name, or |
|
Return an available depth video encoder name, raising on failure. |
- exception embodichain.data_pipeline.depth_video.codec.DepthCodecError[source]#
Bases:
RuntimeErrorRaised when no suitable depth video codec is available.
- embodichain.data_pipeline.depth_video.codec.detect_depth_encoder(vcodec=None)[source]#
Return an available depth video encoder name, or
None.- Parameters:
vcodec (
str|None) – Preferred codec name. IfNoneor unavailable, fall back to the default preference list (libx265thenhevc).- Return type:
str|None- Returns:
The first available encoder name, or
Noneif none are available.
- embodichain.data_pipeline.depth_video.codec.resolve_depth_vcodec(vcodec=None)[source]#
Return an available depth video encoder name, raising on failure.
- Parameters:
vcodec (
str|None) – Preferred codec name.- Return type:
str- Returns:
An available encoder name.
- Raises:
DepthCodecError – If no depth video encoder is available in the bundled FFmpeg build.
Depth quantization/dequantization for compressed depth sidecar videos.
Depth maps are packed into 12-bit integer codes (uint16, values
0…DEPTH_QMAX) so they fit the gray12le pixel format and can be encoded
losslessly by HEVC (libx265). Logarithmic quantization is the default
because it allocates more quanta to near-range depth, matching the
1/depth error profile of typical depth sensors.
The math is ported from lerobot 0.6.0 (itself adapted from BEHAVIOR-1K’s
obs_utils.py) and depends only on av, numpy and
torch – all of which run on Python 3.10–3.12. See issue #424.
Functions:
|
Inverse of |
|
Infer the depth unit from the array dtype. |
|
Quantize depth to 12-bit codes ( |
|
Drop a leading or trailing singleton channel dim: |
|
Copy a 2D |
- embodichain.data_pipeline.depth_video.depth_utils.dequantize_depth(quantized, depth_min=0.01, depth_max=10.0, shift=3.5, use_log=True, pix_fmt='gray12le', output_unit='mm', output_tensor=True, output_channel_last=False)[source]#
Inverse of
quantize_depth().Decoding inverts the same normalized code mapping as
quantize_depth()usingdepth_min/depth_max/shift(in metres), then returns the requested output unit. Tuning arguments must matchquantize_depth().Accepted input layouts :
(H, W, 1)or(H, W)- single frame with channel-last.(..., 1, H, W)- batched frames with channel-first.(..., H, W, 1)- batched frames with channel-last.
Output layout is determined by
output_channel_last.- Parameters:
quantized (
ndarray[tuple[Any,...],dtype[uint16]] |VideoFrame|Tensor) – 12-bit codes in[0, DEPTH_QMAX].np.ndarray,av.VideoFrame, ortorch.Tensor(any integer or float dtype).depth_min (
float) – Same asquantize_depth()(metres).depth_max (
float) – Same asquantize_depth()(metres).shift (
float) – Same asquantize_depth()(metres).use_log (
bool) – Same asquantize_depth()(metres).pix_fmt (
str) – Pixel format used to extract the plane from anav.VideoFrame.output_unit (
Literal['m','mm']) –"mm"returnsuint16millimetres (rint, clip[0, 65535]) when returning a numpy array, orfloat32mm whenoutput_tensor=True."m"returnsfloat32metres in[depth_min, depth_max].output_tensor (
bool) – If True, return atorch.Tensorinstead of a numpy array.output_channel_last (
bool) – Channel layout of the output ((H, W, 1)vs(1, H, W)).
- Return type:
ndarray[tuple[Any,...],dtype[uint16]] |ndarray[tuple[Any,...],dtype[float32]] |Tensor- Returns:
Depth map in the requested unit and dtype.
- Raises:
ValueError – If
output_unitis not"m"or"mm".ValueError – If
use_log=Trueanddepth_min + shift <= 0.
- embodichain.data_pipeline.depth_video.depth_utils.infer_depth_unit(dtype)[source]#
Infer the depth unit from the array dtype.
Floating-point arrays are interpreted as metres, integer arrays as millimetres – the lerobot convention.
- Parameters:
dtype (
dtype|type) – NumPy dtype (or anything coercible to one).- Return type:
str- Returns:
"m"or"mm".
- embodichain.data_pipeline.depth_video.depth_utils.quantize_depth(depth, depth_min=0.01, depth_max=10.0, shift=3.5, use_log=True, pix_fmt='gray12le', video_backend='pyav', input_unit='auto')[source]#
Quantize depth to 12-bit codes (
uint16, values0…DEPTH_QMAX).Depth maps are packed into 12-bit integer frames so they fit in standard high-bit-depth pixel formats (e.g.
yuv420p12le/gray12le) and can be encoded by widely supported video codecs (e.g. HEVC Main 12). Logarithmic quantization is the default because it allocates more quanta to near-range depth, which matches the (1/depth) error profile of typical depth sensors. Math is ported from BEHAVIOR-1K’sobs_utils.py.Input units:
input_unit="auto"(default): infer from dtype (floating = m, non-floating = mm).input_unit="mm": interpret input values as millimetres.input_unit="m": interpret input values as metres.
Quantization math runs in the resolved input unit.
depth_min,depth_max, andshiftare always in metres.- Parameters:
depth (
ndarray[tuple[Any,...],dtype[uint16]] |ndarray[tuple[Any,...],dtype[float32]] |Tensor) – Depth map;torch.Tensoris moved to CPU for conversion.depth_min (
float) – Depth (metres) at quantum0.depth_max (
float) – Depth (metres) at quantumDEPTH_QMAX.shift (
float) – Depth shift (metres); used in log mode. Must satisfydepth_min + shift > 0.use_log (
bool) – IfTrue(default), quantize in log space.video_backend (
str|None) – If"pyav"(default), return anav.VideoFrameready for encoding; otherwise return the rawuint16code array.input_unit (
Literal['auto','m','mm']) – Input unit policy ("auto","mm","m").
- Return type:
ndarray[tuple[Any,...],dtype[uint16]] |VideoFrame- Returns:
av.VideoFrame(whenvideo_backend="pyav") ornumpy.ndarrayofdtype=uint16, same spatial shape asdepth, values in[0, DEPTH_QMAX].- Raises:
ValueError – If
input_unitis not"auto","mm", or"m".ValueError – If
use_log=Trueanddepth_min + shift <= 0.
- embodichain.data_pipeline.depth_video.depth_utils.squeeze_single_channel(array)[source]#
Drop a leading or trailing singleton channel dim:
(1, H, W)/(H, W, 1)->(H, W).Unlike
array.squeeze(), this only removes the channel axis, never anHorWof size 1.- Return type:
ndarray
- embodichain.data_pipeline.depth_video.depth_utils.write_u16_plane(plane, src, fill_value=None)[source]#
Copy a 2D
uint16image into the plane’s memory buffer, row by row.For speed, each row is padded to a wider size than
width, so the true row width in memory isplane.line_size(bytes), notwidth. Copying as one straight stream would skew the image, so we write only the firstwidthcolumns of each row and leave the padding untouched.- Parameters:
plane (
VideoPlane) – Destination 16-bit plane.src (
ndarray) – Source image, shape(height, width), dtypeuint16.fill_value (
int|None) – If given, every pixel (padding included) is set to this first, so the padding holds clean data instead of garbage.
- Return type:
None
Writer#
Streaming depth video writer and per-episode sidecar manager.
Writes camera depth maps as gray12le/HEVC sidecar videos that live
alongside a LeRobot dataset (issue #424, Path A). Depth never enters LeRobot’s
own image/video pipeline (which is RGB-only in 0.4.4); instead each episode is
encoded to a standalone MP4 and indexed by depth_meta.json.
Classes:
Owns the depth sidecar videos and metadata for one dataset recording. |
|
Streaming writer that encodes one depth episode to a single MP4. |
- class embodichain.data_pipeline.depth_video.writer.DepthSidecarManager[source]#
Bases:
objectOwns the depth sidecar videos and metadata for one dataset recording.
For each episode, opens one
DepthVideoWriterper depth sensor (e.g.cameraandcamera_right), feeds frames, and on episode close moves the MP4s into the dataset’sdepth_videos/tree and updatesdepth_meta.json.Methods:
__init__(dataset_root, fps, cfg[, vcodec])Initialize the sidecar manager.
Abort the current episode: discard all partial depth videos.
add_frame(sensor_key, depth)Feed one depth frame for one sensor in the current episode.
end_episode(episode_index)Close all writers for the current episode and update metadata.
finalize()Flush metadata after the last episode.
has_sensor(sensor_key)Return True if sensor_key has been registered.
register_sensor(sensor_key, shape)Register a depth sensor and its static metadata.
start_episode(episode_index, sensor_keys)Open a writer per sensor for a new episode.
- __init__(dataset_root, fps, cfg, vcodec=None)[source]#
Initialize the sidecar manager.
- Parameters:
dataset_root (
Path) – Root directory of the LeRobot dataset. Sidecar videos are written under<root>/depth_videos/and metadata under<root>/depth_meta.json.fps (
int) – Dataset frame rate.cfg (
DepthVideoCfg) – Depth video configuration.vcodec (
Optional[str]) – Resolved codec name. IfNone, resolved fromcfg.
- abort_episode()[source]#
Abort the current episode: discard all partial depth videos.
Called when the surrounding LeRobot
save_episodefails, so the dataset never references a depth video for an episode that was not committed.- Return type:
None
- add_frame(sensor_key, depth)[source]#
Feed one depth frame for one sensor in the current episode.
- Parameters:
sensor_key (
str) – Sensor identifier.depth (
Any) – Depth map.
- Return type:
None
- end_episode(episode_index)[source]#
Close all writers for the current episode and update metadata.
Every writer is attempted. Partial files from failed writers are discarded, metadata for successful writers is flushed, and any error is then raised so a committed LeRobot episode cannot silently lose its depth sidecar.
- Parameters:
episode_index (
int) – Global episode index.- Raises:
RuntimeError – If a depth video or its metadata cannot be finalized.
- Return type:
None
- finalize()[source]#
Flush metadata after the last episode.
- Return type:
Path- Returns:
Path to
depth_meta.json.
- class embodichain.data_pipeline.depth_video.writer.DepthVideoWriter[source]#
Bases:
objectStreaming writer that encodes one depth episode to a single MP4.
Depth frames are quantized to 12-bit codes and encoded as
gray12levideo. Withlossless=Truethe 12-bit codes are bit-exact after a decode round-trip (verified on pyav 15.x + libx265).The video is written to a temporary path and atomically moved to its final location on
close(); on failure the partial file is removed so the dataset never references a corrupt sidecar.Methods:
__init__(final_path, fps, cfg[, vcodec])Initialize the writer.
abort()Abort writing and remove any partial output.
add_frame(depth)Quantize and encode a single depth frame.
close()Flush the encoder and finalize the MP4.
Attributes:
Destination MP4 path.
Number of frames written so far.
- __init__(final_path, fps, cfg, vcodec=None)[source]#
Initialize the writer.
- Parameters:
final_path (
Path) – Destination MP4 path. Written via a temp file in the same directory and atomically renamed on success.fps (
int) – Episode frame rate (frames per second).cfg (
DepthVideoCfg) – Depth video configuration.vcodec (
Optional[str]) – Resolved codec name. IfNone, resolved fromcfg.
- add_frame(depth)[source]#
Quantize and encode a single depth frame.
- Parameters:
depth (
Any) – Depth map (torch.Tensorornp.ndarray), shape(H, W)/(H, W, 1)/(1, H, W).- Return type:
None
- close()[source]#
Flush the encoder and finalize the MP4.
- Return type:
Path- Returns:
The final MP4 path on success.
- Raises:
RuntimeError – If no frames were written.
- property final_path: Path#
Destination MP4 path.
- property frame_count: int#
Number of frames written so far.
Reader#
Readers for compressed depth sidecar videos.
Decodes the gray12le/HEVC MP4s written by DepthSidecarManager and
dequantizes them back to metres or millimetres. load_depth_dataset composes
a LeRobot dataset with its depth sidecar library so callers can read RGB, state,
action, mask and depth together.
Classes:
Index of all depth sidecar videos for a dataset. |
|
Decode and dequantize a single depth sidecar MP4. |
Functions:
|
Load a LeRobot dataset together with its depth sidecar library. |
- class embodichain.data_pipeline.depth_video.reader.DepthVideoLibrary[source]#
Bases:
objectIndex of all depth sidecar videos for a dataset.
Maps
(episode_index, sensor_key)to aDepthVideoReaderand provides random access by within-episode frame index.Methods:
__init__(dataset_root, meta)Initialize the library from parsed metadata.
frame_count(episode_index, sensor_key)Return the number of depth frames for an episode/sensor.
get(episode_index, sensor_key, ...)Read one depth frame.
sensor_meta(sensor_key)Return the static metadata block for a sensor.
Attributes:
List of depth sensor keys recorded in the dataset.
- __init__(dataset_root, meta)[source]#
Initialize the library from parsed metadata.
- Parameters:
dataset_root (
Path) – Root directory of the LeRobot dataset.meta (
Dict[str,Any]) – Parseddepth_meta.jsoncontents.
- frame_count(episode_index, sensor_key)[source]#
Return the number of depth frames for an episode/sensor.
- Return type:
int
- get(episode_index, sensor_key, frame_index_in_episode)[source]#
Read one depth frame.
- Parameters:
episode_index (
int) – Global episode index.sensor_key (
str) – Sensor identifier (e.g."camera","camera_right").frame_index_in_episode (
int) – Frame index within the episode.
- Return type:
ndarray- Returns:
Depth map in the unit recorded in metadata, shape
(1, H, W).
- sensor_meta(sensor_key)[source]#
Return the static metadata block for a sensor.
- Return type:
Dict[str,Any]
- property sensors: list[str]#
List of depth sensor keys recorded in the dataset.
- class embodichain.data_pipeline.depth_video.reader.DepthVideoReader[source]#
Bases:
objectDecode and dequantize a single depth sidecar MP4.
Frames are decoded lazily on first access and cached, since one MP4 holds a single (typically short) episode.
Methods:
__init__(path, depth_min, depth_max, shift, ...)Initialize the reader.
read(frame_index)Decode and dequantize one frame.
Attributes:
Number of frames in the sidecar video.
- __init__(path, depth_min, depth_max, shift, use_log, pix_fmt='gray12le', output_unit='m')[source]#
Initialize the reader.
- Parameters:
path (
Path) – Path to the sidecar MP4.depth_min (
float) – Quantizationdepth_min(metres) used at write time.depth_max (
float) – Quantizationdepth_max(metres) used at write time.shift (
float) – Quantizationshift(metres) used at write time.use_log (
bool) – Quantizationuse_logused at write time.pix_fmt (
str) – Pixel format of the stored video.output_unit (
str) – Unit to return ("m"or"mm").
- property frame_count: int#
Number of frames in the sidecar video.
- embodichain.data_pipeline.depth_video.reader.load_depth_dataset(dataset_root, **lerobot_kwargs)[source]#
Load a LeRobot dataset together with its depth sidecar library.
- Parameters:
dataset_root (
Path) – Root directory of the LeRobot dataset (the directory containingdata/,videos/anddepth_meta.json).**lerobot_kwargs (
Any) – Forwarded toLeRobotDataset(e.g.episodes).
- Return type:
tuple[Any,DepthVideoLibrary]- Returns:
A
(LeRobotDataset, DepthVideoLibrary)tuple.- Raises:
ImportError – If LeRobot is not installed.
FileNotFoundError – If the dataset has no depth sidecar metadata.