Coverage for ibllib/io/video.py: 97%
115 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-17 15:25 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-17 15:25 +0000
1"""Functions for fetching video frames, meta data and file locations"""
2import sys
3import re
4from datetime import timedelta
5from pathlib import Path
7import cv2
8import numpy as np
10from iblutil.util import Bunch
11from one.api import ONE
12from one import params
14VIDEO_LABELS = ('left', 'right', 'body', 'belly')
17class VideoStreamer:
18 """
19 Provides a wrapper to stream a video from a password protected HTTP server using opencv
20 """
22 def __init__(self, url_vid):
23 """
24 TODO Allow auth as input
25 :param url_vid: full url of the video or dataset dictionary as output by alyx rest datasets
26 :returns cv2.VideoCapture object
27 """
28 # pop the data url from the dataset record if the input is a dictionary
29 if isinstance(url_vid, dict): 1rep
30 url_vid = next(fr['data_url'] for fr in url_vid['file_records'] if fr['data_url']) 1p
31 self.url = url_vid 1rep
32 self._par = params.get(silent=True) 1rep
33 self.cap = cv2.VideoCapture(self._url) 1rep
34 self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) 1rep
36 @property
37 def _url(self):
38 username = self._par.HTTP_DATA_SERVER_LOGIN 1rep
39 password = self._par.HTTP_DATA_SERVER_PWD 1rep
40 return re.sub(r'(^https?://)', r'\1' + f'{username}:{password}@', self.url) 1rep
42 def get_frame(self, frame_index):
43 self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index) 1p
44 return self.cap.read() 1p
47def get_video_frame(video_path, frame_number):
48 """
49 Obtain numpy array corresponding to a particular video frame in video_path
50 :param video_path: local path to mp4 file
51 :param frame_number: video frame to be returned
52 :return: numpy array corresponding to frame of interest. Dimensions are (w, h, 3)
53 """
54 is_url = isinstance(video_path, str) and video_path.startswith('http') 1t
55 cap = VideoStreamer(video_path).cap if is_url else cv2.VideoCapture(str(video_path)) 1t
56 # 0-based index of the frame to be decoded/captured next.
57 cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number) 1t
58 ret, frame_image = cap.read() 1t
59 cap.release() 1t
60 return frame_image 1t
63def get_video_frames_preload(vid, frame_numbers=None, mask=Ellipsis, as_list=False,
64 func=lambda x: x, quiet=False):
65 """
66 Obtain numpy array corresponding to a particular video frame in video.
67 Fetching and returning a list is about 33% faster but may be less memory controlled. NB: Any
68 gain in speed will be lost if subsequently converted to array.
69 :param vid: URL or local path to mp4 file or cv2.VideoCapture instance.
70 :param frame_numbers: video frames to be returned. If None, return all frames.
71 :param mask: a logical mask or slice to apply to frames
72 :param as_list: if true the frames are returned as a list, this is faster but may be less
73 memory efficient
74 :param func: Function to be applied to each frame. Applied after masking if applicable.
75 :param quiet: if true, suppress frame loading progress output.
76 :return: numpy array corresponding to frame of interest, or list if as_list is True.
77 Default dimensions are (n, w, h, 3) where n = len(frame_numbers)
79 Example - Load first 1000 frames, keeping only the first colour channel:
80 frames = get_video_frames_preload(vid, range(1000), mask=np.s_[:, :, 0])
81 """
82 is_cap = not isinstance(vid, (str, Path)) 1abfcgdihj
83 if is_cap: 1abfcgdihj
84 cap = vid
85 else:
86 is_url = isinstance(vid, str) and vid.startswith('http') 1abfcgdihj
87 cap = VideoStreamer(vid).cap if is_url else cv2.VideoCapture(str(vid)) 1abfcgdihj
88 assert cap.isOpened(), 'Failed to open video' 1abfcgdihj
90 frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 1abfcgdihj
91 frame_numbers = frame_numbers if frame_numbers is not None else range(frame_count) 1abfcgdihj
93 # Setting the index is extremely slow; determine where frame index must be set
94 # The first index is always explicitly set.
95 to_set = np.insert(np.diff(frame_numbers), 0, 0) != 1 1abfcgdihj
96 if as_list: 1abfcgdihj
97 frame_images = [None] * len(frame_numbers) 1h
98 else:
99 ret, frame = cap.read() 1abfcgdihj
100 frame_images = np.zeros((len(frame_numbers), *func(frame[mask or ...]).shape), np.uint8) 1abfcgdihj
101 for ii, i in enumerate(frame_numbers): 1abfcgdihj
102 if not quiet: 1abfcgdihj
103 sys.stdout.write(f'\rloading frame {ii}/{len(frame_numbers)}') 1abfcgdihj
104 sys.stdout.flush() 1abfcgdihj
105 if to_set[ii]: 1abfcgdihj
106 cap.set(cv2.CAP_PROP_POS_FRAMES, i) 1abfcgdihj
107 ret, frame = cap.read() 1abfcgdihj
108 if ret: 1abfcgdihj
109 frame_images[ii] = func(frame[mask or ...]) 1abfcgdihj
110 else:
111 print(f'failed to read frame #{i}')
112 if not is_cap: 1abfcgdihj
113 cap.release() 1abfcgdihj
114 if not quiet: 1abfcgdihj
115 sys.stdout.write('\x1b[2K\r') # Erase current line in stdout 1abfcgdihj
116 return frame_images 1abfcgdihj
119def get_video_meta(video_path, one=None):
120 """
121 Return a bunch of video information with the fields ('length', 'fps', 'width', 'height',
122 'duration', 'size')
123 :param video_path: A path to the video. May be a file path or URL.
124 :param one: An instance of ONE
125 :return: A Bunch of video mata data
126 """
127 is_url = isinstance(video_path, str) and video_path.startswith('http') 1ablmnocde
128 cap = VideoStreamer(video_path).cap if is_url else cv2.VideoCapture(str(video_path)) 1ablmnocde
129 assert cap.isOpened(), f'Failed to open video file {video_path}' 1ablmnocde
131 # Get basic properties of video
132 meta = Bunch() 1ablmnocde
133 meta.length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 1ablmnocde
134 meta.fps = int(cap.get(cv2.CAP_PROP_FPS)) 1ablmnocde
135 meta.width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) 1ablmnocde
136 meta.height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) 1ablmnocde
137 meta.duration = timedelta(seconds=meta.length / meta.fps) if meta.fps > 0 else 0 1ablmnocde
138 if is_url and one: 1ablmnocde
139 eid = one.path2eid(video_path) 1e
140 datasets = one.list_datasets(eid, details=True) 1e
141 label = label_from_path(video_path) 1e
142 record = datasets[datasets['rel_path'].str.contains(f'_iblrig_{label}Camera.raw')] 1e
143 assert len(record) == 1 1e
144 meta.size = record['file_size'].iloc[0] 1e
145 elif is_url and not one: 1ablmnocde
146 meta.size = None
147 else:
148 meta.size = Path(video_path).stat().st_size 1ablmnocde
149 cap.release() 1ablmnocde
150 return meta 1ablmnocde
153def url_from_eid(eid, label=None, one=None):
154 """Return the video URL(s) for a given eid.
156 Parameters
157 ----------
158 eid : UUID, str
159 The session ID.
160 label : str, tuple of str
161 The video label (e.g. 'body') or a tuple thereof.
162 one : one.api.One
163 An instance of ONE.
165 Returns
166 -------
167 str, dict of str
168 The URL string if the label is a string, otherwise a dict of urls with labels as keys.
170 Raises
171 ------
172 ValueError
173 Video label is unreckognized. See `VIDEO_LABELS` for valid labels.
174 """
175 valid_labels = VIDEO_LABELS 1kq
176 if not (label is None or np.isin(label, valid_labels).all()): 1kq
177 raise ValueError('labels must be one of ("%s")' % '", "'.join(valid_labels)) 1k
178 one = one or ONE() 1kq
179 session_path = one.eid2path(one.to_eid(eid)) 1kq
181 # Filter the video files
182 def match(dataset): 1kq
183 matched = re.match(r'(?:_iblrig_)([a-z]+)(?=Camera.raw.mp4$)', dataset.rsplit('/')[-1]) 1k
184 return matched and matched.group(1) in (label or valid_labels) 1k
186 if one.mode != 'remote': 1kq
187 datasets = one.list_datasets(eid, details=False) 1k
188 datasets = [ds for ds in datasets if match(ds)] 1k
189 urls = [one.path2url(session_path / ds) for ds in datasets] 1k
190 else:
191 datasets = one.get_details(eid, full=True)['data_dataset_session_related'] 1kq
192 urls = [ds['data_url'] for ds in datasets 1kq
193 if ds['dataset_type'] == '_iblrig_Camera.raw' and match(ds['name'])]
195 # If one label specified, return the url, otherwise return a dict
196 if isinstance(label, str): 1kq
197 return urls[0] 1k
198 urls_dict = {label_from_path(url): url for url in urls} 1kq
199 return {**dict.fromkeys(label), **urls_dict} if label else urls_dict 1kq
202def label_from_path(video_name):
203 """
204 Return the video label, e.g.. 'left', 'right' or 'body'
205 :param video_name: A file path, URL or file name for the video
206 :return: The string label or None if the video doesn't match
207 """
208 result = re.search(r'(?<=_)([a-z]+)(?=Camera)', str(video_name)) 1aJkbKufLe
209 return result.group() if result else None 1aJkbKufLe
212def assert_valid_label(label):
213 """
214 Raises a value error is the provided label is not supported.
215 :param label: A video label to verify
216 :return: the label in lowercase
217 """
218 if not isinstance(label, str): 1awsxyzvAbuBCfcDgEFGHI
219 try: 1s
220 return tuple(map(assert_valid_label, label)) 1s
221 except AttributeError: 1s
222 raise ValueError('label must be string or iterable of strings')
223 if label.lower() not in VIDEO_LABELS: 1awsxyzvAbuBCfcDgEFGHI
224 raise ValueError(f"camera must be one of ({', '.join(VIDEO_LABELS)})") 1sv
225 return label.lower() 1awsxyzvAbuBCfcDgEFGHI