megadetector 5.0.20__py3-none-any.whl → 5.0.22__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 (41) hide show
  1. megadetector/data_management/cct_json_utils.py +143 -7
  2. megadetector/data_management/cct_to_md.py +12 -5
  3. megadetector/data_management/databases/integrity_check_json_db.py +83 -77
  4. megadetector/data_management/importers/osu-small-animals-to-json.py +4 -4
  5. megadetector/data_management/importers/raic_csv_to_md_results.py +416 -0
  6. megadetector/data_management/importers/zamba_results_to_md_results.py +1 -2
  7. megadetector/data_management/lila/create_lila_test_set.py +25 -11
  8. megadetector/data_management/lila/download_lila_subset.py +9 -2
  9. megadetector/data_management/lila/generate_lila_per_image_labels.py +3 -2
  10. megadetector/data_management/lila/test_lila_metadata_urls.py +5 -1
  11. megadetector/data_management/read_exif.py +10 -14
  12. megadetector/data_management/rename_images.py +1 -1
  13. megadetector/data_management/yolo_output_to_md_output.py +18 -5
  14. megadetector/detection/process_video.py +14 -3
  15. megadetector/detection/pytorch_detector.py +15 -3
  16. megadetector/detection/run_detector.py +4 -3
  17. megadetector/detection/run_inference_with_yolov5_val.py +121 -13
  18. megadetector/detection/video_utils.py +40 -17
  19. megadetector/postprocessing/classification_postprocessing.py +1 -1
  20. megadetector/postprocessing/combine_api_outputs.py +1 -1
  21. megadetector/postprocessing/compare_batch_results.py +931 -142
  22. megadetector/postprocessing/detector_calibration.py +565 -0
  23. megadetector/postprocessing/md_to_coco.py +85 -19
  24. megadetector/postprocessing/postprocess_batch_results.py +32 -21
  25. megadetector/postprocessing/validate_batch_results.py +174 -64
  26. megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -12
  27. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +1 -1
  28. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +3 -1
  29. megadetector/utils/ct_utils.py +64 -2
  30. megadetector/utils/md_tests.py +15 -13
  31. megadetector/utils/path_utils.py +153 -37
  32. megadetector/utils/process_utils.py +9 -3
  33. megadetector/utils/write_html_image_list.py +21 -6
  34. megadetector/visualization/visualization_utils.py +329 -102
  35. megadetector/visualization/visualize_db.py +104 -63
  36. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/LICENSE +0 -0
  37. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/METADATA +143 -142
  38. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/RECORD +40 -39
  39. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/WHEEL +1 -1
  40. {megadetector-5.0.20.dist-info → megadetector-5.0.22.dist-info}/top_level.txt +0 -0
  41. megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +0 -359
