megadetector 5.0.12__py3-none-any.whl → 5.0.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of megadetector might be problematic. Click here for more details.

Files changed (45) hide show
  1. megadetector/api/batch_processing/api_core/server.py +1 -1
  2. megadetector/api/batch_processing/api_core/server_api_config.py +0 -1
  3. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -3
  4. megadetector/api/batch_processing/api_core/server_utils.py +0 -4
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -1
  6. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -3
  7. megadetector/classification/efficientnet/utils.py +0 -3
  8. megadetector/data_management/camtrap_dp_to_coco.py +0 -2
  9. megadetector/data_management/cct_json_utils.py +15 -6
  10. megadetector/data_management/coco_to_labelme.py +12 -1
  11. megadetector/data_management/databases/integrity_check_json_db.py +43 -27
  12. megadetector/data_management/importers/cacophony-thermal-importer.py +1 -4
  13. megadetector/data_management/ocr_tools.py +0 -4
  14. megadetector/data_management/read_exif.py +178 -44
  15. megadetector/data_management/rename_images.py +187 -0
  16. megadetector/data_management/wi_download_csv_to_coco.py +3 -2
  17. megadetector/data_management/yolo_output_to_md_output.py +7 -2
  18. megadetector/detection/process_video.py +548 -244
  19. megadetector/detection/pytorch_detector.py +33 -14
  20. megadetector/detection/run_detector.py +17 -5
  21. megadetector/detection/run_detector_batch.py +179 -65
  22. megadetector/detection/run_inference_with_yolov5_val.py +527 -357
  23. megadetector/detection/tf_detector.py +14 -3
  24. megadetector/detection/video_utils.py +284 -61
  25. megadetector/postprocessing/categorize_detections_by_size.py +16 -14
  26. megadetector/postprocessing/classification_postprocessing.py +716 -0
  27. megadetector/postprocessing/compare_batch_results.py +101 -93
  28. megadetector/postprocessing/convert_output_format.py +12 -5
  29. megadetector/postprocessing/merge_detections.py +18 -7
  30. megadetector/postprocessing/postprocess_batch_results.py +133 -127
  31. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +236 -232
  32. megadetector/postprocessing/subset_json_detector_output.py +66 -62
  33. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +0 -2
  34. megadetector/utils/ct_utils.py +5 -4
  35. megadetector/utils/md_tests.py +380 -128
  36. megadetector/utils/path_utils.py +39 -6
  37. megadetector/utils/process_utils.py +13 -4
  38. megadetector/visualization/visualization_utils.py +7 -2
  39. megadetector/visualization/visualize_db.py +79 -77
  40. megadetector/visualization/visualize_detector_output.py +0 -1
  41. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/LICENSE +0 -0
  42. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/METADATA +2 -2
  43. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/RECORD +45 -43
  44. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/top_level.txt +0 -0
  45. {megadetector-5.0.12.dist-info → megadetector-5.0.14.dist-info}/WHEEL +0 -0
@@ -12,8 +12,6 @@ writing them to disk. The upside, though, is that this approach allows you to r
12
12
  detection elimination after running MegaDetector, and it allows allows more efficient re-use
13
13
  of frames if you end up running MD more than once, or running multiple versions of MD.
14
14
 
15
- TODO: optionally skip writing frames to disk, and process frames in memory.
16
-
17
15
  """
18
16
 
19
17
  #%% Imports
@@ -25,16 +23,18 @@ import argparse
25
23
  import itertools
26
24
  import json
27
25
  import shutil
26
+ import getpass
28
27
 
29
28
  from uuid import uuid1
30
29
 
31
30
  from megadetector.detection import run_detector_batch
32
31
  from megadetector.visualization import visualize_detector_output
33
32
  from megadetector.utils.ct_utils import args_to_object
34
- from megadetector.utils.path_utils import insert_before_extension
33
+ from megadetector.utils.path_utils import insert_before_extension, clean_path
35
34
  from megadetector.detection.video_utils import video_to_frames
36
35
  from megadetector.detection.video_utils import frames_to_video
37
36
  from megadetector.detection.video_utils import frame_results_to_video_results
37
+ from megadetector.detection.video_utils import _add_frame_numbers_to_results
38
38
  from megadetector.detection.video_utils import video_folder_to_frames
39
39
  from megadetector.detection.video_utils import default_fourcc
40
40
 
@@ -46,99 +46,259 @@ class ProcessVideoOptions:
46
46
  Options controlling the behavior of process_video()
47
47
  """
48
48
 
49
- #: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
50
- model_file = 'MDV5A'
51
-
52
- #: Video (of folder of videos) to process
53
- input_video_file = ''
54
-
55
- #: .json file to which we should write results
56
- output_json_file = None
57
-
58
- #: File to which we should write a video with boxes, only relevant if
59
- #: render_output_video is True
60
- output_video_file = None
61
-
62
- #: Folder to use for extracted frames; will use a folder in system temp space
63
- #: if this is None
64
- frame_folder = None
49
+ def __init__(self):
50
+
51
+ #: Can be a model filename (.pt or .pb) or a model name (e.g. "MDV5A")
52
+ #:
53
+ #: Use the string "no_detection" to indicate that you only want to extract frames,
54
+ #: not run a model. If you do this, you almost definitely want to set
55
+ #: keep_extracted_frames to "True", otherwise everything in this module is a no-op.
56
+ #: I.e., there's no reason to extract frames, do nothing with them, then delete them.
57
+ self.model_file = 'MDV5A'
58
+
59
+ #: Video (of folder of videos) to process
60
+ self.input_video_file = ''
65
61
 
