megadetector 5.0.28__py3-none-any.whl → 5.0.29__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/api/batch_processing/api_core/batch_service/score.py +4 -5
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +1 -1
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +1 -1
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
- megadetector/api/synchronous/api_core/tests/load_test.py +2 -3
- megadetector/classification/aggregate_classifier_probs.py +3 -3
- megadetector/classification/analyze_failed_images.py +5 -5
- megadetector/classification/cache_batchapi_outputs.py +5 -5
- megadetector/classification/create_classification_dataset.py +11 -12
- megadetector/classification/crop_detections.py +10 -10
- megadetector/classification/csv_to_json.py +8 -8
- megadetector/classification/detect_and_crop.py +13 -15
- megadetector/classification/evaluate_model.py +7 -7
- megadetector/classification/identify_mislabeled_candidates.py +6 -6
- megadetector/classification/json_to_azcopy_list.py +1 -1
- megadetector/classification/json_validator.py +29 -32
- megadetector/classification/map_classification_categories.py +9 -9
- megadetector/classification/merge_classification_detection_output.py +12 -9
- megadetector/classification/prepare_classification_script.py +19 -19
- megadetector/classification/prepare_classification_script_mc.py +23 -23
- megadetector/classification/run_classifier.py +4 -4
- megadetector/classification/save_mislabeled.py +6 -6
- megadetector/classification/train_classifier.py +1 -1
- megadetector/classification/train_classifier_tf.py +9 -9
- megadetector/classification/train_utils.py +10 -10
- megadetector/data_management/annotations/annotation_constants.py +1 -1
- megadetector/data_management/camtrap_dp_to_coco.py +45 -45
- megadetector/data_management/cct_json_utils.py +101 -101
- megadetector/data_management/cct_to_md.py +49 -49
- megadetector/data_management/cct_to_wi.py +33 -33
- megadetector/data_management/coco_to_labelme.py +75 -75
- megadetector/data_management/coco_to_yolo.py +189 -189
- megadetector/data_management/databases/add_width_and_height_to_db.py +3 -2
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +38 -38
- megadetector/data_management/databases/integrity_check_json_db.py +202 -188
- megadetector/data_management/databases/subset_json_db.py +33 -33
- megadetector/data_management/generate_crops_from_cct.py +38 -38
- megadetector/data_management/get_image_sizes.py +54 -49
- megadetector/data_management/labelme_to_coco.py +130 -124
- megadetector/data_management/labelme_to_yolo.py +78 -72
- megadetector/data_management/lila/create_lila_blank_set.py +81 -83
- megadetector/data_management/lila/create_lila_test_set.py +32 -31
- megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
- megadetector/data_management/lila/download_lila_subset.py +21 -24
- megadetector/data_management/lila/generate_lila_per_image_labels.py +91 -91
- megadetector/data_management/lila/get_lila_annotation_counts.py +30 -30
- megadetector/data_management/lila/get_lila_image_counts.py +22 -22
- megadetector/data_management/lila/lila_common.py +70 -70
- megadetector/data_management/lila/test_lila_metadata_urls.py +13 -14
- megadetector/data_management/mewc_to_md.py +339 -340
- megadetector/data_management/ocr_tools.py +258 -252
- megadetector/data_management/read_exif.py +231 -224
- megadetector/data_management/remap_coco_categories.py +26 -26
- megadetector/data_management/remove_exif.py +31 -20
- megadetector/data_management/rename_images.py +187 -187
- megadetector/data_management/resize_coco_dataset.py +41 -41
- megadetector/data_management/speciesnet_to_md.py +41 -41
- megadetector/data_management/wi_download_csv_to_coco.py +55 -55
- megadetector/data_management/yolo_output_to_md_output.py +117 -120
- megadetector/data_management/yolo_to_coco.py +195 -188
- megadetector/detection/change_detection.py +831 -0
- megadetector/detection/process_video.py +340 -337
- megadetector/detection/pytorch_detector.py +304 -262
- megadetector/detection/run_detector.py +177 -164
- megadetector/detection/run_detector_batch.py +364 -363
- megadetector/detection/run_inference_with_yolov5_val.py +328 -325
- megadetector/detection/run_tiled_inference.py +256 -249
- megadetector/detection/tf_detector.py +24 -24
- megadetector/detection/video_utils.py +290 -282
- megadetector/postprocessing/add_max_conf.py +15 -11
- megadetector/postprocessing/categorize_detections_by_size.py +44 -44
- megadetector/postprocessing/classification_postprocessing.py +415 -415
- megadetector/postprocessing/combine_batch_outputs.py +20 -21
- megadetector/postprocessing/compare_batch_results.py +528 -517
- megadetector/postprocessing/convert_output_format.py +97 -97
- megadetector/postprocessing/create_crop_folder.py +219 -146
- megadetector/postprocessing/detector_calibration.py +173 -168
- megadetector/postprocessing/generate_csv_report.py +508 -499
- megadetector/postprocessing/load_api_results.py +23 -20
- megadetector/postprocessing/md_to_coco.py +129 -98
- megadetector/postprocessing/md_to_labelme.py +89 -83
- megadetector/postprocessing/md_to_wi.py +40 -40
- megadetector/postprocessing/merge_detections.py +87 -114
- megadetector/postprocessing/postprocess_batch_results.py +313 -298
- megadetector/postprocessing/remap_detection_categories.py +36 -36
- megadetector/postprocessing/render_detection_confusion_matrix.py +205 -199
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +702 -677
- megadetector/postprocessing/separate_detections_into_folders.py +226 -211
- megadetector/postprocessing/subset_json_detector_output.py +265 -262
- megadetector/postprocessing/top_folders_to_bottom.py +45 -45
- megadetector/postprocessing/validate_batch_results.py +70 -70
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -15
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +14 -14
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +66 -66
- megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
- megadetector/taxonomy_mapping/simple_image_download.py +8 -8
- megadetector/taxonomy_mapping/species_lookup.py +33 -33
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
- megadetector/taxonomy_mapping/taxonomy_graph.py +10 -10
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
- megadetector/utils/azure_utils.py +22 -22
- megadetector/utils/ct_utils.py +1018 -200
- megadetector/utils/directory_listing.py +21 -77
- megadetector/utils/gpu_test.py +22 -22
- megadetector/utils/md_tests.py +541 -518
- megadetector/utils/path_utils.py +1457 -398
- megadetector/utils/process_utils.py +41 -41
- megadetector/utils/sas_blob_utils.py +53 -49
- megadetector/utils/split_locations_into_train_val.py +61 -61
- megadetector/utils/string_utils.py +147 -26
- megadetector/utils/url_utils.py +463 -173
- megadetector/utils/wi_utils.py +2629 -2526
- megadetector/utils/write_html_image_list.py +137 -137
- megadetector/visualization/plot_utils.py +21 -21
- megadetector/visualization/render_images_with_thumbnails.py +37 -73
- megadetector/visualization/visualization_utils.py +401 -397
- megadetector/visualization/visualize_db.py +197 -190
- megadetector/visualization/visualize_detector_output.py +79 -73
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/METADATA +135 -132
- megadetector-5.0.29.dist-info/RECORD +163 -0
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
- {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
- megadetector/data_management/importers/add_nacti_sizes.py +0 -52
- megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
- megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
- megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
- megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
- megadetector/data_management/importers/awc_to_json.py +0 -191
- megadetector/data_management/importers/bellevue_to_json.py +0 -272
- megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
- megadetector/data_management/importers/cct_field_adjustments.py +0 -58
- megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
- megadetector/data_management/importers/ena24_to_json.py +0 -276
- megadetector/data_management/importers/filenames_to_json.py +0 -386
- megadetector/data_management/importers/helena_to_cct.py +0 -283
- megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
- megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
- megadetector/data_management/importers/jb_csv_to_json.py +0 -150
- megadetector/data_management/importers/mcgill_to_json.py +0 -250
- megadetector/data_management/importers/missouri_to_json.py +0 -490
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
- megadetector/data_management/importers/noaa_seals_2019.py +0 -181
- megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
- megadetector/data_management/importers/pc_to_json.py +0 -365
- megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
- megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
- megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
- megadetector/data_management/importers/rspb_to_json.py +0 -356
- megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
- megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
- megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
- megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
- megadetector/data_management/importers/sulross_get_exif.py +0 -65
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
- megadetector/data_management/importers/ubc_to_json.py +0 -399
- megadetector/data_management/importers/umn_to_json.py +0 -507
- megadetector/data_management/importers/wellington_to_json.py +0 -263
- megadetector/data_management/importers/wi_to_json.py +0 -442
- megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
- megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
- megadetector-5.0.28.dist-info/RECORD +0 -209
|
@@ -6,15 +6,15 @@ run_tiled_inference.py
|
|
|
6
6
|
|
|
7
7
|
Runs inference on a folder, fist splitting each image up into tiles of size
|
|
8
8
|
MxN (typically the native inference size of your detector), writing those
|
|
9
|
-
tiles out to a temporary folder, then de-duplicating the resulting detections before
|
|
9
|
+
tiles out to a temporary folder, then de-duplicating the resulting detections before
|
|
10
10
|
merging them back into a set of detections that make sense on the original images.
|
|
11
11
|
|
|
12
|
-
This approach will likely fail to detect very large animals, so if you expect both large
|
|
13
|
-
and small animals (in terms of pixel size), this script is best used in
|
|
12
|
+
This approach will likely fail to detect very large animals, so if you expect both large
|
|
13
|
+
and small animals (in terms of pixel size), this script is best used in
|
|
14
14
|
conjunction with a traditional inference pass that looks at whole images.
|
|
15
15
|
|
|
16
16
|
Currently requires temporary storage at least as large as the input data, generally
|
|
17
|
-
a lot more than that (depending on the overlap between adjacent tiles). This is
|
|
17
|
+
a lot more than that (depending on the overlap between adjacent tiles). This is
|
|
18
18
|
inefficient, but easy to debug.
|
|
19
19
|
|
|
20
20
|
Programmatic invocation supports using YOLOv5's inference scripts (and test-time
|
|
@@ -28,6 +28,8 @@ import os
|
|
|
28
28
|
import json
|
|
29
29
|
import tempfile
|
|
30
30
|
import uuid
|
|
31
|
+
import sys
|
|
32
|
+
import argparse
|
|
31
33
|
|
|
32
34
|
from tqdm import tqdm
|
|
33
35
|
|
|
@@ -63,59 +65,59 @@ def get_patch_boundaries(image_size,patch_size,patch_stride=None):
|
|
|
63
65
|
"""
|
|
64
66
|
Computes a list of patch starting coordinates (x,y) given an image size (w,h)
|
|
65
67
|
and a stride (x,y)
|
|
66
|
-
|
|
68
|
+
|
|
67
69
|
Patch size is guaranteed, but the stride may deviate to make sure all pixels are covered.
|
|
68
70
|
I.e., we move by regular strides until the current patch walks off the right/bottom,
|
|
69
71
|
at which point it backs up to one patch from the end. So if your image is 15
|
|
70
|
-
pixels wide and you have a stride of 10 pixels, you will get starting positions
|
|
72
|
+
pixels wide and you have a stride of 10 pixels, you will get starting positions
|
|
71
73
|
of 0 (from 0 to 9) and 5 (from 5 to 14).
|
|
72
|
-
|
|
74
|
+
|
|
73
75
|
Args:
|
|
74
76
|
image_size (tuple): size of the image you want to divide into patches, as a length-2 tuple (w,h)
|
|
75
77
|
patch_size (tuple): patch size into which you want to divide an image, as a length-2 tuple (w,h)
|
|
76
|
-
patch_stride (tuple or float, optional): stride between patches, as a length-2 tuple (x,y), or a
|
|
77
|
-
float; if this is a float, it's interpreted as the stride relative to the patch size
|
|
78
|
+
patch_stride (tuple or float, optional): stride between patches, as a length-2 tuple (x,y), or a
|
|
79
|
+
float; if this is a float, it's interpreted as the stride relative to the patch size
|
|
78
80
|
(0.1 == 10% stride). Defaults to half the patch size.
|
|
79
81
|
|
|
80
82
|
Returns:
|
|
81
|
-
list: list of length-2 tuples, each representing the x/y start position of a patch
|
|
83
|
+
list: list of length-2 tuples, each representing the x/y start position of a patch
|
|
82
84
|
"""
|
|
83
|
-
|
|
85
|
+
|
|
84
86
|
if patch_stride is None:
|
|
85
87
|
patch_stride = (round(patch_size[0]*(1.0-default_patch_overlap)),
|
|
86
88
|
round(patch_size[1]*(1.0-default_patch_overlap)))
|
|
87
89
|
elif isinstance(patch_stride,float):
|
|
88
90
|
patch_stride = (round(patch_size[0]*(patch_stride)),
|
|
89
91
|
round(patch_size[1]*(patch_stride)))
|
|
90
|
-
|
|
92
|
+
|
|
91
93
|
image_width = image_size[0]
|
|
92
94
|
image_height = image_size[1]
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
assert patch_size[0] <= image_size[0], 'Patch width {} is larger than image width {}'.format(
|
|
95
97
|
patch_size[0],image_size[0])
|
|
96
98
|
assert patch_size[1] <= image_size[1], 'Patch height {} is larger than image height {}'.format(
|
|
97
99
|
patch_size[1],image_size[1])
|
|
98
|
-
|
|
100
|
+
|
|
99
101
|
def add_patch_row(patch_start_positions,y_start):
|
|
100
102
|
"""
|
|
101
103
|
Add one row to our list of patch start positions, i.e.
|
|
102
104
|
loop over all columns.
|
|
103
105
|
"""
|
|
104
|
-
|
|
106
|
+
|
|
105
107
|
x_start = 0; x_end = x_start + patch_size[0] - 1
|
|
106
|
-
|
|
108
|
+
|
|
107
109
|
while(True):
|
|
108
|
-
|
|
110
|
+
|
|
109
111
|
patch_start_positions.append([x_start,y_start])
|
|
110
|
-
|
|
112
|
+
|
|
111
113
|
# If this patch put us right at the end of the last column, we're done
|
|
112
114
|
if x_end == image_width - 1:
|
|
113
115
|
break
|
|
114
|
-
|
|
116
|
+
|
|
115
117
|
# Move one patch to the right
|
|
116
118
|
x_start += patch_stride[0]
|
|
117
119
|
x_end = x_start + patch_size[0] - 1
|
|
118
|
-
|
|
120
|
+
|
|
119
121
|
# If this patch flows over the edge, add one more patch to cover
|
|
120
122
|
# the pixels on the end, then we're done.
|
|
121
123
|
if x_end > (image_width - 1):
|
|
@@ -124,27 +126,27 @@ def get_patch_boundaries(image_size,patch_size,patch_stride=None):
|
|
|
124
126
|
x_end = x_start + patch_size[0] - 1
|
|
125
127
|
patch_start_positions.append([x_start,y_start])
|
|
126
128
|
break
|
|
127
|
-
|
|
129
|
+
|
|
128
130
|
# ...for each column
|
|
129
|
-
|
|
131
|
+
|
|
130
132
|
return patch_start_positions
|
|
131
|
-
|
|
133
|
+
|
|
132
134
|
patch_start_positions = []
|
|
133
|
-
|
|
135
|
+
|
|
134
136
|
y_start = 0; y_end = y_start + patch_size[1] - 1
|
|
135
|
-
|
|
137
|
+
|
|
136
138
|
while(True):
|
|
137
|
-
|
|
139
|
+
|
|
138
140
|
patch_start_positions = add_patch_row(patch_start_positions,y_start)
|
|
139
|
-
|
|
141
|
+
|
|
140
142
|
# If this patch put us right at the bottom of the lats row, we're done
|
|
141
143
|
if y_end == image_height - 1:
|
|
142
144
|
break
|
|
143
|
-
|
|
145
|
+
|
|
144
146
|
# Move one patch down
|
|
145
147
|
y_start += patch_stride[1]
|
|
146
148
|
y_end = y_start + patch_size[1] - 1
|
|
147
|
-
|
|
149
|
+
|
|
148
150
|
# If this patch flows over the bottom, add one more patch to cover
|
|
149
151
|
# the pixels at the bottom, then we're done
|
|
150
152
|
if y_end > (image_height - 1):
|
|
@@ -153,24 +155,24 @@ def get_patch_boundaries(image_size,patch_size,patch_stride=None):
|
|
|
153
155
|
y_end = y_start + patch_size[1] - 1
|
|
154
156
|
patch_start_positions = add_patch_row(patch_start_positions,y_start)
|
|
155
157
|
break
|
|
156
|
-
|
|
158
|
+
|
|
157
159
|
# ...for each row
|
|
158
|
-
|
|
160
|
+
|
|
159
161
|
for p in patch_start_positions:
|
|
160
162
|
assert p[0] >= 0 and p[1] >= 0 and p[0] <= image_width and p[1] <= image_height, \
|
|
161
163
|
'Patch generation error (illegal patch {})'.format(p)
|
|
162
|
-
|
|
164
|
+
|
|
163
165
|
# The last patch should always end at the bottom-right of the image
|
|
164
166
|
assert patch_start_positions[-1][0]+patch_size[0] == image_width, \
|
|
165
167
|
'Patch generation error (last patch does not end on the right)'
|
|
166
168
|
assert patch_start_positions[-1][1]+patch_size[1] == image_height, \
|
|
167
169
|
'Patch generation error (last patch does not end at the bottom)'
|
|
168
|
-
|
|
170
|
+
|
|
169
171
|
# All patches should be unique
|
|
170
172
|
patch_start_positions_tuples = [tuple(x) for x in patch_start_positions]
|
|
171
173
|
assert len(patch_start_positions_tuples) == len(set(patch_start_positions_tuples)), \
|
|
172
174
|
'Patch generation error (duplicate start position)'
|
|
173
|
-
|
|
175
|
+
|
|
174
176
|
return patch_start_positions
|
|
175
177
|
|
|
176
178
|
# ...get_patch_boundaries()
|
|
@@ -180,12 +182,12 @@ def patch_info_to_patch_name(image_name,patch_x_min,patch_y_min):
|
|
|
180
182
|
"""
|
|
181
183
|
Gives a unique string name to an x/y coordinate, e.g. turns ("a.jpg",10,20) into
|
|
182
184
|
"a.jpg_0010_0020".
|
|
183
|
-
|
|
185
|
+
|
|
184
186
|
Args:
|
|
185
187
|
image_name (str): image identifier
|
|
186
188
|
patch_x_min (int): x coordinate
|
|
187
189
|
patch_y_min (int): y coordinate
|
|
188
|
-
|
|
190
|
+
|
|
189
191
|
Returns:
|
|
190
192
|
str: name for this patch, e.g. "a.jpg_0010_0020"
|
|
191
193
|
"""
|
|
@@ -203,13 +205,13 @@ def extract_patch_from_image(im,
|
|
|
203
205
|
overwrite=True):
|
|
204
206
|
"""
|
|
205
207
|
Extracts a patch from the provided image, and writes that patch out to a new file.
|
|
206
|
-
|
|
208
|
+
|
|
207
209
|
Args:
|
|
208
210
|
im (str or Image): image from which we should extract a patch, can be a filename or
|
|
209
211
|
a PIL Image object.
|
|
210
|
-
patch_xy (tuple): length-2 tuple of ints (x,y) representing the upper-left corner
|
|
212
|
+
patch_xy (tuple): length-2 tuple of ints (x,y) representing the upper-left corner
|
|
211
213
|
of the patch to extract
|
|
212
|
-
patch_size (tuple): length-2 tuple of ints (w,h) representing the size of the
|
|
214
|
+
patch_size (tuple): length-2 tuple of ints (w,h) representing the size of the
|
|
213
215
|
patch to extract
|
|
214
216
|
patch_image_fn (str, optional): image filename to write the patch to; if this is None
|
|
215
217
|
the filename will be generated from [image_name] and the patch coordinates
|
|
@@ -218,16 +220,16 @@ def extract_patch_from_image(im,
|
|
|
218
220
|
image_name (str, optional): the identifier of the source image; only used to generate
|
|
219
221
|
a patch filename, so only required if [patch_image_fn] is None
|
|
220
222
|
overwrite (bool, optional): whether to overwrite an existing patch image
|
|
221
|
-
|
|
223
|
+
|
|
222
224
|
Returns:
|
|
223
225
|
dict: a dictionary with fields xmin,xmax,ymin,ymax,patch_fn
|
|
224
226
|
"""
|
|
225
|
-
|
|
227
|
+
|
|
226
228
|
if isinstance(im,str):
|
|
227
229
|
pil_im = vis_utils.open_image(im)
|
|
228
230
|
else:
|
|
229
231
|
pil_im = im
|
|
230
|
-
|
|
232
|
+
|
|
231
233
|
patch_x_min = patch_xy[0]
|
|
232
234
|
patch_y_min = patch_xy[1]
|
|
233
235
|
patch_x_max = patch_x_min + patch_size[0] - 1
|
|
@@ -249,19 +251,19 @@ def extract_patch_from_image(im,
|
|
|
249
251
|
"If you don't supply a patch filename to extract_patch_from_image, you need to supply a folder name"
|
|
250
252
|
patch_name = patch_info_to_patch_name(image_name,patch_x_min,patch_y_min)
|
|
251
253
|
patch_image_fn = os.path.join(patch_folder,patch_name + '.jpg')
|
|
252
|
-
|
|
254
|
+
|
|
253
255
|
if os.path.isfile(patch_image_fn) and (not overwrite):
|
|
254
256
|
pass
|
|
255
|
-
else:
|
|
257
|
+
else:
|
|
256
258
|
patch_im.save(patch_image_fn,quality=patch_jpeg_quality)
|
|
257
|
-
|
|
259
|
+
|
|
258
260
|
patch_info = {}
|
|
259
261
|
patch_info['xmin'] = patch_x_min
|
|
260
262
|
patch_info['xmax'] = patch_x_max
|
|
261
263
|
patch_info['ymin'] = patch_y_min
|
|
262
264
|
patch_info['ymax'] = patch_y_max
|
|
263
265
|
patch_info['patch_fn'] = patch_image_fn
|
|
264
|
-
|
|
266
|
+
|
|
265
267
|
return patch_info
|
|
266
268
|
|
|
267
269
|
# ...def extract_patch_from_image(...)
|
|
@@ -270,33 +272,33 @@ def extract_patch_from_image(im,
|
|
|
270
272
|
def in_place_nms(md_results, iou_thres=0.45, verbose=True):
|
|
271
273
|
"""
|
|
272
274
|
Run torch.ops.nms in-place on MD-formatted detection results.
|
|
273
|
-
|
|
275
|
+
|
|
274
276
|
Args:
|
|
275
|
-
md_results (dict): detection results for a list of images, in MD results format (i.e.,
|
|
277
|
+
md_results (dict): detection results for a list of images, in MD results format (i.e.,
|
|
276
278
|
containing a list of image dicts with the key 'images', each of which has a list
|
|
277
279
|
of detections with the key 'detections')
|
|
278
280
|
iou_thres (float, optional): IoU threshold above which we will treat two detections as
|
|
279
281
|
redundant
|
|
280
282
|
verbose (bool, optional): enable additional debug console output
|
|
281
283
|
"""
|
|
282
|
-
|
|
284
|
+
|
|
283
285
|
n_detections_before = 0
|
|
284
286
|
n_detections_after = 0
|
|
285
|
-
|
|
287
|
+
|
|
286
288
|
# i_image = 18; im = md_results['images'][i_image]
|
|
287
289
|
for i_image,im in tqdm(enumerate(md_results['images']),total=len(md_results['images'])):
|
|
288
|
-
|
|
290
|
+
|
|
289
291
|
if (im['detections'] is None) or (len(im['detections']) == 0):
|
|
290
292
|
continue
|
|
291
|
-
|
|
293
|
+
|
|
292
294
|
boxes = []
|
|
293
295
|
scores = []
|
|
294
|
-
|
|
296
|
+
|
|
295
297
|
n_detections_before += len(im['detections'])
|
|
296
|
-
|
|
298
|
+
|
|
297
299
|
# det = im['detections'][0]
|
|
298
300
|
for det in im['detections']:
|
|
299
|
-
|
|
301
|
+
|
|
300
302
|
# Using x1/x2 notation rather than x0/x1 notation to be consistent
|
|
301
303
|
# with the Torch documentation.
|
|
302
304
|
x1 = det['bbox'][0]
|
|
@@ -308,86 +310,86 @@ def in_place_nms(md_results, iou_thres=0.45, verbose=True):
|
|
|
308
310
|
scores.append(det['conf'])
|
|
309
311
|
|
|
310
312
|
# ...for each detection
|
|
311
|
-
|
|
313
|
+
|
|
312
314
|
t_boxes = torch.tensor(boxes)
|
|
313
315
|
t_scores = torch.tensor(scores)
|
|
314
|
-
|
|
316
|
+
|
|
315
317
|
box_indices = ops.nms(t_boxes,t_scores,iou_thres).tolist()
|
|
316
|
-
|
|
318
|
+
|
|
317
319
|
post_nms_detections = [im['detections'][x] for x in box_indices]
|
|
318
|
-
|
|
320
|
+
|
|
319
321
|
assert len(post_nms_detections) <= len(im['detections'])
|
|
320
|
-
|
|
322
|
+
|
|
321
323
|
im['detections'] = post_nms_detections
|
|
322
|
-
|
|
324
|
+
|
|
323
325
|
n_detections_after += len(im['detections'])
|
|
324
|
-
|
|
326
|
+
|
|
325
327
|
# ...for each image
|
|
326
|
-
|
|
328
|
+
|
|
327
329
|
if verbose:
|
|
328
330
|
print('NMS removed {} of {} detections'.format(
|
|
329
331
|
n_detections_before-n_detections_after,
|
|
330
332
|
n_detections_before))
|
|
331
|
-
|
|
333
|
+
|
|
332
334
|
# ...in_place_nms()
|
|
333
335
|
|
|
334
336
|
|
|
335
337
|
def _extract_tiles_for_image(fn_relative,image_folder,tiling_folder,patch_size,patch_stride,overwrite):
|
|
336
338
|
"""
|
|
337
339
|
Private function to extract tiles for a single image.
|
|
338
|
-
|
|
340
|
+
|
|
339
341
|
Returns a dict with fields 'patches' (see extract_patch_from_image) and 'image_fn'.
|
|
340
|
-
|
|
342
|
+
|
|
341
343
|
If there is an error, 'patches' will be None and the 'error' field will contain
|
|
342
344
|
failure details. In that case, some tiles may still be generated.
|
|
343
345
|
"""
|
|
344
|
-
|
|
346
|
+
|
|
345
347
|
fn_abs = os.path.join(image_folder,fn_relative)
|
|
346
348
|
error = None
|
|
347
|
-
patches = []
|
|
348
|
-
|
|
349
|
+
patches = []
|
|
350
|
+
|
|
349
351
|
image_name = path_utils.clean_filename(fn_relative,char_limit=None,force_lower=True)
|
|
350
|
-
|
|
352
|
+
|
|
351
353
|
try:
|
|
352
|
-
|
|
354
|
+
|
|
353
355
|
# Open the image
|
|
354
356
|
im = vis_utils.open_image(fn_abs)
|
|
355
357
|
image_size = [im.width,im.height]
|
|
356
|
-
|
|
358
|
+
|
|
357
359
|
# Generate patch boundaries (a list of [x,y] starting points)
|
|
358
|
-
patch_boundaries = get_patch_boundaries(image_size,patch_size,patch_stride)
|
|
359
|
-
|
|
360
|
+
patch_boundaries = get_patch_boundaries(image_size,patch_size,patch_stride)
|
|
361
|
+
|
|
360
362
|
# Extract patches
|
|
361
363
|
#
|
|
362
|
-
# patch_xy = patch_boundaries[0]
|
|
364
|
+
# patch_xy = patch_boundaries[0]
|
|
363
365
|
for patch_xy in patch_boundaries:
|
|
364
|
-
|
|
366
|
+
|
|
365
367
|
patch_info = extract_patch_from_image(im,patch_xy,patch_size,
|
|
366
368
|
patch_folder=tiling_folder,
|
|
367
369
|
image_name=image_name,
|
|
368
370
|
overwrite=overwrite)
|
|
369
371
|
patch_info['source_fn'] = fn_relative
|
|
370
372
|
patches.append(patch_info)
|
|
371
|
-
|
|
373
|
+
|
|
372
374
|
except Exception as e:
|
|
373
|
-
|
|
375
|
+
|
|
374
376
|
s = 'Patch generation error for {}: \n{}'.format(fn_relative,str(e))
|
|
375
377
|
print(s)
|
|
376
378
|
# patches = None
|
|
377
379
|
error = s
|
|
378
|
-
|
|
380
|
+
|
|
379
381
|
image_patch_info = {}
|
|
380
382
|
image_patch_info['patches'] = patches
|
|
381
383
|
image_patch_info['image_fn'] = fn_relative
|
|
382
384
|
image_patch_info['error'] = error
|
|
383
|
-
|
|
385
|
+
|
|
384
386
|
return image_patch_info
|
|
385
|
-
|
|
386
|
-
|
|
387
|
+
|
|
388
|
+
|
|
387
389
|
#%% Main function
|
|
388
|
-
|
|
389
|
-
def run_tiled_inference(model_file,
|
|
390
|
-
image_folder,
|
|
390
|
+
|
|
391
|
+
def run_tiled_inference(model_file,
|
|
392
|
+
image_folder,
|
|
391
393
|
tiling_folder,
|
|
392
394
|
output_file,
|
|
393
395
|
tile_size_x=1280,
|
|
@@ -395,7 +397,7 @@ def run_tiled_inference(model_file,
|
|
|
395
397
|
tile_overlap=0.5,
|
|
396
398
|
checkpoint_path=None,
|
|
397
399
|
checkpoint_frequency=-1,
|
|
398
|
-
remove_tiles=False,
|
|
400
|
+
remove_tiles=False,
|
|
399
401
|
yolo_inference_options=None,
|
|
400
402
|
n_patch_extraction_workers=default_n_patch_extraction_workers,
|
|
401
403
|
overwrite_tiles=True,
|
|
@@ -406,26 +408,26 @@ def run_tiled_inference(model_file,
|
|
|
406
408
|
preprocess_on_image_queue=True,
|
|
407
409
|
inference_size=None):
|
|
408
410
|
"""
|
|
409
|
-
Runs inference using [model_file] on the images in [image_folder], fist splitting each image up
|
|
411
|
+
Runs inference using [model_file] on the images in [image_folder], fist splitting each image up
|
|
410
412
|
into tiles of size [tile_size_x] x [tile_size_y], writing those tiles to [tiling_folder],
|
|
411
|
-
then de-duplicating the results before merging them back into a set of detections that make
|
|
412
|
-
sense on the original images and writing those results to [output_file].
|
|
413
|
-
|
|
413
|
+
then de-duplicating the results before merging them back into a set of detections that make
|
|
414
|
+
sense on the original images and writing those results to [output_file].
|
|
415
|
+
|
|
414
416
|
[tiling_folder] can be any folder, but this function reserves the right to do whatever it wants
|
|
415
|
-
within that folder, including deleting everything, so it's best if it's a new folder.
|
|
417
|
+
within that folder, including deleting everything, so it's best if it's a new folder.
|
|
416
418
|
Conceptually this folder is temporary, it's just helpful in this case to not actually
|
|
417
|
-
use the system temp folder, because the tile cache may be very large, so the caller may
|
|
418
|
-
want it to be on a specific drive. If this is None, a new folder will be created in
|
|
419
|
+
use the system temp folder, because the tile cache may be very large, so the caller may
|
|
420
|
+
want it to be on a specific drive. If this is None, a new folder will be created in
|
|
419
421
|
system temp space.
|
|
420
|
-
|
|
422
|
+
|
|
421
423
|
tile_overlap is the fraction of overlap between tiles.
|
|
422
|
-
|
|
424
|
+
|
|
423
425
|
Optionally removes the temporary tiles.
|
|
424
|
-
|
|
425
|
-
if yolo_inference_options is supplied, it should be an instance of YoloInferenceOptions; in
|
|
426
|
-
this case the model will be run with run_inference_with_yolov5_val. This is typically used to
|
|
426
|
+
|
|
427
|
+
if yolo_inference_options is supplied, it should be an instance of YoloInferenceOptions; in
|
|
428
|
+
this case the model will be run with run_inference_with_yolov5_val. This is typically used to
|
|
427
429
|
run the model with test-time augmentation.
|
|
428
|
-
|
|
430
|
+
|
|
429
431
|
Args:
|
|
430
432
|
model_file (str): model filename (ending in .pt), or a well-known model name (e.g. "MDV5A")
|
|
431
433
|
image_folder (str): the folder of images to proess (always recursive)
|
|
@@ -445,7 +447,7 @@ def run_tiled_inference(model_file,
|
|
|
445
447
|
run_inference_with_yolov5_val.py, rather than with run_detector_batch.py, using these options
|
|
446
448
|
n_patch_extraction_workers (int, optional): number of workers to use for patch extraction;
|
|
447
449
|
set to <= 1 to disable parallelization
|
|
448
|
-
image_list (list, optional): .json file containing a list of specific images to process. If
|
|
450
|
+
image_list (list, optional): .json file containing a list of specific images to process. If
|
|
449
451
|
this is supplied, and the paths are absolute, [image_folder] will be ignored. If this is supplied,
|
|
450
452
|
and the paths are relative, they should be relative to [image_folder]
|
|
451
453
|
augment (bool, optional): apply test-time augmentation, only relevant if yolo_inference_options
|
|
@@ -453,47 +455,47 @@ def run_tiled_inference(model_file,
|
|
|
453
455
|
detector_options (dict, optional): parameters to pass to run_detector, only relevant if
|
|
454
456
|
yolo_inference_options is None
|
|
455
457
|
use_image_queue (bool, optional): whether to use a loader worker queue, only relevant if
|
|
456
|
-
yolo_inference_options is None
|
|
458
|
+
yolo_inference_options is None
|
|
457
459
|
preprocess_on_image_queue (bool, optional): whether the image queue should also be responsible
|
|
458
460
|
for preprocessing
|
|
459
461
|
inference_size (int, optional): override the default inference image size, only relevant if
|
|
460
462
|
yolo_inference_options is None
|
|
461
|
-
|
|
463
|
+
|
|
462
464
|
Returns:
|
|
463
465
|
dict: MD-formatted results dictionary, identical to what's written to [output_file]
|
|
464
466
|
"""
|
|
465
467
|
|
|
466
468
|
##%% Validate arguments
|
|
467
|
-
|
|
469
|
+
|
|
468
470
|
assert tile_overlap < 1 and tile_overlap >= 0, \
|
|
469
471
|
'Illegal tile overlap value {}'.format(tile_overlap)
|
|
470
|
-
|
|
472
|
+
|
|
471
473
|
if tile_size_x == -1:
|
|
472
474
|
tile_size_x = default_tile_size[0]
|
|
473
475
|
if tile_size_y == -1:
|
|
474
476
|
tile_size_y = default_tile_size[1]
|
|
475
|
-
|
|
477
|
+
|
|
476
478
|
patch_size = [tile_size_x,tile_size_y]
|
|
477
479
|
patch_stride = (round(patch_size[0]*(1.0-tile_overlap)),
|
|
478
480
|
round(patch_size[1]*(1.0-tile_overlap)))
|
|
479
|
-
|
|
481
|
+
|
|
480
482
|
if tiling_folder is None:
|
|
481
483
|
tiling_folder = \
|
|
482
484
|
os.path.join(tempfile.gettempdir(), 'md-tiling', str(uuid.uuid1()))
|
|
483
485
|
print('Creating temporary tiling folder: {}'.format(tiling_folder))
|
|
484
|
-
|
|
486
|
+
|
|
485
487
|
os.makedirs(tiling_folder,exist_ok=True)
|
|
486
|
-
|
|
488
|
+
|
|
487
489
|
##%% List files
|
|
488
|
-
|
|
490
|
+
|
|
489
491
|
if image_list is None:
|
|
490
|
-
|
|
492
|
+
|
|
491
493
|
print('Enumerating images in {}'.format(image_folder))
|
|
492
|
-
image_files_relative = path_utils.find_images(image_folder, recursive=True, return_relative_paths=True)
|
|
494
|
+
image_files_relative = path_utils.find_images(image_folder, recursive=True, return_relative_paths=True)
|
|
493
495
|
assert len(image_files_relative) > 0, 'No images found in folder {}'.format(image_folder)
|
|
494
|
-
|
|
496
|
+
|
|
495
497
|
else:
|
|
496
|
-
|
|
498
|
+
|
|
497
499
|
print('Loading image list from {}'.format(image_list))
|
|
498
500
|
with open(image_list,'r') as f:
|
|
499
501
|
image_files_relative = json.load(f)
|
|
@@ -514,107 +516,114 @@ def run_tiled_inference(model_file,
|
|
|
514
516
|
if (n_absolute_paths != 0) and (n_absolute_paths != len(image_files_relative)):
|
|
515
517
|
raise ValueError('Illegal file list: converted {} of {} paths to relative'.format(
|
|
516
518
|
n_absolute_paths,len(image_files_relative)))
|
|
517
|
-
|
|
519
|
+
|
|
518
520
|
##%% Generate tiles
|
|
519
|
-
|
|
521
|
+
|
|
520
522
|
all_image_patch_info = None
|
|
521
|
-
|
|
523
|
+
|
|
522
524
|
print('Extracting patches from {} images'.format(len(image_files_relative)))
|
|
523
|
-
|
|
525
|
+
|
|
524
526
|
n_workers = n_patch_extraction_workers
|
|
525
|
-
|
|
527
|
+
|
|
526
528
|
if n_workers <= 1:
|
|
527
|
-
|
|
529
|
+
|
|
528
530
|
all_image_patch_info = []
|
|
529
|
-
|
|
530
|
-
# fn_relative = image_files_relative[0]
|
|
531
|
-
for fn_relative in tqdm(image_files_relative):
|
|
531
|
+
|
|
532
|
+
# fn_relative = image_files_relative[0]
|
|
533
|
+
for fn_relative in tqdm(image_files_relative):
|
|
532
534
|
image_patch_info = \
|
|
533
535
|
_extract_tiles_for_image(fn_relative,image_folder,tiling_folder,patch_size,patch_stride,
|
|
534
536
|
overwrite=overwrite_tiles)
|
|
535
537
|
all_image_patch_info.append(image_patch_info)
|
|
536
|
-
|
|
538
|
+
|
|
537
539
|
else:
|
|
538
|
-
|
|
540
|
+
|
|
539
541
|
from multiprocessing.pool import ThreadPool
|
|
540
542
|
from multiprocessing.pool import Pool
|
|
541
543
|
from functools import partial
|
|
542
544
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
545
|
+
pool = None
|
|
546
|
+
try:
|
|
547
|
+
if n_workers > len(image_files_relative):
|
|
548
|
+
|
|
549
|
+
print('Pool of {} requested, but only {} images available, reducing pool to {}'.\
|
|
550
|
+
format(n_workers,len(image_files_relative),len(image_files_relative)))
|
|
551
|
+
n_workers = len(image_files_relative)
|
|
552
|
+
|
|
553
|
+
if parallelization_uses_threads:
|
|
554
|
+
pool = ThreadPool(n_workers); poolstring = 'threads'
|
|
555
|
+
else:
|
|
556
|
+
pool = Pool(n_workers); poolstring = 'processes'
|
|
557
|
+
|
|
558
|
+
print('Starting patch extraction pool with {} {}'.format(n_workers,poolstring))
|
|
559
|
+
|
|
560
|
+
all_image_patch_info = list(tqdm(pool.imap(
|
|
561
|
+
partial(_extract_tiles_for_image,
|
|
562
|
+
image_folder=image_folder,
|
|
563
|
+
tiling_folder=tiling_folder,
|
|
564
|
+
patch_size=patch_size,
|
|
565
|
+
patch_stride=patch_stride,
|
|
566
|
+
overwrite=overwrite_tiles),
|
|
567
|
+
image_files_relative),total=len(image_files_relative)))
|
|
568
|
+
finally:
|
|
569
|
+
if pool is not None:
|
|
570
|
+
pool.close()
|
|
571
|
+
pool.join()
|
|
572
|
+
print("Pool closed and joined for patch extraction")
|
|
573
|
+
|
|
565
574
|
# ...for each image
|
|
566
|
-
|
|
575
|
+
|
|
567
576
|
# Write tile information to file; this is just a debugging convenience
|
|
568
577
|
folder_name = path_utils.clean_filename(image_folder,force_lower=True)
|
|
569
578
|
if folder_name.startswith('_'):
|
|
570
579
|
folder_name = folder_name[1:]
|
|
571
|
-
|
|
580
|
+
|
|
572
581
|
tile_cache_file = os.path.join(tiling_folder,folder_name + '_patch_info.json')
|
|
573
582
|
with open(tile_cache_file,'w') as f:
|
|
574
583
|
json.dump(all_image_patch_info,f,indent=1)
|
|
575
|
-
|
|
584
|
+
|
|
576
585
|
# Keep track of patches that failed
|
|
577
586
|
images_with_patch_errors = {}
|
|
578
587
|
for patch_info in all_image_patch_info:
|
|
579
588
|
if patch_info['error'] is not None:
|
|
580
589
|
images_with_patch_errors[patch_info['image_fn']] = patch_info
|
|
581
|
-
|
|
582
|
-
|
|
590
|
+
|
|
591
|
+
|
|
583
592
|
##%% Run inference on the folder of tiles
|
|
584
|
-
|
|
593
|
+
|
|
585
594
|
# When running with run_inference_with_yolov5_val, we'll pass the folder
|
|
586
595
|
if yolo_inference_options is not None:
|
|
587
|
-
|
|
596
|
+
|
|
588
597
|
patch_level_output_file = os.path.join(tiling_folder,folder_name + '_patch_level_results.json')
|
|
589
|
-
|
|
598
|
+
|
|
590
599
|
if yolo_inference_options.model_filename is None:
|
|
591
600
|
yolo_inference_options.model_filename = model_file
|
|
592
601
|
else:
|
|
593
602
|
assert yolo_inference_options.model_filename == model_file, \
|
|
594
603
|
'Model file between yolo inference file ({}) and model file parameter ({})'.format(
|
|
595
604
|
yolo_inference_options.model_filename,model_file)
|
|
596
|
-
|
|
605
|
+
|
|
597
606
|
yolo_inference_options.input_folder = tiling_folder
|
|
598
607
|
yolo_inference_options.output_file = patch_level_output_file
|
|
599
|
-
|
|
608
|
+
|
|
600
609
|
run_inference_with_yolo_val(yolo_inference_options)
|
|
601
610
|
with open(patch_level_output_file,'r') as f:
|
|
602
611
|
patch_level_results = json.load(f)
|
|
603
|
-
|
|
612
|
+
|
|
604
613
|
# For standard inference, we'll pass a list of files
|
|
605
614
|
else:
|
|
606
|
-
|
|
615
|
+
|
|
607
616
|
patch_file_names = []
|
|
608
617
|
for im in all_image_patch_info:
|
|
609
|
-
# If there was a patch generation error, don't run inference
|
|
618
|
+
# If there was a patch generation error, don't run inference
|
|
610
619
|
if patch_info['error'] is not None:
|
|
611
620
|
assert im['image_fn'] in images_with_patch_errors
|
|
612
621
|
continue
|
|
613
622
|
for patch in im['patches']:
|
|
614
623
|
patch_file_names.append(patch['patch_fn'])
|
|
615
|
-
|
|
616
|
-
inference_results = load_and_run_detector_batch(model_file,
|
|
617
|
-
patch_file_names,
|
|
624
|
+
|
|
625
|
+
inference_results = load_and_run_detector_batch(model_file,
|
|
626
|
+
patch_file_names,
|
|
618
627
|
checkpoint_path=checkpoint_path,
|
|
619
628
|
checkpoint_frequency=checkpoint_frequency,
|
|
620
629
|
quiet=True,
|
|
@@ -623,18 +632,18 @@ def run_tiled_inference(model_file,
|
|
|
623
632
|
use_image_queue=use_image_queue,
|
|
624
633
|
preprocess_on_image_queue=preprocess_on_image_queue,
|
|
625
634
|
image_size=inference_size)
|
|
626
|
-
|
|
635
|
+
|
|
627
636
|
patch_level_output_file = os.path.join(tiling_folder,folder_name + '_patch_level_results.json')
|
|
628
|
-
|
|
629
|
-
patch_level_results = write_results_to_file(inference_results,
|
|
630
|
-
patch_level_output_file,
|
|
631
|
-
relative_path_base=tiling_folder,
|
|
637
|
+
|
|
638
|
+
patch_level_results = write_results_to_file(inference_results,
|
|
639
|
+
patch_level_output_file,
|
|
640
|
+
relative_path_base=tiling_folder,
|
|
632
641
|
detector_file=model_file)
|
|
633
|
-
|
|
642
|
+
|
|
634
643
|
# ...if we are/aren't using run_inference_with_yolov5_val
|
|
635
|
-
|
|
636
|
-
##%% Map patch-level detections back to the original images
|
|
637
|
-
|
|
644
|
+
|
|
645
|
+
##%% Map patch-level detections back to the original images
|
|
646
|
+
|
|
638
647
|
# Map relative paths for patches to detections
|
|
639
648
|
patch_fn_relative_to_results = {}
|
|
640
649
|
for im in tqdm(patch_level_results['images']):
|
|
@@ -644,36 +653,36 @@ def run_tiled_inference(model_file,
|
|
|
644
653
|
image_level_results['info'] = patch_level_results['info']
|
|
645
654
|
image_level_results['detection_categories'] = patch_level_results['detection_categories']
|
|
646
655
|
image_level_results['images'] = []
|
|
647
|
-
|
|
656
|
+
|
|
648
657
|
image_fn_relative_to_patch_info = { x['image_fn']:x for x in all_image_patch_info }
|
|
649
|
-
|
|
658
|
+
|
|
650
659
|
# i_image = 0; image_fn_relative = image_files_relative[i_image]
|
|
651
660
|
for i_image,image_fn_relative in tqdm(enumerate(image_files_relative),
|
|
652
661
|
total=len(image_files_relative)):
|
|
653
|
-
|
|
662
|
+
|
|
654
663
|
image_fn_abs = os.path.join(image_folder,image_fn_relative)
|
|
655
664
|
assert os.path.isfile(image_fn_abs)
|
|
656
|
-
|
|
665
|
+
|
|
657
666
|
output_im = {}
|
|
658
667
|
output_im['file'] = image_fn_relative
|
|
659
|
-
|
|
668
|
+
|
|
660
669
|
# If we had a patch generation error
|
|
661
670
|
if image_fn_relative in images_with_patch_errors:
|
|
662
|
-
|
|
671
|
+
|
|
663
672
|
patch_info = image_fn_relative_to_patch_info[image_fn_relative]
|
|
664
673
|
assert patch_info['error'] is not None
|
|
665
|
-
|
|
674
|
+
|
|
666
675
|
output_im['detections'] = None
|
|
667
676
|
output_im['failure'] = 'Patch generation error'
|
|
668
677
|
output_im['failure_details'] = patch_info['error']
|
|
669
678
|
image_level_results['images'].append(output_im)
|
|
670
679
|
continue
|
|
671
|
-
|
|
680
|
+
|
|
672
681
|
try:
|
|
673
|
-
pil_im = vis_utils.open_image(image_fn_abs)
|
|
682
|
+
pil_im = vis_utils.open_image(image_fn_abs)
|
|
674
683
|
image_w = pil_im.size[0]
|
|
675
684
|
image_h = pil_im.size[1]
|
|
676
|
-
|
|
685
|
+
|
|
677
686
|
# This would be a very unusual situation; we're reading back an image here that we already
|
|
678
687
|
# (successfully) read once during patch generation.
|
|
679
688
|
except Exception as e:
|
|
@@ -683,36 +692,36 @@ def run_tiled_inference(model_file,
|
|
|
683
692
|
output_im['failure'] = 'Patch processing error'
|
|
684
693
|
output_im['failure_details'] = str(e)
|
|
685
694
|
image_level_results['images'].append(output_im)
|
|
686
|
-
continue
|
|
687
|
-
|
|
695
|
+
continue
|
|
696
|
+
|
|
688
697
|
output_im['detections'] = []
|
|
689
|
-
|
|
698
|
+
|
|
690
699
|
image_patch_info = image_fn_relative_to_patch_info[image_fn_relative]
|
|
691
700
|
assert image_patch_info['patches'][0]['source_fn'] == image_fn_relative
|
|
692
|
-
|
|
701
|
+
|
|
693
702
|
# Patches for this image
|
|
694
703
|
patch_fn_abs_to_patch_info_this_image = {}
|
|
695
|
-
|
|
704
|
+
|
|
696
705
|
for patch_info in image_patch_info['patches']:
|
|
697
706
|
patch_fn_abs_to_patch_info_this_image[patch_info['patch_fn']] = patch_info
|
|
698
|
-
|
|
707
|
+
|
|
699
708
|
# For each patch
|
|
700
709
|
#
|
|
701
710
|
# i_patch = 0; patch_fn_abs = list(patch_fn_abs_to_patch_info_this_image.keys())[i_patch]
|
|
702
711
|
for i_patch,patch_fn_abs in enumerate(patch_fn_abs_to_patch_info_this_image.keys()):
|
|
703
|
-
|
|
712
|
+
|
|
704
713
|
patch_fn_relative = os.path.relpath(patch_fn_abs,tiling_folder)
|
|
705
714
|
patch_results = patch_fn_relative_to_results[patch_fn_relative]
|
|
706
715
|
patch_info = patch_fn_abs_to_patch_info_this_image[patch_fn_abs]
|
|
707
|
-
|
|
716
|
+
|
|
708
717
|
# patch_results['file'] is a relative path, and a subset of patch_info['patch_fn']
|
|
709
718
|
assert patch_results['file'] in patch_info['patch_fn']
|
|
710
|
-
|
|
719
|
+
|
|
711
720
|
patch_w = (patch_info['xmax'] - patch_info['xmin']) + 1
|
|
712
721
|
patch_h = (patch_info['ymax'] - patch_info['ymin']) + 1
|
|
713
722
|
assert patch_w == patch_size[0]
|
|
714
723
|
assert patch_h == patch_size[1]
|
|
715
|
-
|
|
724
|
+
|
|
716
725
|
# If there was an inference failure on one patch, report the image
|
|
717
726
|
# as an inference failure
|
|
718
727
|
if 'detections' not in patch_results:
|
|
@@ -720,16 +729,16 @@ def run_tiled_inference(model_file,
|
|
|
720
729
|
output_im['detections'] = None
|
|
721
730
|
output_im['failure'] = patch_results['failure']
|
|
722
731
|
break
|
|
723
|
-
|
|
732
|
+
|
|
724
733
|
# det = patch_results['detections'][0]
|
|
725
734
|
for det in patch_results['detections']:
|
|
726
|
-
|
|
735
|
+
|
|
727
736
|
bbox_patch_relative = det['bbox']
|
|
728
737
|
xmin_patch_relative = bbox_patch_relative[0]
|
|
729
738
|
ymin_patch_relative = bbox_patch_relative[1]
|
|
730
739
|
w_patch_relative = bbox_patch_relative[2]
|
|
731
740
|
h_patch_relative = bbox_patch_relative[3]
|
|
732
|
-
|
|
741
|
+
|
|
733
742
|
# Convert from patch-relative normalized values to image-relative absolute values
|
|
734
743
|
w_pixels = w_patch_relative * patch_w
|
|
735
744
|
h_pixels = h_patch_relative * patch_h
|
|
@@ -737,82 +746,82 @@ def run_tiled_inference(model_file,
|
|
|
737
746
|
ymin_patch_pixels = ymin_patch_relative * patch_h
|
|
738
747
|
xmin_image_pixels = patch_info['xmin'] + xmin_patch_pixels
|
|
739
748
|
ymin_image_pixels = patch_info['ymin'] + ymin_patch_pixels
|
|
740
|
-
|
|
749
|
+
|
|
741
750
|
# ...and now to image-relative normalized values
|
|
742
751
|
w_image_normalized = w_pixels / image_w
|
|
743
752
|
h_image_normalized = h_pixels / image_h
|
|
744
753
|
xmin_image_normalized = xmin_image_pixels / image_w
|
|
745
754
|
ymin_image_normalized = ymin_image_pixels / image_h
|
|
746
|
-
|
|
755
|
+
|
|
747
756
|
bbox_image_normalized = [xmin_image_normalized,
|
|
748
757
|
ymin_image_normalized,
|
|
749
758
|
w_image_normalized,
|
|
750
759
|
h_image_normalized]
|
|
751
|
-
|
|
752
|
-
bbox_image_normalized = round_float_array(bbox_image_normalized,
|
|
760
|
+
|
|
761
|
+
bbox_image_normalized = round_float_array(bbox_image_normalized,
|
|
753
762
|
precision=COORD_DIGITS)
|
|
754
763
|
det['conf'] = round_float(det['conf'], precision=CONF_DIGITS)
|
|
755
|
-
|
|
764
|
+
|
|
756
765
|
output_det = {}
|
|
757
766
|
output_det['bbox'] = bbox_image_normalized
|
|
758
767
|
output_det['conf'] = det['conf']
|
|
759
768
|
output_det['category'] = det['category']
|
|
760
|
-
|
|
769
|
+
|
|
761
770
|
output_im['detections'].append(output_det)
|
|
762
|
-
|
|
771
|
+
|
|
763
772
|
# ...for each detection
|
|
764
|
-
|
|
773
|
+
|
|
765
774
|
# ...for each patch
|
|
766
775
|
|
|
767
776
|
image_level_results['images'].append(output_im)
|
|
768
|
-
|
|
769
|
-
# ...for each image
|
|
777
|
+
|
|
778
|
+
# ...for each image
|
|
770
779
|
|
|
771
780
|
image_level_results_file_pre_nms = \
|
|
772
781
|
os.path.join(tiling_folder,folder_name + '_image_level_results_pre_nms.json')
|
|
773
782
|
with open(image_level_results_file_pre_nms,'w') as f:
|
|
774
783
|
json.dump(image_level_results,f,indent=1)
|
|
775
|
-
|
|
784
|
+
|
|
776
785
|
|
|
777
786
|
##%% Run NMS
|
|
778
|
-
|
|
787
|
+
|
|
779
788
|
in_place_nms(image_level_results,iou_thres=nms_iou_threshold)
|
|
780
789
|
|
|
781
|
-
|
|
790
|
+
|
|
782
791
|
##%% Write output file
|
|
783
|
-
|
|
792
|
+
|
|
784
793
|
print('Saving image-level results (after NMS) to {}'.format(output_file))
|
|
785
|
-
|
|
794
|
+
|
|
786
795
|
with open(output_file,'w') as f:
|
|
787
796
|
json.dump(image_level_results,f,indent=1)
|
|
788
797
|
|
|
789
|
-
|
|
798
|
+
|
|
790
799
|
##%% Possibly remove tiles
|
|
791
|
-
|
|
800
|
+
|
|
792
801
|
if remove_tiles:
|
|
793
|
-
|
|
802
|
+
|
|
794
803
|
patch_file_names = []
|
|
795
804
|
for im in all_image_patch_info:
|
|
796
805
|
for patch in im['patches']:
|
|
797
806
|
patch_file_names.append(patch['patch_fn'])
|
|
798
|
-
|
|
807
|
+
|
|
799
808
|
for patch_fn_abs in patch_file_names:
|
|
800
809
|
os.remove(patch_fn_abs)
|
|
801
|
-
|
|
802
|
-
|
|
810
|
+
|
|
811
|
+
|
|
803
812
|
##%% Return
|
|
804
|
-
|
|
813
|
+
|
|
805
814
|
return image_level_results
|
|
806
815
|
|
|
807
816
|
|
|
808
817
|
#%% Interactive driver
|
|
809
818
|
|
|
810
819
|
if False:
|
|
811
|
-
|
|
820
|
+
|
|
812
821
|
pass
|
|
813
822
|
|
|
814
823
|
#%% Run tiled inference (in Python)
|
|
815
|
-
|
|
824
|
+
|
|
816
825
|
model_file = os.path.expanduser('~/models/camera_traps/megadetector/md_v5.0.0/md_v5a.0.0.pt')
|
|
817
826
|
image_folder = os.path.expanduser('~/data/KRU-test')
|
|
818
827
|
tiling_folder = os.path.expanduser('~/tmp/tiling-test')
|
|
@@ -824,47 +833,47 @@ if False:
|
|
|
824
833
|
checkpoint_path = None
|
|
825
834
|
checkpoint_frequency = -1
|
|
826
835
|
remove_tiles = False
|
|
827
|
-
|
|
836
|
+
|
|
828
837
|
use_yolo_inference = False
|
|
829
|
-
|
|
838
|
+
|
|
830
839
|
if not use_yolo_inference:
|
|
831
|
-
|
|
840
|
+
|
|
832
841
|
yolo_inference_options = None
|
|
833
|
-
|
|
842
|
+
|
|
834
843
|
else:
|
|
835
|
-
|
|
844
|
+
|
|
836
845
|
yolo_inference_options = YoloInferenceOptions()
|
|
837
846
|
yolo_inference_options.yolo_working_folder = os.path.expanduser('~/git/yolov5')
|
|
838
|
-
|
|
847
|
+
|
|
839
848
|
run_tiled_inference(model_file, image_folder, tiling_folder, output_file,
|
|
840
|
-
tile_size_x=tile_size_x, tile_size_y=tile_size_y,
|
|
849
|
+
tile_size_x=tile_size_x, tile_size_y=tile_size_y,
|
|
841
850
|
tile_overlap=tile_overlap,
|
|
842
|
-
checkpoint_path=checkpoint_path,
|
|
843
|
-
checkpoint_frequency=checkpoint_frequency,
|
|
844
|
-
remove_tiles=remove_tiles,
|
|
851
|
+
checkpoint_path=checkpoint_path,
|
|
852
|
+
checkpoint_frequency=checkpoint_frequency,
|
|
853
|
+
remove_tiles=remove_tiles,
|
|
845
854
|
yolo_inference_options=yolo_inference_options)
|
|
846
|
-
|
|
847
|
-
|
|
855
|
+
|
|
856
|
+
|
|
848
857
|
#%% Run tiled inference (generate a command)
|
|
849
|
-
|
|
858
|
+
|
|
850
859
|
import os
|
|
851
|
-
|
|
860
|
+
|
|
852
861
|
model_file = os.path.expanduser('~/models/camera_traps/megadetector/md_v5.0.0/md_v5a.0.0.pt')
|
|
853
862
|
image_folder = os.path.expanduser('~/data/KRU-test')
|
|
854
863
|
tiling_folder = os.path.expanduser('~/tmp/tiling-test')
|
|
855
864
|
output_file = os.path.expanduser('~/tmp/KRU-test-tiled.json')
|
|
856
865
|
tile_size = [5152,3968]
|
|
857
866
|
tile_overlap = 0.8
|
|
858
|
-
|
|
867
|
+
|
|
859
868
|
cmd = f'python run_tiled_inference.py {model_file} {image_folder} {tiling_folder} {output_file} ' + \
|
|
860
869
|
f'--tile_overlap {tile_overlap} --no_remove_tiles --tile_size_x {tile_size[0]} --tile_size_y {tile_size[1]}'
|
|
861
|
-
|
|
870
|
+
|
|
862
871
|
print(cmd)
|
|
863
872
|
import clipboard; clipboard.copy(cmd)
|
|
864
|
-
|
|
865
|
-
|
|
873
|
+
|
|
874
|
+
|
|
866
875
|
#%% Preview tiled inference
|
|
867
|
-
|
|
876
|
+
|
|
868
877
|
from megadetector.postprocessing.postprocess_batch_results import \
|
|
869
878
|
PostProcessingOptions, process_batch_results
|
|
870
879
|
|
|
@@ -893,14 +902,12 @@ if False:
|
|
|
893
902
|
html_output_file = ppresults.output_html_file
|
|
894
903
|
|
|
895
904
|
path_utils.open_file(html_output_file)
|
|
896
|
-
|
|
897
|
-
|
|
905
|
+
|
|
906
|
+
|
|
898
907
|
#%% Command-line driver
|
|
899
908
|
|
|
900
|
-
|
|
909
|
+
def main(): # noqa
|
|
901
910
|
|
|
902
|
-
def main():
|
|
903
|
-
|
|
904
911
|
parser = argparse.ArgumentParser(
|
|
905
912
|
description='Chop a folder of images up into tiles, run MD on the tiles, and stitch the results together')
|
|
906
913
|
parser.add_argument(
|
|
@@ -918,7 +925,7 @@ def main():
|
|
|
918
925
|
parser.add_argument(
|
|
919
926
|
'--no_remove_tiles',
|
|
920
927
|
action='store_true',
|
|
921
|
-
help='Tiles are removed by default; this option suppresses tile deletion')
|
|
928
|
+
help='Tiles are removed by default; this option suppresses tile deletion')
|
|
922
929
|
parser.add_argument(
|
|
923
930
|
'--tile_size_x',
|
|
924
931
|
type=int,
|
|
@@ -949,8 +956,8 @@ def main():
|
|
|
949
956
|
type=str,
|
|
950
957
|
default=None,
|
|
951
958
|
help=('A list of detector options (key-value pairs) to '))
|
|
952
|
-
|
|
953
|
-
# detector_options = parse_kvp_list(args.detector_options)
|
|
959
|
+
|
|
960
|
+
# detector_options = parse_kvp_list(args.detector_options)
|
|
954
961
|
|
|
955
962
|
if len(sys.argv[1:]) == 0:
|
|
956
963
|
parser.print_help()
|
|
@@ -961,7 +968,7 @@ def main():
|
|
|
961
968
|
model_file = try_download_known_detector(args.model_file)
|
|
962
969
|
assert os.path.exists(model_file), \
|
|
963
970
|
'detector file {} does not exist'.format(args.model_file)
|
|
964
|
-
|
|
971
|
+
|
|
965
972
|
if os.path.exists(args.output_file):
|
|
966
973
|
if args.overwrite_handling == 'skip':
|
|
967
974
|
print('Warning: output file {} exists, skipping'.format(args.output_file))
|
|
@@ -972,15 +979,15 @@ def main():
|
|
|
972
979
|
raise ValueError('Output file {} exists'.format(args.output_file))
|
|
973
980
|
else:
|
|
974
981
|
raise ValueError('Unknown output handling method {}'.format(args.overwrite_handling))
|
|
975
|
-
|
|
982
|
+
|
|
976
983
|
|
|
977
984
|
remove_tiles = (not args.no_remove_tiles)
|
|
978
985
|
|
|
979
986
|
run_tiled_inference(model_file, args.image_folder, args.tiling_folder, args.output_file,
|
|
980
|
-
tile_size_x=args.tile_size_x, tile_size_y=args.tile_size_y,
|
|
987
|
+
tile_size_x=args.tile_size_x, tile_size_y=args.tile_size_y,
|
|
981
988
|
tile_overlap=args.tile_overlap,
|
|
982
989
|
remove_tiles=remove_tiles,
|
|
983
990
|
image_list=args.image_list)
|
|
984
|
-
|
|
991
|
+
|
|
985
992
|
if __name__ == '__main__':
|
|
986
993
|
main()
|