@@ -0,0 +1,565 @@
1
+ """
2
+
3
+ detector_calibration.py
4
+
5
+ Tools for comparing/calibrating confidence values from detectors, particularly different
6
+ versions of MegaDetector.
7
+
8
+ """
9
+
10
+ #%% Constants and imports
11
+
12
+ import random
13
+ import copy
14
+
15
+ from tqdm import tqdm
16
+ from enum import IntEnum
17
+ from collections import defaultdict
18
+
19
+ import numpy as np
20
+ import matplotlib
21
+ import matplotlib.pyplot as plt
22
+
23
+ from megadetector.postprocessing.validate_batch_results import \
24
+ validate_batch_results, ValidateBatchResultsOptions
25
+ from megadetector.utils.ct_utils import get_iou, max_none, is_iterable
26
+
27
+
28
+ #%% Classes
29
+
30
+ class CalibrationOptions:
31
+ """
32
+ Options controlling comparison/calibration behavior.
33
+ """
34
+
35
+ def __init__(self):
36
+
37
+ #: IoU threshold used for determining whether two detections are the same
38
+ #:
39
+ #: When multiple detections match, we will only use the highest-matching IoU.
40
+ self.iou_threshold = 0.6
41
+
42
+ #: Minimum confidence threshold to consider for calibration (should be lower than
43
+ #: the lowest value you would use in realistic situations)
44
+ self.confidence_threshold = 0.025
45
+
46
+ #: Should we populate the data_a and data_b fields in the return value?
47
+ self.return_data = False
48
+
49
+ #: Model name to use in printouts and plots for result set A
50
+ self.model_name_a = 'model_a'
51
+
52
+ #: Model name to use in printouts and plots for result set B
53
+ self.model_name_b = 'model_b'
54
+
55
+ #: Maximum number of samples to use for plotting or calibration per category,
56
+ #: or None to use all paired values. If separate_plots_by_category is False,
57
+ #: this is the overall number of points sampled.
58
+ self.max_samples_per_category = None
59
+
60
+ #: Should we make separate plots for each category? Mutually exclusive with
61
+ #: separate_plots_by_correctness.
62
+ self.separate_plots_by_category = True
63
+
64
+ #: Should we make separate plots for TPs/FPs? Mutually exclusive with
65
+ #: separate_plots_by_category.
66
+ self.separate_plots_by_correctness = False
67
+
68
+ #: List of category IDs to use for plotting comparisons, or None to plot
69
+ #: all categories.
70
+ self.categories_to_plot = None
71
+
72
+ #: Optionally map category ID to name in plot labels
73
+ self.category_id_to_name = None
74
+
75
+ #: Enable additional debug output
76
+ self.verbose = True
77
+
78
+ # ...class CalibrationOptions
79
+
80
+ class CalibrationMatchColumns(IntEnum):
81
+
82
+ COLUMN_CONF_A = 0
83
+ COLUMN_CONF_B = 1
84
+ COLUMN_IOU = 2
85
+ COLUMN_I_IMAGE = 3
86
+ COLUMN_CATEGORY_ID = 4
87
+ COLUMN_MATCHES_GT = 5
88
+
89
+ class CalibrationResults:
90
+ """
91
+ Results of a model-to-model comparison.
92
+ """
93
+
94
+ def __init__(self):
95
+
96
+ #: List of tuples: [conf_a, conf_b, iou, i_image, category_id, matches_gt]
97
+ #:
98
+ #: If ground truth is supplied, [matches_gt] is a bool indicating whether either
99
+ #: of the detected boxes matches a ground truth box of the same category. If
100
+ #: ground truth is not supplied, [matches_gt] is None.
101
+ self.calibration_matches = []
102
+
103
+ #: Populated with the data loaded from json_filename_a if options.return_data is True
104
+ self.data_a = None
105
+
106
+ #: Populated with the data loaded from json_filename_b if options.return_data is True
107
+ self.data_b = None
108
+
109
+ # ...class CalibrationResults
110
+
111
+
112
+ #%% Calibration functions
113
+
114
+ def compare_model_confidence_values(json_filename_a,json_filename_b,json_filename_gt=None,options=None):
115
+ """
116
+ Compare confidence values across two .json results files. Compares only detections that
117
+ can be matched by IoU, i.e., does not do anything with detections that only appear in one file.
118
+
119
+ Args:
120
+ json_filename_a (str or dict): filename containing results from the first model to be compared;
121
+ should refer to the same images as [json_filename_b]. Can also be a loaded results dict.
122
+ json_filename_b (str or dict): filename containing results from the second model to be compared;
123
+ should refer to the same images as [json_filename_a]. Can also be a loaded results dict.
124
+ json_filename_gt (str or dict, optional): filename containing ground truth; should refer to the
125
+ same images as [json_filename_a] and [json_filename_b]. Can also be a loaded results dict.
126
+ Should be in COCO format.
127
+ options (CalibrationOptions, optional): all the parameters used to control this process, see
128
+ CalibrationOptions for details
129
+
130
+ Returns:
131
+ CalibrationResults: description of the comparison results
132
+ """
133
+
134
+ ## Option handling
135
+
136
+ if options is None:
137
+ options = CalibrationOptions()
138
+
139
+ validation_options = ValidateBatchResultsOptions()
140
+ validation_options.return_data = True
141
+
142
+ if isinstance(json_filename_a,str):
143
+ results_a = validate_batch_results(json_filename_a,options=validation_options)
144
+ assert len(results_a['validation_results']['errors']) == 0
145
+ else:
146
+ assert isinstance(json_filename_a,dict)
147
+ results_a = json_filename_a
148
+
149
+ if isinstance(json_filename_b,str):
150
+ results_b = validate_batch_results(json_filename_b,options=validation_options)
151
+ assert len(results_b['validation_results']['errors']) == 0
152
+ else:
153
+ assert isinstance(json_filename_b,dict)
154
+ results_b = json_filename_b
155
+
156
+ # Load ground truth, if supplied
157
+ gt_data = None
158
+
159
+ if json_filename_gt is not None:
160
+ if isinstance(json_filename_gt,str):
161
+ gt_data = validate_batch_results(json_filename_gt,
162
+ options=validation_options)
163
+ else:
164
+ assert isinstance(json_filename_gt,dict)
165
+ gt_data = json_filename_gt
166
+
167
+ ## Make sure these results sets are comparable
168
+
169
+ image_filenames_a = [im['file'] for im in results_a['images']]
170
+ image_filenames_b = [im['file'] for im in results_b['images']]
171
+
172
+ assert set(image_filenames_a) == set(image_filenames_b), \
173
+ 'Cannot calibrate non-matching image sets'
174
+
175
+ categories_a = results_a['detection_categories']
176
+ categories_b = results_b['detection_categories']
177
+ assert set(categories_a.keys()) == set(categories_b.keys())
178
+ for k in categories_a.keys():
179
+ assert categories_a[k] == categories_b[k], 'Category mismatch'
180
+
181
+
182
+ ## Load ground truth if necessary
183
+
184
+ gt_category_name_to_id = None
185
+ gt_image_id_to_annotations = None
186
+ image_filename_to_gt_im = None
187
+
188
+ if gt_data is not None:
189
+
190
+ gt_category_name_to_id = {}
191
+ for c in gt_data['categories']:
192
+ gt_category_name_to_id[c['name']] = c['id']
193
+
194
+ image_filename_to_gt_im = {}
195
+ for im in gt_data['images']:
196
+ assert 'width' in im and 'height' in im, \
197
+ 'I can only compare against GT that has "width" and "height" fields'
198
+ image_filename_to_gt_im[im['file_name']] = im
199
+
200
+ assert set(image_filename_to_gt_im.keys()) == set(image_filenames_a), \
201
+ 'Ground truth filename list does not match image filename list'
202
+
203
+ gt_image_id_to_annotations = defaultdict(list)
204
+ for ann in gt_data['annotations']:
205
+ gt_image_id_to_annotations[ann['image_id']].append(ann)
206
+
207
+
208
+ ## Compare detections
209
+
210
+ image_filename_b_to_im = {}
211
+ for im in results_b['images']:
212
+ image_filename_b_to_im[im['file']] = im
213
+
214
+ n_detections_a = 0
215
+ n_detections_a_queried = 0
216
+ n_detections_a_matched = 0
217
+
218
+ calibration_matches = []
219
+
220
+ # For each image
221
+ # im_a = results_a['images'][0]
222
+ for i_image,im_a in tqdm(enumerate(results_a['images']),total=len(results_a['images'])):
223
+
224
+ fn = im_a['file']
225
+ im_b = image_filename_b_to_im[fn]
226
+
227
+ if 'detections' not in im_a or im_a['detections'] is None:
228
+ continue
229
+ if 'detections' not in im_b or im_b['detections'] is None:
230
+ continue
231
+
232
+ im_gt = None
233
+ if gt_data is not None:
234
+ im_gt = image_filename_to_gt_im[fn]
235
+
236
+ # For each detection in result set A...
237
+ #
238
+ # det_a = im_a['detections'][0]
239
+ for det_a in im_a['detections']:
240
+
241
+ n_detections_a += 1
242
+
243
+ conf_a = det_a['conf']
244
+ category_id = det_a['category']
245
+
246
+ # Is this above threshold?
247
+ if conf_a < options.confidence_threshold:
248
+ continue
249
+
250
+ n_detections_a_queried += 1
251
+
252
+ bbox_a = det_a['bbox']
253
+
254
+ best_iou = None
255
+ best_iou_conf = None
256
+ best_bbox_b = None
257
+
258
+ # For each detection in result set B...
259
+ #
260
+ # det_b = im_b['detections'][0]
261
+ for det_b in im_b['detections']:
262
+
263
+ # Is this the same category?
264
+ if det_b['category'] != category_id:
265
+ continue
266
+
267
+ conf_b = det_b['conf']
268
+
269
+ # Is this above threshold?
270
+ if conf_b < options.confidence_threshold:
271
+ continue
272
+
273
+ bbox_b = det_b['bbox']
274
+
275
+ iou = get_iou(bbox_a,bbox_b)
276
+
277
+ # Is this an adequate IoU to consider?
278
+ if iou < options.iou_threshold:
279
+ continue
280
+
281
+ # Is this the best match so far?
282
+ if best_iou is None or iou > best_iou:
283
+ best_iou = iou
284
+ best_iou_conf = conf_b
285
+ best_bbox_b = bbox_b
286
+
287
+ # ...for each detection in im_b
288
+
289
+ # If we found a match between A and B
290
+ if best_iou is not None:
291
+
292
+ n_detections_a_matched += 1
293
+
294
+ # Does this pair of matched detections also match a ground truth box?
295
+ matches_gt = None
296
+
297
+ if im_gt is not None:
298
+
299
+ def max_iou_between_detection_and_gt(detection_box,category_name,im_gt,gt_annotations):
300
+
301
+ max_iou = None
302
+
303
+ # Which category ID are we looking for?
304
+ gt_category_id_for_detected_category_name = \
305
+ gt_category_name_to_id[category_name]
306
+
307
+ # For each GT annotation
308
+ #
309
+ # ann = gt_annotations[0]
310
+ for ann in gt_annotations:
311
+
312
+ # Only match against boxes in the same category
313
+ if ann['category_id'] != gt_category_id_for_detected_category_name:
314
+ continue
315
+ if 'bbox' not in ann:
316
+ continue
317
+
318
+ # Normalize this box
319
+ #
320
+ # COCO format: [x,y,width,height]
321
+ # normalized format: [x_min, y_min, width_of_box, height_of_box]
322
+ normalized_gt_box = [ann['bbox'][0]/im_gt['width'],ann['bbox'][1]/im_gt['height'],
323
+ ann['bbox'][2]/im_gt['width'],ann['bbox'][3]/im_gt['height']]
324
+
325
+ iou = get_iou(detection_box, normalized_gt_box)
326
+ if max_iou is None or iou > max_iou:
327
+ max_iou = iou
328
+
329
+ # ...for each gt box
330
+
331
+ return max_iou
332
+
333
+ # ...def min_iou_between_detections_and_gt(...)
334
+
335
+ gt_annotations = gt_image_id_to_annotations[im_gt['id']]
336
+
337
+ # If they matched, the A and B boxes have the same category by definition
338
+ category_name = categories_a[det_a['category']]
339
+
340
+ max_iou_with_bbox_a = max_iou_between_detection_and_gt(bbox_a,category_name,im_gt,gt_annotations)
341
+ max_iou_with_bbox_b = max_iou_between_detection_and_gt(best_bbox_b,category_name,im_gt,gt_annotations)
342
+
343
+ max_iou_with_either_detection_set = max_none(max_iou_with_bbox_a,
344
+ max_iou_with_bbox_b)
345
+
346
+ matches_gt = False
347
+ if (max_iou_with_either_detection_set is not None) and \
348
+ (max_iou_with_either_detection_set >= options.iou_threshold):
349
+ matches_gt = True
350
+
351
+ # ...if we have ground truth
352
+
353
+ conf_result = [conf_a,best_iou_conf,best_iou,i_image,category_id,matches_gt]
354
+ calibration_matches.append(conf_result)
355
+
356
+ # ...if we had a match between A and B
357
+ # ...for each detection in im_a
358
+
359
+ # ...for each image in result set A
360
+
361
+ if options.verbose:
362
+
363
+ print('\nOf {} detections in result set A, queried {}, matched {}'.format(
364
+ n_detections_a,n_detections_a_queried,n_detections_a_matched))
365
+
366
+ if gt_data is not None:
367
+ n_matches = 0
368
+ for m in calibration_matches:
369
+ assert m[CalibrationMatchColumns.COLUMN_MATCHES_GT] is not None
370
+ if m[CalibrationMatchColumns.COLUMN_MATCHES_GT]:
371
+ n_matches += 1
372
+ print('{} matches also matched ground truth'.format(n_matches))
373
+
374
+ assert len(calibration_matches) == n_detections_a_matched
375
+
376
+ calibration_results = CalibrationResults()
377
+ calibration_results.calibration_matches = calibration_matches
378
+
379
+ if options.return_data:
380
+ calibration_results.data_a = results_a
381
+ calibration_results.data_b = results_b
382
+
383
+ return calibration_results
384
+
385
+ # ...def compare_model_confidence_values(...)
386
+
387
+
388
+ #%% Plotting functions
389
+
390
+ def plot_matched_confidence_values(calibration_results,output_filename,options=None):
391
+ """
392
+ Given a set of paired confidence values for matching detections (from
393
+ compare_model_confidence_values), plot histograms of those pairs for each
394
+ detection category.
395
+
396
+ Args:
397
+ calibration_results (CalibrationResults): output from a call to
398
+ compare_model_confidence_values, containing paired confidence
399
+ values for two sets of detection results.
400
+ output_filename (str): filename to write the plot (.png or .jpg)
401
+ options (CalibrationOptions, optional): plotting options, see
402
+ CalibrationOptions for details.
403
+ """
404
+
405
+ fig_w = 12
406
+ fig_h = 8
407
+ n_hist_bins = 80
408
+
409
+ if options is None:
410
+ options = CalibrationOptions()
411
+
412
+ assert not (options.separate_plots_by_category and \
413
+ options.separate_plots_by_correctness), \
414
+ 'separate_plots_by_category and separate_plots_by_correctness are mutually exclusive'
415
+
416
+ category_id_to_name = None
417
+ category_to_samples = None
418
+
419
+ calibration_matches = calibration_results.calibration_matches
420
+
421
+ # If we're just lumping everything into one plot
422
+ if (not options.separate_plots_by_category) and (not options.separate_plots_by_correctness):
423
+
424
+ category_id_to_name = {'0':'all_categories'}
425
+ category_to_samples = {'0': []}
426
+
427
+ # Make everything category "0" (arbitrary)
428
+ calibration_matches = copy.deepcopy(calibration_matches)
429
+ for m in calibration_matches:
430
+ m[CalibrationMatchColumns.COLUMN_CATEGORY_ID] = '0'
431
+ if (options.max_samples_per_category is not None) and \
432
+ (len(calibration_matches) > options.max_samples_per_category):
433
+ calibration_matches = \
434
+ random.sample(calibration_matches,options.max_samples_per_category)
435
+ category_to_samples['0'] = calibration_matches
436
+
437
+ # If we're separating into lines for FPs and TPs (but not separating by category)
438
+ elif options.separate_plots_by_correctness:
439
+
440
+ assert not options.separate_plots_by_category
441
+
442
+ category_id_tp = '0'
443
+ category_id_fp = '1'
444
+
445
+ category_id_to_name = {category_id_tp:'TP', category_id_fp:'FP'}
446
+ category_to_samples = {category_id_tp: [], category_id_fp: []}
447
+
448
+ for m in calibration_matches:
449
+ assert m[CalibrationMatchColumns.COLUMN_MATCHES_GT] is not None, \
450
+ "Can't plot by correctness when GT status is not available for every match"
451
+ if m[CalibrationMatchColumns.COLUMN_MATCHES_GT]:
452
+ category_to_samples[category_id_tp].append(m)
453
+ else:
454
+ category_to_samples[category_id_fp].append(m)
455
+
456
+ # If we're separating by category
457
+ else:
458
+
459
+ assert options.separate_plots_by_category
460
+
461
+ category_to_samples = defaultdict(list)
462
+
463
+ category_to_matches = defaultdict(list)
464
+ for m in calibration_matches:
465
+ category_id = m[CalibrationMatchColumns.COLUMN_CATEGORY_ID]
466
+ category_to_matches[category_id].append(m)
467
+
468
+ category_id_to_name = None
469
+ if options.category_id_to_name is not None:
470
+ category_id_to_name = options.category_id_to_name
471
+
472
+ for i_category,category_id in enumerate(category_to_matches.keys()):
473
+
474
+ matches_this_category = category_to_matches[category_id]
475
+
476
+ if (options.max_samples_per_category is None) or \
477
+ (len(matches_this_category) <= options.max_samples_per_category):
478
+ category_to_samples[category_id] = matches_this_category
479
+ else:
480
+ assert len(matches_this_category) > options.max_samples_per_category
481
+ category_to_samples[category_id] = random.sample(matches_this_category,options.max_samples_per_category)
482
+
483
+ del category_to_matches
484
+
485
+ del calibration_matches
486
+
487
+ if options.verbose:
488
+ n_samples_for_histogram = 0
489
+ for c in category_to_samples:
490
+ n_samples_for_histogram += len(category_to_samples[c])
491
+ print('Creating a histogram based on {} samples'.format(n_samples_for_histogram))
492
+
493
+ categories_to_plot = list(category_to_samples.keys())
494
+
495
+ if options.categories_to_plot is not None:
496
+ categories_to_plot = [category_id for category_id in categories_to_plot if\
497
+ category_id in options.categories_to_plot]
498
+
499
+ n_subplots = len(categories_to_plot)
500
+
501
+ plt.ioff()
502
+
503
+ fig = matplotlib.figure.Figure(figsize=(fig_w, fig_h), tight_layout=True)
504
+ # fig,axes = plt.subplots(nrows=n_subplots,ncols=1)
505
+
506
+ axes = fig.subplots(n_subplots, 1)
507
+
508
+ if not is_iterable(axes):
509
+ assert n_subplots == 1
510
+ axes = [axes]
511
+
512
+ # i_category = 0; category_id = categories_to_plot[i_category]
513
+ for i_category,category_id in enumerate(categories_to_plot):
514
+
515
+ ax = axes[i_category]
516
+
517
+ category_string = str(category_id)
518
+ if (category_id_to_name is not None) and (category_id in category_id_to_name):
519
+ category_string = category_id_to_name[category_id]
520
+
521
+ samples_this_category = category_to_samples[category_id]
522
+ x = [m[0] for m in samples_this_category]
523
+ y = [m[1] for m in samples_this_category]
524
+
525
+ weights_a = np.ones_like(x)/float(len(x))
526
+ weights_b = np.ones_like(y)/float(len(y))
527
+
528
+ # Plot the first lie a little thicker so the second line will always show up
529
+ ax.hist(x,histtype='step',bins=n_hist_bins,density=False,color='red',weights=weights_a,linewidth=3.0)
530
+ ax.hist(y,histtype='step',bins=n_hist_bins,density=False,color='blue',weights=weights_b,linewidth=1.5)
531
+
532
+ ax.legend([options.model_name_a,options.model_name_b])
533
+ ax.set_ylabel(category_string)
534
+ # plt.tight_layout()
535
+
536
+ # I experimented with heat maps, but they weren't very informative.
537
+ # Leaving this code here in case I revisit. Note to self: scatter plots
538
+ # were a disaster.
539
+ if False:
540
+ heatmap, xedges, yedges = np.histogram2d(x, y, bins=30)
541
+ extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]]
542
+ plt.imshow(heatmap.T, extent=extent, origin='lower', norm='log')
543
+
544
+ # ...for each category for which we need to generate a histogram
545
+
546
+ plt.close(fig)
547
+ fig.savefig(output_filename,dpi=100)
548
+
549
+ # ...def plot_matched_confidence_values(...)
550
+
551
+
552
+ #%% Interactive driver(s)
553
+
554
+ if False:
555
+
556
+ #%%
557
+
558
+ options = ValidateBatchResultsOptions()
559
+ # json_filename = r'g:\temp\format.json'
560
+ # json_filename = r'g:\temp\test-videos\video_results.json'
561
+ json_filename = r'g:\temp\test-videos\image_results.json'
562
+ options.check_image_existence = True
563
+ options.relative_path_base = r'g:\temp\test-videos'
564
+ validate_batch_results(json_filename,options)
565
+