66
- # Folder to use for rendered frames (if rendering output video); will use a folder
67
- #: in system temp space if this is None
68
- frame_rendering_folder = None
62
+ #: .json file to which we should write results
63
+ self.output_json_file = None
64
+
65
+ #: File to which we should write a video with boxes, only relevant if
66
+ #: render_output_video is True
67
+ self.output_video_file = None
68
+
69
+ #: Folder to use for extracted frames; will use a folder in system temp space
70
+ #: if this is None
71
+ self.frame_folder = None
72
+
73
+ #: Folder to use for rendered frames (if rendering output video); will use a folder
74
+ #: in system temp space if this is None
75
+ self.frame_rendering_folder = None
76
+
77
+ #: Should we render a video with detection boxes?
78
+ #:
79
+ #: Only supported when processing a single video, not a folder.
80
+ self.render_output_video = False
81
+
82
+ #: If we are rendering boxes to a new video, should we keep the temporary
83
+ #: rendered frames?
84
+ self.keep_rendered_frames = False
85
+
86
+ #: Should we keep the extracted frames?
87
+ self.keep_extracted_frames = False
88
+
89
+ #: Should we delete the entire folder the extracted frames are written to?
90
+ #:
91
+ #: By default, we delete the frame files but leave the (probably-empty) folder in place,
92
+ #: for no reason other than being paranoid about deleting folders.
93
+ self.force_extracted_frame_folder_deletion = False
94
+
95
+ #: Should we delete the entire folder the rendered frames are written to?
96
+ #:
97
+ #: By default, we delete the frame files but leave the (probably-empty) folder in place,
98
+ #: for no reason other than being paranoid about deleting folders.
99
+ self.force_rendered_frame_folder_deletion = False
100
+
101
+ #: If we've already run MegaDetector on this video or folder of videos, i.e. if we
102
+ #: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
103
+ self.reuse_results_if_available = False
104
+
105
+ #: If we've already split this video or folder of videos into frames, should we
106
+ #: we re-use those extracted frames? Defaults to reprocessing.
107
+ self.reuse_frames_if_available = False
108
+
109
+ #: If [input_video_file] is a folder, should we search for videos recursively?
110
+ self.recursive = False
111
+
112
+ #: Enable additional debug console output
113
+ self.verbose = False
114
+
115
+ #: fourcc code to use for writing videos; only relevant if render_output_video is True
116
+ self.fourcc = None
69
117
 
70
- #: Should we render a video with detection boxes?
71
- #:
72
- #: Only supported when processing a single video, not a folder.
73
- render_output_video = False
118
+ #: force a specific frame rate for output videos; only relevant if render_output_video
119
+ #: is True
120
+ self.rendering_fs = None
121
+
122
+ #: Confidence threshold to use for writing videos with boxes, only relevant if
123
+ #: if render_output_video is True. Defaults to choosing a reasonable threshold
124
+ #: based on the model version.
125
+ self.rendering_confidence_threshold = None
126
+
127
+ #: Detections below this threshold will not be included in the output file.
128
+ self.json_confidence_threshold = 0.005
129
+
130
+ #: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
131
+ #: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
132
+ #: typical value. Mutually exclusive with [frames_to_extract].
133
+ self.frame_sample = None
134
+
135
+ #: Extract a specific set of frames (list of ints, or a single int). Mutually exclusive with
136
+ #: [frame_sample].
137
+ self.frames_to_extract = None
138
+
139
+ #: Number of workers to use for parallelization; set to <= 1 to disable parallelization
140
+ self.n_cores = 1
74
141
 
75
- #: If we are rendering boxes to a new video, should we keep the temporary
76
- #: rendered frames?
77
- keep_rendered_frames = False
142
+ #: For debugging only, stop processing after a certain number of frames.
143
+ self.debug_max_frames = -1
144
+
145
+ #: File containing non-standard categories, typically only used if you're running a non-MD
146
+ #: detector.
147
+ self.class_mapping_filename = None
148
+
149
+ #: JPEG quality for frame output, from 0-100. Defaults to the opencv default (typically 95)
150
+ self.quality = 90
151
+
152
+ #: Resize frames so they're at most this wide
153
+ self.max_width = None
154
+
155
+ #: Run the model at this image size (don't mess with this unless you know what you're
156
+ #: getting into)
157
+ self.image_size = None
158
+
159
+ #: Enable image augmentation
160
+ self.augment = False
78
161
 
79
- #: Should we keep the extracted frames?
80
- keep_extracted_frames = False
162
+ # ...class ProcessVideoOptions
163
+
164
+
165
+ #%% Functions
166
+
167
+ def _select_temporary_output_folders(options):
168
+ """
169
+ Choose folders in system temp space for writing temporary frames. Does not create folders,
170
+ just defines them.
171
+ """
81
172
 
82
- #: Should we delete the entire folder the extracted frames are written to?
83
- #:
84
- #: By default, we delete the frame files but leave the (probably-empty) folder in place,
85
- #: for no reason other than being paranoid about deleting folders.
86
- force_extracted_frame_folder_deletion = False
173
+ tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
87
174
 
88
- #: Should we delete the entire folder the rendered frames are written to?
89
- #:
90
- #: By default, we delete the frame files but leave the (probably-empty) folder in place,
91
- #: for no reason other than being paranoid about deleting folders.
92
- force_rendered_frame_folder_deletion = False
93
-
94
- #: If we've already run MegaDetector on this video or folder of videos, i.e. if we
95
- #: find a corresponding MD results file, should we re-use it? Defaults to reprocessing.
96
- reuse_results_if_available = False
175
+ # If we create a folder like "process_camera_trap_video" in the system temp dir, it may
176
+ # be the case that no one else can write to it, even to create user-specific subfolders.
177
+ # If we create a uuid-named folder in the system temp dir, we make a mess.
178
+ #
179
+ # Compromise with "process_camera_trap_video-[user]".
180
+ user_tempdir = tempdir + '-' + getpass.getuser()
97
181
 
98
- #: If we've already split this video or folder of videos into frames, should we
99
- #: we re-use those extracted frames? Defaults to reprocessing.
100
- reuse_frames_if_available = False
182
+ # I don't know whether it's possible for a username to contain characters that are
183
+ # not valid filename characters, but just to be sure...
184
+ user_tempdir = clean_path(user_tempdir)
101
185
 
