megadetector 5.0.15__py3-none-any.whl → 5.0.16__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.
- megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +387 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +3 -3
- megadetector/data_management/lila/test_lila_metadata_urls.py +2 -2
- megadetector/data_management/remove_exif.py +61 -36
- megadetector/data_management/yolo_to_coco.py +25 -6
- megadetector/detection/process_video.py +259 -126
- megadetector/detection/pytorch_detector.py +13 -11
- megadetector/detection/run_detector.py +9 -2
- megadetector/detection/run_detector_batch.py +7 -0
- megadetector/detection/run_inference_with_yolov5_val.py +58 -10
- megadetector/detection/tf_detector.py +8 -2
- megadetector/detection/video_utils.py +201 -16
- megadetector/postprocessing/md_to_coco.py +31 -9
- megadetector/postprocessing/postprocess_batch_results.py +19 -3
- megadetector/postprocessing/subset_json_detector_output.py +22 -12
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +3 -3
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +2 -1
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +1 -1
- megadetector/taxonomy_mapping/simple_image_download.py +5 -0
- megadetector/taxonomy_mapping/species_lookup.py +1 -1
- megadetector/utils/md_tests.py +196 -49
- megadetector/utils/path_utils.py +2 -2
- megadetector/utils/url_utils.py +7 -1
- megadetector/visualization/visualize_db.py +16 -0
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/LICENSE +0 -0
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/METADATA +2 -2
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/RECORD +29 -28
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/WHEEL +1 -1
- {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/top_level.txt +0 -0
|
@@ -124,7 +124,7 @@ class SubsetJsonDetectorOutputOptions:
|
|
|
124
124
|
self.remove_failed_images = False
|
|
125
125
|
|
|
126
126
|
#: Either a list of category IDs (as string-ints) (not names), or a dictionary mapping category *IDs*
|
|
127
|
-
#: (as string-ints) (not names) to thresholds. Removes
|
|
127
|
+
#: (as string-ints) (not names) to thresholds. Removes non-matching detections, does not
|
|
128
128
|
#: remove images. Not technically mutually exclusize with category_names_to_keep, but it's an esoteric
|
|
129
129
|
#: scenario indeed where you would want to specify both.
|
|
130
130
|
self.categories_to_keep = None
|
|
@@ -517,7 +517,7 @@ def subset_json_detector_output(input_filename, output_filename, options, data=N
|
|
|
517
517
|
else:
|
|
518
518
|
|
|
519
519
|
# Map images to unique folders
|
|
520
|
-
print('Finding unique folders')
|
|
520
|
+
print('Finding unique folders')
|
|
521
521
|
|
|
522
522
|
folders_to_images = {}
|
|
523
523
|
|
|
@@ -670,16 +670,26 @@ def main():
|
|
|
670
670
|
parser = argparse.ArgumentParser()
|
|
671
671
|
parser.add_argument('input_file', type=str, help='Input .json filename')
|
|
672
672
|
parser.add_argument('output_file', type=str, help='Output .json filename')
|
|
673
|
-
parser.add_argument('--query', type=str, default=None,
|
|
674
|
-
|
|
675
|
-
parser.add_argument('--
|
|
676
|
-
|
|
677
|
-
parser.add_argument('--
|
|
678
|
-
|
|
679
|
-
parser.add_argument('--
|
|
680
|
-
|
|
681
|
-
parser.add_argument('--
|
|
682
|
-
|
|
673
|
+
parser.add_argument('--query', type=str, default=None,
|
|
674
|
+
help='Query string to search for (omitting this matches all)')
|
|
675
|
+
parser.add_argument('--replacement', type=str, default=None,
|
|
676
|
+
help='Replace [query] with this')
|
|
677
|
+
parser.add_argument('--confidence_threshold', type=float, default=None,
|
|
678
|
+
help='Remove detections below this confidence level')
|
|
679
|
+
parser.add_argument('--split_folders', action='store_true',
|
|
680
|
+
help='Split .json files by leaf-node folder')
|
|
681
|
+
parser.add_argument('--split_folder_param', type=int,
|
|
682
|
+
help='Directory level count for n_from_bottom and n_from_top splitting')
|
|
683
|
+
parser.add_argument('--split_folder_mode', type=str,
|
|
684
|
+
help='Folder level to use for splitting ("top" or "bottom")')
|
|
685
|
+
parser.add_argument('--make_folder_relative', action='store_true',
|
|
686
|
+
help='Make image paths relative to their containing folder (only meaningful with split_folders)')
|
|
687
|
+
parser.add_argument('--overwrite_json_files', action='store_true',
|
|
688
|
+
help='Overwrite output files')
|
|
689
|
+
parser.add_argument('--copy_jsons_to_folders', action='store_true',
|
|
690
|
+
help='When using split_folders and make_folder_relative, copy jsons to their corresponding folders (relative to output_file)')
|
|
691
|
+
parser.add_argument('--create_folders', action='store_true',
|
|
692
|
+
help='When using copy_jsons_to_folders, create folders that don''t exist')
|
|
683
693
|
|
|
684
694
|
if len(sys.argv[1:]) == 0:
|
|
685
695
|
parser.print_help()
|
|
@@ -15,10 +15,10 @@ import json
|
|
|
15
15
|
# Created by get_lila_category_list.py
|
|
16
16
|
input_lila_category_list_file = os.path.expanduser('~/lila/lila_categories_list/lila_dataset_to_categories.json')
|
|
17
17
|
|
|
18
|
-
output_file = os.path.expanduser('~/lila/
|
|
18
|
+
output_file = os.path.expanduser('~/lila/lila_additions_2024.07.16.csv')
|
|
19
19
|
|
|
20
20
|
datasets_to_map = [
|
|
21
|
-
'
|
|
21
|
+
'Desert Lion Conservation Camera Traps'
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
|
|
@@ -133,7 +133,7 @@ if False:
|
|
|
133
133
|
# q = 'white-throated monkey'
|
|
134
134
|
# q = 'cingulata'
|
|
135
135
|
# q = 'notamacropus'
|
|
136
|
-
q = '
|
|
136
|
+
q = 'aves'
|
|
137
137
|
taxonomy_preference = 'inat'
|
|
138
138
|
m = get_preferred_taxonomic_match(q,taxonomy_preference)
|
|
139
139
|
# print(m.scientific_name); import clipboard; clipboard.copy(m.scientific_name)
|
|
@@ -24,7 +24,7 @@ if False:
|
|
|
24
24
|
release_taxonomy_file = os.path.expanduser('~/lila/lila-taxonomy-mapping_release.csv')
|
|
25
25
|
# import clipboard; clipboard.copy(release_taxonomy_file)
|
|
26
26
|
|
|
27
|
-
# Created by
|
|
27
|
+
# Created by get_lila_annotation_counts.py... contains counts for each category
|
|
28
28
|
lila_dataset_to_categories_file = os.path.expanduser('~/lila/lila_categories_list/lila_dataset_to_categories.json')
|
|
29
29
|
|
|
30
30
|
assert os.path.isfile(lila_dataset_to_categories_file)
|
|
@@ -140,3 +140,4 @@ if False:
|
|
|
140
140
|
|
|
141
141
|
print('Wrote final output to {}'.format(release_taxonomy_file))
|
|
142
142
|
|
|
143
|
+
# ...if False
|
|
@@ -16,7 +16,7 @@ import os
|
|
|
16
16
|
import pandas as pd
|
|
17
17
|
|
|
18
18
|
# lila_taxonomy_file = r"c:\git\agentmorrisprivate\lila-taxonomy\lila-taxonomy-mapping.csv"
|
|
19
|
-
lila_taxonomy_file = os.path.expanduser('~/lila/
|
|
19
|
+
lila_taxonomy_file = os.path.expanduser('~/lila/lila_additions_2024.07.16.csv')
|
|
20
20
|
|
|
21
21
|
preview_base = os.path.expanduser('~/lila/lila_taxonomy_preview')
|
|
22
22
|
os.makedirs(preview_base,exist_ok=True)
|
|
@@ -208,7 +208,7 @@ def initialize_taxonomy_lookup(force_init=False) -> None:
|
|
|
208
208
|
# Load GBIF taxonomy
|
|
209
209
|
gbif_taxonomy_file = os.path.join(taxonomy_download_dir, 'GBIF', 'Taxon.tsv')
|
|
210
210
|
print('Loading GBIF taxonomy from {}'.format(gbif_taxonomy_file))
|
|
211
|
-
gbif_taxonomy = pd.read_csv(gbif_taxonomy_file, sep='\t')
|
|
211
|
+
gbif_taxonomy = pd.read_csv(gbif_taxonomy_file, sep='\t', encoding='utf-8',on_bad_lines='warn')
|
|
212
212
|
gbif_taxonomy['scientificName'] = gbif_taxonomy['scientificName'].fillna('').str.strip()
|
|
213
213
|
gbif_taxonomy['canonicalName'] = gbif_taxonomy['canonicalName'].fillna('').str.strip()
|
|
214
214
|
|
megadetector/utils/md_tests.py
CHANGED
|
@@ -29,6 +29,10 @@ import subprocess
|
|
|
29
29
|
import argparse
|
|
30
30
|
import inspect
|
|
31
31
|
|
|
32
|
+
#: IoU threshold used to determine whether boxes in two detection files likely correspond
|
|
33
|
+
#: to the same box.
|
|
34
|
+
iou_threshold_for_file_comparison = 0.9
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
#%% Classes
|
|
34
38
|
|
|
@@ -359,6 +363,100 @@ def output_files_are_identical(fn1,fn2,verbose=False):
|
|
|
359
363
|
# ...def output_files_are_identical(...)
|
|
360
364
|
|
|
361
365
|
|
|
366
|
+
def compare_detection_lists(detections_a,detections_b,options,bidirectional_comparison=True):
|
|
367
|
+
"""
|
|
368
|
+
Compare two lists of MD-formatted detections, matching detections across lists using IoU
|
|
369
|
+
criteria. Generally used to compare detections for the same image when two sets of results
|
|
370
|
+
are expected to be more or less the same.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
detections_a (list): the first set of detection dicts
|
|
374
|
+
detections_b (list): the second set of detection dicts
|
|
375
|
+
options (MDTestOptions): options that determine tolerable differences between files
|
|
376
|
+
bidirectional_comparison (bool, optional): reverse the arguments and make a recursive
|
|
377
|
+
call.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
dict: a dictionary with keys 'max_conf_error' and 'max_coord_error'.
|
|
381
|
+
"""
|
|
382
|
+
from megadetector.utils.ct_utils import get_iou
|
|
383
|
+
|
|
384
|
+
max_conf_error = 0
|
|
385
|
+
max_coord_error = 0
|
|
386
|
+
|
|
387
|
+
# i_det_a = 0
|
|
388
|
+
for i_det_a in range(0,len(detections_a)):
|
|
389
|
+
|
|
390
|
+
det_a = detections_a[i_det_a]
|
|
391
|
+
|
|
392
|
+
# Don't process very-low-confidence boxes
|
|
393
|
+
if det_a['conf'] < options.max_conf_error:
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
matching_det_b = None
|
|
397
|
+
highest_iou = -1
|
|
398
|
+
|
|
399
|
+
# Find the closest match in the detections_b list
|
|
400
|
+
|
|
401
|
+
# i_det_b = 0
|
|
402
|
+
for i_det_b in range(0,len(detections_b)):
|
|
403
|
+
|
|
404
|
+
b_det = detections_b[i_det_b]
|
|
405
|
+
|
|
406
|
+
if b_det['category'] != det_a['category']:
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
iou = get_iou(det_a['bbox'],b_det['bbox'])
|
|
410
|
+
|
|
411
|
+
# Is this likely the same detection as det_a?
|
|
412
|
+
if iou >= iou_threshold_for_file_comparison and iou > highest_iou:
|
|
413
|
+
matching_det_b = b_det
|
|
414
|
+
highest_iou = iou
|
|
415
|
+
|
|
416
|
+
# If there are no detections in this category in detections_b
|
|
417
|
+
if matching_det_b is None:
|
|
418
|
+
if det_a['conf'] > max_conf_error:
|
|
419
|
+
max_conf_error = det_a['conf']
|
|
420
|
+
# max_coord_error = 1.0
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
assert det_a['category'] == matching_det_b['category']
|
|
424
|
+
conf_err = abs(det_a['conf'] - matching_det_b['conf'])
|
|
425
|
+
coord_differences = []
|
|
426
|
+
for i_coord in range(0,4):
|
|
427
|
+
coord_differences.append(abs(det_a['bbox'][i_coord]-\
|
|
428
|
+
matching_det_b['bbox'][i_coord]))
|
|
429
|
+
coord_err = max(coord_differences)
|
|
430
|
+
|
|
431
|
+
if conf_err >= max_conf_error:
|
|
432
|
+
max_conf_error = conf_err
|
|
433
|
+
|
|
434
|
+
if coord_err >= max_coord_error:
|
|
435
|
+
max_coord_error = coord_err
|
|
436
|
+
|
|
437
|
+
# ...for each detection in detections_a
|
|
438
|
+
|
|
439
|
+
if bidirectional_comparison:
|
|
440
|
+
|
|
441
|
+
reverse_comparison_results = compare_detection_lists(detections_b,
|
|
442
|
+
detections_a,
|
|
443
|
+
options,
|
|
444
|
+
bidirectional_comparison=False)
|
|
445
|
+
|
|
446
|
+
if reverse_comparison_results['max_conf_error'] > max_conf_error:
|
|
447
|
+
max_conf_error = reverse_comparison_results['max_conf_error']
|
|
448
|
+
if reverse_comparison_results['max_coord_error'] > max_coord_error:
|
|
449
|
+
max_coord_error = reverse_comparison_results['max_coord_error']
|
|
450
|
+
|
|
451
|
+
list_comparison_results = {}
|
|
452
|
+
list_comparison_results['max_coord_error'] = max_coord_error
|
|
453
|
+
list_comparison_results['max_conf_error'] = max_conf_error
|
|
454
|
+
|
|
455
|
+
return list_comparison_results
|
|
456
|
+
|
|
457
|
+
# ...def compare_detection_lists(...)
|
|
458
|
+
|
|
459
|
+
|
|
362
460
|
def compare_results(inference_output_file,expected_results_file,options):
|
|
363
461
|
"""
|
|
364
462
|
Compare two MD-formatted output files that should be nearly identical, allowing small
|
|
@@ -369,6 +467,9 @@ def compare_results(inference_output_file,expected_results_file,options):
|
|
|
369
467
|
inference_output_file (str): the first results file to compare
|
|
370
468
|
expected_results_file (str): the second results file to compare
|
|
371
469
|
options (MDTestOptions): options that determine tolerable differences between files
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
dict: dictionary with keys 'max_coord_error' and 'max_conf_error'
|
|
372
473
|
"""
|
|
373
474
|
|
|
374
475
|
# Read results
|
|
@@ -386,8 +487,11 @@ def compare_results(inference_output_file,expected_results_file,options):
|
|
|
386
487
|
len(filename_to_results_expected),
|
|
387
488
|
len(filename_to_results))
|
|
388
489
|
|
|
389
|
-
max_coord_error = 0
|
|
390
490
|
max_conf_error = 0
|
|
491
|
+
max_conf_error_file = None
|
|
492
|
+
|
|
493
|
+
max_coord_error = 0
|
|
494
|
+
max_coord_error_file = None
|
|
391
495
|
|
|
392
496
|
# fn = next(iter(filename_to_results.keys()))
|
|
393
497
|
for fn in filename_to_results.keys():
|
|
@@ -405,36 +509,20 @@ def compare_results(inference_output_file,expected_results_file,options):
|
|
|
405
509
|
actual_detections = actual_image_results['detections']
|
|
406
510
|
expected_detections = expected_image_results['detections']
|
|
407
511
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
512
|
+
comparison_results_this_image = compare_detection_lists(
|
|
513
|
+
detections_a=actual_detections,
|
|
514
|
+
detections_b=expected_detections,
|
|
515
|
+
options=options,
|
|
516
|
+
bidirectional_comparison=True)
|
|
412
517
|
|
|
413
|
-
if
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
continue
|
|
417
|
-
assert len(actual_detections) == len(expected_detections), \
|
|
418
|
-
'Error: {}'.format(s)
|
|
419
|
-
|
|
420
|
-
# i_det = 0
|
|
421
|
-
for i_det in range(0,len(actual_detections)):
|
|
422
|
-
actual_det = actual_detections[i_det]
|
|
423
|
-
expected_det = expected_detections[i_det]
|
|
424
|
-
assert actual_det['category'] == expected_det['category']
|
|
425
|
-
conf_err = abs(actual_det['conf'] - expected_det['conf'])
|
|
426
|
-
coord_differences = []
|
|
427
|
-
for i_coord in range(0,4):
|
|
428
|
-
coord_differences.append(abs(actual_det['bbox'][i_coord]-expected_det['bbox'][i_coord]))
|
|
429
|
-
coord_err = max(coord_differences)
|
|
518
|
+
if comparison_results_this_image['max_conf_error'] > max_conf_error:
|
|
519
|
+
max_conf_error = comparison_results_this_image['max_conf_error']
|
|
520
|
+
max_conf_error_file = fn
|
|
430
521
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
# ...for each detection
|
|
437
|
-
|
|
522
|
+
if comparison_results_this_image['max_coord_error'] > max_coord_error:
|
|
523
|
+
max_coord_error = comparison_results_this_image['max_coord_error']
|
|
524
|
+
max_coord_error_file = fn
|
|
525
|
+
|
|
438
526
|
# ...for each image
|
|
439
527
|
|
|
440
528
|
if not options.warning_mode:
|
|
@@ -447,9 +535,17 @@ def compare_results(inference_output_file,expected_results_file,options):
|
|
|
447
535
|
'Coord error {} is greater than allowable ({})'.format(
|
|
448
536
|
max_coord_error,options.max_coord_error)
|
|
449
537
|
|
|
450
|
-
print('Max conf error: {}'.format(
|
|
451
|
-
|
|
538
|
+
print('Max conf error: {} (file {})'.format(
|
|
539
|
+
max_conf_error,max_conf_error_file))
|
|
540
|
+
print('Max coord error: {} (file {})'.format(
|
|
541
|
+
max_coord_error,max_coord_error_file))
|
|
452
542
|
|
|
543
|
+
comparison_results = {}
|
|
544
|
+
comparison_results['max_conf_error'] = max_conf_error
|
|
545
|
+
comparison_results['max_coord_error'] = max_coord_error
|
|
546
|
+
|
|
547
|
+
return comparison_results
|
|
548
|
+
|
|
453
549
|
# ...def compare_results(...)
|
|
454
550
|
|
|
455
551
|
|
|
@@ -557,6 +653,8 @@ def run_python_tests(options):
|
|
|
557
653
|
|
|
558
654
|
## Run inference on an image
|
|
559
655
|
|
|
656
|
+
print('\n** Running MD on a single image **\n')
|
|
657
|
+
|
|
560
658
|
from megadetector.detection import run_detector
|
|
561
659
|
from megadetector.visualization import visualization_utils as vis_utils
|
|
562
660
|
image_fn = os.path.join(options.scratch_dir,options.test_images[0])
|
|
@@ -567,7 +665,7 @@ def run_python_tests(options):
|
|
|
567
665
|
|
|
568
666
|
## Run inference on a folder
|
|
569
667
|
|
|
570
|
-
print('\n** Running MD on images **\n')
|
|
668
|
+
print('\n** Running MD on a folder of images **\n')
|
|
571
669
|
|
|
572
670
|
from megadetector.detection.run_detector_batch import load_and_run_detector_batch,write_results_to_file
|
|
573
671
|
from megadetector.utils import path_utils
|
|
@@ -584,12 +682,15 @@ def run_python_tests(options):
|
|
|
584
682
|
|
|
585
683
|
|
|
586
684
|
## Verify results
|
|
587
|
-
|
|
685
|
+
|
|
588
686
|
expected_results_file = get_expected_results_filename(is_gpu_available(verbose=False),
|
|
589
687
|
options=options)
|
|
590
688
|
compare_results(inference_output_file,expected_results_file,options)
|
|
591
689
|
|
|
592
|
-
|
|
690
|
+
# Make note of this filename, we will use it again later
|
|
691
|
+
inference_output_file_standard_inference = inference_output_file
|
|
692
|
+
|
|
693
|
+
|
|
593
694
|
## Run and verify again with augmentation enabled
|
|
594
695
|
|
|
595
696
|
print('\n** Running MD on images with augmentation **\n')
|
|
@@ -651,6 +752,7 @@ def run_python_tests(options):
|
|
|
651
752
|
|
|
652
753
|
from megadetector.detection.run_inference_with_yolov5_val import \
|
|
653
754
|
YoloInferenceOptions, run_inference_with_yolo_val
|
|
755
|
+
from megadetector.utils.path_utils import insert_before_extension
|
|
654
756
|
|
|
655
757
|
inference_output_file_yolo_val = os.path.join(options.scratch_dir,'folder_inference_output_yolo_val.json')
|
|
656
758
|
|
|
@@ -661,12 +763,23 @@ def run_python_tests(options):
|
|
|
661
763
|
yolo_inference_options.model_filename = options.default_model
|
|
662
764
|
yolo_inference_options.augment = False
|
|
663
765
|
yolo_inference_options.overwrite_handling = 'overwrite'
|
|
766
|
+
from megadetector.detection.run_detector import DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD
|
|
767
|
+
yolo_inference_options.conf_thres = DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD
|
|
664
768
|
|
|
665
769
|
run_inference_with_yolo_val(yolo_inference_options)
|
|
666
770
|
|
|
771
|
+
## Confirm this matches the standard inference path
|
|
772
|
+
|
|
773
|
+
if False:
|
|
774
|
+
# TODO: compare_results() isn't quite ready for this yet
|
|
775
|
+
compare_results(inference_output_file=inference_output_file_yolo_val,
|
|
776
|
+
expected_results_file=inference_output_file_standard_inference,
|
|
777
|
+
options=options)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
|
|
667
781
|
# Run again, without symlinks this time
|
|
668
782
|
|
|
669
|
-
from megadetector.utils.path_utils import insert_before_extension
|
|
670
783
|
inference_output_file_yolo_val_no_links = insert_before_extension(inference_output_file_yolo_val,
|
|
671
784
|
'no-links')
|
|
672
785
|
yolo_inference_options.output_file = inference_output_file_yolo_val_no_links
|
|
@@ -712,6 +825,7 @@ def run_python_tests(options):
|
|
|
712
825
|
print('\n** Running MD on a single video **\n')
|
|
713
826
|
|
|
714
827
|
from megadetector.detection.process_video import ProcessVideoOptions, process_video
|
|
828
|
+
from megadetector.utils.path_utils import insert_before_extension
|
|
715
829
|
|
|
716
830
|
video_options = ProcessVideoOptions()
|
|
717
831
|
video_options.model_file = options.default_model
|
|
@@ -750,22 +864,23 @@ def run_python_tests(options):
|
|
|
750
864
|
print('\n** Running MD on a folder of videos **\n')
|
|
751
865
|
|
|
752
866
|
from megadetector.detection.process_video import ProcessVideoOptions, process_video_folder
|
|
867
|
+
from megadetector.utils.path_utils import insert_before_extension
|
|
753
868
|
|
|
754
869
|
video_options = ProcessVideoOptions()
|
|
755
870
|
video_options.model_file = options.default_model
|
|
756
871
|
video_options.input_video_file = os.path.join(options.scratch_dir,
|
|
757
872
|
os.path.dirname(options.test_videos[0]))
|
|
758
873
|
video_options.output_json_file = os.path.join(options.scratch_dir,'video_folder_output.json')
|
|
759
|
-
|
|
874
|
+
video_options.output_video_file = None
|
|
760
875
|
video_options.frame_folder = os.path.join(options.scratch_dir,'video_scratch/frame_folder')
|
|
761
876
|
video_options.frame_rendering_folder = os.path.join(options.scratch_dir,'video_scratch/rendered_frame_folder')
|
|
762
877
|
video_options.render_output_video = False
|
|
763
|
-
|
|
764
|
-
|
|
878
|
+
video_options.keep_rendered_frames = False
|
|
879
|
+
video_options.keep_rendered_frames = False
|
|
765
880
|
video_options.force_extracted_frame_folder_deletion = True
|
|
766
881
|
video_options.force_rendered_frame_folder_deletion = True
|
|
767
|
-
|
|
768
|
-
|
|
882
|
+
video_options.reuse_results_if_available = False
|
|
883
|
+
video_options.reuse_frames_if_available = False
|
|
769
884
|
video_options.recursive = True
|
|
770
885
|
video_options.verbose = True
|
|
771
886
|
video_options.fourcc = options.video_fourcc
|
|
@@ -773,12 +888,16 @@ def run_python_tests(options):
|
|
|
773
888
|
# video_options.json_confidence_threshold = 0.005
|
|
774
889
|
video_options.frame_sample = 10
|
|
775
890
|
video_options.n_cores = 5
|
|
891
|
+
|
|
892
|
+
# Force frame extraction to disk, since that's how we generated our expected results file
|
|
893
|
+
video_options.force_on_disk_frame_extraction = True
|
|
776
894
|
# video_options.debug_max_frames = -1
|
|
777
895
|
# video_options.class_mapping_filename = None
|
|
778
896
|
|
|
779
897
|
# Use quality == None, because we can't control whether YOLOv5 has patched cm2.imread,
|
|
780
898
|
# and therefore can't rely on using the quality parameter
|
|
781
|
-
video_options.quality = None
|
|
899
|
+
video_options.quality = None
|
|
900
|
+
video_options.max_width = None
|
|
782
901
|
|
|
783
902
|
_ = process_video_folder(video_options)
|
|
784
903
|
|
|
@@ -796,6 +915,29 @@ def run_python_tests(options):
|
|
|
796
915
|
assert os.path.isfile(expected_results_file)
|
|
797
916
|
compare_results(frame_output_file,expected_results_file,options)
|
|
798
917
|
|
|
918
|
+
|
|
919
|
+
## Run again, this time in memory, and make sure the results are *almost* the same
|
|
920
|
+
|
|
921
|
+
# They won't be quite the same, because the on-disk path goes through a jpeg intermediate
|
|
922
|
+
|
|
923
|
+
print('\n** Running MD on a folder of videos (in memory) **\n')
|
|
924
|
+
|
|
925
|
+
video_options.output_json_file = insert_before_extension(video_options.output_json_file,'in-memory')
|
|
926
|
+
video_options.force_on_disk_frame_extraction = False
|
|
927
|
+
_ = process_video_folder(video_options)
|
|
928
|
+
|
|
929
|
+
frame_output_file_in_memory = insert_before_extension(video_options.output_json_file,'frames')
|
|
930
|
+
assert os.path.isfile(frame_output_file_in_memory)
|
|
931
|
+
|
|
932
|
+
from copy import deepcopy
|
|
933
|
+
options_loose = deepcopy(options)
|
|
934
|
+
options_loose.max_conf_error = 0.05
|
|
935
|
+
options_loose.max_coord_error = 0.01
|
|
936
|
+
|
|
937
|
+
compare_results(inference_output_file=frame_output_file,
|
|
938
|
+
expected_results_file=frame_output_file_in_memory,
|
|
939
|
+
options=options_loose)
|
|
940
|
+
|
|
799
941
|
# ...if we're not skipping video tests
|
|
800
942
|
|
|
801
943
|
print('\n*** Finished module tests ***\n')
|
|
@@ -885,7 +1027,8 @@ def run_cli_tests(options):
|
|
|
885
1027
|
cmd_results = execute_and_print(cmd)
|
|
886
1028
|
|
|
887
1029
|
assert output_files_are_identical(fn1=inference_output_file,
|
|
888
|
-
fn2=inference_output_file_checkpoint,
|
|
1030
|
+
fn2=inference_output_file_checkpoint,
|
|
1031
|
+
verbose=True)
|
|
889
1032
|
|
|
890
1033
|
|
|
891
1034
|
## Run again with the image queue enabled, make sure the results are the same
|
|
@@ -897,7 +1040,8 @@ def run_cli_tests(options):
|
|
|
897
1040
|
cmd_results = execute_and_print(cmd)
|
|
898
1041
|
|
|
899
1042
|
assert output_files_are_identical(fn1=inference_output_file,
|
|
900
|
-
fn2=inference_output_file_queue,
|
|
1043
|
+
fn2=inference_output_file_queue,
|
|
1044
|
+
verbose=True)
|
|
901
1045
|
|
|
902
1046
|
|
|
903
1047
|
## Run again on multiple cores, make sure the results are the same
|
|
@@ -935,7 +1079,8 @@ def run_cli_tests(options):
|
|
|
935
1079
|
del os.environ['CUDA_VISIBLE_DEVICES']
|
|
936
1080
|
|
|
937
1081
|
assert output_files_are_identical(fn1=inference_output_file_cpu,
|
|
938
|
-
fn2=inference_output_file_cpu_multicore,
|
|
1082
|
+
fn2=inference_output_file_cpu_multicore,
|
|
1083
|
+
verbose=True)
|
|
939
1084
|
|
|
940
1085
|
|
|
941
1086
|
## Postprocessing
|
|
@@ -1041,7 +1186,8 @@ def run_cli_tests(options):
|
|
|
1041
1186
|
cmd_results = execute_and_print(cmd)
|
|
1042
1187
|
|
|
1043
1188
|
assert output_files_are_identical(fn1=inference_output_file_yolo_val,
|
|
1044
|
-
fn2=inference_output_file_yolo_val_checkpoint
|
|
1189
|
+
fn2=inference_output_file_yolo_val_checkpoint,
|
|
1190
|
+
verbose=True)
|
|
1045
1191
|
|
|
1046
1192
|
if not options.skip_video_tests:
|
|
1047
1193
|
|
|
@@ -1161,19 +1307,21 @@ if False:
|
|
|
1161
1307
|
options.cpu_execution_is_error = False
|
|
1162
1308
|
options.skip_video_tests = False
|
|
1163
1309
|
options.skip_python_tests = False
|
|
1164
|
-
options.skip_cli_tests =
|
|
1310
|
+
options.skip_cli_tests = True
|
|
1165
1311
|
options.scratch_dir = None
|
|
1166
1312
|
options.test_data_url = 'https://lila.science/public/md-test-package.zip'
|
|
1167
1313
|
options.force_data_download = False
|
|
1168
1314
|
options.force_data_unzip = False
|
|
1169
|
-
options.warning_mode =
|
|
1315
|
+
options.warning_mode = False
|
|
1170
1316
|
options.max_coord_error = 0.001
|
|
1171
1317
|
options.max_conf_error = 0.005
|
|
1172
1318
|
options.cli_working_dir = r'c:\git\MegaDetector'
|
|
1173
1319
|
options.yolo_working_dir = r'c:\git\yolov5-md'
|
|
1174
1320
|
|
|
1175
|
-
import os
|
|
1176
1321
|
|
|
1322
|
+
#%%
|
|
1323
|
+
|
|
1324
|
+
import os
|
|
1177
1325
|
if 'PYTHONPATH' not in os.environ or options.yolo_working_dir not in os.environ['PYTHONPATH']:
|
|
1178
1326
|
os.environ['PYTHONPATH'] += ';' + options.yolo_working_dir
|
|
1179
1327
|
|
|
@@ -1332,4 +1480,3 @@ if False:
|
|
|
1332
1480
|
fn1 = r"G:\temp\md-test-package\mdv5a-image-cpu-pt1.10.1.json"
|
|
1333
1481
|
fn2 = r"G:\temp\md-test-package\mdv5a-augment-image-cpu-pt1.10.1.json"
|
|
1334
1482
|
print(output_files_are_identical(fn1,fn2,verbose=True))
|
|
1335
|
-
|
megadetector/utils/path_utils.py
CHANGED
|
@@ -924,8 +924,8 @@ def zip_files_into_single_zipfile(input_files, output_fn, arc_name_base,
|
|
|
924
924
|
|
|
925
925
|
def zip_folder(input_folder, output_fn=None, overwrite=False, verbose=False, compresslevel=9):
|
|
926
926
|
"""
|
|
927
|
-
Recursively zip everything in [input_folder] into a single zipfile, storing
|
|
928
|
-
|
|
927
|
+
Recursively zip everything in [input_folder] into a single zipfile, storing files as paths
|
|
928
|
+
relative to [input_folder].
|
|
929
929
|
|
|
930
930
|
Args:
|
|
931
931
|
input_folder (str): folder to zip
|
megadetector/utils/url_utils.py
CHANGED
|
@@ -75,7 +75,8 @@ def download_url(url,
|
|
|
75
75
|
destination_filename=None,
|
|
76
76
|
progress_updater=None,
|
|
77
77
|
force_download=False,
|
|
78
|
-
verbose=True
|
|
78
|
+
verbose=True,
|
|
79
|
+
escape_spaces=True):
|
|
79
80
|
"""
|
|
80
81
|
Downloads a URL to a file. If no file is specified, creates a temporary file,
|
|
81
82
|
making a best effort to avoid filename collisions.
|
|
@@ -92,6 +93,7 @@ def download_url(url,
|
|
|
92
93
|
force_download (bool, optional): download this file even if [destination_filename]
|
|
93
94
|
exists.
|
|
94
95
|
verbose (bool, optional): enable additional debug console output
|
|
96
|
+
escape_spaces (bool, optional): replace ' ' with '%20'
|
|
95
97
|
|
|
96
98
|
Returns:
|
|
97
99
|
str: the filename to which [url] was downloaded, the same as [destination_filename]
|
|
@@ -107,6 +109,7 @@ def download_url(url,
|
|
|
107
109
|
url_no_sas = url.split('?')[0]
|
|
108
110
|
|
|
109
111
|
if destination_filename is None:
|
|
112
|
+
|
|
110
113
|
target_folder = get_temp_folder()
|
|
111
114
|
url_without_sas = url.split('?', 1)[0]
|
|
112
115
|
|
|
@@ -119,6 +122,9 @@ def download_url(url,
|
|
|
119
122
|
destination_filename = \
|
|
120
123
|
os.path.join(target_folder,url_as_filename)
|
|
121
124
|
|
|
125
|
+
if escape_spaces:
|
|
126
|
+
url = url.replace(' ','%20')
|
|
127
|
+
|
|
122
128
|
if (not force_download) and (os.path.isfile(destination_filename)):
|
|
123
129
|
if verbose:
|
|
124
130
|
print('Bypassing download of already-downloaded file {}'.format(os.path.basename(url_no_sas)))
|
|
@@ -122,6 +122,14 @@ class DbVizOptions:
|
|
|
122
122
|
|
|
123
123
|
#: Enable additionald debug console output
|
|
124
124
|
self.verbose = False
|
|
125
|
+
|
|
126
|
+
#: COCO files used for evaluation may contain confidence scores, this
|
|
127
|
+
#: determines the field name used for confidence scores
|
|
128
|
+
self.confidence_field_name = 'score'
|
|
129
|
+
|
|
130
|
+
#: Optionally apply a confidence threshold; this requires that [confidence_field_name]
|
|
131
|
+
#: be present in all detections.
|
|
132
|
+
self.confidence_threshold = None
|
|
125
133
|
|
|
126
134
|
|
|
127
135
|
#%% Helper functions
|
|
@@ -294,6 +302,14 @@ def visualize_db(db_path, output_dir, image_base_dir, options=None):
|
|
|
294
302
|
# iAnn = 0; anno = annos_i.iloc[iAnn]
|
|
295
303
|
for iAnn,anno in annos_i.iterrows():
|
|
296
304
|
|
|
305
|
+
if options.confidence_threshold is not None:
|
|
306
|
+
assert options.confidence_field_name in anno, \
|
|
307
|
+
'Error: confidence thresholding requested, ' + \
|
|
308
|
+
'but at least one annotation does not have the {} field'.format(
|
|
309
|
+
options.confidence_field_name)
|
|
310
|
+
if anno[options.confidence_field_name] < options.confidence_threshold:
|
|
311
|
+
continue
|
|
312
|
+
|
|
297
313
|
if 'sequence_level_annotation' in anno:
|
|
298
314
|
bSequenceLevelAnnotation = anno['sequence_level_annotation']
|
|
299
315
|
if bSequenceLevelAnnotation:
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: megadetector
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.16
|
|
4
4
|
Summary: MegaDetector is an AI model that helps conservation folks spend less time doing boring things with camera trap images.
|
|
5
5
|
Author-email: Your friendly neighborhood MegaDetector team <cameratraps@lila.science>
|
|
6
6
|
Maintainer-email: Your friendly neighborhood MegaDetector team <cameratraps@lila.science>
|
|
@@ -54,7 +54,7 @@ Requires-Dist: ultralytics-yolov5 ==0.1.1
|
|
|
54
54
|
|
|
55
55
|
This package is a pip-installable version of the support/inference code for [MegaDetector](https://github.com/agentmorris/MegaDetector/?tab=readme-ov-file#megadetector), an object detection model that helps conservation biologists spend less time doing boring things with camera trap images. Complete documentation for this Python package is available at [megadetector.readthedocs.io](https://megadetector.readthedocs.io).
|
|
56
56
|
|
|
57
|
-
If you aren't looking for the Python package
|
|
57
|
+
If you aren't looking for the Python package specifically, and you just want to learn more about what MegaDetector is all about, head over to the [MegaDetector repo](https://github.com/agentmorris/MegaDetector/?tab=readme-ov-file#megadetector).
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
## Reasons you probably aren't looking for this package
|