megadetector 10.0.13__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/__init__.py +0 -0
- megadetector/api/__init__.py +0 -0
- megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
- megadetector/classification/__init__.py +0 -0
- megadetector/classification/aggregate_classifier_probs.py +108 -0
- megadetector/classification/analyze_failed_images.py +227 -0
- megadetector/classification/cache_batchapi_outputs.py +198 -0
- megadetector/classification/create_classification_dataset.py +626 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +853 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +608 -0
- megadetector/classification/evaluate_model.py +520 -0
- megadetector/classification/identify_mislabeled_candidates.py +152 -0
- megadetector/classification/json_to_azcopy_list.py +63 -0
- megadetector/classification/json_validator.py +696 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +509 -0
- megadetector/classification/prepare_classification_script.py +194 -0
- megadetector/classification/prepare_classification_script_mc.py +228 -0
- megadetector/classification/run_classifier.py +287 -0
- megadetector/classification/save_mislabeled.py +110 -0
- megadetector/classification/train_classifier.py +827 -0
- megadetector/classification/train_classifier_tf.py +725 -0
- megadetector/classification/train_utils.py +323 -0
- megadetector/data_management/__init__.py +0 -0
- megadetector/data_management/animl_to_md.py +161 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +33 -0
- megadetector/data_management/camtrap_dp_to_coco.py +270 -0
- megadetector/data_management/cct_json_utils.py +566 -0
- megadetector/data_management/cct_to_md.py +184 -0
- megadetector/data_management/cct_to_wi.py +293 -0
- megadetector/data_management/coco_to_labelme.py +284 -0
- megadetector/data_management/coco_to_yolo.py +702 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +107 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +210 -0
- megadetector/data_management/databases/integrity_check_json_db.py +528 -0
- megadetector/data_management/databases/subset_json_db.py +195 -0
- megadetector/data_management/generate_crops_from_cct.py +200 -0
- megadetector/data_management/get_image_sizes.py +164 -0
- megadetector/data_management/labelme_to_coco.py +559 -0
- megadetector/data_management/labelme_to_yolo.py +349 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/create_lila_blank_set.py +556 -0
- megadetector/data_management/lila/create_lila_test_set.py +187 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +182 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +777 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +174 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +319 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +164 -0
- megadetector/data_management/mewc_to_md.py +344 -0
- megadetector/data_management/ocr_tools.py +873 -0
- megadetector/data_management/read_exif.py +964 -0
- megadetector/data_management/remap_coco_categories.py +195 -0
- megadetector/data_management/remove_exif.py +156 -0
- megadetector/data_management/rename_images.py +194 -0
- megadetector/data_management/resize_coco_dataset.py +663 -0
- megadetector/data_management/speciesnet_to_md.py +41 -0
- megadetector/data_management/wi_download_csv_to_coco.py +247 -0
- megadetector/data_management/yolo_output_to_md_output.py +594 -0
- megadetector/data_management/yolo_to_coco.py +876 -0
- megadetector/data_management/zamba_to_md.py +188 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/change_detection.py +840 -0
- megadetector/detection/process_video.py +479 -0
- megadetector/detection/pytorch_detector.py +1451 -0
- megadetector/detection/run_detector.py +1267 -0
- megadetector/detection/run_detector_batch.py +2159 -0
- megadetector/detection/run_inference_with_yolov5_val.py +1314 -0
- megadetector/detection/run_md_and_speciesnet.py +1494 -0
- megadetector/detection/run_tiled_inference.py +1038 -0
- megadetector/detection/tf_detector.py +209 -0
- megadetector/detection/video_utils.py +1379 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +72 -0
- megadetector/postprocessing/categorize_detections_by_size.py +166 -0
- megadetector/postprocessing/classification_postprocessing.py +1752 -0
- megadetector/postprocessing/combine_batch_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +2110 -0
- megadetector/postprocessing/convert_output_format.py +403 -0
- megadetector/postprocessing/create_crop_folder.py +629 -0
- megadetector/postprocessing/detector_calibration.py +570 -0
- megadetector/postprocessing/generate_csv_report.py +522 -0
- megadetector/postprocessing/load_api_results.py +223 -0
- megadetector/postprocessing/md_to_coco.py +428 -0
- megadetector/postprocessing/md_to_labelme.py +351 -0
- megadetector/postprocessing/md_to_wi.py +41 -0
- megadetector/postprocessing/merge_detections.py +392 -0
- megadetector/postprocessing/postprocess_batch_results.py +2077 -0
- megadetector/postprocessing/remap_detection_categories.py +226 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +677 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +206 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +82 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1665 -0
- megadetector/postprocessing/separate_detections_into_folders.py +795 -0
- megadetector/postprocessing/subset_json_detector_output.py +964 -0
- megadetector/postprocessing/top_folders_to_bottom.py +238 -0
- megadetector/postprocessing/validate_batch_results.py +332 -0
- megadetector/taxonomy_mapping/__init__.py +0 -0
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +213 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +165 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +543 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +224 -0
- megadetector/taxonomy_mapping/species_lookup.py +1008 -0
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
- megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
- megadetector/tests/__init__.py +0 -0
- megadetector/tests/test_nms_synthetic.py +335 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/ct_utils.py +1857 -0
- megadetector/utils/directory_listing.py +199 -0
- megadetector/utils/extract_frames_from_video.py +307 -0
- megadetector/utils/gpu_test.py +125 -0
- megadetector/utils/md_tests.py +2072 -0
- megadetector/utils/path_utils.py +2832 -0
- megadetector/utils/process_utils.py +172 -0
- megadetector/utils/split_locations_into_train_val.py +237 -0
- megadetector/utils/string_utils.py +234 -0
- megadetector/utils/url_utils.py +825 -0
- megadetector/utils/wi_platform_utils.py +968 -0
- megadetector/utils/wi_taxonomy_utils.py +1759 -0
- megadetector/utils/write_html_image_list.py +239 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +309 -0
- megadetector/visualization/render_images_with_thumbnails.py +243 -0
- megadetector/visualization/visualization_utils.py +1940 -0
- megadetector/visualization/visualize_db.py +630 -0
- megadetector/visualization/visualize_detector_output.py +479 -0
- megadetector/visualization/visualize_video_output.py +705 -0
- megadetector-10.0.13.dist-info/METADATA +134 -0
- megadetector-10.0.13.dist-info/RECORD +147 -0
- megadetector-10.0.13.dist-info/WHEEL +5 -0
- megadetector-10.0.13.dist-info/licenses/LICENSE +19 -0
- megadetector-10.0.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
yolo_output_to_md_output.py
|
|
4
|
+
|
|
5
|
+
Converts the output of YOLOv5's detect.py or val.py to the MD output format.
|
|
6
|
+
|
|
7
|
+
**Converting .txt files**
|
|
8
|
+
|
|
9
|
+
detect.py writes a .txt file per image, in YOLO training format. Converting from this
|
|
10
|
+
format does not currently support recursive results, since detect.py doesn't save filenames
|
|
11
|
+
in a way that allows easy inference of folder names. Requires access to the input
|
|
12
|
+
images, because the YOLO format uses the *absence* of a results file to indicate that
|
|
13
|
+
no detections are present.
|
|
14
|
+
|
|
15
|
+
YOLOv5 output has one text file per image, like so:
|
|
16
|
+
|
|
17
|
+
0 0.0141693 0.469758 0.0283385 0.131552 0.761428
|
|
18
|
+
|
|
19
|
+
That's [class, x_center, y_center, width_of_box, height_of_box, confidence]
|
|
20
|
+
|
|
21
|
+
val.py can write in this format as well, using the --save-txt argument.
|
|
22
|
+
|
|
23
|
+
In both cases, a confidence value is only written to each line if you include the --save-conf
|
|
24
|
+
argument. Confidence values are required by this conversion script.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
**Converting .json files**
|
|
28
|
+
|
|
29
|
+
val.py can also write a .json file in COCO-ish format. It's "COCO-ish" because it's
|
|
30
|
+
just the "images" portion of a COCO .json file.
|
|
31
|
+
|
|
32
|
+
Converting from this format also requires access to the original images, since the format
|
|
33
|
+
written by YOLOv5 uses absolute coordinates, but MD results are in relative coordinates.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
#%% Imports and constants
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import csv
|
|
41
|
+
import os
|
|
42
|
+
import re
|
|
43
|
+
import sys
|
|
44
|
+
import argparse
|
|
45
|
+
|
|
46
|
+
from collections import defaultdict
|
|
47
|
+
from tqdm import tqdm
|
|
48
|
+
|
|
49
|
+
from megadetector.utils import path_utils
|
|
50
|
+
from megadetector.utils import ct_utils
|
|
51
|
+
from megadetector.visualization import visualization_utils as vis_utils
|
|
52
|
+
from megadetector.detection.run_detector import CONF_DIGITS, COORD_DIGITS
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
#%% Support functions
|
|
56
|
+
|
|
57
|
+
def read_classes_from_yolo_dataset_file(fn):
|
|
58
|
+
"""
|
|
59
|
+
Reads a dictionary mapping integer class IDs to class names from a YOLOv5/YOLOv8
|
|
60
|
+
dataset.yaml file or a .json file. A .json file should contain a dictionary mapping
|
|
61
|
+
integer category IDs to string category names.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
fn (str): YOLOv5/YOLOv8 dataset file with a .yml or .yaml extension, a .json file
|
|
65
|
+
mapping integer category IDs to category names, or a .txt file with a flat
|
|
66
|
+
list of classes.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
dict: a mapping from integer category IDs to category names
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
category_id_to_name = {}
|
|
73
|
+
|
|
74
|
+
if fn.endswith('.yml') or fn.endswith('.yaml'):
|
|
75
|
+
|
|
76
|
+
with open(fn,'r') as f:
|
|
77
|
+
lines = f.readlines()
|
|
78
|
+
|
|
79
|
+
pat = r'\d+:.+'
|
|
80
|
+
for s in lines:
|
|
81
|
+
if re.search(pat,s) is not None:
|
|
82
|
+
tokens = s.split(':')
|
|
83
|
+
assert len(tokens) == 2, 'Invalid token in category file {}'.format(fn)
|
|
84
|
+
category_id_to_name[int(tokens[0].strip())] = tokens[1].strip()
|
|
85
|
+
|
|
86
|
+
elif fn.endswith('.json'):
|
|
87
|
+
|
|
88
|
+
with open(fn,'r') as f:
|
|
89
|
+
d_in = json.load(f)
|
|
90
|
+
for k in d_in.keys():
|
|
91
|
+
category_id_to_name[int(k)] = d_in[k]
|
|
92
|
+
|
|
93
|
+
elif fn.endswith('.txt'):
|
|
94
|
+
|
|
95
|
+
with open(fn,'r') as f:
|
|
96
|
+
lines = f.readlines()
|
|
97
|
+
next_category_id = 0
|
|
98
|
+
for line in lines:
|
|
99
|
+
s = line.strip()
|
|
100
|
+
if len(s) == 0:
|
|
101
|
+
continue
|
|
102
|
+
category_id_to_name[next_category_id] = s
|
|
103
|
+
next_category_id += 1
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
|
|
107
|
+
raise ValueError('Unrecognized category file type: {}'.format(fn))
|
|
108
|
+
|
|
109
|
+
assert len(category_id_to_name) > 0, 'Failed to read class mappings from {}'.format(fn)
|
|
110
|
+
|
|
111
|
+
return category_id_to_name
|
|
112
|
+
|
|
113
|
+
# ...def def read_classes_from_yolo_dataset_file(...)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def yolo_json_output_to_md_output(yolo_json_file,
|
|
117
|
+
image_folder,
|
|
118
|
+
output_file,
|
|
119
|
+
yolo_category_id_to_name,
|
|
120
|
+
detector_name='unknown',
|
|
121
|
+
image_id_to_relative_path=None,
|
|
122
|
+
offset_yolo_class_ids=True,
|
|
123
|
+
truncate_to_standard_md_precision=True,
|
|
124
|
+
image_id_to_error=None,
|
|
125
|
+
convert_slashes=True):
|
|
126
|
+
"""
|
|
127
|
+
Converts a YOLOv5/YOLOv8 .json file to MD .json format.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
yolo_json_file (str): the YOLO-formatted .json file to convert to MD output format
|
|
131
|
+
image_folder (str): the .json file contains relative path names, this is the path base
|
|
132
|
+
output_file (str): the MD-formatted .json file to write
|
|
133
|
+
yolo_category_id_to_name (str or dict): the .json results file contains only numeric
|
|
134
|
+
identifiers for categories, but we want names and numbers for the output format;
|
|
135
|
+
yolo_category_id_to_name provides that mapping either as a dict or as a YOLOv5
|
|
136
|
+
dataset.yaml file.
|
|
137
|
+
detector_name (str, optional): a string that gets put in the output file, not otherwise
|
|
138
|
+
used within this function
|
|
139
|
+
image_id_to_relative_path (dict, optional): YOLOv5 .json uses only basenames (e.g.
|
|
140
|
+
abc1234.JPG); by default these will be appended to the input path to create pathnames.
|
|
141
|
+
If you have a flat folder, this is fine. If you want to map base names to relative paths in
|
|
142
|
+
a more complicated way, use this parameter.
|
|
143
|
+
offset_yolo_class_ids (bool, optional): YOLOv5 class IDs always start at zero; if you want to
|
|
144
|
+
make the output classes start at 1, set offset_yolo_class_ids to True.
|
|
145
|
+
truncate_to_standard_md_precision (bool, optional): YOLOv5 .json includes lots of
|
|
146
|
+
(not-super-meaningful) precision, set this to truncate to COORD_DIGITS and CONF_DIGITS.
|
|
147
|
+
image_id_to_error (dict, optional): if you want to include image IDs in the output file for which
|
|
148
|
+
you couldn't prepare the input file in the first place due to errors, include them here.
|
|
149
|
+
convert_slashes (bool, optional): force all slashes to be forward slashes in the output file
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
assert os.path.isfile(yolo_json_file), \
|
|
153
|
+
'Could not find YOLO .json file {}'.format(yolo_json_file)
|
|
154
|
+
assert os.path.isdir(image_folder), \
|
|
155
|
+
'Could not find image folder {}'.format(image_folder)
|
|
156
|
+
|
|
157
|
+
if image_id_to_error is None:
|
|
158
|
+
image_id_to_error = {}
|
|
159
|
+
|
|
160
|
+
print('Converting {} to MD format and writing results to {}'.format(
|
|
161
|
+
yolo_json_file,output_file))
|
|
162
|
+
|
|
163
|
+
if isinstance(yolo_category_id_to_name,str):
|
|
164
|
+
assert os.path.isfile(yolo_category_id_to_name), \
|
|
165
|
+
'YOLO category mapping specified as a string, but file does not exist: {}'.format(
|
|
166
|
+
yolo_category_id_to_name)
|
|
167
|
+
yolo_category_id_to_name = read_classes_from_yolo_dataset_file(yolo_category_id_to_name)
|
|
168
|
+
|
|
169
|
+
if image_id_to_relative_path is None:
|
|
170
|
+
|
|
171
|
+
image_files = path_utils.find_images(image_folder,recursive=True)
|
|
172
|
+
image_files = [os.path.relpath(fn,image_folder) for fn in image_files]
|
|
173
|
+
|
|
174
|
+
# YOLOv5 identifies images in .json output by ID, which is the filename without
|
|
175
|
+
# extension. If a mapping is not provided, these need to be unique.
|
|
176
|
+
image_id_to_relative_path = {}
|
|
177
|
+
|
|
178
|
+
for fn in image_files:
|
|
179
|
+
image_id = os.path.splitext(os.path.basename(fn))[0]
|
|
180
|
+
if image_id in image_id_to_relative_path:
|
|
181
|
+
print('Error: image ID {} refers to:\n{}\n{}'.format(
|
|
182
|
+
image_id,image_id_to_relative_path[image_id],fn))
|
|
183
|
+
raise ValueError('Duplicate image ID {}'.format(image_id))
|
|
184
|
+
image_id_to_relative_path[image_id] = fn
|
|
185
|
+
|
|
186
|
+
image_files_relative = sorted(list(image_id_to_relative_path.values()))
|
|
187
|
+
|
|
188
|
+
image_file_relative_to_image_id = {}
|
|
189
|
+
for image_id in image_id_to_relative_path:
|
|
190
|
+
relative_path = image_id_to_relative_path[image_id]
|
|
191
|
+
assert relative_path not in image_file_relative_to_image_id, \
|
|
192
|
+
'Duplicate image IDs in YOLO output conversion for image {}'.format(relative_path)
|
|
193
|
+
image_file_relative_to_image_id[relative_path] = image_id
|
|
194
|
+
|
|
195
|
+
with open(yolo_json_file,'r') as f:
|
|
196
|
+
detections = json.load(f)
|
|
197
|
+
assert isinstance(detections,list)
|
|
198
|
+
|
|
199
|
+
image_id_to_detections = defaultdict(list)
|
|
200
|
+
|
|
201
|
+
int_formatted_image_ids = False
|
|
202
|
+
|
|
203
|
+
# det = detections[0]
|
|
204
|
+
for det in detections:
|
|
205
|
+
|
|
206
|
+
# This could be a string, but if the YOLOv5 inference script sees that the strings
|
|
207
|
+
# are really ints, it converts to ints.
|
|
208
|
+
image_id = det['image_id']
|
|
209
|
+
image_id_to_detections[image_id].append(det)
|
|
210
|
+
if isinstance(image_id,int):
|
|
211
|
+
int_formatted_image_ids = True
|
|
212
|
+
|
|
213
|
+
# If there are any ints present, everything should be ints
|
|
214
|
+
if int_formatted_image_ids:
|
|
215
|
+
for det in detections:
|
|
216
|
+
assert isinstance(det['image_id'],int), \
|
|
217
|
+
'Found mixed int and string image IDs'
|
|
218
|
+
|
|
219
|
+
# Convert the keys in image_id_to_error to ints
|
|
220
|
+
#
|
|
221
|
+
# This should error if we're given non-int-friendly IDs
|
|
222
|
+
int_formatted_image_id_to_error = {}
|
|
223
|
+
for image_id in image_id_to_error:
|
|
224
|
+
int_formatted_image_id_to_error[int(image_id)] = \
|
|
225
|
+
image_id_to_error[image_id]
|
|
226
|
+
image_id_to_error = int_formatted_image_id_to_error
|
|
227
|
+
|
|
228
|
+
# ...if image IDs are formatted as integers in YOLO output
|
|
229
|
+
|
|
230
|
+
# In a modified version of val.py, we use negative category IDs to indicate an error
|
|
231
|
+
# that happened during inference (typically truncated images with valid headers,
|
|
232
|
+
# so corruption was not detected during val.py's initial corruption check pass.
|
|
233
|
+
for det in detections:
|
|
234
|
+
if det['category_id'] < 0:
|
|
235
|
+
assert 'error' in det, 'Negative category ID present with no error string'
|
|
236
|
+
error_string = det['error']
|
|
237
|
+
print('Caught inference-time failure {} for image {}'.format(error_string,det['image_id']))
|
|
238
|
+
image_id_to_error[det['image_id']] = error_string
|
|
239
|
+
|
|
240
|
+
output_images = []
|
|
241
|
+
|
|
242
|
+
# image_file_relative = image_files_relative[10]
|
|
243
|
+
for image_file_relative in tqdm(image_files_relative):
|
|
244
|
+
|
|
245
|
+
im = {}
|
|
246
|
+
im['file'] = image_file_relative
|
|
247
|
+
if convert_slashes:
|
|
248
|
+
im['file'] = im['file'].replace('\\','/')
|
|
249
|
+
|
|
250
|
+
image_id = image_file_relative_to_image_id[image_file_relative]
|
|
251
|
+
if int_formatted_image_ids:
|
|
252
|
+
image_id = int(image_id)
|
|
253
|
+
if image_id in image_id_to_error:
|
|
254
|
+
im['failure'] = str(image_id_to_error[image_id])
|
|
255
|
+
output_images.append(im)
|
|
256
|
+
continue
|
|
257
|
+
elif image_id not in image_id_to_detections:
|
|
258
|
+
detections = []
|
|
259
|
+
else:
|
|
260
|
+
detections = image_id_to_detections[image_id]
|
|
261
|
+
|
|
262
|
+
image_full_path = os.path.join(image_folder,image_file_relative)
|
|
263
|
+
try:
|
|
264
|
+
pil_im = vis_utils.open_image(image_full_path)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
s = str(e).replace('\n',' ')
|
|
267
|
+
print('Warning: error opening image {}: {}, outputting as a failure'.format(image_full_path,s))
|
|
268
|
+
im['failure'] = 'Conversion error: {}'.format(s)
|
|
269
|
+
output_images.append(im)
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
im['detections'] = []
|
|
273
|
+
|
|
274
|
+
image_w = pil_im.size[0]
|
|
275
|
+
image_h = pil_im.size[1]
|
|
276
|
+
|
|
277
|
+
# det = detections[0]
|
|
278
|
+
for det in detections:
|
|
279
|
+
|
|
280
|
+
output_det = {}
|
|
281
|
+
|
|
282
|
+
yolo_cat_id = int(det['category_id'])
|
|
283
|
+
if offset_yolo_class_ids:
|
|
284
|
+
yolo_cat_id += 1
|
|
285
|
+
output_det['category'] = str(int(yolo_cat_id))
|
|
286
|
+
conf = det['score']
|
|
287
|
+
if truncate_to_standard_md_precision:
|
|
288
|
+
conf = ct_utils.round_float(conf,CONF_DIGITS)
|
|
289
|
+
output_det['conf'] = conf
|
|
290
|
+
input_bbox = det['bbox']
|
|
291
|
+
|
|
292
|
+
# YOLO's COCO .json is not *that* COCO-like, but it is COCO-like in
|
|
293
|
+
# that the boxes are already [xmin/ymin/w/h]
|
|
294
|
+
box_xmin_absolute = input_bbox[0]
|
|
295
|
+
box_ymin_absolute = input_bbox[1]
|
|
296
|
+
box_width_absolute = input_bbox[2]
|
|
297
|
+
box_height_absolute = input_bbox[3]
|
|
298
|
+
|
|
299
|
+
box_xmin_relative = box_xmin_absolute / image_w
|
|
300
|
+
box_ymin_relative = box_ymin_absolute / image_h
|
|
301
|
+
box_width_relative = box_width_absolute / image_w
|
|
302
|
+
box_height_relative = box_height_absolute / image_h
|
|
303
|
+
|
|
304
|
+
output_bbox = [box_xmin_relative,box_ymin_relative,
|
|
305
|
+
box_width_relative,box_height_relative]
|
|
306
|
+
|
|
307
|
+
if truncate_to_standard_md_precision:
|
|
308
|
+
output_bbox = ct_utils.round_float_array(output_bbox,COORD_DIGITS)
|
|
309
|
+
|
|
310
|
+
output_det['bbox'] = output_bbox
|
|
311
|
+
im['detections'].append(output_det)
|
|
312
|
+
|
|
313
|
+
# ...for each detection
|
|
314
|
+
|
|
315
|
+
output_images.append(im)
|
|
316
|
+
|
|
317
|
+
# ...for each image file
|
|
318
|
+
|
|
319
|
+
d = {}
|
|
320
|
+
d['images'] = output_images
|
|
321
|
+
d['info'] = {'format_version':'1.4','detector':detector_name}
|
|
322
|
+
d['detection_categories'] = {}
|
|
323
|
+
|
|
324
|
+
for cat_id in yolo_category_id_to_name:
|
|
325
|
+
yolo_cat_id = int(cat_id)
|
|
326
|
+
if offset_yolo_class_ids:
|
|
327
|
+
yolo_cat_id += 1
|
|
328
|
+
d['detection_categories'][str(yolo_cat_id)] = yolo_category_id_to_name[cat_id]
|
|
329
|
+
|
|
330
|
+
ct_utils.write_json(output_file, d)
|
|
331
|
+
|
|
332
|
+
# ...def yolo_json_output_to_md_output(...)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def yolo_txt_output_to_md_output(input_results_folder,
|
|
336
|
+
image_folder,
|
|
337
|
+
output_file,
|
|
338
|
+
detector_tag=None,
|
|
339
|
+
truncate_to_standard_md_precision=True):
|
|
340
|
+
"""
|
|
341
|
+
Converts a folder of YOLO-output .txt files to MD .json format.
|
|
342
|
+
|
|
343
|
+
Less finished than the .json conversion function; this .txt conversion assumes
|
|
344
|
+
a hard-coded mapping representing the standard MD categories (in MD indexing,
|
|
345
|
+
1/2/3=animal/person/vehicle; in YOLO indexing, 0/1/2=animal/person/vehicle).
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
input_results_folder (str): the folder containing YOLO-output .txt files
|
|
349
|
+
image_folder (str): the folder where images live, may be the same as
|
|
350
|
+
[input_results_folder]
|
|
351
|
+
output_file (str): the MD-formatted .json file to which we should write
|
|
352
|
+
results
|
|
353
|
+
detector_tag (str, optional): string to put in the 'detector' field in the
|
|
354
|
+
output file
|
|
355
|
+
truncate_to_standard_md_precision (bool, optional): set this to truncate to
|
|
356
|
+
COORD_DIGITS and CONF_DIGITS, like the standard MD pipeline does.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
assert os.path.isdir(input_results_folder)
|
|
360
|
+
assert os.path.isdir(image_folder)
|
|
361
|
+
|
|
362
|
+
## Enumerate results files and image files
|
|
363
|
+
|
|
364
|
+
yolo_results_files = os.listdir(input_results_folder)
|
|
365
|
+
yolo_results_files = [f for f in yolo_results_files if f.lower().endswith('.txt')]
|
|
366
|
+
# print('Found {} results files'.format(len(yolo_results_files)))
|
|
367
|
+
|
|
368
|
+
image_files = path_utils.find_images(image_folder,recursive=False)
|
|
369
|
+
image_files_relative = [os.path.basename(f) for f in image_files]
|
|
370
|
+
# print('Found {} images'.format(len(image_files)))
|
|
371
|
+
|
|
372
|
+
image_files_relative_no_extension = [os.path.splitext(f)[0] for f in image_files_relative]
|
|
373
|
+
|
|
374
|
+
## Make sure that every results file corresponds to an image
|
|
375
|
+
|
|
376
|
+
for f in yolo_results_files:
|
|
377
|
+
result_no_extension = os.path.splitext(f)[0]
|
|
378
|
+
assert result_no_extension in image_files_relative_no_extension
|
|
379
|
+
|
|
380
|
+
## Build MD output data
|
|
381
|
+
|
|
382
|
+
# Map 0-indexed YOLO categories to 1-indexed MD categories
|
|
383
|
+
yolo_cat_map = { 0: 1, 1: 2, 2: 3 }
|
|
384
|
+
|
|
385
|
+
images_entries = []
|
|
386
|
+
|
|
387
|
+
# image_fn = image_files_relative[0]
|
|
388
|
+
for image_fn in image_files_relative:
|
|
389
|
+
|
|
390
|
+
image_name, ext = os.path.splitext(image_fn)
|
|
391
|
+
label_fn = image_name + '.txt'
|
|
392
|
+
label_path = os.path.join(input_results_folder, label_fn)
|
|
393
|
+
|
|
394
|
+
detections = []
|
|
395
|
+
|
|
396
|
+
if not os.path.exists(label_path):
|
|
397
|
+
# This is assumed to be an image with no detections
|
|
398
|
+
pass
|
|
399
|
+
else:
|
|
400
|
+
with open(label_path, newline='') as f:
|
|
401
|
+
reader = csv.reader(f, delimiter=' ')
|
|
402
|
+
for row in reader:
|
|
403
|
+
category = yolo_cat_map[int(row[0])]
|
|
404
|
+
api_box = ct_utils.convert_yolo_to_xywh([float(row[1]), float(row[2]),
|
|
405
|
+
float(row[3]), float(row[4])])
|
|
406
|
+
|
|
407
|
+
conf = float(row[5])
|
|
408
|
+
|
|
409
|
+
if truncate_to_standard_md_precision:
|
|
410
|
+
conf = ct_utils.round_float(conf, precision=CONF_DIGITS)
|
|
411
|
+
api_box = ct_utils.round_float_array(api_box, precision=COORD_DIGITS)
|
|
412
|
+
|
|
413
|
+
detections.append({
|
|
414
|
+
'category': str(category),
|
|
415
|
+
'conf': conf,
|
|
416
|
+
'bbox': api_box
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
images_entries.append({
|
|
420
|
+
'file': image_fn,
|
|
421
|
+
'detections': detections
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
# ...for each image
|
|
425
|
+
|
|
426
|
+
## Save output file
|
|
427
|
+
|
|
428
|
+
detector_string = 'converted_from_yolo_format'
|
|
429
|
+
|
|
430
|
+
if detector_tag is not None:
|
|
431
|
+
detector_string = detector_tag
|
|
432
|
+
|
|
433
|
+
output_content = {
|
|
434
|
+
'info': {
|
|
435
|
+
'detector': detector_string,
|
|
436
|
+
'detector_metadata': {},
|
|
437
|
+
'format_version': '1.4'
|
|
438
|
+
},
|
|
439
|
+
'detection_categories': {
|
|
440
|
+
'1': 'animal',
|
|
441
|
+
'2': 'person',
|
|
442
|
+
'3': 'vehicle'
|
|
443
|
+
},
|
|
444
|
+
'images': images_entries
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
ct_utils.write_json(output_file, output_content)
|
|
448
|
+
|
|
449
|
+
# ...def yolo_txt_output_to_md_output(...)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
#%% Interactive driver
|
|
453
|
+
|
|
454
|
+
if False:
|
|
455
|
+
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
#%%
|
|
459
|
+
|
|
460
|
+
input_results_folder = os.path.expanduser('~/tmp/model-version-experiments/pt-test-kru/exp/labels')
|
|
461
|
+
image_folder = os.path.expanduser('~/data/KRU-test')
|
|
462
|
+
output_file = os.path.expanduser('~/data/mdv5a-yolo-pt-kru.json')
|
|
463
|
+
yolo_txt_output_to_md_output(input_results_folder,image_folder,output_file)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
#%% Command-line driver
|
|
467
|
+
|
|
468
|
+
def main():
|
|
469
|
+
"""
|
|
470
|
+
Command-line interface to convert YOLOv5/YOLOv8 output (.json or .txt)
|
|
471
|
+
to MegaDetector output format.
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
parser = argparse.ArgumentParser(
|
|
475
|
+
description='Converts YOLOv5 output (.json or .txt) to MD output format.'
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# The first argument determines which series of additional arguments are supported, for
|
|
479
|
+
# json/txt input
|
|
480
|
+
subparsers = parser.add_subparsers(dest='mode', required=True,
|
|
481
|
+
help="Mode of operation: 'json' for YOLO JSON output, 'txt' for YOLO TXT output.")
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
## 'json' mode subparser
|
|
485
|
+
|
|
486
|
+
parser_json = subparsers.add_parser('json', help='Convert YOLO-formatted .json results.')
|
|
487
|
+
|
|
488
|
+
parser_json.add_argument(
|
|
489
|
+
'yolo_json_file', type=str,
|
|
490
|
+
help='Path to the input YOLO-formatted .json results file'
|
|
491
|
+
)
|
|
492
|
+
parser_json.add_argument(
|
|
493
|
+
'image_folder', type=str,
|
|
494
|
+
help='Path to the image folder'
|
|
495
|
+
)
|
|
496
|
+
parser_json.add_argument(
|
|
497
|
+
'output_file', type=str,
|
|
498
|
+
help='Path to the MD-formatted .json output file'
|
|
499
|
+
)
|
|
500
|
+
parser_json.add_argument(
|
|
501
|
+
'yolo_category_id_to_name_file', type=str,
|
|
502
|
+
help='Path to the .yml, .yaml, .json, or .txt file mapping YOLO category IDs to names'
|
|
503
|
+
)
|
|
504
|
+
parser_json.add_argument(
|
|
505
|
+
'--detector_name', type=str, default='unknown',
|
|
506
|
+
help="Detector name to store in the output file (default: 'unknown')"
|
|
507
|
+
)
|
|
508
|
+
parser_json.add_argument(
|
|
509
|
+
'--image_id_to_relative_path_file', type=str, default=None,
|
|
510
|
+
help='Path to a .json file mapping image IDs to relative paths'
|
|
511
|
+
)
|
|
512
|
+
parser_json.add_argument(
|
|
513
|
+
'--offset_yolo_class_ids', type=str, default='true', choices=['true', 'false'],
|
|
514
|
+
help="Offset YOLO class IDs in the output (default: 'true')"
|
|
515
|
+
)
|
|
516
|
+
parser_json.add_argument(
|
|
517
|
+
'--truncate_to_standard_md_precision', type=str, default='true', choices=['true', 'false'],
|
|
518
|
+
help="Truncate coordinates and confidences to standard MD precision (default: 'true')"
|
|
519
|
+
)
|
|
520
|
+
parser_json.add_argument(
|
|
521
|
+
'--convert_slashes', type=str, default='true', choices=['true', 'false'],
|
|
522
|
+
help="Convert backslashes to forward slashes in output file paths (default: 'true')"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
## 'txt' mode subparser
|
|
527
|
+
|
|
528
|
+
parser_txt = subparsers.add_parser('txt', help='Convert YOLO-formatted .txt results from a folder')
|
|
529
|
+
parser_txt.add_argument(
|
|
530
|
+
'input_results_folder', type=str,
|
|
531
|
+
help='Path to the folder containing YOLO .txt output files'
|
|
532
|
+
)
|
|
533
|
+
parser_txt.add_argument(
|
|
534
|
+
'image_folder', type=str,
|
|
535
|
+
help='Path to the image folder'
|
|
536
|
+
)
|
|
537
|
+
parser_txt.add_argument(
|
|
538
|
+
'output_file', type=str,
|
|
539
|
+
help='Path to the MD-formatted .json file output'
|
|
540
|
+
)
|
|
541
|
+
parser_txt.add_argument(
|
|
542
|
+
'--detector_tag', type=str, default=None,
|
|
543
|
+
help='Detector tag to store in the output file'
|
|
544
|
+
)
|
|
545
|
+
parser_txt.add_argument(
|
|
546
|
+
'--truncate_to_standard_md_precision', type=str, default='true', choices=['true', 'false'],
|
|
547
|
+
help="Truncate coordinates and confidences to standard MD precision (default: 'true')."
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
args = parser.parse_args()
|
|
551
|
+
|
|
552
|
+
if args.mode == 'json':
|
|
553
|
+
|
|
554
|
+
image_id_to_relative_path = None
|
|
555
|
+
if args.image_id_to_relative_path_file:
|
|
556
|
+
try:
|
|
557
|
+
with open(args.image_id_to_relative_path_file, 'r') as f:
|
|
558
|
+
image_id_to_relative_path = json.load(f)
|
|
559
|
+
except Exception as e:
|
|
560
|
+
print(f"Error loading image_id_to_relative_path_file: {e}")
|
|
561
|
+
sys.exit(1)
|
|
562
|
+
|
|
563
|
+
offset_yolo_class_ids = args.offset_yolo_class_ids.lower() == 'true'
|
|
564
|
+
truncate_json = args.truncate_to_standard_md_precision.lower() == 'true'
|
|
565
|
+
convert_slashes = args.convert_slashes.lower() == 'true'
|
|
566
|
+
|
|
567
|
+
yolo_json_output_to_md_output(
|
|
568
|
+
yolo_json_file=args.yolo_json_file,
|
|
569
|
+
image_folder=args.image_folder,
|
|
570
|
+
output_file=args.output_file,
|
|
571
|
+
yolo_category_id_to_name=args.yolo_category_id_to_name_file, # Function handles reading this file
|
|
572
|
+
detector_name=args.detector_name,
|
|
573
|
+
image_id_to_relative_path=image_id_to_relative_path,
|
|
574
|
+
offset_yolo_class_ids=offset_yolo_class_ids,
|
|
575
|
+
truncate_to_standard_md_precision=truncate_json,
|
|
576
|
+
convert_slashes=convert_slashes
|
|
577
|
+
)
|
|
578
|
+
print('Converted {} to {}'.format(args.yolo_json_file,args.output_file))
|
|
579
|
+
|
|
580
|
+
elif args.mode == 'txt':
|
|
581
|
+
|
|
582
|
+
truncate_txt = args.truncate_to_standard_md_precision.lower() == 'true'
|
|
583
|
+
|
|
584
|
+
yolo_txt_output_to_md_output(
|
|
585
|
+
input_results_folder=args.input_results_folder,
|
|
586
|
+
image_folder=args.image_folder,
|
|
587
|
+
output_file=args.output_file,
|
|
588
|
+
detector_tag=args.detector_tag,
|
|
589
|
+
truncate_to_standard_md_precision=truncate_txt
|
|
590
|
+
)
|
|
591
|
+
print('Converted results from {} to {}'.format(args.input_results_folder,args.output_file))
|
|
592
|
+
|
|
593
|
+
if __name__ == '__main__':
|
|
594
|
+
main()
|