102
- #: If [input_video_file] is a folder, should we search for videos recursively?
103
- recursive = False
186
+ frame_output_folder = os.path.join(
187
+ user_tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
104
188
 
105
- #: Enable additional debug console output
106
- verbose = False
189
+ rendering_output_folder = os.path.join(
190
+ tempdir, os.path.basename(options.input_video_file) + '_detections_' + str(uuid1()))
191
+
192
+ temporary_folder_info = \
193
+ {
194
+ 'temp_folder_base':user_tempdir,
195
+ 'frame_output_folder':frame_output_folder,
196
+ 'rendering_output_folder':rendering_output_folder
197
+ }
107
198
 
108
- #: fourcc code to use for writing videos; only relevant if render_output_video is True
109
- fourcc = None
199
+ return temporary_folder_info
110
200
 
111
- #: Confidence threshold to use for writing videos with boxes, only relevant if
112
- #: if render_output_video is True. Defaults to choosing a reasonable threshold
113
- #: based on the model version.
114
- rendering_confidence_threshold = None
115
-
116
- #: Detections below this threshold will not be included in the output file.
117
- json_confidence_threshold = 0.005
201
+ # ...def _create_frame_output_folders(...)
202
+
203
+
204
+ def _clean_up_rendered_frames(options,rendering_output_folder,detected_frame_files):
205
+ """
206
+ If necessary, delete rendered frames and/or the entire rendering output folder.
207
+ """
118
208
 
119
- #: Sample every Nth frame; set to None (default) or 1 to sample every frame. Typically
120
- #: we sample down to around 3 fps, so for typical 30 fps videos, frame_sample=10 is a
121
- #: typical value.
122
- frame_sample = None
209
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
123
210
 
124
- #: Number of workers to use for parallelization; set to <= 1 to disable parallelization
125
- n_cores = 1
211
+ # (Optionally) delete the temporary directory we used for rendered detection images
212
+ if not options.keep_rendered_frames:
213
+
214
+ try:
215
+
216
+ # If (a) we're supposed to delete the temporary rendering folder no
217
+ # matter where it is and (b) we created it in temp space, delete the
218
+ # whole tree
219
+ if options.force_rendered_frame_folder_deletion and \
220
+ (not caller_provided_rendering_output_folder):
221
+
222
+ if options.verbose:
223
+ print('Recursively deleting rendered frame folder {}'.format(
224
+ rendering_output_folder))
225
+
226
+ shutil.rmtree(rendering_output_folder)
227
+
228
+ # ...otherwise just delete the frames, but leave the folder in place
229
+ else:
230
+
231
+ if options.force_rendered_frame_folder_deletion:
232
+ assert caller_provided_rendering_output_folder
233
+ print('Warning: force_rendered_frame_folder_deletion supplied with a ' + \
234
+ 'user-provided folder, only removing frames')
235
+
236
+ for rendered_frame_fn in detected_frame_files:
237
+ os.remove(rendered_frame_fn)
238
+
239
+ except Exception as e:
240
+ print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
241
+ rendering_output_folder,str(e)))
242
+ pass
126
243
 
127
- #: For debugging only, stop processing after a certain number of frames.
128
- debug_max_frames = -1
129
-
130
- #: File containing non-standard categories, typically only used if you're running a non-MD
131
- #: detector.
132
- class_mapping_filename = None
244
+ elif options.force_rendered_frame_folder_deletion:
245
+
246
+ print('Warning: keep_rendered_frames and force_rendered_frame_folder_deletion both ' + \
247
+ 'specified, not deleting')
248
+
249
+ # ...def _clean_up_rendered_frames(...)
133
250
 
134
- # ...class ProcessVideoOptions
135
251
 
252
+ def _clean_up_extracted_frames(options,frame_output_folder,frame_filenames):
253
+ """
254
+ If necessary, delete extracted frames and/or the entire temporary frame folder.
255
+ """
256
+
257
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
258
+
259
+ if not options.keep_extracted_frames:
260
+
261
+ try:
262
+
263
+ # If (a) we're supposed to delete the temporary frame folder no
264
+ # matter where it is and (b) we created it in temp space, delete the
265
+ # whole tree.
266
+ if options.force_extracted_frame_folder_deletion and \
267
+ (not caller_provided_frame_output_folder):
268
+
269
+ if options.verbose:
270
+ print('Recursively deleting frame output folder {}'.format(frame_output_folder))
271
+
272
+ shutil.rmtree(frame_output_folder)
273
+
274
+ # ...otherwise just delete the frames, but leave the folder in place
275
+ else:
276
+
277
+ if options.force_extracted_frame_folder_deletion:
278
+ assert caller_provided_frame_output_folder
279
+ print('Warning: force_extracted_frame_folder_deletion supplied with a ' + \
280
+ 'user-provided folder, only removing frames')
281
+
282
+ for extracted_frame_fn in frame_filenames:
283
+ os.remove(extracted_frame_fn)
284
+
285
+ except Exception as e:
286
+ print('Warning: error removing extracted frames from folder {}:\n{}'.format(
287
+ frame_output_folder,str(e)))
288
+ pass
289
+
290
+ elif options.force_extracted_frame_folder_deletion:
291
+
292
+ print('Warning: keep_extracted_frames and force_extracted_frame_folder_deletion both ' + \
293
+ 'specified, not deleting')
294
+
295
+ # ...def _clean_up_extracted_frames
136
296
 
137
- #%% Functions
138
297
 
139
298
  def process_video(options):
