megadetector 5.0.25__py3-none-any.whl → 5.0.26__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 (34) hide show
  1. megadetector/data_management/cct_json_utils.py +15 -2
  2. megadetector/data_management/coco_to_yolo.py +53 -31
  3. megadetector/data_management/databases/combine_coco_camera_traps_files.py +7 -3
  4. megadetector/data_management/databases/integrity_check_json_db.py +2 -2
  5. megadetector/data_management/lila/generate_lila_per_image_labels.py +2 -2
  6. megadetector/data_management/lila/test_lila_metadata_urls.py +21 -10
  7. megadetector/data_management/remap_coco_categories.py +60 -11
  8. megadetector/data_management/yolo_to_coco.py +45 -15
  9. megadetector/postprocessing/classification_postprocessing.py +788 -524
  10. megadetector/postprocessing/create_crop_folder.py +95 -33
  11. megadetector/postprocessing/load_api_results.py +4 -1
  12. megadetector/postprocessing/md_to_coco.py +1 -1
  13. megadetector/postprocessing/postprocess_batch_results.py +156 -42
  14. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +3 -8
  15. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
  16. megadetector/postprocessing/separate_detections_into_folders.py +20 -4
  17. megadetector/postprocessing/subset_json_detector_output.py +180 -15
  18. megadetector/postprocessing/validate_batch_results.py +13 -5
  19. megadetector/taxonomy_mapping/map_new_lila_datasets.py +6 -6
  20. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +3 -58
  21. megadetector/taxonomy_mapping/species_lookup.py +45 -2
  22. megadetector/utils/ct_utils.py +4 -2
  23. megadetector/utils/directory_listing.py +1 -1
  24. megadetector/utils/md_tests.py +2 -1
  25. megadetector/utils/path_utils.py +308 -19
  26. megadetector/utils/wi_utils.py +363 -186
  27. megadetector/visualization/visualization_utils.py +2 -1
  28. megadetector/visualization/visualize_db.py +1 -1
  29. megadetector/visualization/visualize_detector_output.py +1 -4
  30. {megadetector-5.0.25.dist-info → megadetector-5.0.26.dist-info}/METADATA +4 -3
  31. {megadetector-5.0.25.dist-info → megadetector-5.0.26.dist-info}/RECORD +34 -34
  32. {megadetector-5.0.25.dist-info → megadetector-5.0.26.dist-info}/WHEEL +1 -1
  33. {megadetector-5.0.25.dist-info → megadetector-5.0.26.dist-info/licenses}/LICENSE +0 -0
  34. {megadetector-5.0.25.dist-info → megadetector-5.0.26.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,7 @@ from collections import defaultdict
18
18
  from functools import partial
19
19
 
20
20
  from megadetector.utils.path_utils import insert_before_extension
21
+ from megadetector.utils.ct_utils import invert_dictionary
21
22
  from megadetector.visualization.visualization_utils import crop_image
22
23
  from megadetector.visualization.visualization_utils import exif_preserving_save
23
24
 
@@ -48,6 +49,11 @@ class CreateCropFolderOptions:
48
49
 
49
50
  #: Whether to use processes ('process') or threads ('thread') for parallelization
50
51
  self.pool_type = 'thread'
52
+
53
+ #: Include only these categories, or None to include all
54
+ #:
55
+ #: options.category_names_to_include = ['animal']
56
+ self.category_names_to_include = None
51
57
 
52
58
 
53
59
  #%% Support functions
@@ -106,7 +112,8 @@ def _generate_crops_for_single_image(crops_this_image,
106
112
 
107
113
  def crop_results_to_image_results(image_results_file_with_crop_ids,
108
114
  crop_results_file,
109
- output_file):
115
+ output_file,
116
+ delete_crop_information=True):
110
117
  """
111
118
  This function is intended to be run after you have:
112
119
 
@@ -115,14 +122,18 @@ def crop_results_to_image_results(image_results_file_with_crop_ids,
115
122
  3. Run a species classifier on those crops
116
123
 
117
124
  This function will take the crop-level results and transform them back
118
- to the original images.
125
+ to the original images. Classification categories, if available, are taken
126
+ from [crop_results_file].
119
127
 
120
128
  Args:
121
129
  image_results_file_with_crop_ids (str): results file for the original images,
122
- containing crop IDs, likely generated via create_crop_folder.
130
+ containing crop IDs, likely generated via create_crop_folder. All
131
+ non-standard fields in this file will be passed along to [output_file].
123
132
  crop_results_file (str): results file for the crop folder
124
133
  output_file (str): ouptut .json file, containing crop-level classifications
125
- mapped back to the image level
134
+ mapped back to the image level.
135
+ delete_crop_information (bool, optional): whether to delete the "crop_id" and
136
+ "crop_filename_relative" fields from each detection, if present.
126
137
  """
127
138
 
128
139
  ##%% Validate inputs
@@ -136,26 +147,45 @@ def crop_results_to_image_results(image_results_file_with_crop_ids,
136
147
 
137
148
  ##%% Read input files
138
149
 
150
+ print('Reading input...')
151
+
139
152
  with open(image_results_file_with_crop_ids,'r') as f:
140
153
  image_results_with_crop_ids = json.load(f)
141
154
  with open(crop_results_file,'r') as f:
142
155
  crop_results = json.load(f)
143
-
144
- assert crop_results['detection_categories'] == \
145
- image_results_with_crop_ids['detection_categories'], \
146
- 'Crop results and image-level results use different detection categories'
147
-
156
+
157
+ # Find all the detection categories that need to be consistent
158
+ used_category_ids = set()
159
+ for im in tqdm(image_results_with_crop_ids['images']):
160
+ if 'detections' not in im or im['detections'] is None:
161
+ continue
162
+ for det in im['detections']:
163
+ if 'crop_id' in det:
164
+ used_category_ids.add(det['category'])
165
+
166
+ # Make sure the categories that matter are consistent across the two files
167
+ for category_id in used_category_ids:
168
+ category_name = image_results_with_crop_ids['detection_categories'][category_id]
169
+ assert category_id in crop_results['detection_categories'] and \
170
+ category_name == crop_results['detection_categories'][category_id], \
171
+ 'Crop results and detection results use incompatible categories'
172
+
148
173
  crop_filename_to_results = {}
149
174
 
150
175
  # im = crop_results['images'][0]
151
176
  for im in crop_results['images']:
152
177
  crop_filename_to_results[im['file']] = im
153
178
 
154
- image_results_with_crop_ids['classification_categories'] = \
155
- crop_results['classification_categories']
179
+ if 'classification_categories' in crop_results:
180
+ image_results_with_crop_ids['classification_categories'] = \
181
+ crop_results['classification_categories']
182
+
183
+ if 'classification_category_descriptions' in crop_results:
184
+ image_results_with_crop_ids['classification_category_descriptions'] = \
185
+ crop_results['classification_category_descriptions']
156
186
 
157
187
 
158
- ##%% Read classifications from crop results
188
+ ##%% Read classifications from crop results, merge into image-level results
159
189
 
160
190
  # im = image_results_with_crop_ids['images'][0]
161
191
  for im in tqdm(image_results_with_crop_ids['images']):
@@ -175,11 +205,18 @@ def crop_results_to_image_results(image_results_file_with_crop_ids,
175
205
  crop_results_this_detection = crop_filename_to_results[crop_filename_relative]
176
206
  assert crop_results_this_detection['file'] == crop_filename_relative
177
207
  assert len(crop_results_this_detection['detections']) == 1
178
- assert crop_results_this_detection['detections'][0]['conf'] == det['conf']
208
+ # Allow a slight confidence difference for the case where output precision was truncated
209
+ assert abs(crop_results_this_detection['detections'][0]['conf'] - det['conf']) < 0.01
179
210
  assert crop_results_this_detection['detections'][0]['category'] == det['category']
180
211
  assert crop_results_this_detection['detections'][0]['bbox'] == [0,0,1,1]
181
212
  det['classifications'] = crop_results_this_detection['detections'][0]['classifications']
182
-
213
+
214
+ if delete_crop_information:
215
+ if 'crop_id' in det:
216
+ del det['crop_id']
217
+ if 'crop_filename_relative' in det:
218
+ del det['crop_filename_relative']
219
+
183
220
  # ...for each detection
184
221
 
185
222
  # ...for each image
@@ -187,6 +224,8 @@ def crop_results_to_image_results(image_results_file_with_crop_ids,
187
224
 
188
225
  ##%% Write output file
189
226
 
227
+ print('Writing output file...')
228
+
190
229
  with open(output_file,'w') as f:
191
230
  json.dump(image_results_with_crop_ids,f,indent=1)
192
231
 
@@ -223,21 +262,35 @@ def create_crop_folder(input_file,
223
262
  assert os.path.isfile(input_file), 'Input file {} not found'.format(input_file)
224
263
  assert os.path.isdir(input_folder), 'Input folder {} not found'.format(input_folder)
225
264
  os.makedirs(output_folder,exist_ok=True)
226
- os.makedirs(os.path.dirname(output_file),exist_ok=True)
265
+ if output_file is not None:
266
+ os.makedirs(os.path.dirname(output_file),exist_ok=True)
227
267
 
228
268
 
229
269
  ##%% Read input
230
270
 
271
+ print('Reading MD results file...')
231
272
  with open(input_file,'r') as f:
232
273
  detection_results = json.load(f)
233
-
234
-
235
- ##%% Make a list crops that we need to create
274
+
275
+ category_ids_to_include = None
276
+
277
+ if options.category_names_to_include is not None:
278
+ category_id_to_name = detection_results['detection_categories']
279
+ category_name_to_id = invert_dictionary(category_id_to_name)
280
+ category_ids_to_include = set()
281
+ for category_name in options.category_names_to_include:
282
+ assert category_name in category_name_to_id, \
283
+ 'Unrecognized category name {}'.format(category_name)
284
+ category_ids_to_include.add(category_name_to_id[category_name])
285
+
286
+ ##%% Make a list of crops that we need to create
236
287
 
237
288
  # Maps input images to list of dicts, with keys 'crop_id','detection'
238
289
  image_fn_relative_to_crops = defaultdict(list)
239
290
  n_crops = 0
240
291
 
292
+ n_detections_excluded_by_category = 0
293
+
241
294
  # im = detection_results['images'][0]
242
295
  for i_image,im in enumerate(detection_results['images']):
243
296
 
@@ -249,27 +302,35 @@ def create_crop_folder(input_file,
249
302
  image_fn_relative = im['file']
250
303
 
251
304
  for i_detection,det in enumerate(detections_this_image):
305
+
306
+ if det['conf'] < options.confidence_threshold:
307
+ continue
308
+
309
+ if (category_ids_to_include is not None) and \
310
+ (det['category'] not in category_ids_to_include):
311
+ n_detections_excluded_by_category += 1
312
+ continue
313
+
314
+ det['crop_id'] = i_detection
252
315
 
253
- if det['conf'] > options.confidence_threshold:
254
-
255
- det['crop_id'] = i_detection
256
-
257
- crop_info = {'image_fn_relative':image_fn_relative,
258
- 'crop_id':i_detection,
259
- 'detection':det}
260
-
261
- crop_filename_relative = _get_crop_filename(image_fn_relative,
262
- crop_info['crop_id'])
263
- det['crop_filename_relative'] = crop_filename_relative
316
+ crop_info = {'image_fn_relative':image_fn_relative,
317
+ 'crop_id':i_detection,
318
+ 'detection':det}
319
+
320
+ crop_filename_relative = _get_crop_filename(image_fn_relative,
321
+ crop_info['crop_id'])
322
+ det['crop_filename_relative'] = crop_filename_relative
264
323
 
265
- image_fn_relative_to_crops[image_fn_relative].append(crop_info)
266
- n_crops += 1
267
-
324
+ image_fn_relative_to_crops[image_fn_relative].append(crop_info)
325
+ n_crops += 1
326
+
268
327
  # ...for each input image
269
328
 
270
329
  print('Prepared a list of {} crops from {} of {} input images'.format(
271
330
  n_crops,len(image_fn_relative_to_crops),len(detection_results['images'])))
272
-
331
+
332
+ if n_detections_excluded_by_category > 0:
333
+ print('Excluded {} detections by category'.format(n_detections_excluded_by_category))
273
334
 
274
335
  ##%% Generate crops
275
336
 
@@ -307,6 +368,7 @@ def create_crop_folder(input_file,
307
368
 
308
369
  # ...if we're using parallel processing
309
370
 
371
+
310
372
  ##%% Write output file
311
373
 
312
374
  if output_file is not None:
@@ -107,6 +107,9 @@ def write_api_results(detection_results_table, other_fields, out_path):
107
107
  images = detection_results_table.to_json(orient='records',
108
108
  double_precision=3)
109
109
  images = json.loads(images)
110
+ for im in images:
111
+ if 'failure' in im and im['failure'] is None:
112
+ del im['failure']
110
113
  fields['images'] = images
111
114
 
112
115
  # Convert the 'version' field back to a string as per format convention
@@ -129,7 +132,7 @@ def write_api_results(detection_results_table, other_fields, out_path):
129
132
  except Exception:
130
133
  print('Warning: error removing max_detection_conf from output')
131
134
  pass
132
-
135
+
133
136
  with open(out_path, 'w') as f:
134
137
  json.dump(fields, f, indent=1)
135
138
 
@@ -181,7 +181,7 @@ def md_to_coco(md_results_file,
181
181
 
182
182
  w = im['width']
183
183
  h = im['height']
184
-
184
+
185
185
  coco_im['width'] = w
186
186
  coco_im['height'] = h
187
187
 
@@ -30,6 +30,7 @@ import time
30
30
  import uuid
31
31
  import warnings
32
32
  import random
33
+ import json
33
34
 
34
35
  from enum import IntEnum
35
36
  from multiprocessing.pool import ThreadPool
@@ -48,8 +49,11 @@ from megadetector.visualization import visualization_utils as vis_utils
48
49
  from megadetector.visualization import plot_utils
49
50
  from megadetector.utils.write_html_image_list import write_html_image_list
50
51
  from megadetector.utils import path_utils
51
- from megadetector.utils.ct_utils import args_to_object, sets_overlap
52
- from megadetector.data_management.cct_json_utils import (CameraTrapJsonUtils, IndexedJsonDb)
52
+ from megadetector.utils.ct_utils import args_to_object
53
+ from megadetector.utils.ct_utils import sets_overlap
54
+ from megadetector.utils.ct_utils import sort_dictionary_by_value
55
+ from megadetector.data_management.cct_json_utils import CameraTrapJsonUtils
56
+ from megadetector.data_management.cct_json_utils import IndexedJsonDb
53
57
  from megadetector.postprocessing.load_api_results import load_api_results
54
58
  from megadetector.detection.run_detector import get_typical_confidence_threshold_from_results
55
59
 
@@ -214,6 +218,15 @@ class PostProcessingOptions:
214
218
  #: Character encoding to use when writing the index HTML html
215
219
  self.output_html_encoding = None
216
220
 
221
+ #: Additional image fields to display in image headers. If this is a list,
222
+ #: we'll include those fields; if this is a dict, we'll use that dict to choose
223
+ #: alternative display names for each field.
224
+ self.additional_image_fields_to_display = None
225
+
226
+ #: If classification results are present, should we include a summary of
227
+ #: classification categories?
228
+ self.include_classification_category_report = True
229
+
217
230
  # ...__init__()
218
231
 
219
232
  # ...PostProcessingOptions
@@ -434,15 +447,6 @@ def _render_bounding_boxes(
434
447
  if options is None:
435
448
  options = PostProcessingOptions()
436
449
 
437
- # Leaving code in place for reading from blob storage, may support this
438
- # in the future.
439
- """
440
- stream = io.BytesIO()
441
- _ = blob_service.get_blob_to_stream(container_name, image_id, stream)
442
- # resize is to display them in this notebook or in the HTML more quickly
443
- image = Image.open(stream).resize(viz_size)
444
- """
445
-
446
450
  image_full_path = None
447
451
 
448
452
  if res in options.rendering_bypass_sets:
@@ -472,10 +476,12 @@ def _render_bounding_boxes(
472
476
  if image is not None:
473
477
 
474
478
  original_size = image.size
475
-
479
+
480
+ # Resize the image if necessary
476
481
  if options.viz_target_width is not None:
477
482
  image = vis_utils.resize_image(image, options.viz_target_width)
478
483
 
484
+ # Render ground truth boxes if necessary
479
485
  if ground_truth_boxes is not None and len(ground_truth_boxes) > 0:
480
486
 
481
487
  # Create class labels like "gt_1" or "gt_27"
@@ -487,8 +493,7 @@ def _render_bounding_boxes(
487
493
  original_size=original_size,label_map=label_map,
488
494
  thickness=4,expansion=4)
489
495
 
490
- # render_detection_bounding_boxes expects either a float or a dict mapping
491
- # category IDs to names.
496
+ # Preprare per-category confidence thresholds
492
497
  if isinstance(options.confidence_threshold,float):
493
498
  rendering_confidence_threshold = options.confidence_threshold
494
499
  else:
@@ -499,12 +504,14 @@ def _render_bounding_boxes(
499
504
  for category_id in category_ids:
500
505
  rendering_confidence_threshold[category_id] = \
501
506
  _get_threshold_for_category_id(category_id, options, detection_categories)
502
-
507
+
508
+ # Render detection boxes
503
509
  vis_utils.render_detection_bounding_boxes(
504
510
  detections, image,
505
511
  label_map=detection_categories,
506
512
  classification_label_map=classification_categories,
507
513
  confidence_threshold=rendering_confidence_threshold,
514
+ classification_confidence_threshold=options.classification_confidence_threshold,
508
515
  thickness=options.line_thickness,
509
516
  expansion=options.box_expansion)
510
517
 
@@ -686,9 +693,11 @@ def _has_positive_detection(detections,options,detection_categories):
686
693
  return found_positive_detection
687
694
 
688
695
 
689
- def _render_image_no_gt(file_info,detection_categories_to_results_name,
690
- detection_categories,classification_categories,
691
- options):
696
+ def _render_image_no_gt(file_info,
697
+ detection_categories_to_results_name,
698
+ detection_categories,
699
+ classification_categories,
700
+ options):
692
701
  """
693
702
  Renders an image (with no ground truth information)
694
703
 
@@ -713,9 +722,15 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
713
722
  Returns None if there are any errors.
714
723
  """
715
724
 
716
- image_relative_path = file_info[0]
717
- max_conf = file_info[1]
718
- detections = file_info[2]
725
+ image_relative_path = file_info['file']
726
+
727
+ # Useful debug snippet
728
+ #
729
+ # if 'filename' in image_relative_path:
730
+ # import pdb; pdb.set_trace()
731
+
732
+ max_conf = file_info['max_detection_conf']
733
+ detections = file_info['detections']
719
734
 
720
735
  # Determine whether any positive detections are present (using a threshold that
721
736
  # may vary by category)
@@ -749,9 +764,31 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
749
764
  assert detection_status == DetectionStatus.DS_ALMOST
750
765
  res = 'almost_detections'
751
766
 
752
- display_name = '<b>Result type</b>: {}, <b>Image</b>: {}, <b>Max conf</b>: {:0.3f}'.format(
767
+ display_name = '<b>Result type</b>: {}, <b>image</b>: {}, <b>max conf</b>: {:0.3f}'.format(
753
768
  res, image_relative_path, max_conf)
754
769
 
770
+ # Are there any bonus fields we need to include in each image header?
771
+ if options.additional_image_fields_to_display is not None:
772
+
773
+ for field_name in options.additional_image_fields_to_display:
774
+
775
+ if field_name in file_info:
776
+
777
+ field_value = file_info[field_name]
778
+
779
+ if (field_value is None) or \
780
+ (isinstance(field_value,float) and np.isnan(field_value)):
781
+ continue
782
+
783
+ # Optionally use a display name that's different from the field name
784
+ if isinstance(options.additional_image_fields_to_display,dict):
785
+ field_display_name = \
786
+ options.additional_image_fields_to_display[field_name]
787
+ else:
788
+ field_display_name = field_name
789
+ field_string = '<b>{}</b>: {}'.format(field_display_name,field_value)
790
+ display_name += ', {}'.format(field_string)
791
+
755
792
  rendering_options = copy.copy(options)
756
793
  if detection_status == DetectionStatus.DS_ALMOST:
757
794
  rendering_options.confidence_threshold = \
@@ -781,17 +818,24 @@ def _render_image_no_gt(file_info,detection_categories_to_results_name,
781
818
  if det['conf'] > max_conf:
782
819
  max_conf = det['conf']
783
820
 
821
+ # We make the decision here that only "detections" (not "almost-detections")
822
+ # will appear on the classification category pages
823
+ detection_threshold = \
824
+ _get_threshold_for_category_id(det['category'], options, detection_categories)
825
+ if det['conf'] < detection_threshold:
826
+ continue
827
+
784
828
  if ('classifications' in det) and (len(det['classifications']) > 0) and \
785
829
  (res != 'non_detections'):
786
830
 
787
- # This is a list of [class,confidence] pairs, sorted by confidence
831
+ # This is a list of [class,confidence] pairs, sorted by classification confidence
788
832
  classifications = det['classifications']
789
833
  top1_class_id = classifications[0][0]
790
834
  top1_class_name = classification_categories[top1_class_id]
791
835
  top1_class_score = classifications[0][1]
792
836
 
793
- # If we either don't have a confidence threshold, or we've met our
794
- # confidence threshold
837
+ # If we either don't have a classification confidence threshold, or
838
+ # we've met our classification confidence threshold
795
839
  if (options.classification_confidence_threshold < 0) or \
796
840
  (top1_class_score >= options.classification_confidence_threshold):
797
841
  class_string = 'class_{}'.format(top1_class_name)
@@ -823,9 +867,9 @@ def _render_image_with_gt(file_info,ground_truth_indexed_db,
823
867
  data format.
824
868
  """
825
869
 
826
- image_relative_path = file_info[0]
827
- max_conf = file_info[1]
828
- detections = file_info[2]
870
+ image_relative_path = file_info['file']
871
+ max_conf = file_info['max_detection_conf']
872
+ detections = file_info['detections']
829
873
 
830
874
  # This should already have been normalized to either '/' or '\'
831
875
 
@@ -971,6 +1015,7 @@ def process_batch_results(options):
971
1015
  print('\n*** Warning: {} images with ambiguous positive/negative status found in ground truth ***\n'.format(
972
1016
  n_ambiguous))
973
1017
 
1018
+
974
1019
  ##%% Load detection (and possibly classification) results
975
1020
 
976
1021
  # If the caller hasn't supplied results, load them
@@ -1028,6 +1073,8 @@ def process_batch_results(options):
1028
1073
  n_positives = 0
1029
1074
  n_almosts = 0
1030
1075
 
1076
+ print('Assigning images to rendering categories')
1077
+
1031
1078
  for i_row,row in tqdm(detections_df.iterrows(),total=len(detections_df)):
1032
1079
 
1033
1080
  detections = row['detections']
@@ -1372,7 +1419,7 @@ def process_batch_results(options):
1372
1419
  for _, row in images_to_visualize.iterrows():
1373
1420
 
1374
1421
  # Filenames should already have been normalized to either '/' or '\'
1375
- files_to_render.append([row['file'], row['max_detection_conf'], row['detections']])
1422
+ files_to_render.append(row.to_dict())
1376
1423
 
1377
1424
  start_time = time.time()
1378
1425
  if options.parallelize_rendering:
@@ -1523,8 +1570,13 @@ def process_batch_results(options):
1523
1570
  len(images_html['class_{}'.format(cname)]))
1524
1571
  index_page += '</div>'
1525
1572
 
1526
- # Close body and html tags
1527
- index_page += '{}</body></html>'.format(options.footer_text)
1573
+ # Write custom footer if it was provided
1574
+ if (options.footer_text is not None) and (len(options.footer_text) > 0):
1575
+ index_page += '{}\n'.format(options.footer_text)
1576
+
1577
+ # Close open html tags
1578
+ index_page += '\n</body></html>\n'
1579
+
1528
1580
  output_html_file = os.path.join(output_dir, 'index.html')
1529
1581
  with open(output_html_file, 'w',
1530
1582
  encoding=options.output_html_encoding) as f:
@@ -1532,7 +1584,7 @@ def process_batch_results(options):
1532
1584
 
1533
1585
  print('Finished writing html to {}'.format(output_html_file))
1534
1586
 
1535
- # ...for each image
1587
+ # ...if we have ground truth
1536
1588
 
1537
1589
 
1538
1590
  ##%% Otherwise, if we don't have ground truth...
@@ -1618,9 +1670,7 @@ def process_batch_results(options):
1618
1670
  assert isinstance(row['detections'],list)
1619
1671
 
1620
1672
  # Filenames should already have been normalized to either '/' or '\'
1621
- files_to_render.append([row['file'],
1622
- row['max_detection_conf'],
1623
- row['detections']])
1673
+ files_to_render.append(row.to_dict())
1624
1674
 
1625
1675
  start_time = time.time()
1626
1676
  if options.parallelize_rendering:
@@ -1691,8 +1741,7 @@ def process_batch_results(options):
1691
1741
  # Write index.html
1692
1742
 
1693
1743
  # We can't just sum these, because image_counts includes images in both their
1694
- # detection and classification classes
1695
- # total_images = sum(image_counts.values())
1744
+ # detection and classification classes
1696
1745
  total_images = 0
1697
1746
  for k in image_counts.keys():
1698
1747
  v = image_counts[k]
@@ -1779,9 +1828,15 @@ def process_batch_results(options):
1779
1828
  else:
1780
1829
  index_page += '<a href="{}">{}</a> ({}, {:.1%})<br/>\n'.format(
1781
1830
  filename,label,image_count,image_fraction)
1782
-
1831
+
1832
+ # ...for each result set
1833
+
1783
1834
  index_page += '</div>\n'
1784
1835
 
1836
+ # If classification information is present and we're supposed to create
1837
+ # a summary of classifications, we'll put it here
1838
+ category_count_footer = None
1839
+
1785
1840
  if has_classification_info:
1786
1841
 
1787
1842
  index_page += '<h3>Species classification results</h3>'
@@ -1810,15 +1865,74 @@ def process_batch_results(options):
1810
1865
  cname, cname.lower(), ccount)
1811
1866
  index_page += '</div>\n'
1812
1867
 
1813
- index_page += '{}</body></html>'.format(options.footer_text)
1868
+ if options.include_classification_category_report:
1869
+
1870
+ # TODO: it's only for silly historical reasons that we re-read
1871
+ # the input file in this case; we're not currently carrying the json
1872
+ # representation around, only the Pandas representation.
1873
+
1874
+ print('Generating classification category report')
1875
+
1876
+ with open(options.md_results_file,'r') as f:
1877
+ d = json.load(f)
1878
+
1879
+ classification_category_to_count = {}
1880
+
1881
+ # im = d['images'][0]
1882
+ for im in d['images']:
1883
+ if 'detections' in im and im['detections'] is not None:
1884
+ for det in im['detections']:
1885
+ if 'classifications' in det:
1886
+ class_id = det['classifications'][0][0]
1887
+ if class_id not in classification_category_to_count:
1888
+ classification_category_to_count[class_id] = 0
1889
+ else:
1890
+ classification_category_to_count[class_id] = \
1891
+ classification_category_to_count[class_id] + 1
1892
+
1893
+ category_name_to_count = {}
1894
+
1895
+ for class_id in classification_category_to_count:
1896
+ category_name = d['classification_categories'][class_id]
1897
+ category_name_to_count[category_name] = \
1898
+ classification_category_to_count[class_id]
1899
+
1900
+ category_name_to_count = sort_dictionary_by_value(
1901
+ category_name_to_count,reverse=True)
1902
+
1903
+ category_count_footer = ''
1904
+ category_count_footer += '<br/>\n'
1905
+ category_count_footer += \
1906
+ '<h3>Category counts (for the whole dataset, not just the sample used for this page)</h3>\n'
1907
+ category_count_footer += '<div class="contentdiv">\n'
1908
+
1909
+ for category_name in category_name_to_count.keys():
1910
+ count = category_name_to_count[category_name]
1911
+ category_count_html = '{}: {}<br>\n'.format(category_name,count)
1912
+ category_count_footer += category_count_html
1913
+
1914
+ category_count_footer += '</div>\n'
1915
+
1916
+ # ...if we're generating a classification category report
1917
+
1918
+ # ...if classification info is present
1919
+
1920
+ if category_count_footer is not None:
1921
+ index_page += category_count_footer + '\n'
1922
+
1923
+ # Write custom footer if it was provided
1924
+ if (options.footer_text is not None) and (len(options.footer_text) > 0):
1925
+ index_page += options.footer_text + '\n'
1926
+
1927
+ # Close open html tags
1928
+ index_page += '\n</body></html>\n'
1929
+
1814
1930
  output_html_file = os.path.join(output_dir, 'index.html')
1815
1931
  with open(output_html_file, 'w',
1816
1932
  encoding=options.output_html_encoding) as f:
1817
1933
  f.write(index_page)
1818
1934
 
1819
- print('Finished writing html to {}'.format(output_html_file))
1820
-
1821
- # os.startfile(output_html_file)
1935
+ print('Finished writing html to {}'.format(output_html_file))
1822
1936
 
1823
1937
  # ...if we do/don't have ground truth
1824
1938
 
@@ -41,7 +41,6 @@ if False:
41
41
  baseDir = ''
42
42
 
43
43
  options = repeat_detections_core.RepeatDetectionOptions()
44
- options.bRenderHtml = True
45
44
  options.imageBase = baseDir
46
45
  options.outputBase = os.path.join(baseDir, 'repeat_detections')
47
46
  options.filenameReplacements = {} # E.g., {'20190430cameratraps\\':''}
@@ -85,11 +84,10 @@ def main():
85
84
  'do manual review of the repeat detection images (which you should)')
86
85
 
87
86
  parser.add_argument('--imageBase', action='store', type=str, default='',
88
- help='Image base dir, relevant if renderHtml is True or if ' + \
89
- '"omitFilteringFolder" is not set')
87
+ help='Image base dir')
90
88
 
91
89
  parser.add_argument('--outputBase', action='store', type=str, default='',
92
- help='HTML or filtering folder output dir')
90
+ help='filtering folder output dir')
93
91
 
94
92
  parser.add_argument('--confidenceMin', action='store', type=float,
95
93
  default=defaultOptions.confidenceMin,
@@ -146,7 +144,7 @@ def main():
146
144
 
147
145
  parser.add_argument('--omitFilteringFolder', action='store_false',
148
146
  dest='bWriteFilteringFolder',
149
- help='Should we create a folder of rendered detections for post-filtering?')
147
+ help='Should we skip creating the folder of rendered detections filtering?')
150
148
 
151
149
  parser.add_argument('--debugMaxDir', action='store', type=int, default=-1,
152
150
  help='For debugging only, limit the number of directories we process')
@@ -191,9 +189,6 @@ def main():
191
189
  default=defaultOptions.detectionTilesPrimaryImageWidth,
192
190
  help='The width of the main image when rendering images with detection tiles')
193
191
 
194
- parser.add_argument('--renderHtml', action='store_true',
195
- dest='bRenderHtml', help='Should we render HTML output?')
196
-
197
192
  if len(sys.argv[1:]) == 0:
198
193
  parser.print_help()
199
194
  parser.exit()