Skip to content

Commit ad2c554

Browse files
authored
refactor: testing sample_video (#518)
* handle creation time not found * add tests * fix types * tests * remove gopro segments handling for now * less error handling in ffmpeg.py * improve error message
1 parent 778e66d commit ad2c554

File tree

4 files changed

+295
-104
lines changed

4 files changed

+295
-104
lines changed

mapillary_tools/ffmpeg.py

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@
44
import subprocess
55
import logging
66

7-
from . import exceptions
8-
97
LOG = logging.getLogger(__name__)
108
MAPILLARY_FFPROBE_PATH = os.getenv("MAPILLARY_FFPROBE_PATH", "ffprobe")
119
MAPILLARY_FFMPEG_PATH = os.getenv("MAPILLARY_FFMPEG_PATH", "ffmpeg")
1210
FRAME_EXT = ".jpg"
1311

1412

15-
def run_ffprobe_json(cmd: T.List[str]) -> T.Dict:
13+
class FFmpegNotFoundError(Exception):
14+
pass
15+
16+
17+
def _run_ffprobe_json(cmd: T.List[str]) -> T.Dict:
1618
full_cmd = [MAPILLARY_FFPROBE_PATH, "-print_format", "json", *cmd]
1719
LOG.info(f"Extracting video information: {' '.join(full_cmd)}")
1820
try:
1921
output = subprocess.check_output(full_cmd)
2022
except FileNotFoundError:
21-
raise exceptions.MapillaryFFmpegNotFoundError(
22-
f'The ffprobe command "{MAPILLARY_FFPROBE_PATH}" not found. Make sure it is installed in your $PATH or it is available in $MAPILLARY_FFPROBE_PATH. See https://github.com/mapillary/mapillary_tools#video-support for instructions'
23+
raise FFmpegNotFoundError(
24+
f'The ffprobe command "{MAPILLARY_FFPROBE_PATH}" is not found in your $PATH or $MAPILLARY_FFPROBE_PATH'
2325
)
2426
try:
2527
return json.loads(output)
@@ -29,34 +31,24 @@ def run_ffprobe_json(cmd: T.List[str]) -> T.Dict:
2931
)
3032

3133

32-
def run_ffmpeg(cmd: T.List[str]) -> None:
34+
def _run_ffmpeg(cmd: T.List[str]) -> None:
3335
full_cmd = [MAPILLARY_FFMPEG_PATH, *cmd]
3436
LOG.info(f"Extracting frames: {' '.join(full_cmd)}")
3537
try:
3638
subprocess.check_call(full_cmd)
3739
except FileNotFoundError:
38-
raise exceptions.MapillaryFFmpegNotFoundError(
39-
f'The ffmpeg command "{MAPILLARY_FFMPEG_PATH}" not found. Make sure it is installed in your $PATH or it is available in $MAPILLARY_FFMPEG_PATH. See https://github.com/mapillary/mapillary_tools#video-support for instructions'
40+
raise FFmpegNotFoundError(
41+
f'The ffmpeg command "{MAPILLARY_FFMPEG_PATH}" is not found in your $PATH or $MAPILLARY_FFMPEG_PATH'
4042
)
4143

4244

4345
def probe_video_format_and_streams(video_path: str) -> T.Dict:
44-
if not os.path.isfile(video_path):
45-
raise exceptions.MapillaryFileNotFoundError(
46-
f"Video file not found: {video_path}"
47-
)
48-
4946
cmd = ["-loglevel", "quiet", "-show_format", "-show_streams", video_path]
50-
return run_ffprobe_json(cmd)
47+
return _run_ffprobe_json(cmd)
5148

5249

53-
def probe_video_streams(video_path: str):
54-
if not os.path.isfile(video_path):
55-
raise exceptions.MapillaryFileNotFoundError(
56-
f"Video file not found: {video_path}"
57-
)
58-
59-
output = run_ffprobe_json(
50+
def probe_video_streams(video_path: str) -> T.List[T.Dict]:
51+
output = _run_ffprobe_json(
6052
[
6153
"-loglevel",
6254
"quiet",
@@ -70,9 +62,6 @@ def probe_video_streams(video_path: str):
7062

7163

7264
def extract_stream(source: str, dest: str, stream_id: int) -> None:
73-
if not os.path.isfile(source):
74-
raise exceptions.MapillaryFileNotFoundError(f"Video file not found: {source}")
75-
7665
cmd = [
7766
"-i",
7867
source,
@@ -90,19 +79,14 @@ def extract_stream(source: str, dest: str, stream_id: int) -> None:
9079
dest,
9180
]
9281

93-
run_ffmpeg(cmd)
82+
_run_ffmpeg(cmd)
9483

9584

9685
def extract_frames(
9786
video_path: str,
9887
sample_path: str,
99-
video_sample_interval: float = 2.0,
88+
video_sample_interval: float,
10089
) -> None:
101-
if not os.path.isfile(video_path):
102-
raise exceptions.MapillaryFileNotFoundError(
103-
f"Video file not found: {video_path}"
104-
)
105-
10690
video_basename_no_ext, ext = os.path.splitext(os.path.basename(video_path))
10791
frame_path_prefix = os.path.join(sample_path, video_basename_no_ext)
10892
cmd = [
@@ -117,7 +101,7 @@ def extract_frames(
117101
"-nostdin",
118102
f"{frame_path_prefix}_%06d{FRAME_EXT}",
119103
]
120-
run_ffmpeg(cmd)
104+
_run_ffmpeg(cmd)
121105

122106

123107
def extract_idx_from_frame_filename(

mapillary_tools/sample_video.py

Lines changed: 89 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -17,157 +17,174 @@ def sample_video(
1717
import_path: str,
1818
skip_subfolders=False,
1919
video_sample_interval=constants.VIDEO_SAMPLE_INTERVAL,
20-
video_start_time: T.Optional[str] = None,
2120
video_duration_ratio=constants.VIDEO_DURATION_RATIO,
21+
video_start_time: T.Optional[str] = None,
2222
skip_sample_errors: bool = False,
2323
rerun: bool = False,
2424
) -> None:
25-
if not os.path.exists(video_import_path):
26-
raise exceptions.MapillaryFileNotFoundError(
27-
f"Video file or directory not found: {video_import_path}"
28-
)
29-
3025
if os.path.isdir(video_import_path):
3126
video_list = utils.get_video_file_list(
3227
video_import_path, skip_subfolders, abs_path=True
3328
)
3429
video_dir = video_import_path
35-
else:
30+
LOG.debug(f"Found %d videos in %s", len(video_list), video_dir)
31+
elif os.path.isfile(video_import_path):
3632
video_list = [video_import_path]
3733
video_dir = os.path.dirname(video_import_path)
38-
39-
LOG.debug(f"Found {len(video_list)} videos in {video_import_path}")
34+
else:
35+
raise exceptions.MapillaryFileNotFoundError(
36+
f"Video file or directory not found: {video_import_path}"
37+
)
4038

4139
if rerun:
4240
for video_path in video_list:
4341
relpath = os.path.relpath(video_path, video_dir)
4442
video_sample_path = os.path.join(import_path, relpath)
45-
LOG.info(f"Removing the sample directory {video_sample_path}")
43+
LOG.info(f"Removing the sample directory %s", video_sample_path)
4644
if os.path.isdir(video_sample_path):
4745
shutil.rmtree(video_sample_path)
4846
elif os.path.isfile(video_sample_path):
4947
os.remove(video_sample_path)
5048

51-
for video_path in video_list:
52-
video_start_time_dt: T.Optional[datetime.datetime] = None
53-
if video_start_time is not None:
49+
video_start_time_dt: T.Optional[datetime.datetime] = None
50+
if video_start_time is not None:
51+
try:
5452
video_start_time_dt = types.map_capture_time_to_datetime(video_start_time)
53+
except ValueError as ex:
54+
raise exceptions.MapillaryBadParameterError(str(ex))
5555

56+
for video_path in video_list:
5657
relpath = os.path.relpath(video_path, video_dir)
5758
video_sample_path = os.path.join(import_path, relpath)
5859
if os.path.exists(video_sample_path):
5960
LOG.warning(
60-
f"Skip sampling video {os.path.basename(video_path)} as it has been sampled in {video_sample_path}"
61+
f"Skip sampling video %s as it has been sampled in %s",
62+
os.path.basename(video_path),
63+
video_sample_path,
6164
)
6265
continue
6366

64-
# extract frames in the temporary folder and then rename it
65-
now = datetime.datetime.utcnow()
66-
video_sample_path_temporary = os.path.join(
67-
os.path.dirname(video_sample_path),
68-
f"{os.path.basename(video_sample_path)}.{os.getpid()}.{int(now.timestamp())}",
69-
)
70-
os.makedirs(video_sample_path_temporary)
7167
try:
72-
ffmpeg.extract_frames(
68+
_sample_single_video(
7369
video_path,
74-
video_sample_path_temporary,
75-
video_sample_interval,
70+
video_sample_path,
71+
sample_interval=video_sample_interval,
72+
duration_ratio=video_duration_ratio,
73+
start_time=video_start_time_dt,
7674
)
77-
if video_start_time_dt is None:
78-
video_start_time_dt = extract_video_start_time(video_path)
79-
insert_video_frame_timestamp(
80-
os.path.basename(video_path),
81-
video_sample_path_temporary,
82-
video_start_time_dt,
83-
video_sample_interval,
84-
video_duration_ratio,
85-
)
86-
except (
87-
exceptions.MapillaryFileNotFoundError,
88-
exceptions.MapillaryFFmpegNotFoundError,
89-
):
90-
raise
75+
except ffmpeg.FFmpegNotFoundError as ex:
76+
# fatal errors
77+
raise exceptions.MapillaryFFmpegNotFoundError(str(ex)) from ex
9178
except Exception:
9279
if skip_sample_errors:
93-
LOG.warning(f"Skipping the error sampling {video_path}", exc_info=True)
94-
else:
95-
raise
96-
else:
97-
LOG.debug(f"Renaming {video_sample_path_temporary} to {video_sample_path}")
98-
try:
99-
os.rename(video_sample_path_temporary, video_sample_path)
100-
except IOError:
101-
# video_sample_path might have been created by another process during the sampling
10280
LOG.warning(
103-
f"Skip the error renaming {video_sample_path} to {video_sample_path}",
104-
exc_info=True,
81+
f"Skipping the error sampling %s", video_path, exc_info=True
10582
)
106-
finally:
107-
if os.path.isdir(video_sample_path_temporary):
108-
shutil.rmtree(video_sample_path_temporary)
83+
else:
84+
raise
10985

11086

111-
def extract_video_start_time(video_path: str) -> datetime.datetime:
87+
def _sample_single_video(
88+
video_path: str,
89+
sample_path: str,
90+
sample_interval: float,
91+
duration_ratio: float,
92+
start_time: T.Optional[datetime.datetime] = None,
93+
) -> None:
94+
# extract frames in the temporary folder and then rename it
95+
now = datetime.datetime.utcnow()
96+
tmp_sample_path = os.path.join(
97+
os.path.dirname(sample_path),
98+
f"{os.path.basename(sample_path)}.{os.getpid()}.{int(now.timestamp())}",
99+
)
100+
os.makedirs(tmp_sample_path)
101+
LOG.debug("Sampling in the temporary sample path %s", tmp_sample_path)
102+
103+
try:
104+
if start_time is None:
105+
duration, video_creation_time = extract_duration_and_creation_time(
106+
video_path
107+
)
108+
start_time = video_creation_time - datetime.timedelta(seconds=duration)
109+
ffmpeg.extract_frames(
110+
video_path,
111+
tmp_sample_path,
112+
sample_interval,
113+
)
114+
insert_video_frame_timestamp(
115+
os.path.basename(video_path),
116+
tmp_sample_path,
117+
start_time,
118+
sample_interval=sample_interval,
119+
duration_ratio=duration_ratio,
120+
)
121+
if os.path.isdir(sample_path):
122+
shutil.rmtree(sample_path)
123+
os.rename(tmp_sample_path, sample_path)
124+
finally:
125+
if os.path.isdir(tmp_sample_path):
126+
LOG.debug("Cleaning up the temporary sample path %s", tmp_sample_path)
127+
shutil.rmtree(tmp_sample_path)
128+
129+
130+
def extract_duration_and_creation_time(
131+
video_path: str,
132+
) -> T.Tuple[float, datetime.datetime]:
112133
streams = ffmpeg.probe_video_streams(video_path)
113134
if not streams:
114-
raise exceptions.MapillaryVideoError(
115-
f"Failed to find video streams in {video_path}"
116-
)
135+
raise exceptions.MapillaryVideoError(f"No video streams found in {video_path}")
117136

137+
# TODO: we should use the one with max resolution
118138
if 2 <= len(streams):
119139
LOG.warning(
120-
"Found more than one (%s) video streams -- will use the first stream",
140+
"Found %d video streams -- will use the first one",
121141
len(streams),
122142
)
123-
124143
stream = streams[0]
125144

126-
duration_str = stream["duration"]
145+
duration_str = stream.get("duration")
127146
try:
128-
duration = float(duration_str)
147+
# cast for type checking
148+
duration = float(T.cast(str, duration_str))
129149
except (TypeError, ValueError) as exc:
130150
raise exceptions.MapillaryVideoError(
131151
f"Failed to find video stream duration {duration_str} from video {video_path}"
132152
) from exc
133-
134153
LOG.debug("Extracted video duration: %s", duration)
135154

136155
time_string = stream.get("tags", {}).get("creation_time")
137156
if time_string is None:
138157
raise exceptions.MapillaryVideoError(
139158
f"Failed to find video creation_time in {video_path}"
140159
)
141-
142160
try:
143-
video_end_time = datetime.datetime.strptime(time_string, TIME_FORMAT)
161+
creation_time = datetime.datetime.strptime(time_string, TIME_FORMAT)
144162
except ValueError:
145163
try:
146-
video_end_time = datetime.datetime.strptime(time_string, TIME_FORMAT_2)
164+
creation_time = datetime.datetime.strptime(time_string, TIME_FORMAT_2)
147165
except ValueError:
148166
raise exceptions.MapillaryVideoError(
149167
f"Failed to parse {time_string} as {TIME_FORMAT} or {TIME_FORMAT_2}"
150168
)
169+
LOG.debug("Extracted video creation time: %s", creation_time)
151170

152-
LOG.debug("Extracted video end time (creation time): %s", video_end_time)
153-
154-
return video_end_time - datetime.timedelta(seconds=duration)
171+
return duration, creation_time
155172

156173

157174
def insert_video_frame_timestamp(
158175
video_basename: str,
159-
video_sampling_path: str,
176+
sample_path: str,
160177
start_time: datetime.datetime,
161-
sample_interval: float = constants.VIDEO_SAMPLE_INTERVAL,
162-
duration_ratio: float = constants.VIDEO_DURATION_RATIO,
178+
sample_interval: float,
179+
duration_ratio: float,
163180
) -> None:
164-
for image in utils.get_image_file_list(video_sampling_path, abs_path=True):
181+
for image in utils.get_image_file_list(sample_path, abs_path=True):
165182
idx = ffmpeg.extract_idx_from_frame_filename(
166183
video_basename,
167184
os.path.basename(image),
168185
)
169186
if idx is None:
170-
LOG.warning(f"Unabele to extract timestamp from the sample image {image}")
187+
LOG.warning(f"Unabele to find the sample index from %s", image)
171188
continue
172189

173190
seconds = idx * sample_interval * duration_ratio

0 commit comments

Comments
 (0)