140
299
  """
141
- Process a single video through MD, optionally writing a new video with boxes
300
+ Process a single video through MD, optionally writing a new video with boxes.
301
+ Can also be used just to split a video into frames, without running a model.
142
302
 
143
303
  Args:
144
304
  options (ProcessVideoOptions): all the parameters used to control this process,
@@ -154,40 +314,50 @@ def process_video(options):
154
314
  if options.render_output_video and (options.output_video_file is None):
155
315
  options.output_video_file = options.input_video_file + '.detections.mp4'
156
316
 
157
- tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
158
- os.makedirs(tempdir,exist_ok=True)
159
-
160
- # TODO:
161
- #
162
- # This is a lazy fix to an issue... if multiple users run this script, the
163
- # "process_camera_trap_video" folder is owned by the first person who creates it, and others
164
- # can't write to it. I could create uniquely-named folders, but I philosophically prefer
165
- # to put all the individual UUID-named folders within a larger folder, so as to be a
166
- # good tempdir citizen. So, the lazy fix is to make this world-writable.
167
- try:
168
- os.chmod(tempdir,0o777)
169
- except Exception:
170
- pass
317
+ if options.model_file == 'no_detection' and not options.keep_extracted_frames:
318
+ print('Warning: you asked for no detection, but did not specify keep_extracted_frames, this is a no-op')
319
+ return
171
320
 
172
- if options.frame_folder is not None:
321
+ # Track whether frame and rendering folders were created by this script
322
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
323
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
324
+
325
+ # This does not create any folders, just defines temporary folder names in
326
+ # case we need them.
327
+ temporary_folder_info = _select_temporary_output_folders(options)
328
+
329
+ if (caller_provided_frame_output_folder):
173
330
  frame_output_folder = options.frame_folder
174
331
  else:
175
- frame_output_folder = os.path.join(
176
- tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
177
-
178
- # TODO: keep track of whether we created this folder, delete if we're deleting the extracted
179
- # frames and we created the folder, and the output files aren't in the same folder. For now,
180
- # we're just deleting the extracted frames and leaving the empty folder around in this case.
332
+ frame_output_folder = temporary_folder_info['frame_output_folder']
333
+
181
334
  os.makedirs(frame_output_folder, exist_ok=True)
182
335
 
336
+
337
+ ## Extract frames
338
+
183
339
  frame_filenames, Fs = video_to_frames(
184
- options.input_video_file, frame_output_folder,
185
- every_n_frames=options.frame_sample, overwrite=(not options.reuse_frames_if_available))
340
+ options.input_video_file,
341
+ frame_output_folder,
342
+ every_n_frames=options.frame_sample,
343
+ overwrite=(not options.reuse_frames_if_available),
344
+ quality=options.quality,
345
+ max_width=options.max_width,
346
+ verbose=options.verbose,
347
+ frames_to_extract=options.frames_to_extract)
186
348
 
187
349
  image_file_names = frame_filenames
188
350
  if options.debug_max_frames > 0:
189
351
  image_file_names = image_file_names[0:options.debug_max_frames]
190
-
352
+
353
+ if options.model_file == 'no_detection':
354
+ assert options.keep_extracted_frames, \
355
+ 'Internal error: keep_extracted_frames not set, but no model specified'
356
+ return
357
+
358
+
359
+ ## Run MegaDetector
360
+
191
361
  if options.reuse_results_if_available and \
192
362
  os.path.isfile(options.output_json_file):
193
363
  print('Loading results from {}'.format(options.output_json_file))
@@ -195,12 +365,17 @@ def process_video(options):
195
365
  results = json.load(f)
196
366
  else:
197
367
  results = run_detector_batch.load_and_run_detector_batch(
198
- options.model_file, image_file_names,
368
+ options.model_file,
369
+ image_file_names,
199
370
  confidence_threshold=options.json_confidence_threshold,
200
371
  n_cores=options.n_cores,
201
- quiet=(not options.verbose),
202
- class_mapping_filename=options.class_mapping_filename)
203
-
372
+ class_mapping_filename=options.class_mapping_filename,
373
+ quiet=True,
374
+ augment=options.augment,
375
+ image_size=options.image_size)
376
+
377
+ _add_frame_numbers_to_results(results)
378
+
204
379
  run_detector_batch.write_results_to_file(
205
380
  results, options.output_json_file,
206
381
  relative_path_base=frame_output_folder,
@@ -213,15 +388,11 @@ def process_video(options):
213
388
  if options.render_output_video:
214
389
 
215
390
  # Render detections to images
216
- if options.frame_rendering_folder is not None:
391
+ if (caller_provided_rendering_output_folder):
217
392
  rendering_output_dir = options.frame_rendering_folder
218
393
  else:
219
- rendering_output_dir = os.path.join(
220
- tempdir, os.path.basename(options.input_video_file) + '_detections')
394
+ rendering_output_dir = temporary_folder_info['rendering_output_folder']
221
395
 
222
- # TODO: keep track of whether we created this folder, delete if we're deleting the rendered
223
- # frames and we created the folder, and the output files aren't in the same folder. For now,
224
- # we're just deleting the rendered frames and leaving the empty folder around in this case.
225
396
  os.makedirs(rendering_output_dir,exist_ok=True)
226
397
 
227
398
  detected_frame_files = visualize_detector_output.visualize_detector_output(
@@ -231,55 +402,43 @@ def process_video(options):
231
402
  confidence_threshold=options.rendering_confidence_threshold)
232
403
 
233
404
  # Combine into a video
234
- if options.frame_sample is None:
405
+ if options.rendering_fs is not None:
406
+ rendering_fs = options.rendering_fs
407
+ elif options.frame_sample is None:
235
408
  rendering_fs = Fs
236
409
  else:
410
+ # If the original video was 30fps and we sampled every 10th frame,
411
+ # render at 3fps
237
412
  rendering_fs = Fs / options.frame_sample
238
413
 
239
- print('Rendering video to {} at {} fps (original video {} fps)'.format(
240
- options.output_video_file,rendering_fs,Fs))
241
- frames_to_video(detected_frame_files, rendering_fs, options.output_video_file, codec_spec=options.fourcc)
414
+ print('Rendering {} frames to {} at {} fps (original video {} fps)'.format(
415
+ len(detected_frame_files), options.output_video_file,rendering_fs,Fs))
416
+ frames_to_video(detected_frame_files,
417
+ rendering_fs,
418
+ options.output_video_file,
419
+ codec_spec=options.fourcc)
242
420
 
243
- # Delete the temporary directory we used for detection images
244
- if not options.keep_rendered_frames:
245
- try:
246
- if options.force_rendered_frame_folder_deletion:
247
- shutil.rmtree(rendering_output_dir)
248
- else:
249
- for rendered_frame_fn in detected_frame_files:
250
- os.remove(rendered_frame_fn)
251
- except Exception as e:
252
- print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
253
- rendering_output_dir,str(e)))
254
- pass
421
+ # Possibly clean up rendered frames
422
+ _clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
255
423
 
256
424
  # ...if we're rendering video
257
425
 
258
426
 
259
427
  ## (Optionally) delete the extracted frames
428
+ _clean_up_extracted_frames(options, frame_output_folder, frame_filenames)
260
429
 
261
- if not options.keep_extracted_frames:
262
-
263
- try:
264
- if options.force_extracted_frame_folder_deletion:
265
- print('Recursively deleting frame output folder {}'.format(frame_output_folder))
266
- shutil.rmtree(frame_output_folder)
267
- else:
268
- for extracted_frame_fn in frame_filenames:
269
- os.remove(extracted_frame_fn)
270
- except Exception as e:
271
- print('Warning: error removing extracted frames from folder {}:\n{}'.format(
272
- frame_output_folder,str(e)))
273
- pass
274
-
275
- return results
276
-
277
430
  # ...process_video()
278
431
 
279
432
 
280
433
  def process_video_folder(options):
281
434
  """
