megadetector 10.0.8__py3-none-any.whl → 10.0.10__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.

@@ -61,7 +61,18 @@ DEFAULT_DETECTION_CONFIDENCE_THRESHOLD_FOR_OUTPUT = DEFAULT_OUTPUT_CONFIDENCE_TH
61
61
  DEFAULT_DETECTOR_BATCH_SIZE = 1
62
62
  DEFAULT_CLASSIFIER_BATCH_SIZE = 8
63
63
  DEFAULT_LOADER_WORKERS = 4
64
- MAX_QUEUE_SIZE_IMAGES_PER_WORKER = 10
64
+
65
+ # This determines the maximum number of images that can get read from disk
66
+ # on each of the producer workers before blocking. The actual size of the queue
67
+ # will be MAX_IMAGE_QUEUE_SIZE_PER_WORKER * n_workers. This is only used for
68
+ # the classification step.
69
+ MAX_IMAGE_QUEUE_SIZE_PER_WORKER = 10
70
+
71
+ # This determines the maximum number of crops that can accumulate in the queue
72
+ # used to communicate between the producers (which read and crop images) and the
73
+ # consumer (which runs the classifier). This is only used for the classification step.
74
+ MAX_BATCH_QUEUE_SIZE = 300
75
+
65
76
  DEAFULT_SECONDS_PER_VIDEO_FRAME = 1.0
66
77
 
67
78
  # Max number of classification scores to include per detection
@@ -71,6 +82,11 @@ DEFAULT_TOP_N_SCORES = 2
71
82
  # cumulative confidence is above this value
72
83
  ROLLUP_TARGET_CONFIDENCE = 0.5
73
84
 
85
+ # When the called supplies an existing MD results file, should we validate it before
86
+ # starting classification? This tends
87
+ VALIDATE_DETECTION_FILE = False
88
+
89
+
74
90
  verbose = False
75
91
 
76
92
 
