megadetector 5.0.23__py3-none-any.whl → 5.0.25__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 (42) hide show
  1. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +2 -3
  2. megadetector/classification/merge_classification_detection_output.py +2 -2
  3. megadetector/data_management/coco_to_labelme.py +2 -1
  4. megadetector/data_management/databases/integrity_check_json_db.py +15 -14
  5. megadetector/data_management/databases/subset_json_db.py +49 -21
  6. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +73 -69
  7. megadetector/data_management/lila/add_locations_to_nacti.py +114 -110
  8. megadetector/data_management/mewc_to_md.py +340 -0
  9. megadetector/data_management/speciesnet_to_md.py +41 -0
  10. megadetector/data_management/yolo_output_to_md_output.py +15 -8
  11. megadetector/detection/process_video.py +24 -7
  12. megadetector/detection/pytorch_detector.py +841 -160
  13. megadetector/detection/run_detector.py +341 -146
  14. megadetector/detection/run_detector_batch.py +307 -70
  15. megadetector/detection/run_inference_with_yolov5_val.py +61 -4
  16. megadetector/detection/tf_detector.py +6 -1
  17. megadetector/postprocessing/{combine_api_outputs.py → combine_batch_outputs.py} +10 -13
  18. megadetector/postprocessing/compare_batch_results.py +236 -7
  19. megadetector/postprocessing/create_crop_folder.py +358 -0
  20. megadetector/postprocessing/md_to_labelme.py +7 -7
  21. megadetector/postprocessing/md_to_wi.py +40 -0
  22. megadetector/postprocessing/merge_detections.py +1 -1
  23. megadetector/postprocessing/postprocess_batch_results.py +12 -5
  24. megadetector/postprocessing/separate_detections_into_folders.py +32 -4
  25. megadetector/postprocessing/validate_batch_results.py +9 -4
  26. megadetector/utils/ct_utils.py +236 -45
  27. megadetector/utils/directory_listing.py +3 -3
  28. megadetector/utils/gpu_test.py +125 -0
  29. megadetector/utils/md_tests.py +455 -116
  30. megadetector/utils/path_utils.py +43 -2
  31. megadetector/utils/wi_utils.py +2691 -0
  32. megadetector/visualization/visualization_utils.py +95 -18
  33. megadetector/visualization/visualize_db.py +25 -7
  34. megadetector/visualization/visualize_detector_output.py +60 -13
  35. {megadetector-5.0.23.dist-info → megadetector-5.0.25.dist-info}/METADATA +11 -23
  36. {megadetector-5.0.23.dist-info → megadetector-5.0.25.dist-info}/RECORD +39 -36
  37. {megadetector-5.0.23.dist-info → megadetector-5.0.25.dist-info}/WHEEL +1 -1
  38. megadetector/detection/detector_training/__init__.py +0 -0
  39. megadetector/detection/detector_training/model_main_tf2.py +0 -114
  40. megadetector/utils/torch_test.py +0 -32
  41. {megadetector-5.0.23.dist-info → megadetector-5.0.25.dist-info}/LICENSE +0 -0
  42. {megadetector-5.0.23.dist-info → megadetector-5.0.25.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,340 @@
1
+ """
2
+
3
+ mewc_to_md.py
4
+
5
+ Converts the output of the MEWC inference scripts to the MD output format.
6
+
7
+ """
8
+
9
+ #%% Imports and constants
10
+
11
+ import os
12
+ import json
13
+ import pandas as pd
14
+
15
+ from copy import deepcopy
16
+ from collections import defaultdict
17
+ from megadetector.utils.ct_utils import sort_list_of_dicts_by_key, invert_dictionary # noqa
18
+ from megadetector.utils.path_utils import recursive_file_list
19
+
20
+ from megadetector.postprocessing.validate_batch_results import \
21
+ ValidateBatchResultsOptions, validate_batch_results
22
+
23
+ default_mewc_mount_prefix = '/images/'
24
+ default_mewc_category_name_column = 'class_id'
25
+
26
+
27
+ #%% Functions
28
+
29
+ def mewc_to_md(mewc_input_folder,
30
+ output_file=None,
31
+ mount_prefix=default_mewc_mount_prefix,
32
+ category_name_column=default_mewc_category_name_column,
33
+ mewc_out_filename='mewc_out.csv',
34
+ md_out_filename='md_out.json'):
35
+ """
36
+
37
+ Args:
38
+ mewc_input_folder (str): the folder we'll search for MEWC output files
39
+ output_file (str, optional): .json file to write with class information
40
+ mount_prefix (str, optional): string to remove from all filenames in the MD
41
+ .json file, typically the prefix used to mount the image folder.
42
+ category_name_column (str, optional): column in the MEWC results .csv to use for
43
+ category naming.
44
+
45
+ Returns:
46
+ dict: an MD-formatted dict, the same as what's written to [output_file]
47
+ """
48
+
49
+ ##%% Read input files
50
+
51
+ assert os.path.isdir(mewc_input_folder), \
52
+ 'Could not find folder {}'.format(mewc_input_folder)
53
+
54
+
55
+ ##%% Find MEWC output files
56
+
57
+ relative_path_to_mewc_info = {}
58
+
59
+ print('Listing files in folder {}'.format(mewc_input_folder))
60
+ all_files_relative = set(recursive_file_list(mewc_input_folder,return_relative_paths=True))
61
+
62
+ for fn_relative in all_files_relative:
63
+ if fn_relative.endswith(mewc_out_filename):
64
+ folder_relative = '/'.join(fn_relative.split('/')[:-1])
65
+ assert folder_relative not in relative_path_to_mewc_info
66
+ md_output_file_relative = os.path.join(folder_relative,md_out_filename).replace('\\','/')
67
+ assert md_output_file_relative in all_files_relative, \
68
+ 'Could not find MD output file {} to match to {}'.format(
69
+ md_output_file_relative,fn_relative)
70
+ relative_path_to_mewc_info[folder_relative] = \
71
+ {'mewc_predict_file':fn_relative,'md_file':md_output_file_relative}
72
+
73
+ del folder_relative
74
+
75
+ print('Found {} MEWC results files'.format(len(relative_path_to_mewc_info)))
76
+
77
+
78
+ ##%% Prepare to loop over results files
79
+
80
+ md_results_all = {}
81
+ md_results_all['images'] = []
82
+ md_results_all['detection_categories'] = {}
83
+ md_results_all['classification_categories'] = {}
84
+ md_results_all['info'] = None
85
+
86
+ classification_category_name_to_id = {}
87
+
88
+
89
+ ##%% Loop over results files
90
+
91
+ # relative_folder = next(iter(relative_path_to_mewc_info.keys()))
92
+ for relative_folder in relative_path_to_mewc_info:
93
+
94
+ ##%%
95
+
96
+ mewc_info = relative_path_to_mewc_info[relative_folder]
97
+ mewc_csv_fn_abs = os.path.join(mewc_input_folder,mewc_info['mewc_predict_file'])
98
+ mewc_md_fn_abs = os.path.join(mewc_input_folder,mewc_info['md_file'])
99
+
100
+ mewc_classification_info = pd.read_csv(mewc_csv_fn_abs)
101
+ mewc_classification_info = mewc_classification_info.to_dict('records')
102
+
103
+ assert os.path.isfile(mewc_md_fn_abs), \
104
+ 'Could not find file {}'.format(mewc_md_fn_abs)
105
+ with open(mewc_md_fn_abs,'r') as f:
106
+ md_results = json.load(f)
107
+
108
+
109
+ ##%% Remove the mount prefix from MD files if necessary
110
+ if mount_prefix is not None and len(mount_prefix) > 0:
111
+
112
+ n_files_without_mount_prefix = 0
113
+
114
+ # im = md_results['images'][0]
115
+ for im in md_results['images']:
116
+ if not im['file'].startswith(mount_prefix):
117
+ n_files_without_mount_prefix += 1
118
+ else:
119
+ im['file'] = im['file'].replace(mount_prefix,'',1)
120
+
121
+ if n_files_without_mount_prefix > 0:
122
+ print('Warning {} of {} files in the MD results did not include the mount prefix {}'.format(
123
+ n_files_without_mount_prefix,len(md_results['images']),mount_prefix))
124
+
125
+
126
+ ##%% Convert MEWC snip IDs to image files
127
+
128
+ # r = mewc_classification_info[0]
129
+ for r in mewc_classification_info:
130
+
131
+ # E.g. "IMG0-0.jpg"
132
+ snip_file = r['filename']
133
+
134
+ # E.g. "IMG0-0"
135
+ snip_file_no_ext = os.path.splitext(snip_file)[0]
136
+ ext = os.path.splitext(snip_file)[1] # noqa
137
+
138
+ tokens = snip_file_no_ext.split('-')
139
+
140
+ if len(tokens) == 1:
141
+ print('Warning: in folder {}, detection ID not found in snip filename {}, skipping'.format(
142
+ relative_folder,snip_file_no_ext))
143
+ r['image_filename_without_extension'] = snip_file_no_ext
144
+ r['snip_id'] = None
145
+
146
+ continue
147
+
148
+ filename_without_snip_id = '-'.join(tokens[0:-1])
149
+ snip_id = int(tokens[-1])
150
+ image_filename_without_extension = filename_without_snip_id
151
+
152
+ r['image_filename_without_extension'] = image_filename_without_extension
153
+ r['snip_id'] = snip_id
154
+
155
+ # ...for each MEWC result record
156
+
157
+
158
+ ##%% Make sure MD results and MEWC results refer to the same files
159
+
160
+ images_in_md_results_no_extension = \
161
+ set([os.path.splitext(im['file'])[0] for im in md_results['images']])
162
+ images_in_mewc_results_no_extension = set(r['image_filename_without_extension'] \
163
+ for r in mewc_classification_info)
164
+
165
+ # All files with classification results should also have detection results
166
+ for fn in images_in_mewc_results_no_extension:
167
+ assert fn in images_in_md_results_no_extension, \
168
+ 'Error: file {} is present in mewc-predict results, but not in MD results'.format(fn)
169
+
170
+ # This is just a note to self: no classification results are present for empty images
171
+ if False:
172
+ for fn in images_in_md_results_no_extension:
173
+ if fn not in images_in_mewc_results_no_extension:
174
+ print('Warning: file {}/{} is present in MD results, but not in mewc-predict results'.format(
175
+ relative_folder,fn))
176
+
177
+
178
+ ##%% Validate images
179
+
180
+ for im in md_results['images']:
181
+ fn_relative = im['file']
182
+ fn_abs = os.path.join(mewc_input_folder,relative_folder,fn_relative)
183
+ if not os.path.isfile(fn_abs):
184
+ print('Warning: image file {} does not exist'.format(fn_abs))
185
+
186
+
187
+ ##%% Map filenames to MEWC results
188
+
189
+ image_id_to_mewc_records = defaultdict(list)
190
+ for r in mewc_classification_info:
191
+ image_id_to_mewc_records[r['image_filename_without_extension']].append(r)
192
+
193
+
194
+ ##%% Add classification info to MD results
195
+
196
+ # im = md_results['images'][0]
197
+ for im in md_results['images']:
198
+
199
+ if ('detections' not in im) or (im['detections'] is None) or (len(im['detections']) == 0):
200
+ continue
201
+
202
+ detections = im['detections']
203
+
204
+ # *Don't* sort by confidence, it looks like snip IDs use the original sort order
205
+ # detections = sort_list_of_dicts_by_key(detections,'conf',reverse=True)
206
+
207
+ # This is just a debug assist, so I can run this cell more than once
208
+ for det in detections:
209
+ det['classifications'] = []
210
+
211
+ image_id = os.path.splitext(im['file'])[0]
212
+ mewc_records_this_image = image_id_to_mewc_records[image_id]
213
+
214
+ # r = mewc_records_this_image[0]
215
+ for r in mewc_records_this_image:
216
+
217
+ if r['snip_id'] is None:
218
+ continue
219
+
220
+ category_name = r[category_name_column]
221
+
222
+ # This is a *global* list of category mappings, across all mewc .csv files
223
+ if category_name not in classification_category_name_to_id:
224
+ category_id = str(len(classification_category_name_to_id))
225
+ classification_category_name_to_id[category_name] = category_id
226
+ else:
227
+ category_id = classification_category_name_to_id[category_name]
228
+
229
+ snip_id = r['snip_id']
230
+ if snip_id >= len(detections):
231
+ print('Warning: image {} has a classified snip ID of {}, but only {} detections are present'.format(
232
+ image_id,snip_id,len(detections)))
233
+ continue
234
+
235
+ det = detections[snip_id]
236
+
237
+ if 'classifications' not in det:
238
+ det['classifications'] = []
239
+ det['classifications'].append([category_id,r['prob']])
240
+
241
+ # ...for each classification in this image
242
+
243
+ # ...for each image
244
+
245
+ ##%% Map MD reults to the global level
246
+
247
+ if md_results_all['info'] is None:
248
+ md_results_all['info'] = md_results['info']
249
+
250
+ for category_id in md_results['detection_categories']:
251
+ if category_id not in md_results_all['detection_categories']:
252
+ md_results_all['detection_categories'][category_id] = \
253
+ md_results['detection_categories'][category_id]
254
+ else:
255
+ assert md_results_all['detection_categories'][category_id] == \
256
+ md_results['detection_categories'][category_id], \
257
+ 'MD results present with incompatible detection categories'
258
+
259
+ # im = md_results['images'][0]
260
+ for im in md_results['images']:
261
+ im_copy = deepcopy(im)
262
+ im_copy['file'] = os.path.join(relative_folder,im['file']).replace('\\','/')
263
+ md_results_all['images'].append(im_copy)
264
+
265
+ # ...for each folder that contains MEWC results
266
+
267
+ del md_results
268
+
269
+ ##%% Write output
270
+
271
+ md_results_all['classification_categories'] = invert_dictionary(classification_category_name_to_id)
272
+
273
+ if output_file is not None:
274
+ output_dir = os.path.dirname(output_file)
275
+ os.makedirs(output_dir,exist_ok=True)
276
+ with open(output_file,'w') as f:
277
+ json.dump(md_results_all,f,indent=1)
278
+
279
+ validation_options = ValidateBatchResultsOptions()
280
+ validation_options.check_image_existence = True
281
+ validation_options.relative_path_base = mewc_input_folder
282
+ validation_options.raise_errors = True
283
+ validation_results = validate_batch_results(output_file,validation_options) # noqa
284
+
285
+ # ...def mewc_to_md(...)
286
+
287
+
288
+ #%% Interactive driver
289
+
290
+ if False:
291
+
292
+ pass
293
+
294
+ #%%
295
+
296
+ mewc_input_folder = r'G:\temp\mewc-test'
297
+ mount_prefix = '/images/'
298
+ output_file = os.path.join(mewc_input_folder,'results_with_classes.json')
299
+
300
+ _ = mewc_to_md(mewc_input_folder=mewc_input_folder,
301
+ output_file=output_file,
302
+ mount_prefix=mount_prefix,
303
+ category_name_column='class_id')
304
+
305
+
306
+ #%% Command-line driver
307
+
308
+ import sys
309
+ import argparse
310
+
311
+ def main():
312
+
313
+ parser = argparse.ArgumentParser()
314
+
315
+ parser.add_argument(
316
+ 'input_folder',type=str,
317
+ help='Folder containing images and MEWC .json/.csv files')
318
+ parser.add_argument(
319
+ 'output_file',type=str,
320
+ help='.json file where output will be written')
321
+ parser.add_argument(
322
+ '--mount_prefix',type=str,default=default_mewc_mount_prefix,
323
+ help='prefix to remove from each filename in MEWC results, typically the Docker mount point')
324
+ parser.add_argument(
325
+ '--category_name_column',type=str,default=default_mewc_category_name_column,
326
+ help='column in the MEWC .csv file to use for category names')
327
+
328
+ if len(sys.argv[1:]) == 0:
329
+ parser.print_help()
330
+ parser.exit()
331
+
332
+ args = parser.parse_args()
333
+
334
+ _ = mewc_to_md(mewc_input_folder=args.input_folder,
335
+ output_file=args.output_file,
336
+ mount_prefix=args.mount_prefix,
337
+ category_name_column=args.category_name_column)
338
+
339
+ if __name__ == '__main__':
340
+ main()
@@ -0,0 +1,41 @@
1
+ """
2
+
3
+ wi_to_md.py
4
+
5
+ Converts the WI (SpeciesNet) predictions.json format to MD .json format. This is just a
6
+ command-line wrapper around utils.wi_utils.generate_md_results_from_predictions_json.
7
+
8
+ """
9
+
10
+ #%% Imports and constants
11
+
12
+ import sys
13
+ import argparse
14
+ from megadetector.utils.wi_utils import generate_md_results_from_predictions_json
15
+
16
+
17
+ #%% Command-line driver
18
+
19
+ def main():
20
+
21
+ parser = argparse.ArgumentParser()
22
+ parser.add_argument('predictions_json_file', action='store', type=str,
23
+ help='.json file to convert from SpeciesNet predictions.json format to MD format')
24
+ parser.add_argument('md_results_file', action='store', type=str,
25
+ help='output file to write in MD format')
26
+ parser.add_argument('--base_folder', action='store', type=str, default=None,
27
+ help='leading string to remove from each path in the predictions.json ' + \
28
+ 'file (to convert from absolute to relative paths)')
29
+
30
+ if len(sys.argv[1:]) == 0:
31
+ parser.print_help()
32
+ parser.exit()
33
+
34
+ args = parser.parse_args()
35
+
36
+ generate_md_results_from_predictions_json(args.predictions_json_file,
37
+ args.md_results_file,
38
+ args.base_folder)
39
+
40
+ if __name__ == '__main__':
41
+ main()
@@ -2,7 +2,7 @@
2
2
 
3
3
  yolo_output_to_md_output.py
4
4
 
5
- Converts the output of YOLOv5's detect.py or val.py to the MD API output format.
5
+ Converts the output of YOLOv5's detect.py or val.py to the MD output format.
6
6
 
7
7
  **Converting .txt files**
8
8
 
@@ -74,7 +74,7 @@ def read_classes_from_yolo_dataset_file(fn):
74
74
  with open(fn,'r') as f:
75
75
  lines = f.readlines()
76
76
 
77
- pat = '\d+:.+'
77
+ pat = r'\d+:.+'
78
78
  for s in lines:
79
79
  if re.search(pat,s) is not None:
80
80
  tokens = s.split(':')
@@ -281,7 +281,7 @@ def yolo_json_output_to_md_output(yolo_json_file,
281
281
  output_det['category'] = str(int(yolo_cat_id))
282
282
  conf = det['score']
283
283
  if truncate_to_standard_md_precision:
284
- conf = ct_utils.truncate_float(conf,CONF_DIGITS)
284
+ conf = ct_utils.round_float(conf,CONF_DIGITS)
285
285
  output_det['conf'] = conf
286
286
  input_bbox = det['bbox']
287
287
 
@@ -301,7 +301,7 @@ def yolo_json_output_to_md_output(yolo_json_file,
301
301
  box_width_relative,box_height_relative]
302
302
 
303
303
  if truncate_to_standard_md_precision:
304
- output_bbox = ct_utils.truncate_float_array(output_bbox,COORD_DIGITS)
304
+ output_bbox = ct_utils.round_float_array(output_bbox,COORD_DIGITS)
305
305
 
306
306
  output_det['bbox'] = output_bbox
307
307
  im['detections'].append(output_det)
@@ -332,7 +332,8 @@ def yolo_json_output_to_md_output(yolo_json_file,
332
332
  def yolo_txt_output_to_md_output(input_results_folder,
333
333
  image_folder,
334
334
  output_file,
335
- detector_tag=None):
335
+ detector_tag=None,
336
+ truncate_to_standard_md_precision=True):
336
337
  """
337
338
  Converts a folder of YOLO-output .txt files to MD .json format.
338
339
 
@@ -347,7 +348,9 @@ def yolo_txt_output_to_md_output(input_results_folder,
347
348
  output_file (str): the MD-formatted .json file to which we should write
348
349
  results
349
350
  detector_tag (str, optional): string to put in the 'detector' field in the
350
- output file
351
+ output file
352
+ truncate_to_standard_md_precision (bool, optional): set this to truncate to
353
+ COORD_DIGITS and CONF_DIGITS, like the standard MD pipeline does.
351
354
  """
352
355
 
353
356
  assert os.path.isdir(input_results_folder)
@@ -398,12 +401,16 @@ def yolo_txt_output_to_md_output(input_results_folder,
398
401
  api_box = ct_utils.convert_yolo_to_xywh([float(row[1]), float(row[2]),
399
402
  float(row[3]), float(row[4])])
400
403
 
401
- conf = ct_utils.truncate_float(float(row[5]), precision=4)
404
+ conf = float(row[5])
405
+
406
+ if truncate_to_standard_md_precision:
407
+ conf = ct_utils.round_float(conf, precision=CONF_DIGITS)
408
+ api_box = ct_utils.round_float_array(api_box, precision=COORD_DIGITS)
402
409
 
403
410
  detections.append({
404
411
  'category': str(category),
405
412
  'conf': conf,
406
- 'bbox': ct_utils.truncate_float_array(api_box, precision=4)
413
+ 'bbox': api_box
407
414
  })
408
415
 
409
416
  images_entries.append({
@@ -28,6 +28,7 @@ from uuid import uuid1
28
28
  from megadetector.detection import run_detector_batch
29
29
  from megadetector.visualization import visualize_detector_output
30
30
  from megadetector.utils.ct_utils import args_to_object
31
+ from megadetector.utils.ct_utils import dict_to_kvp_list, parse_kvp_list
31
32
  from megadetector.utils.path_utils import insert_before_extension, clean_path
32
33
  from megadetector.detection.video_utils import video_to_frames
33
34
  from megadetector.detection.video_utils import run_callback_on_frames
@@ -163,7 +164,7 @@ class ProcessVideoOptions:
163
164
  self.max_width = None
164
165
 
165
166
  #: Run the model at this image size (don't mess with this unless you know what you're
166
- #: getting into)
167
+ #: getting into)... if you just want to pass smaller frames to MD, use max_width
167
168
  self.image_size = None
168
169
 
169
170
  #: Enable image augmentation
@@ -178,6 +179,9 @@ class ProcessVideoOptions:
178
179
  #: frame result for each video (default), or every frame that was processed?
179
180
  self.include_all_processed_frames = False
180
181
 
182
+ #: Detector-specific options
183
+ self.detector_options = None
184
+
181
185
  # ...class ProcessVideoOptions
182
186
 
183
187
 
@@ -402,7 +406,7 @@ def process_video(options):
402
406
  print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
403
407
  'not; no raw frames will be written')
404
408
 
405
- detector = load_detector(options.model_file)
409
+ detector = load_detector(options.model_file,detector_options=options.detector_options)
406
410
 
407
411
  def frame_callback(image_np,image_id):
408
412
  return detector.generate_detections_one_image(image_np,
@@ -475,7 +479,8 @@ def process_video(options):
475
479
  class_mapping_filename=options.class_mapping_filename,
476
480
  quiet=True,
477
481
  augment=options.augment,
478
- image_size=options.image_size)
482
+ image_size=options.image_size,
483
+ detector_options=options.detector_options)
479
484
 
480
485
  results = _add_frame_numbers_to_results(results)
481
486
 
@@ -612,7 +617,7 @@ def process_video_folder(options):
612
617
  print('Warning: frame_folder specified, but keep_extracted_frames is ' + \
613
618
  'not; no raw frames will be written')
614
619
 
615
- detector = load_detector(options.model_file)
620
+ detector = load_detector(options.model_file,detector_options=options.detector_options)
616
621
 
617
622
  def frame_callback(image_np,image_id):
618
623
  return detector.generate_detections_one_image(image_np,
@@ -719,7 +724,8 @@ def process_video_folder(options):
719
724
  class_mapping_filename=options.class_mapping_filename,
720
725
  quiet=True,
721
726
  augment=options.augment,
722
- image_size=options.image_size)
727
+ image_size=options.image_size,
728
+ detector_options=options.detector_options)
723
729
 
724
730
  _add_frame_numbers_to_results(results)
725
731
 
@@ -910,6 +916,8 @@ def options_to_command(options):
910
916
  cmd += ' --force_extracted_frame_folder_deletion'
911
917
  if options.force_rendered_frame_folder_deletion:
912
918
  cmd += ' --force_rendered_frame_folder_deletion'
919
+ if options.detector_options is not None and len(options.detector_options) > 0:
920
+ cmd += '--detector_options {}'.format(dict_to_kvp_list(options.detector_options))
913
921
 
914
922
  return cmd
915
923
 
@@ -1209,14 +1217,23 @@ def main():
1209
1217
  parser.add_argument('--allow_empty_videos',
1210
1218
  action='store_true',
1211
1219
  help='By default, videos with no retrievable frames cause an error, this makes it a warning')
1212
-
1220
+
1221
+ parser.add_argument(
1222
+ '--detector_options',
1223
+ nargs='*',
1224
+ metavar='KEY=VALUE',
1225
+ default='',
1226
+ help='Detector-specific options, as a space-separated list of key-value pairs')
1227
+
1213
1228
  if len(sys.argv[1:]) == 0:
1214
1229
  parser.print_help()
1215
1230
  parser.exit()
1216
1231
 
1217
1232
  args = parser.parse_args()
1218
- options = ProcessVideoOptions()
1233
+ options = ProcessVideoOptions()
1219
1234
  args_to_object(args,options)
1235
+
1236
+ options.detector_options = parse_kvp_list(args.detector_options)
1220
1237
 
1221
1238
  if os.path.isdir(options.input_video_file):
1222
1239
  process_video_folder(options)