282
- Process a folder of videos through MD
435
+ Process a folder of videos through MD. Can also be used just to split a folder of
436
+ videos into frames, without running a model.
437
+
438
+ When this function is used to run MD, two .json files will get written, one with
439
+ an entry for each *frame* (identical to what's created by process_video()), and
440
+ one with an entry for each *video* (which is more suitable for, e.g., reading into
441
+ Timelapse).
283
442
 
284
443
  Args:
285
444
  options (ProcessVideoOptions): all the parameters used to control this process,
@@ -291,43 +450,51 @@ def process_video_folder(options):
291
450
  assert os.path.isdir(options.input_video_file), \
292
451
  '{} is not a folder'.format(options.input_video_file)
293
452
 
294
- assert options.output_json_file is not None, \
295
- 'When processing a folder, you must specify an output .json file'
296
-
297
- assert options.output_json_file.endswith('.json')
298
- video_json = options.output_json_file
299
- frames_json = options.output_json_file.replace('.json','.frames.json')
300
- os.makedirs(os.path.dirname(video_json),exist_ok=True)
453
+ if options.model_file == 'no_detection' and not options.keep_extracted_frames:
454
+ print('Warning: you asked for no detection, but did not specify keep_extracted_frames, this is a no-op')
455
+ return
456
+
457
+ if options.model_file != 'no_detection':
458
+ assert options.output_json_file is not None, \
459
+ 'When processing a folder, you must specify an output .json file'
460
+ assert options.output_json_file.endswith('.json')
461
+ video_json = options.output_json_file
462
+ frames_json = options.output_json_file.replace('.json','.frames.json')
463
+ os.makedirs(os.path.dirname(video_json),exist_ok=True)
464
+
465
+ # Track whether frame and rendering folders were created by this script
466
+ caller_provided_frame_output_folder = (options.frame_folder is not None)
467
+ caller_provided_rendering_output_folder = (options.frame_rendering_folder is not None)
468
+
469
+ # This does not create any folders, just defines temporary folder names in
470
+ # case we need them.
471
+ temporary_folder_info = _select_temporary_output_folders(options)
301
472
 
302
473
 
303
474
  ## Split every video into frames
304
475
 
305
- if options.frame_folder is not None:
476
+ if caller_provided_frame_output_folder:
306
477
  frame_output_folder = options.frame_folder
307
478
  else:
308
- tempdir = os.path.join(tempfile.gettempdir(), 'process_camera_trap_video')
309
- os.makedirs(tempdir,exist_ok=True)
479
+ frame_output_folder = temporary_folder_info['frame_output_folder']
310
480
 
311
- # TODO: see above; this is a lazy fix to a permissions issue
312
- try:
313
- os.chmod(tempdir,0o777)
314
- except Exception:
315
- pass
316
-
317
- frame_output_folder = os.path.join(
318
- tempdir, os.path.basename(options.input_video_file) + '_frames_' + str(uuid1()))
319
-
320
481
  os.makedirs(frame_output_folder, exist_ok=True)
321
482
 
322
483
  print('Extracting frames')
484
+
323
485
  frame_filenames, Fs, video_filenames = \
324
486
  video_folder_to_frames(input_folder=options.input_video_file,
325
487
  output_folder_base=frame_output_folder,
326
488
  recursive=options.recursive,
327
489
  overwrite=(not options.reuse_frames_if_available),
328
- n_threads=options.n_cores,every_n_frames=options.frame_sample,
329
- verbose=options.verbose)
330
-
490
+ n_threads=options.n_cores,
491
+ every_n_frames=options.frame_sample,
492
+ verbose=options.verbose,
493
+ quality=options.quality,
494
+ max_width=options.max_width,
495
+ frames_to_extract=options.frames_to_extract)
496
+
497
+ print('Extracted frames for {} videos'.format(len(set(video_filenames))))
331
498
  image_file_names = list(itertools.chain.from_iterable(frame_filenames))
332
499
 
333
500
  if len(image_file_names) == 0:
@@ -340,23 +507,33 @@ def process_video_folder(options):
340
507
 
341
508
  if options.debug_max_frames is not None and options.debug_max_frames > 0:
342
509
  image_file_names = image_file_names[0:options.debug_max_frames]
510
+
511
+ if options.model_file == 'no_detection':
512
+ assert options.keep_extracted_frames, \
513
+ 'Internal error: keep_extracted_frames not set, but no model specified'
514
+ return
343
515
 
344
516
 
345
517
  ## Run MegaDetector on the extracted frames
346
518
 
347
519
  if options.reuse_results_if_available and \
348
520
  os.path.isfile(frames_json):
349
- print('Loading results from {}'.format(frames_json))
521
+ print('Bypassing inference, loading results from {}'.format(frames_json))
350
522
  results = None
351
523
  else:
352
524
  print('Running MegaDetector')
353
525
  results = run_detector_batch.load_and_run_detector_batch(
354
- options.model_file, image_file_names,
526
+ options.model_file,
527
+ image_file_names,
355
528
  confidence_threshold=options.json_confidence_threshold,
356
529
  n_cores=options.n_cores,
357
- quiet=(not options.verbose),
358
- class_mapping_filename=options.class_mapping_filename)
530
+ class_mapping_filename=options.class_mapping_filename,
531
+ quiet=True,
532
+ augment=options.augment,
533
+ image_size=options.image_size)
359
534
 
