Skip to content

Commit b408812

Browse files
authored
Merge pull request #308 from clamsproject/307-ffmpeg-errors
307 ffmpeg errors
2 parents 969f393 + 1688804 commit b408812

File tree

2 files changed

+43
-20
lines changed

2 files changed

+43
-20
lines changed

mmif/utils/video_document_helper.py

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import importlib
2+
import sys
3+
24
import math
35
import warnings
6+
from io import StringIO
7+
from typing import Iterable # todo: replace with collections.abc.Iterable in Python 3.9
48
from typing import List, Union, Tuple
59

610
import mmif
711
from mmif import Annotation, Document, Mmif
812
from mmif.utils.timeunit_helper import convert
913
from mmif.vocabulary import DocumentTypes
1014

11-
for cv_dep in ('cv2', 'ffmpeg', 'PIL'):
15+
for cv_dep in ('cv2', 'ffmpeg', 'PIL', 'wurlitzer'):
1216
try:
1317
importlib.__import__(cv_dep)
1418
except ImportError as e:
@@ -63,41 +67,59 @@ def get_framerate(video_document: Document) -> float:
6367
if k in video_document:
6468
fps = round(video_document.get_property(k), 2)
6569
return fps
66-
capture(video_document)
67-
return video_document.get_property(FPS_DOCPROP_KEY)
70+
cap = capture(video_document)
71+
fps = video_document.get_property(FPS_DOCPROP_KEY)
72+
cap.release()
73+
return fps
6874

6975

70-
def extract_frames_as_images(video_document: Document, framenums: List[int], as_PIL: bool = False):
76+
def extract_frames_as_images(video_document: Document, framenums: Iterable[int], as_PIL: bool = False, record_ffmpeg_errors: bool = False):
7177
"""
7278
Extracts frames from a video document as a list of :py:class:`numpy.ndarray`.
7379
Use with :py:func:`sample_frames` function to get the list of frame numbers first.
7480
7581
:param video_document: :py:class:`~mmif.serialize.annotation.Document` instance that holds a video document (``"@type": ".../VideoDocument/..."``)
76-
:param framenums: integers representing the frame numbers to extract
82+
:param framenums: iterable integers representing the frame numbers to extract
7783
:param as_PIL: return :py:class:`PIL.Image.Image` instead of :py:class:`~numpy.ndarray`
7884
:return: frames as a list of :py:class:`~numpy.ndarray` or :py:class:`~PIL.Image.Image`
7985
"""
80-
import cv2 # pytype: disable=import-error
86+
import cv2
8187
if as_PIL:
8288
from PIL import Image
8389
frames = []
8490
video = capture(video_document)
8591
cur_f = 0
8692
tot_fcount = video_document.get_property(FRAMECOUNT_DOCPROP_KEY)
87-
framenums_copy = framenums.copy()
88-
while True:
89-
if not framenums_copy or cur_f > tot_fcount:
90-
break
91-
ret, frame = video.read()
92-
if cur_f == framenums_copy[0]:
93-
if not ret:
94-
sec = convert(cur_f, 'f', 's', video_document.get_property(FPS_DOCPROP_KEY))
95-
warnings.warn(f'Frame #{cur_f} ({sec}s) could not be read from the video {video_document.id}.')
96-
cur_f += 1
97-
continue
98-
frames.append(Image.fromarray(frame[:, :, ::-1]) if as_PIL else frame)
99-
framenums_copy.pop(0)
100-
cur_f += 1
93+
# when the target frame is more than this frames away, fast-forward instead of reading frame by frame
94+
# this is sanity-checked with a small number of video samples
95+
# (frame-by-frame ndarrays are compared with fast-forwarded ndarrays)
96+
skip_threadhold = 1000
97+
framenumi = iter(framenums) # make sure that it's actually an iterator, in case a list is passed
98+
next_target_f = next(framenumi, None)
99+
from wurlitzer import pipes as cpipes
100+
ffmpeg_errs = StringIO()
101+
with cpipes(stderr=ffmpeg_errs, stdout=sys.stdout):
102+
while True:
103+
if next_target_f is None or cur_f > tot_fcount or next_target_f > tot_fcount:
104+
break
105+
if next_target_f - cur_f > skip_threadhold:
106+
while next_target_f - cur_f > skip_threadhold:
107+
cur_f += skip_threadhold
108+
else:
109+
video.set(cv2.CAP_PROP_POS_FRAMES, cur_f)
110+
ret, frame = video.read()
111+
if cur_f == next_target_f:
112+
if not ret:
113+
sec = convert(cur_f, 'f', 's', video_document.get_property(FPS_DOCPROP_KEY))
114+
warnings.warn(f'Frame #{cur_f} ({sec}s) could not be read from the video {video_document.id} @ {video_document.location} .')
115+
else:
116+
frames.append(Image.fromarray(frame[:, :, ::-1]) if as_PIL else frame)
117+
next_target_f = next(framenumi, None)
118+
cur_f += 1
119+
ffmpeg_err_str = ffmpeg_errs.getvalue()
120+
if ffmpeg_err_str and record_ffmpeg_errors:
121+
warnings.warn(f'FFmpeg output during extracting frames: {ffmpeg_err_str}')
122+
video.release()
101123
return frames
102124

103125

requirements.cv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pillow
22
opencv-python
33
ffmpeg-python
4+
wurlitzer

0 commit comments

Comments
 (0)