Coverage for ibllib/io/video.py: 97%
115 statements
« prev ^ index » next coverage.py v7.5.4, created at 2024-07-08 17:16 +0100
« prev ^ index » next coverage.py v7.5.4, created at 2024-07-08 17:16 +0100
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): 1vgu
30 url_vid = next(fr['data_url'] for fr in url_vid['file_records'] if fr['data_url']) 1u
31 self.url = url_vid 1vgu
32 self._par = params.get(silent=True) 1vgu
33 self.cap = cv2.VideoCapture(self._url) 1vgu
34 self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) 1vgu
36 @property
37 def _url(self):
38 username = self._par.HTTP_DATA_SERVER_LOGIN 1vgu
39 password = self._par.HTTP_DATA_SERVER_PWD 1vgu
40 return re.sub(r'(^https?://)', r'\1' + f'{username}:{password}@', self.url) 1vgu
42 def get_frame(self, frame_index):
43 self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index) 1u
44 return self.cap.read() 1u
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') 1A
55 cap = VideoStreamer(video_path).cap if is_url else cv2.VideoCapture(str(video_path)) 1A
56 # 0-based index of the frame to be decoded/captured next.
57 cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number) 1A
58 ret, frame_image = cap.read() 1A
59 cap.release() 1A
60 return frame_image 1A
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)) 1abfhdiekjlc
83 if is_cap: 1abfhdiekjlc
84 cap = vid
85 else:
86 is_url = isinstance(vid, str) and vid.startswith('http') 1abfhdiekjlc
87 cap = VideoStreamer(vid).cap if is_url else cv2.VideoCapture(str(vid)) 1abfhdiekjlc
88 assert cap.isOpened(), 'Failed to open video' 1abfhdiekjlc
90 frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 1abfhdiekjlc
91 frame_numbers = frame_numbers if frame_numbers is not None else range(frame_count) 1abfhdiekjlc
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 1abfhdiekjlc
96 if as_list: 1abfhdiekjlc
97 frame_images = [None] * len(frame_numbers) 1j
98 else:
99 ret, frame = cap.read() 1abfhdiekjlc
100 frame_images = np.zeros((len(frame_numbers), *func(frame[mask or ...]).shape), np.uint8) 1abhdiekjlc
101 for ii, i in enumerate(frame_numbers): 1abhdiekjlc
102 if not quiet: 1abhdiekjlc
103 sys.stdout.write(f'\rloading frame {ii}/{len(frame_numbers)}') 1abhdiekjlc
104 sys.stdout.flush() 1abhdiekjlc
105 if to_set[ii]: 1abhdiekjlc
106 cap.set(cv2.CAP_PROP_POS_FRAMES, i) 1abhdiekjlc
107 ret, frame = cap.read() 1abhdiekjlc
108 if ret: 1abhdiekjlc
109 frame_images[ii] = func(frame[mask or ...]) 1abhdiekjlc
110 else:
111 print(f'failed to read frame #{i}')
112 if not is_cap: 1abhdiekjlc
113 cap.release() 1abhdiekjlc
114 if not quiet: 1abhdiekjlc
115 sys.stdout.write('\x1b[2K\r') # Erase current line in stdout 1abhdiekjlc
116 return frame_images 1abhdiekjlc
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') 1abopqrfdensgc
128 cap = VideoStreamer(video_path).cap if is_url else cv2.VideoCapture(str(video_path)) 1abopqrfdensgc
129 assert cap.isOpened(), f'Failed to open video file {video_path}' 1abopqrfdensgc
131 # Get basic properties of video
132 meta = Bunch() 1abopqrfdensgc
133 meta.length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 1abopqrfdensgc
134 meta.fps = int(cap.get(cv2.CAP_PROP_FPS)) 1abopqrfdensgc
135 meta.width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) 1abopqrfdensgc
136 meta.height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) 1abopqrfdensgc
137 meta.duration = timedelta(seconds=meta.length / meta.fps) if meta.fps > 0 else 0 1abopqrfdensgc
138 if is_url and one: 1abopqrfdensgc
139 eid = one.path2eid(video_path) 1g
140 datasets = one.list_datasets(eid, details=True) 1g
141 label = label_from_path(video_path) 1g
142 record = datasets[datasets['rel_path'].str.contains(f'_iblrig_{label}Camera.raw')] 1g
143 assert len(record) == 1 1g
144 meta.size = record['file_size'].iloc[0] 1g
145 elif is_url and not one: 1abopqrfdensgc
146 meta.size = None
147 else:
148 meta.size = Path(video_path).stat().st_size 1abopqrfdensgc
149 cap.release() 1abopqrfdensgc
150 return meta 1abopqrfdensgc
153def url_from_eid(eid, label=None, one=None):
154 """Return the video URL(s) for a given eid
156 :param eid: The session id
157 :param label: The video label (e.g. 'body') or a tuple thereof
158 :param one: An instance of ONE
159 :return: The URL string if the label is a string, otherwise a dict of urls with labels as keys
160 """
161 valid_labels = VIDEO_LABELS 1mt
162 if not (label is None or np.isin(label, valid_labels).all()): 1mt
163 raise ValueError('labels must be one of ("%s")' % '", "'.join(valid_labels)) 1m
164 one = one or ONE() 1mt
165 session_path = one.eid2path(one.to_eid(eid)) 1mt
167 # Filter the video files
168 def match(dataset): 1mt
169 matched = re.match(r'(?:_iblrig_)([a-z]+)(?=Camera.raw.mp4$)', dataset.rsplit('/')[-1]) 1mt
170 return matched and matched.group(1) in (label or valid_labels) 1mt
172 if one.mode != 'remote': 1mt
173 datasets = one.list_datasets(eid, details=False) 1mt
174 datasets = [ds for ds in datasets if match(ds)] 1mt
175 urls = [one.path2url(session_path / ds) for ds in datasets] 1mt
176 else:
177 datasets = one.get_details(eid, full=True)['data_dataset_session_related'] 1m
178 urls = [ds['data_url'] for ds in datasets 1m
179 if ds['dataset_type'] == '_iblrig_Camera.raw' and match(ds['name'])]
181 # If one label specified, return the url, otherwise return a dict
182 if isinstance(label, str): 1mt
183 return urls[0] 1m
184 urls_dict = {label_from_path(url): url for url in urls} 1mt
185 return {**dict.fromkeys(label), **urls_dict} if label else urls_dict 1mt
188def label_from_path(video_name):
189 """
190 Return the video label, e.g.. 'left', 'right' or 'body'
191 :param video_name: A file path, URL or file name for the video
192 :return: The string label or None if the video doesn't match
193 """
194 result = re.search(r'(?<=_)([a-z]+)(?=Camera)', str(video_name)) 1aRmbSBwxfhTUVngc
195 return result.group() if result else None 1aRmbSBwxfhTUVngc
198def assert_valid_label(label):
199 """
200 Raises a value error is the provided label is not supported.
201 :param label: A video label to verify
202 :return: the label in lowercase
203 """
204 if not isinstance(label, str): 1aEyFGHDIbBwxfJhdKiLzCMNOPQc
205 try: 1ywxfzC
206 return tuple(map(assert_valid_label, label)) 1ywxfzC
207 except AttributeError: 1yz
208 raise ValueError('label must be string or iterable of strings')
209 if label.lower() not in VIDEO_LABELS: 1aEyFGHDIbBwxfJhdKiLzCMNOPQc
210 raise ValueError(f"camera must be one of ({', '.join(VIDEO_LABELS)})") 1yDz
211 return label.lower() 1aEyFGHDIbBwxfJhdKiLzCMNOPQc