535
+ _add_frame_numbers_to_results(results)
536
+
360
537
  run_detector_batch.write_results_to_file(
361
538
  results, frames_json,
362
539
  relative_path_base=frame_output_folder,
@@ -373,26 +550,23 @@ def process_video_folder(options):
373
550
  ## (Optionally) render output videos
374
551
 
375
552
  if options.render_output_video:
376
-
553
+
377
554
  # Render detections to images
378
- if options.frame_rendering_folder is not None:
379
- frame_rendering_output_dir = options.frame_rendering_folder
555
+ if (caller_provided_rendering_output_folder):
556
+ rendering_output_dir = options.frame_rendering_folder
380
557
  else:
381
- frame_rendering_output_dir = os.path.join(
382
- tempdir, os.path.basename(options.input_video_file) + '_detections')
383
-
384
- # TODO: keep track of whether we created this folder, delete if we're deleting the rendered
385
- # frames and we created the folder, and the output files aren't in the same folder. For now,
386
- # we're just deleting the rendered frames and leaving the empty folder around in this case.
387
- os.makedirs(frame_rendering_output_dir,exist_ok=True)
558
+ rendering_output_dir = temporary_folder_info['rendering_output_folder']
559
+
560
+ os.makedirs(rendering_output_dir,exist_ok=True)
388
561
 
389
562
  detected_frame_files = visualize_detector_output.visualize_detector_output(
390
563
  detector_output_path=frames_json,
391
- out_dir=frame_rendering_output_dir,
564
+ out_dir=rendering_output_dir,
392
565
  images_dir=frame_output_folder,
393
566
  confidence_threshold=options.rendering_confidence_threshold,
394
567
  preserve_path_structure=True,
395
568
  output_image_width=-1)
569
+ detected_frame_files = [s.replace('\\','/') for s in detected_frame_files]
396
570
 
397
571
  # Choose an output folder
398
572
  output_folder_is_input_folder = False
@@ -418,13 +592,19 @@ def process_video_folder(options):
418
592
 
419
593
  video_fs = Fs[i_video]
420
594
 
421
- if options.frame_sample is None:
595
+ if options.rendering_fs is not None:
596
+ rendering_fs = options.rendering_fs
597
+ elif options.frame_sample is None:
422
598
  rendering_fs = video_fs
423
599
  else:
600
+ # If the original video was 30fps and we sampled every 10th frame,
601
+ # render at 3fps
424
602
  rendering_fs = video_fs / options.frame_sample
425
603
 
426
604
  input_video_file_relative = os.path.relpath(input_video_file_abs,options.input_video_file)
427
- video_frame_output_folder = os.path.join(frame_rendering_output_dir,input_video_file_relative)
605
+ video_frame_output_folder = os.path.join(rendering_output_dir,input_video_file_relative)
606
+
607
+ video_frame_output_folder = video_frame_output_folder.replace('\\','/')
428
608
  assert os.path.isdir(video_frame_output_folder), \
429
609
  'Could not find frame folder for video {}'.format(input_video_file_relative)
430
610
 
@@ -445,47 +625,37 @@ def process_video_folder(options):
445
625
  # Create the output video
446
626
  print('Rendering detections for video {} to {} at {} fps (original video {} fps)'.format(
447
627
  input_video_file_relative,video_output_file,rendering_fs,video_fs))
448
- frames_to_video(video_frame_files, rendering_fs, video_output_file, codec_spec=options.fourcc)
628
+ frames_to_video(video_frame_files,
629
+ rendering_fs,
630
+ video_output_file,
631
+ codec_spec=options.fourcc)
449
632
 
450
633
  # ...for each video
451
634
 
452
635
  # Possibly clean up rendered frames
453
- if not options.keep_rendered_frames:
454
- try:
455
- if options.force_rendered_frame_folder_deletion:
456
- shutil.rmtree(frame_rendering_output_dir)
457
- else:
458
- for rendered_frame_fn in detected_frame_files:
459
- os.remove(rendered_frame_fn)
460
- except Exception as e:
461
- print('Warning: error deleting rendered frames from folder {}:\n{}'.format(
462
- frame_rendering_output_dir,str(e)))
463
- pass
636
+ _clean_up_rendered_frames(options,rendering_output_dir,detected_frame_files)
464
637
 
465
638
  # ...if we're rendering video
466
639
 
467
640
 
468
641
  ## (Optionally) delete the extracted frames
469
-
470
- if not options.keep_extracted_frames:
471
- try:
472
- print('Deleting frame cache')
473
- if options.force_extracted_frame_folder_deletion:
474
- print('Recursively deleting frame output folder {}'.format(frame_output_folder))
475
- shutil.rmtree(frame_output_folder)
476
- else:
477
- for frame_fn in image_file_names:
478
- os.remove(frame_fn)
479
- except Exception as e:
480
- print('Warning: error deleting frames from folder {}:\n{}'.format(
481
- frame_output_folder,str(e)))
482
- pass
642
+ _clean_up_extracted_frames(options, frame_output_folder, image_file_names)
483
643
 
484
644
  # ...process_video_folder()
485
645
 
486
646
 
487
647
  def options_to_command(options):
648
+ """
649
+ Convert a ProcessVideoOptions obejct to a corresponding command line.
488
650
 
651
+ Args:
652
+ options (ProcessVideoOptions): the options set to render as a command line
653
+
654
+ Returns:
655
+ str: the command line coresponding to [options]
656
+
657
+ :meta private:
658
+ """
489
659
  cmd = 'python process_video.py'
490
660
  cmd += ' "' + options.model_file + '"'
491
661
  cmd += ' "' + options.input_video_file + '"'
@@ -518,12 +688,30 @@ def options_to_command(options):
518
688
  cmd += ' --n_cores ' + str(options.n_cores)
519
689
  if options.frame_sample is not None:
520
690
  cmd += ' --frame_sample ' + str(options.frame_sample)
691
+ if options.frames_to_extract is not None:
692
+ cmd += ' --frames_to_extract '
693
+ if isinstance(options.frames_to_extract,int):
694
+ frames_to_extract = [options.frames_to_extract]
695
+ else:
696
+ frames_to_extract = options.frames_to_extract
697
+ for frame_number in frames_to_extract:
698
+ cmd += ' {}'.format(frame_number)
521
699
  if options.debug_max_frames is not None:
522
700
  cmd += ' --debug_max_frames ' + str(options.debug_max_frames)
523
701
  if options.class_mapping_filename is not None:
524
702
  cmd += ' --class_mapping_filename ' + str(options.class_mapping_filename)
525
703
  if options.fourcc is not None:
526
704
  cmd += ' --fourcc ' + options.fourcc
705
+ if options.quality is not None:
706
+ cmd += ' --quality ' + str(options.quality)
707
+ if options.max_width is not None:
708
+ cmd += ' --max_width ' + str(options.max_width)
709
+ if options.verbose:
710
+ cmd += ' --verbose'
711
+ if options.force_extracted_frame_folder_deletion:
712
+ cmd += ' --force_extracted_frame_folder_deletion'
713
+ if options.force_rendered_frame_folder_deletion:
714
+ cmd += ' --force_rendered_frame_folder_deletion'
527
715
 
528
716
  return cmd
529
717
 
@@ -532,14 +720,19 @@ def options_to_command(options):
532
720
 
533
721
  if False:
534
722
 
723
+ pass
724
+
535
725
  #%% Process a folder of videos
536
726
 
537
727
  model_file = 'MDV5A'
538
- input_dir = r'c:\git\MegaDetector\test_images\test_images'
539
- frame_folder = r'g:\temp\video_test\frames'
540
- rendering_folder = r'g:\temp\video_test\rendered-frames'
541
- output_json_file = r'g:\temp\video_test\video-test.json'
542
- output_video_folder = r'g:\temp\video_test\output_videos'
728
+ # input_dir = r'g:\temp\test-videos'
729
+ input_dir = r'G:\temp\md-test-package\md-test-images\video-samples'
730
+ output_base = r'g:\temp\video_test'
731
+ frame_folder = os.path.join(output_base,'frames')
732
+ rendering_folder = os.path.join(output_base,'rendered-frames')
733
+ output_json_file = os.path.join(output_base,'video-test.json')
734
+ output_video_folder = os.path.join(output_base,'output_videos')
735
+
543
736
 
544
737
  print('Processing folder {}'.format(input_dir))
545
738
 
@@ -547,50 +740,125 @@ if False:
547
740
  options.model_file = model_file
548
741
  options.input_video_file = input_dir
549
742
  options.output_video_file = output_video_folder
550
- options.frame_folder = frame_folder
551
743
  options.output_json_file = output_json_file
552
- options.frame_rendering_folder = rendering_folder
553
- options.render_output_video = True
744
+ options.recursive = True
745
+ options.reuse_frames_if_available = False
746
+ options.reuse_results_if_available = False
747
+ options.quality = 90
748
+ options.frame_sample = 10
749
+ options.max_width = 1280
750
+ options.n_cores = 4
751
+ options.verbose = True
752
+ options.render_output_video = True
753
+ options.frame_folder = frame_folder
754
+ options.frame_rendering_folder = rendering_folder
554
755
  options.keep_extracted_frames = True
555
756
  options.keep_rendered_frames = True
556
- options.recursive = True
557
- options.reuse_frames_if_available = True
558
- options.reuse_results_if_available = True
559
- # options.confidence_threshold = 0.15
560
- # options.fourcc = 'mp4v'
757
+ options.force_extracted_frame_folder_deletion = False
758
+ options.force_rendered_frame_folder_deletion = False
759
+ options.fourcc = 'mp4v'
760
+ # options.rendering_confidence_threshold = 0.15
561
761
 
562
- cmd = options_to_command(options)
563
- print(cmd)
762
+ cmd = options_to_command(options); print(cmd)
763
+
564
764
  # import clipboard; clipboard.copy(cmd)
565
-
566
- if False:
567
- process_video_folder(options)
765
+ # process_video_folder(options)
568
766
 
569
767
 
570
768
  #%% Process a single video
571
769
 
572
- fn = os.path.expanduser('~/tmp/video-test/test-video.mp4')
770
+ fn = r'g:\temp\test-videos\person_and_dog\DSCF0056.AVI'
771
+ assert os.path.isfile(fn)
573
772
  model_file = 'MDV5A'
574
773
  input_video_file = fn
575
- frame_folder = os.path.expanduser('~/tmp/video-test/frames')
576
- rendering_folder = os.path.expanduser('~/tmp/video-test/rendered-frames')
774
+
775
+ output_base = r'g:\temp\video_test'
776
+ frame_folder = os.path.join(output_base,'frames')
777
+ rendering_folder = os.path.join(output_base,'rendered-frames')
778
+ output_json_file = os.path.join(output_base,'video-test.json')
779
+ output_video_file = os.path.join(output_base,'output_video.mp4')
577
780
 
578
781
  options = ProcessVideoOptions()
579
782
  options.model_file = model_file
580
783
  options.input_video_file = input_video_file
581
- options.frame_folder = frame_folder
582
- options.frame_rendering_folder = rendering_folder
583
784
  options.render_output_video = True
584
- options.output_video_file = os.path.expanduser('~/tmp/video-test/detections.mp4')
785
+ options.output_video_file = output_video_file
786
+ options.output_json_file = output_json_file
787
+ options.verbose = True
788
+ options.quality = 75
789
+ options.frame_sample = 10
790
+ options.max_width = 1600
791
+ options.frame_folder = frame_folder
792
+ options.frame_rendering_folder = rendering_folder
793
+ options.keep_extracted_frames = False
794
+ options.keep_rendered_frames = False
795
+ options.force_extracted_frame_folder_deletion = True
796
+ options.force_rendered_frame_folder_deletion = True
797
+ options.fourcc = 'mp4v'
798
+ # options.rendering_confidence_threshold = 0.15
799
+
800
+ cmd = options_to_command(options); print(cmd)
801
+
802
+ # import clipboard; clipboard.copy(cmd)
803
+ process_video(options)
804
+
805
+
806
+ #%% Extract specific frames from a single video, no detection
807
+
808
+ fn = r'g:\temp\test-videos\person_and_dog\DSCF0064.AVI'
809
+ assert os.path.isfile(fn)
810
+ model_file = 'no_detection'
811
+ input_video_file = fn
812
+
813
+ output_base = r'g:\temp\video_test'
814
+ frame_folder = os.path.join(output_base,'frames')
815
+ output_video_file = os.path.join(output_base,'output_videos.mp4')
816
+
817
+ options = ProcessVideoOptions()
818
+ options.model_file = model_file
819
+ options.input_video_file = input_video_file
820
+ options.verbose = True
821
+ options.quality = 90
822
+ options.frame_sample = None
823
+ options.frames_to_extract = [0,100]
824
+ options.max_width = None
825
+ options.frame_folder = frame_folder
826
+ options.keep_extracted_frames = True
827
+
828
+ cmd = options_to_command(options); print(cmd)
585
829
 
586
- cmd = options_to_command(options)
587
- print(cmd)
588
830
  # import clipboard; clipboard.copy(cmd)
831
+ process_video(options)
832
+
833
+
834
+ #%% Extract specific frames from a folder, no detection
835
+
836
+ fn = r'g:\temp\test-videos\person_and_dog'
837
+ assert os.path.isdir(fn)
838
+ model_file = 'no_detection'
839
+ input_video_file = fn
589
840
 
590
- if False:
591
- process_video(options)
592
-
841
+ output_base = r'g:\temp\video_test'
842
+ frame_folder = os.path.join(output_base,'frames')
843
+ output_video_file = os.path.join(output_base,'output_videos.mp4')
844
+
845
+ options = ProcessVideoOptions()
846
+ options.model_file = model_file
847
+ options.input_video_file = input_video_file
848
+ options.verbose = True
849
+ options.quality = 90
850
+ options.frame_sample = None
851
+ options.frames_to_extract = [0,100]
852
+ options.max_width = None
853
+ options.frame_folder = frame_folder
854
+ options.keep_extracted_frames = True
593
855
 
856
+ cmd = options_to_command(options); print(cmd)
857
+
858
+ # import clipboard; clipboard.copy(cmd)
859
+ process_video(options)
860
+
861
+
594
862
  #%% Command-line driver
595
863
 
596
864
  def main():
@@ -602,7 +870,8 @@ def main():
602
870
  'producing a new video with detections annotated'))
