daio 0.0.1__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.
- daio/h5.py +172 -0
- daio/video.py +265 -0
- daio-0.0.1.dist-info/LICENSE +22 -0
- daio-0.0.1.dist-info/METADATA +39 -0
- daio-0.0.1.dist-info/RECORD +7 -0
- daio-0.0.1.dist-info/WHEEL +5 -0
- daio-0.0.1.dist-info/top_level.txt +1 -0
daio/h5.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import json
|
|
2
|
+
#TODO: test replacing json with orjson
|
|
3
|
+
import warnings
|
|
4
|
+
from os.path import expanduser
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import h5py
|
|
8
|
+
#import hdf5plugin
|
|
9
|
+
#TODO: test defaulting to hdf5plugin.Zstd compression for json-serialized strings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def save_to_h5(filename, data, serialize=True, compression=None, json_compression='gzip', verbosity=1, file_mode='w', convert_numpy_to_native=False):
|
|
13
|
+
''' Save a nested dictionary data structure to an HDF5 file.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
filename (string): file name of the HDF5 file
|
|
17
|
+
data (dict): Nested dictionary whose contents may be dict, ndarray, str, bytes, DataFrame and JSON-serializable objects
|
|
18
|
+
serialize (boolean): enable JSON serialization
|
|
19
|
+
compression (string): h5py compression type (e.g. 'gzip', 'lzf' or None)
|
|
20
|
+
json_compression (string): h5py compression type for serialized JSON (default: 'gzip')
|
|
21
|
+
file_mode (string): h5py.File access mode. 'w' (default) for create/detete and 'a' for create/append
|
|
22
|
+
|
|
23
|
+
based on https://github.com/danionella/lib2p/blob/master/lib2putils.py
|
|
24
|
+
'''
|
|
25
|
+
|
|
26
|
+
def recursively_save_contents_to_group(h5file, path, data_item):
|
|
27
|
+
assert isinstance(data_item, (dict))
|
|
28
|
+
for key, item in data_item.items():
|
|
29
|
+
if verbosity > 1:
|
|
30
|
+
print('saving entry: {} -- {}'.format(path + key, type(item)))
|
|
31
|
+
if isinstance(item, (np.ndarray, np.int64, np.float64, str, bytes, int, float)):
|
|
32
|
+
comp = None if np.isscalar(item) else compression
|
|
33
|
+
try:
|
|
34
|
+
h5file[path].create_dataset(key, data=item, compression=comp)
|
|
35
|
+
except TypeError:
|
|
36
|
+
warnings.warn(f'\n\nkey: {key} -- Saving data with compression failed. Saving without compression.\n')
|
|
37
|
+
h5file[path].create_dataset(key, data=item, compression=None)
|
|
38
|
+
elif type(item).__name__ == 'DataFrame':
|
|
39
|
+
json_bytes = np.frombuffer(item.to_json().encode('utf-8'), dtype='byte')
|
|
40
|
+
h5file[path].create_dataset(key, data=json_bytes, compression=json_compression)
|
|
41
|
+
h5file[path + key].attrs['pandas_json_type'] = f'This {type(item)} was JSON serialized and UTF-8 encoded.'
|
|
42
|
+
elif isinstance(item, dict):
|
|
43
|
+
h5file[path].create_group(key)
|
|
44
|
+
recursively_save_contents_to_group(h5file, path + key + '/', item)
|
|
45
|
+
elif serialize:
|
|
46
|
+
if verbosity > 0:
|
|
47
|
+
print(f'serializing {type(item)} at {path+key}', flush=True)
|
|
48
|
+
#TODO: test replacing json with orjson
|
|
49
|
+
json_bytes = json.dumps(item).encode('utf-8')
|
|
50
|
+
h5file[path].create_dataset(key, data=np.frombuffer(json_bytes, dtype='byte'), compression=json_compression)
|
|
51
|
+
h5file[path + key].attrs['json_type'] = f'This {type(item)} was JSON serialized and UTF-8 encoded.'
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f'Cannot save {type(item)} to {path+key}. Consider enabling serialisation.')
|
|
54
|
+
|
|
55
|
+
if convert_numpy_to_native:
|
|
56
|
+
data = convert_numpy_to_native(data)
|
|
57
|
+
|
|
58
|
+
filename = expanduser(filename)
|
|
59
|
+
with h5py.File(filename, file_mode) as h5file:
|
|
60
|
+
recursively_save_contents_to_group(h5file, '/', data)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_from_h5(filename):
|
|
64
|
+
''' Load an HDF5 file to a dictionary
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
filename (string): file name of the HDF5 file
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
dict: file contents
|
|
71
|
+
'''
|
|
72
|
+
|
|
73
|
+
def recursively_load_contents_from_group(h5file, path):
|
|
74
|
+
ans = dict()
|
|
75
|
+
for key, item in h5file[path].items():
|
|
76
|
+
if 'pandas_type' in item.attrs.keys():
|
|
77
|
+
import pandas as pd
|
|
78
|
+
ans[key] = pd.read_hdf(filename, path + key)
|
|
79
|
+
elif 'pandas_json_type' in item.attrs.keys():
|
|
80
|
+
import pandas as pd
|
|
81
|
+
json_str = item[()].tobytes().decode('utf-8')
|
|
82
|
+
ans[key] = pd.read_json(json_str)
|
|
83
|
+
elif 'json_type' in item.attrs.keys():
|
|
84
|
+
#TODO: test replacing json with orjson
|
|
85
|
+
ans[key] = json.loads(item[()].tobytes())
|
|
86
|
+
elif isinstance(item, h5py._hl.dataset.Dataset):
|
|
87
|
+
if h5py.check_string_dtype(item.dtype) is not None:
|
|
88
|
+
item = item.asstr()
|
|
89
|
+
ans[key] = item[()]
|
|
90
|
+
elif isinstance(item, h5py._hl.group.Group):
|
|
91
|
+
ans[key] = recursively_load_contents_from_group(h5file, path + key + '/')
|
|
92
|
+
else:
|
|
93
|
+
raise ValueError(f"I don't know what to do about {path+key}.")
|
|
94
|
+
return ans
|
|
95
|
+
|
|
96
|
+
filename = expanduser(filename)
|
|
97
|
+
with h5py.File(filename, 'r') as h5file:
|
|
98
|
+
return recursively_load_contents_from_group(h5file, '/')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class lazyh5:
|
|
102
|
+
""" A lazy-loading interface for HDF5 files.
|
|
103
|
+
|
|
104
|
+
This class provides an easy way to access HDF5 file content without fully
|
|
105
|
+
loading it into memory. It supports dynamic access to datasets and subgroups.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
filepath (str): Path to the HDF5 file.
|
|
109
|
+
h5path (str, optional): HDF5 group path. Defaults to '/'.
|
|
110
|
+
overwrite (bool, optional): Whether to overwrite existing items. Defaults to False.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, filepath, h5path='/', overwrite=False):
|
|
114
|
+
self._filepath = filepath
|
|
115
|
+
self._h5path = h5path
|
|
116
|
+
self._overwrite = overwrite
|
|
117
|
+
|
|
118
|
+
def keys(self):
|
|
119
|
+
"""Lists the keys of the current HDF5 group.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
list: List of keys in the current HDF5 group.
|
|
123
|
+
"""
|
|
124
|
+
with h5py.File(self._filepath, 'r') as f:
|
|
125
|
+
return list(f[self._h5path].keys())
|
|
126
|
+
|
|
127
|
+
def __getitem__(self, key):
|
|
128
|
+
"""Gets an item by key."""
|
|
129
|
+
with h5py.File(self._filepath, 'r') as f:
|
|
130
|
+
item = f[self._h5path][key]
|
|
131
|
+
if isinstance(item, h5py.Group):
|
|
132
|
+
return lazyh5(self._filepath, h5path=f"{self._h5path}/{key}")
|
|
133
|
+
elif isinstance(item, h5py.Dataset):
|
|
134
|
+
return item[()]
|
|
135
|
+
else:
|
|
136
|
+
raise KeyError(f"Unknown item type for key: {key}")
|
|
137
|
+
|
|
138
|
+
def __getattr__(self, key):
|
|
139
|
+
"""Provides dynamic attribute-style access."""
|
|
140
|
+
try:
|
|
141
|
+
return self[key]
|
|
142
|
+
except KeyError:
|
|
143
|
+
raise AttributeError(key)
|
|
144
|
+
|
|
145
|
+
def __setattr__(self, key, value):
|
|
146
|
+
"""Sets an attribute or creates a new dataset."""
|
|
147
|
+
if key.startswith('_'):
|
|
148
|
+
super().__setattr__(key, value)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
with h5py.File(self._filepath, 'a') as f:
|
|
152
|
+
full_path = f"{self._h5path}/{key}".lstrip('/')
|
|
153
|
+
if (full_path in f) and (not self._overwrite):
|
|
154
|
+
raise AttributeError(f"Dataset or group '{key}' already exists.")
|
|
155
|
+
else:
|
|
156
|
+
f[self._h5path].create_dataset(key, data=value)
|
|
157
|
+
|
|
158
|
+
def __len__(self):
|
|
159
|
+
"""Gets the number of items in the current HDF5 group."""
|
|
160
|
+
return len(self.keys())
|
|
161
|
+
|
|
162
|
+
def __repr__(self):
|
|
163
|
+
"""Provides a string representation of the object."""
|
|
164
|
+
return f"<lazyh5 for file '{self._filepath}', HDF5 path '{self._h5path}' with {len(self)} items>"
|
|
165
|
+
|
|
166
|
+
def _ipython_key_completions_(self):
|
|
167
|
+
"""Enables key completions in IPython."""
|
|
168
|
+
return self.keys()
|
|
169
|
+
|
|
170
|
+
def __dir__(self):
|
|
171
|
+
"""Lists all accessible attributes and keys."""
|
|
172
|
+
return self.keys() + dir(super())
|
daio/video.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import av
|
|
5
|
+
|
|
6
|
+
warnings.simplefilter("once")
|
|
7
|
+
|
|
8
|
+
class VideoReader:
|
|
9
|
+
''' A class to read video files quickly, using PyAV. It allows for fast seeking and reading of frames.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
filename (str): path to the video file
|
|
13
|
+
format (str): format to read the video in. Default is 'gray'. Other options include 'rgb24', 'yuv420p', etc.
|
|
14
|
+
threading (bool): enable threading in the decoder. Default is True.
|
|
15
|
+
thread_count (int): number of threads to use for decoding. Default is 0 (auto).
|
|
16
|
+
tolerance (float): tolerance for frame time stamp deviations (fraction of frame interval). Default is 0.1.
|
|
17
|
+
'''
|
|
18
|
+
def __init__(self, filename, format='gray', threading=True, thread_count=0, tolerance=0.1):
|
|
19
|
+
self.container = av.open(filename)
|
|
20
|
+
self.stream = self.container.streams.video[0]
|
|
21
|
+
self.stream.codec_context.thread_count = thread_count
|
|
22
|
+
self.stream.codec_context.thread_type = 'AUTO' if threading else 'SLICE' # FRAME/AUTO/SLICE
|
|
23
|
+
self.framegenerator = self.container.decode(video=0)
|
|
24
|
+
self.read_format = format
|
|
25
|
+
self._pts_per_frame = 1 / (self.stream.guessed_rate * self.stream.time_base)
|
|
26
|
+
self._frame_to_pts = lambda n: round(n * self._pts_per_frame) + self.stream.start_time
|
|
27
|
+
self.wiggle_pts = tolerance * self._pts_per_frame
|
|
28
|
+
self.rewind()
|
|
29
|
+
if self.container.format.variable_fps:
|
|
30
|
+
warn_transcode(f'Variable frame rate video detected. Seeking will likely be unrealiable. I will warn again if I detect seek gitches')
|
|
31
|
+
if self.stream.average_rate != self.stream.guessed_rate:
|
|
32
|
+
warn_transcode(f'Average frame rate ({self.stream.average_rate}) is different from nominal frame rate ({self.stream.guessed_rate}). Seeking may be unrealiable. I will warn again if I detect seek gitches')
|
|
33
|
+
|
|
34
|
+
def read(self):
|
|
35
|
+
''' Read the next frame in the specified format. '''
|
|
36
|
+
frame_obj = next(self.framegenerator)
|
|
37
|
+
self.last_pts = frame_obj.pts
|
|
38
|
+
im = frame_obj.to_ndarray(format=self.read_format)
|
|
39
|
+
return im
|
|
40
|
+
|
|
41
|
+
def rewind(self):
|
|
42
|
+
''' Rewind the video to the beginning. '''
|
|
43
|
+
self.container.seek(0)
|
|
44
|
+
self.framegenerator = self.container.decode(video=0)
|
|
45
|
+
self.last_pts = None
|
|
46
|
+
|
|
47
|
+
def read_frame(self, frame_idx):
|
|
48
|
+
''' Read the specified frame index.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
frame_idx (int): index of the frame to read
|
|
52
|
+
'''
|
|
53
|
+
if frame_idx == 0:
|
|
54
|
+
self.rewind()
|
|
55
|
+
return self.read()
|
|
56
|
+
if self.last_pts is not None and (np.abs(self.last_pts - self._frame_to_pts(frame_idx-1)) <= self.wiggle_pts):
|
|
57
|
+
return self.read()
|
|
58
|
+
target_pts = self._frame_to_pts(frame_idx)
|
|
59
|
+
self.container.seek(target_pts-self.stream.start_time, backward=True, stream=self.container.streams.video[0])
|
|
60
|
+
self.framegenerator = self.container.decode(video=0)
|
|
61
|
+
frame_obj = next(self.framegenerator)
|
|
62
|
+
if frame_obj.pts > target_pts: #detecting overshoot (may happen due to variable frame rate)
|
|
63
|
+
n_back = 100
|
|
64
|
+
warn_transcode(f'Seek overshoot ({frame_obj.pts} > {target_pts}). Backtracking by {n_back} frames...')
|
|
65
|
+
self.container.seek(self._frame_to_pts(frame_idx-n_back)-self.stream.start_time, backward=True, stream=self.container.streams.video[0])
|
|
66
|
+
self.framegenerator = self.container.decode(video=0)
|
|
67
|
+
frame_obj = next(self.framegenerator)
|
|
68
|
+
while frame_obj.pts < (target_pts - self.wiggle_pts):
|
|
69
|
+
frame_obj = next(self.framegenerator)
|
|
70
|
+
# frame_obj.pts should now be equal to target_pts
|
|
71
|
+
if np.abs(frame_obj.pts - target_pts) > self.wiggle_pts:
|
|
72
|
+
warn_transcode(f'Seek problem with frame {frame_idx}! pts: {frame_obj.pts}; target: {target_pts}; dts: {frame_obj.dts}; pict_type: {str(frame_obj.pict_type)}')
|
|
73
|
+
frame = frame_obj.to_ndarray(format=self.read_format)
|
|
74
|
+
self.last_pts = frame_obj.pts
|
|
75
|
+
return frame
|
|
76
|
+
|
|
77
|
+
def close(self):
|
|
78
|
+
self.container.close()
|
|
79
|
+
|
|
80
|
+
def __del__(self):
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
def __getitem__(self, index):
|
|
84
|
+
if isinstance(index, (int, np.integer)): # single frame
|
|
85
|
+
return self.read_frame(index)
|
|
86
|
+
elif isinstance(index, slice):
|
|
87
|
+
frames = [self.__getitem__(i) for i in np.r_[index]]
|
|
88
|
+
return np.array(frames)
|
|
89
|
+
elif isinstance(index, tuple):
|
|
90
|
+
frames = [self.__getitem__(i)[index[1:]] for i in np.r_[index[0]]]
|
|
91
|
+
if len(frames) == 1:
|
|
92
|
+
return frames[0]
|
|
93
|
+
else:
|
|
94
|
+
return np.array(frames)
|
|
95
|
+
else:
|
|
96
|
+
raise NotImplementedError(f"slicing of {type(index)} : {index} not implemented yet")
|
|
97
|
+
|
|
98
|
+
def __enter__(self):
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
102
|
+
self.close()
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def __iter__(self):
|
|
106
|
+
self.rewind()
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def __next__(self):
|
|
110
|
+
try:
|
|
111
|
+
return self.read()
|
|
112
|
+
except:
|
|
113
|
+
self.rewind()
|
|
114
|
+
raise StopIteration
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def frame_shape(self):
|
|
118
|
+
''' Return the shape of the video frames. '''
|
|
119
|
+
return self.container.streams.video[0].codec_context.height, self.container.streams.video[0].codec_context.width
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def nframes(self):
|
|
123
|
+
''' Return the number of frames in the video. '''
|
|
124
|
+
return self.container.streams.video[0].frames
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def dtype(self):
|
|
128
|
+
return np.uint8
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def shape(self):
|
|
132
|
+
if self.read_format == 'gray':
|
|
133
|
+
return (self.nframes, *self.frame_shape)
|
|
134
|
+
elif self.read_format in ['rgb24', 'bgr24', 'yuv420p']:
|
|
135
|
+
return (self.nframes, *self.frame_shape, 3)
|
|
136
|
+
else:
|
|
137
|
+
raise NotImplementedError(f"shape not implemented for format {self.read_format}")
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def ndim(self):
|
|
141
|
+
return len(self.shape)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def size(self):
|
|
145
|
+
return np.product(self.shape)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def static_shape(filename):
|
|
149
|
+
''' Get the shape of a video (static method).
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
filename (str): path to the video file'''
|
|
153
|
+
with av.open(filename) as container:
|
|
154
|
+
stream = container.streams.video[0]
|
|
155
|
+
shape = np.array([stream.frames, stream.codec_context.height, stream.codec_context.width])
|
|
156
|
+
return shape
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def warn_transcode(msg):
|
|
160
|
+
warnings.warn(msg, stacklevel=2)
|
|
161
|
+
warnings.warn('Consider transcoding (ffmpeg -y -i "input.mp4" -c:v libx264 -pix_fmt yuv420p -preset superfast -crf 23 "output.mp4").', stacklevel=2)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class VideoWriter:
|
|
165
|
+
''' A class to write video files, using PyAV.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
filename (str): path to the output video file
|
|
169
|
+
codec (str): codec to use. Default is 'libx264'. Other options include 'h264', etc. Check VideoWriter.codecs_available for a list of available codecs.
|
|
170
|
+
fps (int): frames per second. Default is 25.
|
|
171
|
+
frame_shape (tuple): shape of the frames. Default is None (to be determined by the first frame written).
|
|
172
|
+
**kwargs: additional keyword arguments to pass to the codec. Check e.g. `ffmpeg -h encoder=libx264`
|
|
173
|
+
'''
|
|
174
|
+
|
|
175
|
+
def __init__(self, filename, codec='libx264', fps=25, frame_shape=None, pix_fmt='yuv420p', **kwargs):
|
|
176
|
+
self.container = av.open(filename, mode='w')
|
|
177
|
+
#define codec defaults:
|
|
178
|
+
if codec == 'h264':
|
|
179
|
+
kwargs = dict(bit_rate=1000000, pix_fmt='yuv420p').update(kwargs)
|
|
180
|
+
elif codec == 'libx264':
|
|
181
|
+
kwargs = dict(crf=23, preset='superfast', pix_fmt='yuv420p').update(kwargs)
|
|
182
|
+
#add video stream, if frame_shape is provided (otherwise create when the first frame is written)
|
|
183
|
+
if frame_shape is not None:
|
|
184
|
+
self.stream = self.container.add_stream(codec, rate=fps, height=frame_shape[0], width=frame_shape[1], pix_fmt=pix_fmt, **kwargs)
|
|
185
|
+
else:
|
|
186
|
+
self.stream = None
|
|
187
|
+
self.codec = codec
|
|
188
|
+
self.fps = fps
|
|
189
|
+
self.pix_fmt = pix_fmt
|
|
190
|
+
self.kwargs = kwargs if kwargs is not None else {}
|
|
191
|
+
|
|
192
|
+
def write(self, im):
|
|
193
|
+
""" Write a frame to the video file
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
im (np.ndarray): frame to write
|
|
197
|
+
"""
|
|
198
|
+
if im.ndim == 2:
|
|
199
|
+
format = 'gray'
|
|
200
|
+
elif im.ndim == 3 and im.shape[2] == 3:
|
|
201
|
+
format = 'rgb24'
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError(f"Unsupported frame shape: {im.shape}")
|
|
204
|
+
if self.stream is None:
|
|
205
|
+
self.stream = self.container.add_stream(self.codec, rate=self.fps, height=im.shape[0], width=im.shape[1], pix_fmt=self.pix_fmt, **self.kwargs)
|
|
206
|
+
out_frame = av.VideoFrame.from_ndarray(im, format=format)
|
|
207
|
+
for packet in self.stream.encode(out_frame):
|
|
208
|
+
self.container.mux(packet)
|
|
209
|
+
|
|
210
|
+
def write_frames(self, frames):
|
|
211
|
+
for frame in frames:
|
|
212
|
+
self.write(frame)
|
|
213
|
+
|
|
214
|
+
def write_audio(self, audio_chunk, audio_rate, pts=None, stream_args=None):
|
|
215
|
+
""" Write an audio chunk to the video file
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
audio_chunk (np.ndarray): audio chunk to write
|
|
219
|
+
audio_rate (int): audio sample rate
|
|
220
|
+
pts (int): presentation timestamp of the audio chunk. Default is None (auto-increment)
|
|
221
|
+
stream_args (dict): additional arguments to pass to the audio stream.
|
|
222
|
+
"""
|
|
223
|
+
if stream_args is None:
|
|
224
|
+
stream_args = dict(codec_name='aac', rate=44100)
|
|
225
|
+
if self.audio_stream is None:
|
|
226
|
+
self.audio_stream = self.container.add_stream(**stream_args)
|
|
227
|
+
self.channel_layout = 'stereo' if (audio_chunk.ndim > 1 and audio_chunk.shape[0] == 2) else 'mono'
|
|
228
|
+
self.audio_pts = 0
|
|
229
|
+
audio_frame = av.AudioFrame.from_ndarray(audio_chunk, layout=self.channel_layout)
|
|
230
|
+
audio_frame.sample_rate = audio_rate
|
|
231
|
+
audio_frame.pts = self.audio_pts if pts is None else pts
|
|
232
|
+
self.audio_pts = audio_frame.pts + audio_chunk.shape[1]
|
|
233
|
+
for packet in self.audio_stream.encode(audio_frame):
|
|
234
|
+
self.container.mux(packet)
|
|
235
|
+
|
|
236
|
+
def close(self):
|
|
237
|
+
""" Close the video file. """
|
|
238
|
+
try:
|
|
239
|
+
for packet in self.stream.encode():
|
|
240
|
+
self.container.mux(packet)
|
|
241
|
+
except:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
for packet in self.audio_stream.encode():
|
|
246
|
+
self.container.mux(packet)
|
|
247
|
+
except:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
self.container.close()
|
|
251
|
+
|
|
252
|
+
def __del__(self):
|
|
253
|
+
self.close()
|
|
254
|
+
|
|
255
|
+
def __enter__(self):
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
259
|
+
self.close()
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def codecs_available(self):
|
|
264
|
+
''' Return a list of available codecs. '''
|
|
265
|
+
return sorted(av.codec.codecs_available)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright (c) jlab.berlin
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: daio
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: video and data IO tools for Python
|
|
5
|
+
Author: jlab.berlin
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/danionella/daio
|
|
8
|
+
Keywords: python,HDF5,data IO,video IO
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: tqdm
|
|
14
|
+
Requires-Dist: av
|
|
15
|
+
Requires-Dist: h5py
|
|
16
|
+
Requires-Dist: hdf5plugin
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
[](https://opensource.org/licenses/MIT)
|
|
20
|
+

|
|
21
|
+
[](https://pypi.org/project/daio/)
|
|
22
|
+
[](https://anaconda.org/danionella/daio)
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
# daio
|
|
26
|
+
Video and data IO tools for Python.
|
|
27
|
+
|
|
28
|
+
Links: [API documentation](http://danionella.github.io/my_package), [GitHub repository](https://github.com/danionella/my_package)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
- via conda or mamba: `conda install danionella::daio `
|
|
32
|
+
- if you prefer pip: `pip install daio`
|
|
33
|
+
- for development, clone this repository change to the directory containing `pyproject.toml`
|
|
34
|
+
- `conda env create -n env_name -f environment.yml`
|
|
35
|
+
- `conda activate env_name`
|
|
36
|
+
- `pip install -e .`
|
|
37
|
+
|
|
38
|
+
## Use
|
|
39
|
+
TODO.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
daio/h5.py,sha256=IjKugDGb6QzHqmGwj_7QSvV1ZbVFf5zJqvxLVr3Rf0I,7305
|
|
2
|
+
daio/video.py,sha256=24_0q10WM5TEfu7em0d7C2XaxyAwmbzhQ8jz6BR1ZIo,10862
|
|
3
|
+
daio-0.0.1.dist-info/LICENSE,sha256=mUNAxIWH48FIpcbQFXkPZWqds0NLZZedQaVjySqcCak,1074
|
|
4
|
+
daio-0.0.1.dist-info/METADATA,sha256=CJODXFFrA-NddeJ9dNGbAEPsEE16-GapVgrtXQDW-lE,1452
|
|
5
|
+
daio-0.0.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
6
|
+
daio-0.0.1.dist-info/top_level.txt,sha256=SysaGdJINFYG21hBxF3d5zqIDJXhPHL0lc65lYd1N04,5
|
|
7
|
+
daio-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
daio
|