@@ -109,10 +125,10 @@ class CropBatch:
109
125
  """
110
126
 
111
127
  def __init__(self):
112
- # List of preprocessed images
128
+ #: List of preprocessed images
113
129
  self.crops = []
114
130
 
115
- # List of CropMetadata objects
131
+ #: List of CropMetadata objects
116
132
  self.metadata = []
117
133
 
118
134
  def add_crop(self, crop_data, metadata):
@@ -192,6 +208,7 @@ def _process_image_detections(file_path: str,
192
208
 
193
209
  # Preprocess the crop
194
210
  try:
211
+
195
212
  preprocessed_crop = classifier.preprocess(
196
213
  image,
197
214
  bboxes=[speciesnet_bbox],
@@ -199,6 +216,7 @@ def _process_image_detections(file_path: str,
199
216
  )
200
217
 
201
218
  if preprocessed_crop is not None:
219
+
202
220
  metadata = CropMetadata(
203
221
  image_file=file_path,
204
222
  detection_index=detection_index,
@@ -207,10 +225,11 @@ def _process_image_detections(file_path: str,
207
225
  original_height=original_height
208
226
  )
209
227
 
210
- # Send individual crop immediately to consumer
228
+ # Send individual crop to the consumer
211
229
  batch_queue.put(('crop', preprocessed_crop, metadata))
212
230
 
213
231
  except Exception as e:
232
+
214
233
  print('Warning: failed to preprocess crop from {}, detection {}: {}'.format(
215
234
  file_path, detection_index, str(e)))
216
235
 
@@ -226,6 +245,8 @@ def _process_image_detections(file_path: str,
226
245
  'Failed to preprocess crop: {}'.format(str(e)),
227
246
  failure_metadata))
228
247
 
248
+ # ...try/except
249
+
229
250
  # ...for each detection in this image
230
251
 
231
252
  # ...def _process_image_detections(...)
@@ -256,6 +277,7 @@ def _process_video_detections(file_path: str,
256
277
  frame_to_detections = {}
257
278
 
258
279
  for detection_index, detection in enumerate(detections):
280
+
259
281
  conf = detection['conf']
260
282
  if conf < detection_confidence_threshold:
261
283
  continue
@@ -267,6 +289,8 @@ def _process_video_detections(file_path: str,
267
289
  frame_to_detections[frame_number] = []
268
290
  frame_to_detections[frame_number].append((detection_index, detection))
269
291
 
292
+ # ...for each detection in this video
293
+
270
294
  if len(frames_with_detections) == 0:
271
295
  return
272
296
 
@@ -290,6 +314,7 @@ def _process_video_detections(file_path: str,
290
314
  return
291
315
  frame_number = int(match.group(1))
292
316
 
317
+ # Only process frames for which we have detection results
293
318
  if frame_number not in frame_to_detections:
294
319
  return
295
320
 
@@ -360,13 +385,16 @@ def _process_video_detections(file_path: str,
360
385
 
361
386
  # Process the video frames
362
387
  try:
388
+
363
389
  run_callback_on_frames(
364
390
  input_video_file=absolute_file_path,
365
391
  frame_callback=frame_callback,
366
392
  frames_to_process=frames_to_process,
367
393
  verbose=verbose
368
394
  )
395
+
369
396
  except Exception as e:
397
+
370
398
  print('Warning: failed to process video {}: {}'.format(file_path, str(e)))
371
399
 
372
400
  # Send failure information to consumer for the whole video
@@ -448,6 +476,7 @@ def _crop_producer_func(image_queue: JoinableQueue,
448
476
  is_video = is_video_file(file_path)
449
477
 
450
478
  if is_video:
479
+
451
480
  # Process video
452
481
  _process_video_detections(
453
482
  file_path=file_path,
@@ -457,7 +486,9 @@ def _crop_producer_func(image_queue: JoinableQueue,
457
486
  detection_confidence_threshold=detection_confidence_threshold,
458
487
  batch_queue=batch_queue
459
488
  )
489
+
460
490
  else:
491
+
461
492
  # Process image
462
493
  _process_image_detections(
463
494
  file_path=file_path,
@@ -571,9 +602,9 @@ def _crop_consumer_func(batch_queue: Queue,
571
602
  item_type, data, metadata = item
572
603
 
573
604
  if metadata.image_file not in all_results:
574
- all_results[metadata.image_file] = {}
605
+ all_results[metadata.image_file] = {}
575
606
 
576
- # We should never be processing the same detetion twice
607
+ # We should never be processing the same detection twice
577
608
  assert metadata.detection_index not in all_results[metadata.image_file]
578
609
 
579
610
  if item_type == 'failure':
@@ -601,6 +632,7 @@ def _crop_consumer_func(batch_queue: Queue,
601
632
 
602
633
  # ...while (we have items to process)
603
634
 
635
+ # Send all the results at once back to the main process
604
636
  results_queue.put(all_results)
605
637
 
606
638
  if verbose:
@@ -828,7 +860,7 @@ def _run_detection_step(source_folder: str,
828
860
  batch_size=detector_batch_size,
829
861
  include_image_size=False,
830
862
  include_image_timestamp=False,
831
- include_exif_data=False,
863
+ include_exif_tags=None,
832
864
  loader_workers=detector_worker_threads,
833
865
  preprocess_on_image_queue=True
834
866
  )
@@ -914,9 +946,11 @@ def _run_classification_step(detector_results_file: str,
914
946
  top_n_scores (int, optional): maximum number of scores to include for each detection
915
947
  """
916
948
 
917
- print('Starting SpeciesNet classification step...')
949
+ print('Starting classification step...')
918
950
 
919
951
  # Load MegaDetector results
952
+ print('Reading detection results from {}'.format(detector_results_file))
953
+
920
954
  with open(detector_results_file, 'r') as f:
921
955
  detector_results = json.load(f)
922
956
 