603
871
 
604
872
  parser.add_argument('model_file', type=str,
605
- help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A")')
873
+ help='MegaDetector model file (.pt or .pb) or model name (e.g. "MDV5A"), '\
874
+ 'or the string "no_detection" to run just frame extraction')
606
875
 
607
876
  parser.add_argument('input_video_file', type=str,
608
877
  help='video file (or folder) to process')
@@ -640,7 +909,8 @@ def main():
640
909
  help='enable video output rendering (not rendered by default)')
641
910
 
642
911
  parser.add_argument('--fourcc', default=default_fourcc,
643
- help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(default_fourcc))
912
+ help='fourcc code to use for video encoding (default {}), only used if render_output_video is True'.format(
913
+ default_fourcc))
644
914
 
645
915
  parser.add_argument('--keep_rendered_frames',
646
916
  action='store_true', help='Disable the deletion of rendered (w/boxes) frames')
@@ -658,24 +928,45 @@ def main():
658
928
  'whether other files were present in the folder.')
659
929
 
660
930
  parser.add_argument('--rendering_confidence_threshold', type=float,
661
- default=None, help="don't render boxes with confidence below this threshold (defaults to choosing based on the MD version)")
931
+ default=None,
932
+ help="don't render boxes with confidence below this threshold (defaults to choosing based on the MD version)")
933
+
934
+ parser.add_argument('--rendering_fs', type=float,
935
+ default=None,
936
+ help='force a specific frame rate for output videos (only relevant when using '\
937
+ '--render_output_video) (defaults to the original frame rate)')
662
938
 
