|
1 | 1 | import importlib
|
| 2 | +import sys |
| 3 | + |
2 | 4 | import math
|
3 | 5 | import warnings
|
| 6 | +from io import StringIO |
| 7 | +from typing import Iterable # todo: replace with collections.abc.Iterable in Python 3.9 |
4 | 8 | from typing import List, Union, Tuple
|
5 | 9 |
|
6 | 10 | import mmif
|
7 | 11 | from mmif import Annotation, Document, Mmif
|
8 | 12 | from mmif.utils.timeunit_helper import convert
|
9 | 13 | from mmif.vocabulary import DocumentTypes
|
10 | 14 |
|
11 |
| -for cv_dep in ('cv2', 'ffmpeg', 'PIL'): |
| 15 | +for cv_dep in ('cv2', 'ffmpeg', 'PIL', 'wurlitzer'): |
12 | 16 | try:
|
13 | 17 | importlib.__import__(cv_dep)
|
14 | 18 | except ImportError as e:
|
@@ -63,41 +67,59 @@ def get_framerate(video_document: Document) -> float:
|
63 | 67 | if k in video_document:
|
64 | 68 | fps = round(video_document.get_property(k), 2)
|
65 | 69 | 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 |
68 | 74 |
|
69 | 75 |
|
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): |
71 | 77 | """
|
72 | 78 | Extracts frames from a video document as a list of :py:class:`numpy.ndarray`.
|
73 | 79 | Use with :py:func:`sample_frames` function to get the list of frame numbers first.
|
74 | 80 |
|
75 | 81 | :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 |
77 | 83 | :param as_PIL: return :py:class:`PIL.Image.Image` instead of :py:class:`~numpy.ndarray`
|
78 | 84 | :return: frames as a list of :py:class:`~numpy.ndarray` or :py:class:`~PIL.Image.Image`
|
79 | 85 | """
|
80 |
| - import cv2 # pytype: disable=import-error |
| 86 | + import cv2 |
81 | 87 | if as_PIL:
|
82 | 88 | from PIL import Image
|
83 | 89 | frames = []
|
84 | 90 | video = capture(video_document)
|
85 | 91 | cur_f = 0
|
86 | 92 | 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() |
101 | 123 | return frames
|
102 | 124 |
|
103 | 125 |
|
|
0 commit comments