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