@@ -936,10 +970,22 @@ def _run_classification_step(detector_results_file: str,
936
970
  print('Set multiprocessing start method to spawn (was {})'.format(
937
971
  original_start_method))
938
972
 
939
- # Set up multiprocessing queues
940
- max_queue_size = classifier_worker_threads * MAX_QUEUE_SIZE_IMAGES_PER_WORKER
941
- image_queue = JoinableQueue(max_queue_size)
942
- batch_queue = Queue()
973
+ ## Set up multiprocessing queues
974
+
975
+ # This queue receives lists of image filenames (and associated detection results)
976
+ # from the "main" thread (the one you're reading right now). Items are pulled off
977
+ # of this queue by producer workers (on _crop_producer_func), where the corresponding
978
+ # images are loaded from disk and preprocessed into crops.
979
+ image_queue = JoinableQueue(maxsize= \
980
+ classifier_worker_threads * MAX_IMAGE_QUEUE_SIZE_PER_WORKER)
981
+
982
+ # This queue receives cropped images from producers (on _crop_producer_func); those
983
+ # crops are pulled off of this queue by the consumer (on _crop_consumer_func).
984
+ batch_queue = Queue(maxsize=MAX_BATCH_QUEUE_SIZE)
985
+
986
+ # This is not really used as a queue, rather it's just used to send all the results
987
+ # at once from the consumer process to the main process (the one you're reading right
988
+ # now).
943
989
  results_queue = Queue()
944
990
 
945
991
  # Start producer workers
@@ -951,7 +997,9 @@ def _run_classification_step(detector_results_file: str,
951
997
  p.start()
952
998
  producers.append(p)
953
999
 
954
- # Start consumer worker
1000
+
1001
+ ## Start consumer worker
1002
+
955
1003
  consumer = Process(target=_crop_consumer_func,
956
1004
  args=(batch_queue, results_queue, classifier_model,
957
1005
  classifier_batch_size, classifier_worker_threads,
@@ -974,16 +1022,23 @@ def _run_classification_step(detector_results_file: str,
974
1022
 
975
1023
  print('Finished waiting for input queue')
976
1024
 
977
- # Wait for results
1025
+
1026
+ ## Wait for results
1027
+
978
1028
  classification_results = results_queue.get()
979
1029
 
980
- # Clean up processes
1030
+
1031
+ ## Clean up processes
1032
+
981
1033
  for p in producers:
982
1034
  p.join()
983
1035
  consumer.join()
984
1036
 
985
1037
  print('Finished waiting for workers')
986
1038
 
1039
+
1040
+ ## Format results and write output
1041
+
987
1042
  class CategoryState:
988
1043
  """
989
1044
  Helper class to manage classification category IDs.
@@ -1257,15 +1312,18 @@ def main():
1257
1312
  print('Intermediate files: {}'.format(temp_folder))
1258
1313
 
1259
1314
  # Determine detector output file path
1260
- if args.detections_file:
1315
+ if args.detections_file is not None:
1261
1316
  detector_output_file = args.detections_file
1262
- print('Using existing detections file: {}'.format(detector_output_file))
1263
- validation_options = ValidateBatchResultsOptions()
1264
- validation_options.check_image_existence = True
1265
- validation_options.relative_path_base = args.source
1266
- validation_options.raise_errors = True
1267
- validate_batch_results(detector_output_file,options=validation_options)
1268
- print('Validated detections file')
1317
+ if VALIDATE_DETECTION_FILE:
1318
+ print('Using existing detections file: {}'.format(detector_output_file))
1319
+ validation_options = ValidateBatchResultsOptions()
1320
+ validation_options.check_image_existence = True
1321
+ validation_options.relative_path_base = args.source
1322
+ validation_options.raise_errors = True
1323
+ validate_batch_results(detector_output_file,options=validation_options)
1324
+ print('Validated detections file')
1325
+ else:
1326
+ print('Bypassing validation of {}'.format(args.detections_file))
1269
1327
  else:
1270
1328
  detector_output_file = os.path.join(temp_folder, 'detector_output.json')
1271
1329
 
@@ -39,7 +39,7 @@ from torchvision import ops
39
39
  from megadetector.detection.run_inference_with_yolov5_val import \
40
40
  YoloInferenceOptions,run_inference_with_yolo_val
41
41
  from megadetector.detection.run_detector_batch import \
42
- load_and_run_detector_batch,write_results_to_file
42
+ load_and_run_detector_batch,write_results_to_file,default_loaders
43
43
  from megadetector.detection.run_detector import \
44
44
  try_download_known_detector, CONF_DIGITS, COORD_DIGITS
45
45
  from megadetector.utils import path_utils
@@ -406,7 +406,9 @@ def run_tiled_inference(model_file,
406
406
  detector_options=None,
407
407
  use_image_queue=True,
408
408
  preprocess_on_image_queue=True,
409
- inference_size=None):
409
+ loader_workers=default_loaders,
410
+ inference_size=None,
411
+ verbose=False):
410
412
  """
411
413
  Runs inference using [model_file] on the images in [image_folder], fist splitting each image up
412
414
  into tiles of size [tile_size_x] x [tile_size_y], writing those tiles to [tiling_folder],
@@ -451,16 +453,17 @@ def run_tiled_inference(model_file,
451
453
  image_list (list, optional): .json file containing a list of specific images to process. If
452
454
  this is supplied, and the paths are absolute, [image_folder] will be ignored. If this is supplied,
453
455
  and the paths are relative, they should be relative to [image_folder]
454
- augment (bool, optional): apply test-time augmentation, only relevant if yolo_inference_options
455
- is None
456
+ augment (bool, optional): apply test-time augmentation
456
457
  detector_options (dict, optional): parameters to pass to run_detector, only relevant if
457
458
  yolo_inference_options is None
458
459
  use_image_queue (bool, optional): whether to use a loader worker queue, only relevant if
459
460
  yolo_inference_options is None
460
461
  preprocess_on_image_queue (bool, optional): whether the image queue should also be responsible
461
462
  for preprocessing
463
+ loader_workers (int, optional): number of preprocessing loader workers to use
462
464
  inference_size (int, optional): override the default inference image size, only relevant if
463
465
  yolo_inference_options is None
466
+ verbose (bool, optional): enable additional debug output
464
467
 
465
468
  Returns:
466
469
  dict: MD-formatted results dictionary, identical to what's written to [output_file]
@@ -522,7 +525,8 @@ def run_tiled_inference(model_file,
522
525
 
523
526
  all_image_patch_info = None
524
527
 
525
- print('Extracting patches from {} images'.format(len(image_files_relative)))
528
+ print('Extracting patches from {} images on {} workers'.format(
529
+ len(image_files_relative),n_patch_extraction_workers))
526
530
 
527
531
  n_workers = n_patch_extraction_workers
528
532
 
@@ -632,7 +636,9 @@ def run_tiled_inference(model_file,
632
636
  detector_options=detector_options,
633
637
  use_image_queue=use_image_queue,
634
638
  preprocess_on_image_queue=preprocess_on_image_queue,
635
- image_size=inference_size)
639
+ image_size=inference_size,
640
+ verbose_output=verbose,
641
+ loader_workers=loader_workers)
636
642
 
637
643
  patch_level_output_file = os.path.join(tiling_folder,folder_name + '_patch_level_results.json')
638
644
 
@@ -847,12 +853,12 @@ if False:
847
853
  yolo_inference_options.yolo_working_folder = os.path.expanduser('~/git/yolov5')
848
854
 
849
855
  run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
850
- tile_size_x=tile_size_x, tile_size_y=tile_size_y,
851
- tile_overlap=tile_overlap,
852
- checkpoint_path=checkpoint_path,
853
- checkpoint_frequency=checkpoint_frequency,
854
- remove_tiles=remove_tiles,
855
- yolo_inference_options=yolo_inference_options)
856
+ tile_size_x=tile_size_x, tile_size_y=tile_size_y,
857
+ tile_overlap=tile_overlap,
858
+ checkpoint_path=checkpoint_path,
859
+ checkpoint_frequency=checkpoint_frequency,
860
+ remove_tiles=remove_tiles,
861
+ yolo_inference_options=yolo_inference_options)
856
862
 
857
863
 
858
864
  #%% Run tiled inference (generate a command)
@@ -930,6 +936,14 @@ def main():
930
936
  '--no_remove_tiles',
931
937
  action='store_true',
932
938
  help='Tiles are removed by default; this option suppresses tile deletion')
939
+ parser.add_argument(
940
+ '--augment',
941
+ action='store_true',
942
+ help='Enable test-time augmentation')
943
+ parser.add_argument(
944
+ '--verbose',
945
+ action='store_true',
946
+ help='Enable additional debug output')
933
947
  parser.add_argument(
934
948
  '--tile_size_x',
935
949
  type=int,
@@ -960,6 +974,21 @@ def main():
960
974
  type=str,
961
975
  default=None,
962
976
  help=('A list of detector options (key-value pairs)'))
977
+ parser.add_argument(
978
+ '--inference_size',
979
+ type=int,
980
+ default=None,
981
+ help=('Run inference at a non-default size'))
982
+ parser.add_argument(
983
+ '--n_patch_extraction_workers',
984
+ type=int,
985
+ default=1,
986
+ help=('Number of workers to use for patch extraction'))
987
+ parser.add_argument(
988
+ '--loader_workers',
989
+ type=int,
990
+ default=default_loaders,
991
+ help=('Number of workers to use for image loading and preprocessing (0 to disable)'))
963
992
 
964
993
  # detector_options = parse_kvp_list(args.detector_options)
965
994
 
@@ -987,11 +1016,23 @@ def main():
987
1016
 
988
1017
  remove_tiles = (not args.no_remove_tiles)
989
1018
 
990
- run_tiled_inference(model_file, args.image_folder, args.tiling_folder, args.output_file,
991
- tile_size_x=args.tile_size_x, tile_size_y=args.tile_size_y,
1019
+ use_image_queue = (args.loader_workers > 0)
1020
+
1021
+ run_tiled_inference(model_file,
1022
+ args.image_folder,
1023
+ args.tiling_folder,
1024
+ args.output_file,
1025
+ tile_size_x=args.tile_size_x,
1026
+ tile_size_y=args.tile_size_y,
992
1027
  tile_overlap=args.tile_overlap,
993
1028
  remove_tiles=remove_tiles,
994
- image_list=args.image_list)
1029
+ image_list=args.image_list,
1030
+ augment=args.augment,
1031
+ inference_size=args.inference_size,
1032
+ verbose=args.verbose,
1033
+ n_patch_extraction_workers=args.n_patch_extraction_workers,
1034
+ loader_workers=args.loader_workers,
1035
+ use_image_queue=use_image_queue)
995
1036
 
996
1037
  if __name__ == '__main__':
997
1038
  main()
@@ -138,8 +138,8 @@ class TFDetector:
138
138
  image_id,
139
139
  detection_threshold,
140
140
  image_size=None,
141
- skip_image_resizing=False,
142
- augment=False):
141
+ augment=False,
142
+ verbose=False):
143
143
  """
144
144
  Runs the detector on an image.
145
145
 
@@ -152,10 +152,9 @@ class TFDetector:
152
152
  image_size (tuple, optional): image size to use for inference, only mess with this
153
153
  if (a) you're using a model other than MegaDetector or (b) you know what you're
154
154
  doing
155
- skip_image_resizing (bool, optional): whether to skip internal image resizing (and rely on external
156
- resizing). Not currently supported, but included here for compatibility with PTDetector.
157
155
  augment (bool, optional): enable image augmentation. Not currently supported, but included
158
156
  here for compatibility with PTDetector.
157
+ verbose (bool, optional): enable additional debug output
159
158
 
160
159
  Returns:
161
160
  dict: a dictionary with the following fields:
@@ -166,7 +165,6 @@ class TFDetector:
166
165
  """
167
166
 
168
167
  assert image_size is None, 'Image sizing not supported for TF detectors'
169
- assert not skip_image_resizing, 'Image sizing not supported for TF detectors'
170
168
  assert not augment, 'Image augmentation is not supported for TF detectors'
171
169
 
172
170
  if detection_threshold is None:
@@ -1168,7 +1168,7 @@ def restrict_to_taxa_list(taxa_list,
1168
1168
  # Convert all NaN values in the "common" column to empty strings
1169
1169
  taxa_list_df['common'] = taxa_list_df['common'].fillna('')
1170
1170
 
1171
- # Create a dictionary mapping latin names to common names
1171
+ # Create a dictionary mapping source Latin names to target common names
1172
1172
  target_latin_to_common = {}
1173
1173
 
1174
1174
  for i_row,row in taxa_list_df.iterrows():
@@ -1332,7 +1332,7 @@ def restrict_to_taxa_list(taxa_list,
1332
1332
  _insert_taxonomy_string(new_taxon_string)
1333
1333
 
1334
1334
 
1335
- ##%% Make sure all species on the allow-list are in the taxonomy
1335
+ ##%% Make sure all taxa on the allow-list are in the taxonomy
1336
1336
 
1337
1337
  n_failed_mappings = 0
1338
1338
 
@@ -1498,7 +1498,8 @@ def restrict_to_taxa_list(taxa_list,
1498
1498
  if (protected_common_names is not None) and \
1499
1499
  (common_name in protected_common_names):
1500
1500
  if verbose:
1501
- print('Not messing with protected category {}'.format(common_name))
1501
+ print('Not messing with protected category {}:\n{}'.format(
1502
+ common_name,input_taxon_string))
1502
1503
  input_category_id_to_output_taxon_string[input_category_id] = \
1503
1504
  input_taxon_string
1504
1505
  continue
@@ -1578,12 +1579,13 @@ def restrict_to_taxa_list(taxa_list,
1578
1579
  output_taxon_string = speciesnet_latin_name_to_taxon_string[target_taxon]
1579
1580
  input_category_id_to_output_taxon_string[input_category_id] = output_taxon_string
1580
1581
 
1581
- # ...for each category
1582
+ # ...for each category (mapping input category IDs to output taxon strings)
1582
1583
 
1583
1584
 
1584
- ##%% Build the new tables
1585
+ ##%% Map input category IDs to output category IDs
1585
1586
 
1586
- speciesnet_taxon_string_to_latin_name = invert_dictionary(speciesnet_latin_name_to_taxon_string)
1587
+ speciesnet_taxon_string_to_latin_name = \
1588
+ invert_dictionary(speciesnet_latin_name_to_taxon_string)
1587
1589
 
1588
1590
  input_category_id_to_output_category_id = {}
1589
1591
  output_taxon_string_to_category_id = {}
@@ -1604,7 +1606,8 @@ def restrict_to_taxa_list(taxa_list,
1604
1606
  if speciesnet_latin_name in speciesnet_latin_name_to_output_common_name:
1605
1607
  custom_common_name = speciesnet_latin_name_to_output_common_name[speciesnet_latin_name]
1606
1608
  if custom_common_name != output_common_name:
1607
- print('Substituting common name {} for {}'.format(custom_common_name,output_common_name))
1609
+ if verbose:
1610
+ print('Substituting common name {} for {}'.format(custom_common_name,output_common_name))
1608
1611
  output_common_name = custom_common_name
1609
1612
 
1610
1613
  # Do we need to create a new output category?
@@ -1625,20 +1628,16 @@ def restrict_to_taxa_list(taxa_list,
1625
1628
  if False:
1626
1629
  original_common_name = \
1627
1630
  input_category_id_to_common_name[input_category_id]
1628
-
1629
1631
  original_taxon_string = \
1630
1632
  input_category_id_to_taxonomy_string[input_category_id]
1631
-
1632
1633
  print('Mapping {} ({}) to:\n{} ({})\n'.format(
1633
1634
  original_common_name,original_taxon_string,
1634
1635
  output_common_name,output_taxon_string))
1635
- print('Mapping {} to {}'.format(
1636
- original_common_name,output_common_name,))
1637
1636
 
1638
- # ...for each category
1637
+ # ...for each category (mapping input category IDs to output category IDs)
1639
1638
 
1640
1639
 
1641
- #%% Remap all category labels
1640
+ ##%% Remap all category labels
1642
1641
 
1643
1642
  assert len(set(output_taxon_string_to_category_id.keys())) == \
1644
1643
  len(set(output_taxon_string_to_category_id.values())), \