mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-29 20:49:42 +00:00
169 lines
6.2 KiB
Python
169 lines
6.2 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
JsonLike = str | int | float | bool | None | list["JsonLike"] | dict[str, "JsonLike"] | tuple["JsonLike", ...]
|
|
|
|
|
|
def load_json(fpath: Path) -> Any:
|
|
"""Load data from a JSON file.
|
|
|
|
Args:
|
|
fpath (Path): Path to the JSON file.
|
|
|
|
Returns:
|
|
Any: The data loaded from the JSON file.
|
|
"""
|
|
with open(fpath) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def write_json(data: dict, fpath: Path) -> None:
|
|
"""Write data to a JSON file.
|
|
|
|
Creates parent directories if they don't exist.
|
|
|
|
Args:
|
|
data (dict): The dictionary to write.
|
|
fpath (Path): The path to the output JSON file.
|
|
"""
|
|
fpath.parent.mkdir(exist_ok=True, parents=True)
|
|
with open(fpath, "w") as f:
|
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
|
|
|
|
def write_video(video_path: str | Path, stacked_frames: list, fps: int) -> None:
|
|
"""Write a sequence of RGB frames to an MP4 video file using libx264.
|
|
|
|
Args:
|
|
video_path: Output file path.
|
|
stacked_frames: List of HWC uint8 numpy arrays (RGB).
|
|
fps: Frames per second for the output video.
|
|
"""
|
|
from .import_utils import require_package
|
|
|
|
require_package("av", extra="av-dep")
|
|
import av
|
|
|
|
with av.open(str(video_path), mode="w") as container:
|
|
orig_height, orig_width = stacked_frames[0].shape[:2]
|
|
# yuv420p requires even dimensions; crop by one pixel if needed
|
|
height = orig_height if orig_height % 2 == 0 else orig_height - 1
|
|
width = orig_width if orig_width % 2 == 0 else orig_width - 1
|
|
if height != orig_height or width != orig_width:
|
|
logger.warning(
|
|
"Frame dimensions %dx%d are not even; cropping to %dx%d for yuv420p compatibility.",
|
|
orig_width,
|
|
orig_height,
|
|
width,
|
|
height,
|
|
)
|
|
stream = container.add_stream("libx264", rate=fps)
|
|
stream.width = width
|
|
stream.height = height
|
|
stream.pix_fmt = "yuv420p"
|
|
for frame_array in stacked_frames:
|
|
if height != orig_height or width != orig_width:
|
|
frame_array = frame_array[:height, :width]
|
|
frame = av.VideoFrame.from_ndarray(frame_array, format="rgb24")
|
|
for packet in stream.encode(frame):
|
|
container.mux(packet)
|
|
for packet in stream.encode():
|
|
container.mux(packet)
|
|
|
|
|
|
def deserialize_json_into_object[T: JsonLike](fpath: Path, obj: T) -> T:
|
|
"""
|
|
Loads the JSON data from `fpath` and recursively fills `obj` with the
|
|
corresponding values (strictly matching structure and types).
|
|
Tuples in `obj` are expected to be lists in the JSON data, which will be
|
|
converted back into tuples.
|
|
"""
|
|
with open(fpath, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
def _deserialize(target, source):
|
|
"""
|
|
Recursively overwrite the structure in `target` with data from `source`,
|
|
performing strict checks on structure and type.
|
|
Returns the updated version of `target` (especially important for tuples).
|
|
"""
|
|
|
|
# If the target is a dictionary, source must be a dictionary as well.
|
|
if isinstance(target, dict):
|
|
if not isinstance(source, dict):
|
|
raise TypeError(f"Type mismatch: expected dict, got {type(source)}")
|
|
|
|
# Check that they have exactly the same set of keys.
|
|
if target.keys() != source.keys():
|
|
raise ValueError(
|
|
f"Dictionary keys do not match.\nExpected: {target.keys()}, got: {source.keys()}"
|
|
)
|
|
|
|
# Recursively update each key.
|
|
for k in target:
|
|
target[k] = _deserialize(target[k], source[k])
|
|
|
|
return target
|
|
|
|
# If the target is a list, source must be a list as well.
|
|
elif isinstance(target, list):
|
|
if not isinstance(source, list):
|
|
raise TypeError(f"Type mismatch: expected list, got {type(source)}")
|
|
|
|
# Check length
|
|
if len(target) != len(source):
|
|
raise ValueError(f"List length mismatch: expected {len(target)}, got {len(source)}")
|
|
|
|
# Recursively update each element.
|
|
for i in range(len(target)):
|
|
target[i] = _deserialize(target[i], source[i])
|
|
|
|
return target
|
|
|
|
# If the target is a tuple, the source must be a list in JSON,
|
|
# which we'll convert back to a tuple.
|
|
elif isinstance(target, tuple):
|
|
if not isinstance(source, list):
|
|
raise TypeError(f"Type mismatch: expected list (for tuple), got {type(source)}")
|
|
|
|
if len(target) != len(source):
|
|
raise ValueError(f"Tuple length mismatch: expected {len(target)}, got {len(source)}")
|
|
|
|
# Convert each element, forming a new tuple.
|
|
converted_items = []
|
|
for t_item, s_item in zip(target, source, strict=False):
|
|
converted_items.append(_deserialize(t_item, s_item))
|
|
|
|
# Return a brand new tuple (tuples are immutable in Python).
|
|
return tuple(converted_items)
|
|
|
|
# Otherwise, we're dealing with a "primitive" (int, float, str, bool, None).
|
|
else:
|
|
# Check the exact type. If these must match 1:1, do:
|
|
if type(target) is not type(source):
|
|
raise TypeError(f"Type mismatch: expected {type(target)}, got {type(source)}")
|
|
return source
|
|
|
|
# Perform the in-place/recursive deserialization
|
|
updated_obj = _deserialize(obj, data)
|
|
return updated_obj
|