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.

Files changed (29) hide show
  1. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +387 -0
  2. megadetector/data_management/lila/generate_lila_per_image_labels.py +3 -3
  3. megadetector/data_management/lila/test_lila_metadata_urls.py +2 -2
  4. megadetector/data_management/remove_exif.py +61 -36
  5. megadetector/data_management/yolo_to_coco.py +25 -6
  6. megadetector/detection/process_video.py +259 -126
  7. megadetector/detection/pytorch_detector.py +13 -11
  8. megadetector/detection/run_detector.py +9 -2
  9. megadetector/detection/run_detector_batch.py +7 -0
  10. megadetector/detection/run_inference_with_yolov5_val.py +58 -10
  11. megadetector/detection/tf_detector.py +8 -2
  12. megadetector/detection/video_utils.py +201 -16
  13. megadetector/postprocessing/md_to_coco.py +31 -9
  14. megadetector/postprocessing/postprocess_batch_results.py +19 -3
  15. megadetector/postprocessing/subset_json_detector_output.py +22 -12
  16. megadetector/taxonomy_mapping/map_new_lila_datasets.py +3 -3
  17. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +2 -1
  18. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +1 -1
  19. megadetector/taxonomy_mapping/simple_image_download.py +5 -0
  20. megadetector/taxonomy_mapping/species_lookup.py +1 -1
  21. megadetector/utils/md_tests.py +196 -49
  22. megadetector/utils/path_utils.py +2 -2
  23. megadetector/utils/url_utils.py +7 -1
  24. megadetector/visualization/visualize_db.py +16 -0
  25. {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/LICENSE +0 -0
  26. {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/METADATA +2 -2
  27. {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/RECORD +29 -28
  28. {megadetector-5.0.15.dist-info → megadetector-5.0.16.dist-info}/WHEEL +1 -1
  29. {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 non-matching detections, does not
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, help='Query string to search for (omitting this matches all)')
674
- parser.add_argument('--replacement', type=str, default=None, help='Replace [query] with this')
675
- parser.add_argument('--confidence_threshold', type=float, default=None, help='Remove detections below this confidence level')
676
- parser.add_argument('--split_folders', action='store_true', help='Split .json files by leaf-node folder')
677
- parser.add_argument('--split_folder_param', type=int, help='Directory level count for n_from_bottom and n_from_top splitting')
678
- parser.add_argument('--split_folder_mode', type=str, help='Folder level to use for splitting ("top" or "bottom")')
679
- parser.add_argument('--make_folder_relative', action='store_true', help='Make image paths relative to their containing folder (only meaningful with split_folders)')
680
- parser.add_argument('--overwrite_json_files', action='store_true', help='Overwrite output files')
681
- parser.add_argument('--copy_jsons_to_folders', action='store_true', help='When using split_folders and make_folder_relative, copy jsons to their corresponding folders (relative to output_file)')
682
- parser.add_argument('--create_folders', action='store_true', help='When using copy_jsons_to_folders, create folders that don''t exist')
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/lila_additions_2023.12.29.csv')
18
+ output_file = os.path.expanduser('~/lila/lila_additions_2024.07.16.csv')
19
19
 
20
20
  datasets_to_map = [
21
- 'Trail Camera Images of New Zealand Animals'
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 = 'porzana'
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 get_lila_category_list.py... contains counts for each category
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/lila_additions_2023.12.29.csv')
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)
@@ -8,6 +8,11 @@ Slightly modified from:
8
8
 
9
9
  https://github.com/RiddlerQ/simple_image_download
10
10
 
11
+ pip install python-magic
12
+
13
+ # On Windows, also run:
14
+ pip install python-magic-bin
15
+
11
16
  """
12
17
 
13
18
  #%% Imports
@@ -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
 
@@ -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
- s = 'expected {} detections for file {}, found {}'.format(
409
- len(expected_detections),fn,len(actual_detections))
410
- s += '\nExpected results file: {}\nActual results file: {}'.format(
411
- expected_results_file,inference_output_file)
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 options.warning_mode:
414
- if len(actual_detections) != len(expected_detections):
415
- print('Warning: {}'.format(s))
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
- if conf_err > max_conf_error:
432
- max_conf_error = conf_err
433
- if coord_err > max_coord_error:
434
- max_coord_error = coord_err
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(max_conf_error))
451
- print('Max coord error: {}'.format(max_coord_error))
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
- # video_options.output_video_file = None
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
- # video_options.keep_rendered_frames = False
764
- # video_options.keep_rendered_frames = False
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
- # video_options.reuse_results_if_available = False
768
- # video_options.reuse_frames_if_available = False
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,verbose=True)
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,verbose=True)
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,verbose=True)
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 = False
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 = True
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
-
@@ -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 outputs as relative
928
- paths.
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
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: megadetector
3
- Version: 5.0.15
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 specificaly, 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).
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