megadetector 5.0.25__py3-none-any.whl → 5.0.27__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/cct_json_utils.py +15 -2
- megadetector/data_management/coco_to_yolo.py +53 -31
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +7 -3
- megadetector/data_management/databases/integrity_check_json_db.py +2 -2
- megadetector/data_management/lila/generate_lila_per_image_labels.py +2 -2
- megadetector/data_management/lila/test_lila_metadata_urls.py +21 -10
- megadetector/data_management/remap_coco_categories.py +60 -11
- megadetector/data_management/yolo_to_coco.py +45 -15
- megadetector/postprocessing/classification_postprocessing.py +788 -524
- megadetector/postprocessing/create_crop_folder.py +95 -33
- megadetector/postprocessing/load_api_results.py +4 -1
- megadetector/postprocessing/md_to_coco.py +1 -1
- megadetector/postprocessing/postprocess_batch_results.py +156 -42
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +3 -8
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
- megadetector/postprocessing/separate_detections_into_folders.py +20 -4
- megadetector/postprocessing/subset_json_detector_output.py +180 -15
- megadetector/postprocessing/validate_batch_results.py +13 -5
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +6 -6
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +3 -58
- megadetector/taxonomy_mapping/species_lookup.py +45 -2
- megadetector/utils/ct_utils.py +4 -2
- megadetector/utils/directory_listing.py +1 -1
- megadetector/utils/md_tests.py +2 -1
- megadetector/utils/path_utils.py +308 -19
- megadetector/utils/wi_utils.py +363 -186
- megadetector/visualization/visualization_utils.py +2 -1
- megadetector/visualization/visualize_db.py +1 -1
- megadetector/visualization/visualize_detector_output.py +1 -4
- {megadetector-5.0.25.dist-info → megadetector-5.0.27.dist-info}/METADATA +4 -3
- {megadetector-5.0.25.dist-info → megadetector-5.0.27.dist-info}/RECORD +34 -34
- {megadetector-5.0.25.dist-info → megadetector-5.0.27.dist-info}/WHEEL +1 -1
- {megadetector-5.0.25.dist-info → megadetector-5.0.27.dist-info/licenses}/LICENSE +0 -0
- {megadetector-5.0.25.dist-info → megadetector-5.0.27.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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
|
|
@@ -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
|
|
52
|
-
from megadetector.
|
|
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
|
-
#
|
|
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,
|
|
690
|
-
|
|
691
|
-
|
|
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[
|
|
717
|
-
|
|
718
|
-
|
|
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>
|
|
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
|
|
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[
|
|
827
|
-
max_conf = file_info[
|
|
828
|
-
detections = file_info[
|
|
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(
|
|
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
|
-
#
|
|
1527
|
-
|
|
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
|
-
# ...
|
|
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(
|
|
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
|
-
|
|
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
|
|
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='
|
|
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
|
|
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()
|