unienv 0.0.1b5__py3-none-any.whl → 0.0.1b6__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.
- {unienv-0.0.1b5.dist-info → unienv-0.0.1b6.dist-info}/METADATA +3 -2
- {unienv-0.0.1b5.dist-info → unienv-0.0.1b6.dist-info}/RECORD +30 -21
- {unienv-0.0.1b5.dist-info → unienv-0.0.1b6.dist-info}/WHEEL +1 -1
- unienv_data/base/common.py +25 -10
- unienv_data/batches/backend_compat.py +1 -1
- unienv_data/batches/combined_batch.py +1 -1
- unienv_data/replay_buffer/replay_buffer.py +51 -8
- unienv_data/storages/_episode_storage.py +438 -0
- unienv_data/storages/_list_storage.py +136 -0
- unienv_data/storages/backend_compat.py +268 -0
- unienv_data/storages/flattened.py +3 -3
- unienv_data/storages/hdf5.py +7 -2
- unienv_data/storages/image_storage.py +144 -0
- unienv_data/storages/npz_storage.py +135 -0
- unienv_data/storages/pytorch.py +16 -9
- unienv_data/storages/video_storage.py +297 -0
- unienv_data/third_party/tensordict/memmap_tensor.py +1174 -0
- unienv_data/transformations/image_compress.py +81 -18
- unienv_interface/space/space_utils/batch_utils.py +5 -1
- unienv_interface/space/spaces/dict.py +6 -0
- unienv_interface/transformations/__init__.py +3 -1
- unienv_interface/transformations/batch_and_unbatch.py +42 -4
- unienv_interface/transformations/chained_transform.py +9 -8
- unienv_interface/transformations/crop.py +69 -0
- unienv_interface/transformations/dict_transform.py +8 -2
- unienv_interface/transformations/identity.py +16 -0
- unienv_interface/transformations/rescale.py +24 -5
- unienv_interface/wrapper/backend_compat.py +1 -1
- {unienv-0.0.1b5.dist-info → unienv-0.0.1b6.dist-info}/licenses/LICENSE +0 -0
- {unienv-0.0.1b5.dist-info → unienv-0.0.1b6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from typing import Generic, TypeVar, Generic, Optional, Any, Dict, Tuple, Sequence, Union, List, Iterable, Type, Literal, cast
|
|
2
|
+
from fractions import Fraction
|
|
3
|
+
from unienv_interface.space import Space, BoxSpace
|
|
4
|
+
from unienv_interface.space.space_utils import batch_utils as sbu, flatten_utils as sfu
|
|
5
|
+
from unienv_interface.backends import ComputeBackend, BArrayType, BDeviceType, BDtypeType, BRNGType
|
|
6
|
+
from unienv_interface.utils.symbol_util import *
|
|
7
|
+
|
|
8
|
+
from unienv_data.base import SpaceStorage
|
|
9
|
+
from ._episode_storage import IndexableType, EpisodeStorageBase
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import os
|
|
13
|
+
import json
|
|
14
|
+
import shutil
|
|
15
|
+
|
|
16
|
+
import av
|
|
17
|
+
import imageio.v3 as iio
|
|
18
|
+
from imageio.plugins.pyav import PyAVPlugin
|
|
19
|
+
from av.codec.hwaccel import HWAccel, hwdevices_available
|
|
20
|
+
from av.codec import codecs_available
|
|
21
|
+
|
|
22
|
+
class VideoStorage(EpisodeStorageBase[
|
|
23
|
+
BArrayType,
|
|
24
|
+
BArrayType,
|
|
25
|
+
BDeviceType,
|
|
26
|
+
BDtypeType,
|
|
27
|
+
BRNGType,
|
|
28
|
+
]):
|
|
29
|
+
"""
|
|
30
|
+
A storage for RGB or depth video data using video files
|
|
31
|
+
If encoding RGB video
|
|
32
|
+
- Set `buffer_pixel_format` to `rgb24`
|
|
33
|
+
- Set `file_pixel_format` to `None`
|
|
34
|
+
- Set `file_ext` to anything you like (e.g., "mp4", "avi", "mkv", etc.)
|
|
35
|
+
If encoding depth video
|
|
36
|
+
- Set `buffer_pixel_format` to `gray16le` (You can use rescale transform inside a `TransformedStorage` to convert depth values to this format, where `dtype` should be `np.uint16`) - if in meters, set min to 0 and max to 65.535 as the multiplication factor is 1000 (i.e., depth in mm)
|
|
37
|
+
- Set `file_pixel_format` to `gray16le`
|
|
38
|
+
- Set `codec` to a lossless codec that supports gray16le, e.g., `ffv1`
|
|
39
|
+
- Set `file_ext` to `mkv`
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# ========== Class Attributes ==========
|
|
43
|
+
@classmethod
|
|
44
|
+
def create(
|
|
45
|
+
cls,
|
|
46
|
+
single_instance_space: BoxSpace[BArrayType, BDeviceType, BDtypeType, BRNGType],
|
|
47
|
+
*args,
|
|
48
|
+
hardware_acceleration : Optional[Union[HWAccel, Literal['auto']]] = 'auto',
|
|
49
|
+
codec : Union[str, Literal['auto']] = 'auto',
|
|
50
|
+
file_ext : str = "mp4",
|
|
51
|
+
file_pixel_format : Optional[str] = None,
|
|
52
|
+
buffer_pixel_format : str = "rgb24",
|
|
53
|
+
fps : int = 15,
|
|
54
|
+
capacity : Optional[int] = None,
|
|
55
|
+
cache_path : Optional[str] = None,
|
|
56
|
+
multiprocessing : bool = False,
|
|
57
|
+
**kwargs
|
|
58
|
+
) -> "VideoStorage[BArrayType, BDeviceType, BDtypeType, BRNGType]":
|
|
59
|
+
assert not multiprocessing, "VideoStorage does not support multiprocessing mode when creating a new mutable storage"
|
|
60
|
+
if cache_path is None:
|
|
61
|
+
raise ValueError("cache_path must be provided for ImageStorage.create")
|
|
62
|
+
assert not os.path.exists(cache_path), f"Cache path {cache_path} already exists"
|
|
63
|
+
os.makedirs(cache_path, exist_ok=True)
|
|
64
|
+
return VideoStorage(
|
|
65
|
+
single_instance_space,
|
|
66
|
+
cache_filename=cache_path,
|
|
67
|
+
hardware_acceleration=hardware_acceleration,
|
|
68
|
+
codec=codec,
|
|
69
|
+
file_ext=file_ext,
|
|
70
|
+
file_pixel_format=file_pixel_format,
|
|
71
|
+
buffer_pixel_format=buffer_pixel_format,
|
|
72
|
+
fps=fps,
|
|
73
|
+
capacity=capacity,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def load_from(
|
|
78
|
+
cls,
|
|
79
|
+
path : Union[str, os.PathLike],
|
|
80
|
+
single_instance_space : BoxSpace[BArrayType, BDeviceType, BDtypeType, BRNGType],
|
|
81
|
+
*,
|
|
82
|
+
hardware_acceleration : Optional[Union[HWAccel, Literal['auto']]] = 'auto',
|
|
83
|
+
codec : Union[str, Literal['auto']] = 'auto',
|
|
84
|
+
capacity : Optional[int] = None,
|
|
85
|
+
read_only : bool = True,
|
|
86
|
+
multiprocessing : bool = False,
|
|
87
|
+
**kwargs
|
|
88
|
+
) -> "VideoStorage[BArrayType, BDeviceType, BDtypeType, BRNGType]":
|
|
89
|
+
assert read_only or (not multiprocessing), "VideoStorage does not support multiprocessing mode when loading a mutable storage"
|
|
90
|
+
metadata_path = os.path.join(path, "image_metadata.json")
|
|
91
|
+
assert os.path.exists(metadata_path), f"Metadata file {metadata_path} does not exist"
|
|
92
|
+
with open(metadata_path, "r") as f:
|
|
93
|
+
metadata = json.load(f)
|
|
94
|
+
assert metadata["storage_type"] == cls.__name__, \
|
|
95
|
+
f"Expected storage type {cls.__name__}, but found {metadata['storage_type']}"
|
|
96
|
+
|
|
97
|
+
if codec == "auto":
|
|
98
|
+
codec_base = metadata.get("codec_base", None)
|
|
99
|
+
if hardware_acceleration is not None:
|
|
100
|
+
codec = cls.get_auto_codec(base=codec_base)
|
|
101
|
+
else:
|
|
102
|
+
assert codec_base is not None, "Codec base must be specified in metadata to load without hardware acceleration"
|
|
103
|
+
codec = codec_base
|
|
104
|
+
file_ext = metadata["file_ext"]
|
|
105
|
+
fps = int(metadata["fps"])
|
|
106
|
+
file_pixel_format = metadata.get("file_pixel_format", None)
|
|
107
|
+
buffer_pixel_format = metadata.get("buffer_pixel_format", "rgb24")
|
|
108
|
+
|
|
109
|
+
if "capacity" in metadata:
|
|
110
|
+
capacity = None if metadata['capacity'] is None else int(metadata["capacity"])
|
|
111
|
+
length = None if capacity is None else metadata["length"]
|
|
112
|
+
|
|
113
|
+
return VideoStorage(
|
|
114
|
+
single_instance_space,
|
|
115
|
+
cache_filename=path,
|
|
116
|
+
hardware_acceleration=hardware_acceleration,
|
|
117
|
+
codec=codec,
|
|
118
|
+
file_ext=file_ext,
|
|
119
|
+
file_pixel_format=file_pixel_format,
|
|
120
|
+
buffer_pixel_format=buffer_pixel_format,
|
|
121
|
+
fps=fps,
|
|
122
|
+
mutable=not read_only,
|
|
123
|
+
capacity=capacity,
|
|
124
|
+
length=length,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ========== Instance Implementations ==========
|
|
128
|
+
single_file_ext = None
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def get_auto_hwaccel() -> Optional[HWAccel]:
|
|
132
|
+
if hasattr(__class__, "_auto_hwaccel"):
|
|
133
|
+
return __class__._auto_hwaccel
|
|
134
|
+
available_hwdevices = hwdevices_available()
|
|
135
|
+
target_hwaccel = None
|
|
136
|
+
if "d3d11va" in available_hwdevices:
|
|
137
|
+
target_hwaccel = HWAccel(device_type="d3d11va", allow_software_fallback=True)
|
|
138
|
+
elif "cuda" in available_hwdevices:
|
|
139
|
+
target_hwaccel = HWAccel(device_type="cuda", allow_software_fallback=True)
|
|
140
|
+
elif "vaapi" in available_hwdevices:
|
|
141
|
+
target_hwaccel = HWAccel(device_type="vaapi", allow_software_fallback=True)
|
|
142
|
+
elif "videotoolbox" in available_hwdevices:
|
|
143
|
+
target_hwaccel = HWAccel(device_type="videotoolbox", allow_software_fallback=True)
|
|
144
|
+
__class__._auto_hwaccel = target_hwaccel
|
|
145
|
+
return target_hwaccel
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def get_auto_codec(
|
|
149
|
+
base : Optional[str] = None
|
|
150
|
+
) -> str:
|
|
151
|
+
if hasattr(__class__, "_auto_codec"):
|
|
152
|
+
return __class__._auto_codec
|
|
153
|
+
preferred_codecs = ["av1", "hevc", "h264", "mpeg4", "vp9", "vp8"] if base is None else [base]
|
|
154
|
+
preferred_suffixes = ["_nvenc", "_amf", "_qsv"]
|
|
155
|
+
|
|
156
|
+
target_codec = None
|
|
157
|
+
for codec in preferred_codecs:
|
|
158
|
+
for suffix in preferred_suffixes:
|
|
159
|
+
full_codec_name = codec + suffix
|
|
160
|
+
if full_codec_name in codecs_available:
|
|
161
|
+
target_codec = full_codec_name
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
if target_codec is None:
|
|
165
|
+
for codec in preferred_codecs:
|
|
166
|
+
if codec in codecs_available:
|
|
167
|
+
target_codec = codec
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
if target_codec is None:
|
|
171
|
+
raise RuntimeError("No suitable video codec found in available codecs.")
|
|
172
|
+
|
|
173
|
+
__class__._auto_codec = target_codec
|
|
174
|
+
return target_codec
|
|
175
|
+
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
single_instance_space: BoxSpace[BArrayType, BDeviceType, BDtypeType, BRNGType],
|
|
179
|
+
cache_filename : Union[str, os.PathLike],
|
|
180
|
+
hardware_acceleration : Optional[Union[HWAccel, Literal['auto']]] = 'auto',
|
|
181
|
+
codec : Union[str, Literal['auto']] = 'auto',
|
|
182
|
+
file_ext : str = "mp4",
|
|
183
|
+
file_pixel_format : Optional[str] = None,
|
|
184
|
+
buffer_pixel_format : str = "rgb24",
|
|
185
|
+
fps : int = 15,
|
|
186
|
+
mutable : bool = True,
|
|
187
|
+
capacity : Optional[int] = None,
|
|
188
|
+
length : int = 0,
|
|
189
|
+
):
|
|
190
|
+
super().__init__(
|
|
191
|
+
single_instance_space,
|
|
192
|
+
file_ext=file_ext,
|
|
193
|
+
cache_filename=cache_filename,
|
|
194
|
+
mutable=mutable,
|
|
195
|
+
capacity=capacity,
|
|
196
|
+
length=length,
|
|
197
|
+
)
|
|
198
|
+
self.hwaccel = None if hardware_acceleration is None else (
|
|
199
|
+
self.get_auto_hwaccel() if hardware_acceleration == 'auto' else hardware_acceleration
|
|
200
|
+
)
|
|
201
|
+
self.codec = self.get_auto_codec() if codec == 'auto' else codec
|
|
202
|
+
self.fps = fps
|
|
203
|
+
self.file_pixel_format = file_pixel_format
|
|
204
|
+
self.buffer_pixel_format = buffer_pixel_format
|
|
205
|
+
|
|
206
|
+
def get_from_file(self, filename : str, index : Union[IndexableType, BArrayType], total_length : int) -> BArrayType:
|
|
207
|
+
with iio.imopen(filename, 'r', plugin='pyav', hwaccel=self.hwaccel) as video:
|
|
208
|
+
video = cast(PyAVPlugin, video)
|
|
209
|
+
if isinstance(index, int):
|
|
210
|
+
frame_np = video.read(index=index, format=self.buffer_pixel_format)
|
|
211
|
+
frame = self.backend.from_numpy(frame_np)
|
|
212
|
+
if self.device is not None:
|
|
213
|
+
frame = self.backend.to_device(frame, self.device)
|
|
214
|
+
return frame
|
|
215
|
+
else:
|
|
216
|
+
if index is Ellipsis:
|
|
217
|
+
index = np.arange(total_length)
|
|
218
|
+
elif isinstance(index, slice):
|
|
219
|
+
index = np.arange(*index.indices(total_length))
|
|
220
|
+
elif self.backend.is_backendarray(index) and self.backend.dtype_is_boolean(index.dtype):
|
|
221
|
+
index = self.backend.nonzero(index)[0]
|
|
222
|
+
if self.backend.is_backendarray(index):
|
|
223
|
+
index = self.backend.to_numpy(index)
|
|
224
|
+
|
|
225
|
+
argsorted_indices = np.argsort(index)
|
|
226
|
+
sorted_index = index[argsorted_indices]
|
|
227
|
+
reserve_index = np.argsort(argsorted_indices)
|
|
228
|
+
|
|
229
|
+
if len(index) < total_length // 2:
|
|
230
|
+
all_frames_np = []
|
|
231
|
+
for frame_i in sorted_index:
|
|
232
|
+
frame_np = video.read(index=frame_i, format=self.buffer_pixel_format)
|
|
233
|
+
all_frames_np.append(frame_np)
|
|
234
|
+
all_frames_np = np.stack(all_frames_np, axis=0)
|
|
235
|
+
# Reorder from sorted order back to original order
|
|
236
|
+
all_frames_np = all_frames_np[reserve_index]
|
|
237
|
+
else:
|
|
238
|
+
# Create a set for O(1) lookup and a mapping from frame index to position in sorted_index
|
|
239
|
+
sorted_index_set = set(sorted_index)
|
|
240
|
+
frame_to_sorted_pos = {int(frame_idx): pos for pos, frame_idx in enumerate(sorted_index)}
|
|
241
|
+
|
|
242
|
+
# Pre-allocate array to store frames in sorted order
|
|
243
|
+
all_frames_list = [None] * len(sorted_index)
|
|
244
|
+
past_frame_np = None
|
|
245
|
+
video_iter = video.iter(format=self.buffer_pixel_format)
|
|
246
|
+
for frame_i in range(total_length):
|
|
247
|
+
try:
|
|
248
|
+
frame_np = next(video_iter)
|
|
249
|
+
except StopIteration:
|
|
250
|
+
frame_np = past_frame_np
|
|
251
|
+
if frame_i in sorted_index_set:
|
|
252
|
+
# Store at the position corresponding to sorted_index order
|
|
253
|
+
all_frames_list[frame_to_sorted_pos[frame_i]] = frame_np
|
|
254
|
+
past_frame_np = frame_np
|
|
255
|
+
all_frames_np = np.stack(all_frames_list, axis=0)
|
|
256
|
+
# Reorder from sorted order back to original order
|
|
257
|
+
all_frames_np = all_frames_np[reserve_index]
|
|
258
|
+
all_frames = self.backend.from_numpy(all_frames_np)
|
|
259
|
+
if self.device is not None:
|
|
260
|
+
all_frames = self.backend.to_device(all_frames, self.device)
|
|
261
|
+
return all_frames
|
|
262
|
+
|
|
263
|
+
def set_to_file(self, filename : str, value : BArrayType):
|
|
264
|
+
value_np = self.backend.to_numpy(value)
|
|
265
|
+
with iio.imopen(
|
|
266
|
+
filename,
|
|
267
|
+
'w',
|
|
268
|
+
plugin='pyav',
|
|
269
|
+
) as video:
|
|
270
|
+
video = cast(PyAVPlugin, video)
|
|
271
|
+
video.init_video_stream(self.codec, fps=self.fps, pixel_format=self.file_pixel_format)
|
|
272
|
+
|
|
273
|
+
# Fix codec time base if not set:
|
|
274
|
+
if video._video_stream.codec_context.time_base is None:
|
|
275
|
+
video._video_stream.codec_context.time_base = Fraction(1 / self.fps).limit_denominator(int(2**16 - 1))
|
|
276
|
+
|
|
277
|
+
for i, frame in enumerate(value_np):
|
|
278
|
+
video.write_frame(frame, pixel_format=self.buffer_pixel_format)
|
|
279
|
+
|
|
280
|
+
def dumps(self, path):
|
|
281
|
+
assert os.path.samefile(path, self.cache_filename), \
|
|
282
|
+
f"Dump path {path} does not match cache filename {self.cache_filename}"
|
|
283
|
+
metadata = {
|
|
284
|
+
"storage_type": __class__.__name__,
|
|
285
|
+
"codec_base": self.codec if "_" not in self.codec else self.codec.split("_")[0],
|
|
286
|
+
"file_ext": self.file_ext,
|
|
287
|
+
"fps": self.fps,
|
|
288
|
+
"file_pixel_format": self.file_pixel_format,
|
|
289
|
+
"buffer_pixel_format": self.buffer_pixel_format,
|
|
290
|
+
"capacity": self.capacity,
|
|
291
|
+
"length": self.length,
|
|
292
|
+
}
|
|
293
|
+
with open(os.path.join(path, "image_metadata.json"), "w") as f:
|
|
294
|
+
json.dump(metadata, f)
|
|
295
|
+
|
|
296
|
+
def close(self):
|
|
297
|
+
pass
|