663
939
  parser.add_argument('--json_confidence_threshold', type=float,
664
- default=0.0, help="don't include boxes in the .json file with confidence "\
940
+ default=default_options.json_confidence_threshold,
941
+ help="don't include boxes in the .json file with confidence "\
665
942
  'below this threshold (default {})'.format(
666
943
  default_options.json_confidence_threshold))
667
944
 
668
945
  parser.add_argument('--n_cores', type=int,
669
- default=1, help='number of cores to use for frame separation and detection. '\
946
+ default=default_options.n_cores,
947
+ help='Number of cores to use for frame separation and detection. '\
670
948
  'If using a GPU, this option will be respected for frame separation but '\
671
949
  'ignored for detection. Only relevant to frame separation when processing '\
672
- 'a folder.')
950
+ 'a folder. Default {}.'.format(default_options.n_cores))
673
951
 
674
952
  parser.add_argument('--frame_sample', type=int,
675
953
  default=None, help='process every Nth frame (defaults to every frame)')
676
954
 
955
+ parser.add_argument('--frames_to_extract', nargs='+', type=int,
956
+ default=None, help='extract specific frames (one or more ints)')
957
+
958
+ parser.add_argument('--quality', type=int,
959
+ default=default_options.quality,
960
+ help='JPEG quality for extracted frames (defaults to {})'.format(
961
+ default_options.quality))
962
+
963
+ parser.add_argument('--max_width', type=int,
964
+ default=default_options.max_width,
965
+ help='Resize frames larger than this before writing (defaults to {})'.format(
966
+ default_options.max_width))
967
+
677
968
  parser.add_argument('--debug_max_frames', type=int,
678
- default=-1, help='trim to N frames for debugging (impacts model execution, '\
969
+ default=-1, help='Trim to N frames for debugging (impacts model execution, '\
679
970
  'not frame rendering)')
680
971
 
681
972
  parser.add_argument('--class_mapping_filename',
@@ -685,6 +976,19 @@ def main():
685
976
  'the addition of "1" to all category IDs, so your class mapping should start '\
686
977
  'at zero.')
687
978
 
979
+ parser.add_argument('--verbose', action='store_true',
980
+ help='Enable additional debug output')
981
+
982
+ parser.add_argument('--image_size',
983
+ type=int,
984
+ default=None,
985
+ help=('Force image resizing to a specific integer size on the long '\
986
+ 'axis (not recommended to change this)'))
987
+
988
+ parser.add_argument('--augment',
989
+ action='store_true',
990
+ help='Enable image augmentation')
991
+
688
992
  if len(sys.argv[1:]) == 0:
689
993
  parser.print_help()
690
994
  parser.exit()