megadetector 5.0.6__py3-none-any.whl → 5.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of megadetector might be problematic. Click here for more details.

Files changed (62) hide show
  1. api/batch_processing/data_preparation/manage_local_batch.py +278 -197
  2. api/batch_processing/data_preparation/manage_video_batch.py +7 -2
  3. api/batch_processing/postprocessing/add_max_conf.py +1 -0
  4. api/batch_processing/postprocessing/compare_batch_results.py +110 -60
  5. api/batch_processing/postprocessing/load_api_results.py +55 -69
  6. api/batch_processing/postprocessing/md_to_labelme.py +1 -0
  7. api/batch_processing/postprocessing/postprocess_batch_results.py +158 -50
  8. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +625 -0
  9. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +71 -23
  10. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +1 -1
  11. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +222 -74
  12. api/batch_processing/postprocessing/subset_json_detector_output.py +132 -5
  13. api/batch_processing/postprocessing/top_folders_to_bottom.py +1 -1
  14. classification/prepare_classification_script.py +191 -191
  15. data_management/coco_to_yolo.py +65 -44
  16. data_management/databases/integrity_check_json_db.py +7 -5
  17. data_management/generate_crops_from_cct.py +1 -1
  18. data_management/importers/animl_results_to_md_results.py +2 -2
  19. data_management/importers/noaa_seals_2019.py +1 -1
  20. data_management/importers/zamba_results_to_md_results.py +2 -2
  21. data_management/labelme_to_coco.py +34 -6
  22. data_management/labelme_to_yolo.py +1 -1
  23. data_management/lila/create_lila_blank_set.py +474 -0
  24. data_management/lila/create_lila_test_set.py +2 -1
  25. data_management/lila/create_links_to_md_results_files.py +1 -1
  26. data_management/lila/download_lila_subset.py +46 -21
  27. data_management/lila/generate_lila_per_image_labels.py +23 -14
  28. data_management/lila/get_lila_annotation_counts.py +16 -10
  29. data_management/lila/lila_common.py +14 -11
  30. data_management/lila/test_lila_metadata_urls.py +116 -0
  31. data_management/resize_coco_dataset.py +12 -10
  32. data_management/yolo_output_to_md_output.py +40 -13
  33. data_management/yolo_to_coco.py +34 -21
  34. detection/process_video.py +36 -14
  35. detection/pytorch_detector.py +1 -1
  36. detection/run_detector.py +73 -18
  37. detection/run_detector_batch.py +104 -24
  38. detection/run_inference_with_yolov5_val.py +127 -26
  39. detection/run_tiled_inference.py +153 -43
  40. detection/video_utils.py +3 -1
  41. md_utils/ct_utils.py +79 -3
  42. md_utils/md_tests.py +253 -15
  43. md_utils/path_utils.py +129 -24
  44. md_utils/process_utils.py +26 -7
  45. md_utils/split_locations_into_train_val.py +215 -0
  46. md_utils/string_utils.py +10 -0
  47. md_utils/url_utils.py +0 -2
  48. md_utils/write_html_image_list.py +1 -0
  49. md_visualization/visualization_utils.py +17 -2
  50. md_visualization/visualize_db.py +8 -0
  51. md_visualization/visualize_detector_output.py +185 -104
  52. {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/METADATA +2 -2
  53. {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/RECORD +62 -58
  54. {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/WHEEL +1 -1
  55. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +1 -1
  56. taxonomy_mapping/map_new_lila_datasets.py +43 -39
  57. taxonomy_mapping/prepare_lila_taxonomy_release.py +5 -2
  58. taxonomy_mapping/preview_lila_taxonomy.py +27 -27
  59. taxonomy_mapping/species_lookup.py +33 -13
  60. taxonomy_mapping/taxonomy_csv_checker.py +7 -5
  61. {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/LICENSE +0 -0
  62. {megadetector-5.0.6.dist-info → megadetector-5.0.7.dist-info}/top_level.txt +0 -0
@@ -22,6 +22,9 @@ import json
22
22
  import pandas as pd
23
23
  import numpy as np
24
24
  import dateparser
25
+ import csv
26
+ import urllib
27
+ import urllib.request
25
28
 
26
29
  from collections import defaultdict
27
30
  from tqdm import tqdm
@@ -30,6 +33,9 @@ from data_management.lila.lila_common import read_lila_metadata, \
30
33
  read_metadata_file_for_dataset, \
31
34
  read_lila_taxonomy_mapping
32
35
 
36
+ from md_utils import write_html_image_list
37
+ from md_utils.path_utils import zip_file
38
+ from md_utils.path_utils import open_file
33
39
  from md_utils.url_utils import download_url
34
40
 
35
41
  # We'll write images, metadata downloads, and temporary files here
@@ -56,7 +62,7 @@ ds_name_to_annotation_level['NACTI'] = 'unknown'
56
62
 
57
63
  known_unmapped_labels = set(['WCS Camera Traps:#ref!'])
58
64
 
59
- debug_max_images_per_dataset = 0
65
+ debug_max_images_per_dataset = -1
60
66
  if debug_max_images_per_dataset > 0:
61
67
  print('Running in debug mode')
62
68
  output_file = output_file.replace('.csv','_debug.csv')
@@ -72,7 +78,7 @@ if False:
72
78
  metadata_table = {k:metadata_table[k]}
73
79
 
74
80
 
75
- #%% Download and extract metadata for the datasets we're interested in
81
+ #%% Download and extract metadata for each dataset
76
82
 
77
83
  for ds_name in metadata_table.keys():
78
84
  metadata_table[ds_name]['metadata_filename'] = read_metadata_file_for_dataset(ds_name=ds_name,
@@ -101,8 +107,6 @@ for i_row,row in taxonomy_df.iterrows():
101
107
 
102
108
  # Takes several hours
103
109
 
104
- import csv
105
-
106
110
  header = ['dataset_name','url','image_id','sequence_id','location_id','frame_num','original_label',\
107
111
  'scientific_name','common_name','datetime','annotation_level']
108
112
 
@@ -122,7 +126,7 @@ def clearnan(v):
122
126
  assert isinstance(v,str)
123
127
  return v
124
128
 
125
- with open(output_file,'w') as f:
129
+ with open(output_file,'w',encoding='utf-8',newline='') as f:
126
130
 
127
131
  csv_writer = csv.writer(f)
128
132
  csv_writer.writerow(header)
@@ -334,6 +338,8 @@ with open(output_file,'w') as f:
334
338
 
335
339
  # ...with open()
336
340
 
341
+ print('Processed {} datsets'.format(len(metadata_table)))
342
+
337
343
 
338
344
  #%% Read the .csv back
339
345
 
@@ -352,6 +358,8 @@ def isint(v):
352
358
 
353
359
  valid_annotation_levels = set(['sequence','image','unknown'])
354
360
 
361
+ # Collect a list of locations within each dataset; we'll use this
362
+ # in the next cell to look for datasets that only have a single location
355
363
  dataset_name_to_locations = defaultdict(set)
356
364
 
357
365
  def check_row(row):
@@ -386,6 +394,8 @@ else:
386
394
 
387
395
  #%% Check for datasets that have only one location string
388
396
 
397
+ # Expected: ENA24, Missouri Camera Traps
398
+
389
399
  for ds_name in dataset_name_to_locations.keys():
390
400
  if len(dataset_name_to_locations[ds_name]) == 1:
391
401
  print('No location information for {}'.format(ds_name))
@@ -440,8 +450,8 @@ print('Selected {} total images'.format(len(images_to_download)))
440
450
 
441
451
  # Expect a few errors for images with human or vehicle labels (or things like "ignore" that *could* be humans)
442
452
 
443
- import urllib.request
444
-
453
+ # TODO: trivially parallelizable
454
+ #
445
455
  # i_image = 10; image = images_to_download[i_image]
446
456
  for i_image,image in tqdm(enumerate(images_to_download),total=len(images_to_download)):
447
457
 
@@ -450,17 +460,17 @@ for i_image,image in tqdm(enumerate(images_to_download),total=len(images_to_down
450
460
  image_file = os.path.join(preview_folder,'image_{}'.format(str(i_image).zfill(4)) + ext)
451
461
  relative_file = os.path.relpath(image_file,preview_folder)
452
462
  try:
453
- download_url(url,output_file,verbose=False)
463
+ download_url(url,image_file,verbose=False)
454
464
  image['relative_file'] = relative_file
455
465
  except urllib.error.HTTPError:
456
466
  print('Image {} does not exist ({}:{})'.format(
457
467
  i_image,image['dataset_name'],image['original_label']))
458
468
  image['relative_file'] = None
459
469
 
470
+ # ...for each image we need to download
460
471
 
461
- #%% Write preview HTML
462
472
 
463
- from md_utils import write_html_image_list
473
+ #%% Write preview HTML
464
474
 
465
475
  html_filename = os.path.join(preview_folder,'index.html')
466
476
 
@@ -475,19 +485,18 @@ for im in images_to_download:
475
485
  output_im = {}
476
486
  output_im['filename'] = im['relative_file']
477
487
  output_im['linkTarget'] = im['url']
478
- output_im['title'] = str(im)
488
+ output_im['title'] = '<b>{}: {}</b><br/><br/>'.format(im['dataset_name'],im['original_label']) + str(im)
479
489
  output_im['imageStyle'] = 'width:600px;'
480
490
  output_im['textStyle'] = 'font-weight:normal;font-size:100%;'
481
491
  html_images.append(output_im)
482
492
 
483
493
  write_html_image_list.write_html_image_list(html_filename,html_images)
484
494
 
485
- from md_utils.path_utils import open_file
486
495
  open_file(html_filename)
487
496
 
488
497
 
489
498
  #%% Zip output file
490
499
 
491
- from md_utils.path_utils import zip_file
500
+ zipped_output_file = zip_file(output_file,verbose=True)
492
501
 
493
- zip_file(output_file,verbose=True)
502
+ print('Zipped {} to {}'.format(output_file,zipped_output_file))
@@ -34,18 +34,9 @@ os.makedirs(metadata_dir,exist_ok=True)
34
34
 
35
35
  output_file = os.path.join(output_dir,'lila_dataset_to_categories.json')
36
36
 
37
- # Created by get_lila_category_list.py... contains counts for each category
38
- category_list_dir = os.path.join(lila_local_base,'lila_categories_list')
39
- lila_dataset_to_categories_file = os.path.join(category_list_dir,'lila_dataset_to_categories.json')
40
-
41
- assert os.path.isfile(lila_dataset_to_categories_file)
42
-
43
37
 
44
38
  #%% Load category and taxonomy files
45
39
 
46
- with open(lila_dataset_to_categories_file,'r') as f:
47
- lila_dataset_to_categories = json.load(f)
48
-
49
40
  taxonomy_df = read_lila_taxonomy_mapping(metadata_dir)
50
41
 
51
42
 
@@ -55,9 +46,13 @@ ds_query_to_scientific_name = {}
55
46
 
56
47
  unmapped_queries = set()
57
48
 
49
+ datasets_with_taxonomy_mapping = set()
50
+
58
51
  # i_row = 1; row = taxonomy_df.iloc[i_row]; row
59
52
  for i_row,row in taxonomy_df.iterrows():
60
53
 
54
+ datasets_with_taxonomy_mapping.add(row['dataset_name'])
55
+
61
56
  ds_query = row['dataset_name'] + ':' + row['query']
62
57
  ds_query = ds_query.lower()
63
58
 
@@ -68,13 +63,17 @@ for i_row,row in taxonomy_df.iterrows():
68
63
 
69
64
  ds_query_to_scientific_name[ds_query] = row['scientific_name']
70
65
 
66
+ print('Loaded taxonomy mappings for {} datasets'.format(len(datasets_with_taxonomy_mapping)))
71
67
 
68
+
72
69
  #%% Download and parse the metadata file
73
70
 
74
71
  metadata_table = read_lila_metadata(metadata_dir)
75
72
 
73
+ print('Loaded metadata URLs for {} datasets'.format(len(metadata_table)))
74
+
76
75
 
77
- #%% Download and extract metadata for the datasets we're interested in
76
+ #%% Download and extract metadata for each dataset
78
77
 
79
78
  for ds_name in metadata_table.keys():
80
79
  metadata_table[ds_name]['json_filename'] = read_metadata_file_for_dataset(ds_name=ds_name,
@@ -91,6 +90,11 @@ dataset_to_categories = {}
91
90
  # ds_name = 'NACTI'
92
91
  for ds_name in metadata_table.keys():
93
92
 
93
+ taxonomy_mapping_available = (ds_name in datasets_with_taxonomy_mapping)
94
+
95
+ if not taxonomy_mapping_available:
96
+ print('Warning: taxonomy mapping not available for {}'.format(ds_name))
97
+
94
98
  print('Finding categories in {}'.format(ds_name))
95
99
 
96
100
  json_filename = metadata_table[ds_name]['json_filename']
@@ -122,6 +126,8 @@ for ds_name in metadata_table.keys():
122
126
  # always redundant with the class-level data sets.
123
127
  if 'bbox' in ds_name:
124
128
  c['scientific_name_from_taxonomy_mapping'] = None
129
+ elif not taxonomy_mapping_available:
130
+ c['scientific_name_from_taxonomy_mapping'] = None
125
131
  else:
126
132
  taxonomy_query_string = ds_name.lower().strip() + ':' + c['name'].lower()
127
133
  if taxonomy_query_string not in ds_query_to_scientific_name:
@@ -21,7 +21,7 @@ from md_utils.path_utils import unzip_file
21
21
 
22
22
  # LILA camera trap primary metadata file
23
23
  lila_metadata_url = 'http://lila.science/wp-content/uploads/2023/06/lila_camera_trap_datasets.csv'
24
- lila_taxonomy_mapping_url = 'https://lila.science/wp-content/uploads/2022/07/lila-taxonomy-mapping_release.csv'
24
+ lila_taxonomy_mapping_url = 'https://lila.science/public/lila-taxonomy-mapping_release.csv'
25
25
  lila_all_images_url = 'https://lila.science/public/lila_image_urls_and_labels.csv.zip'
26
26
 
27
27
  wildlife_insights_page_size = 30000
@@ -165,16 +165,18 @@ def read_lila_all_images_file(metadata_dir):
165
165
  return df
166
166
 
167
167
 
168
- def read_metadata_file_for_dataset(ds_name,metadata_dir,metadata_table=None):
168
+ def read_metadata_file_for_dataset(ds_name,metadata_dir,metadata_table=None,json_url=None):
169
169
  """
170
170
  Downloads if necessary - then unzips if necessary - the .json file for a specific dataset.
171
171
  Returns the .json filename on the local disk.
172
172
  """
173
173
 
174
- if metadata_table is None:
175
- metadata_table = read_lila_metadata(metadata_dir)
174
+ if json_url is None:
176
175
 
177
- json_url = metadata_table[ds_name]['metadata_url']
176
+ if metadata_table is None:
177
+ metadata_table = read_lila_metadata(metadata_dir)
178
+
179
+ json_url = metadata_table[ds_name]['metadata_url']
178
180
 
179
181
  p = urlparse(json_url)
180
182
  json_filename = os.path.join(metadata_dir,os.path.basename(p.path))
@@ -196,25 +198,26 @@ def read_metadata_file_for_dataset(ds_name,metadata_dir,metadata_table=None):
196
198
  return json_filename
197
199
 
198
200
 
199
- def azure_url_to_gcp_http_url(url):
201
+ def azure_url_to_gcp_http_url(url,error_if_not_azure_url=True):
200
202
  """
201
203
  Most URLs point to Azure by default, but most files are available on both Azure and GCP.
202
204
  This function converts an Azure URL to the corresponding GCP http:// url.
203
205
  """
204
206
 
205
- assert url.startswith(lila_azure_storage_account)
207
+ if error_if_not_azure_url:
208
+ assert url.startswith(lila_azure_storage_account)
206
209
  gcp_url = url.replace(lila_azure_storage_account,gcp_bucket_api_url,1)
207
210
  return gcp_url
208
211
 
209
212
 
210
- def azure_url_to_gcp_gs_url(url):
213
+ def azure_url_to_gcp_gs_url(url,error_if_not_azure_url=True):
211
214
  """
212
215
  Most URLs point to Azure by default, but most files are available on both Azure and GCP.
213
216
  This function converts an Azure URL to the corresponding GCP gs:// url.
214
217
  """
215
218
 
216
- return azure_url_to_gcp_http_url(url).replace(gcp_bucket_api_url,
217
- gcp_bucket_gs_url,1)
219
+ return azure_url_to_gcp_http_url(url,error_if_not_azure_url).\
220
+ replace(gcp_bucket_api_url,gcp_bucket_gs_url,1)
218
221
 
219
222
 
220
223
  #%% Interactive test driver
@@ -261,4 +264,4 @@ if False:
261
264
  gcp_url = url.replace(lila_azure_storage_account,gcp_bucket_api_url,1)
262
265
  gcp_urls.append(gcp_url)
263
266
 
264
- status_codes = url_utils.test_urls(gcp_urls)
267
+ status_codes = url_utils.test_urls(gcp_urls)
@@ -0,0 +1,116 @@
1
+ ########
2
+ #
3
+ # test_lila_metadata_urls.py
4
+ #
5
+ # Test that all the metadata URLs for LILA camera trap datasets are valid, and
6
+ # test that at least one image within each URL is valid, including MegaDetector results
7
+ # files.
8
+ #
9
+ ########
10
+
11
+ #%% Constants and imports
12
+
13
+ import json
14
+ import os
15
+
16
+ from data_management.lila.lila_common import read_lila_metadata,\
17
+ read_metadata_file_for_dataset, read_lila_taxonomy_mapping
18
+
19
+ # We'll write images, metadata downloads, and temporary files here
20
+ lila_local_base = os.path.expanduser('~/lila')
21
+
22
+ output_dir = os.path.join(lila_local_base,'lila_metadata_tests')
23
+ os.makedirs(output_dir,exist_ok=True)
24
+
25
+ metadata_dir = os.path.join(lila_local_base,'metadata')
26
+ os.makedirs(metadata_dir,exist_ok=True)
27
+
28
+ md_results_dir = os.path.join(lila_local_base,'md_results')
29
+ os.makedirs(md_results_dir,exist_ok=True)
30
+
31
+ md_results_keys = ['mdv4_results_raw','mdv5a_results_raw','mdv5b_results_raw','md_results_with_rde']
32
+
33
+
34
+ #%% Load category and taxonomy files
35
+
36
+ taxonomy_df = read_lila_taxonomy_mapping(metadata_dir)
37
+
38
+
39
+ #%% Download and parse the metadata file
40
+
41
+ metadata_table = read_lila_metadata(metadata_dir)
42
+
43
+ print('Loaded metadata URLs for {} datasets'.format(len(metadata_table)))
44
+
45
+
46
+ #%% Download and extract metadata and MD results for each dataset
47
+
48
+ for ds_name in metadata_table.keys():
49
+
50
+ metadata_table[ds_name]['json_filename'] = read_metadata_file_for_dataset(ds_name=ds_name,
51
+ metadata_dir=metadata_dir,
52
+ metadata_table=metadata_table)
53
+ for k in md_results_keys:
54
+ md_results_url = metadata_table[ds_name][k]
55
+ if md_results_url is None:
56
+ metadata_table[ds_name][k + '_filename'] = None
57
+ else:
58
+ metadata_table[ds_name][k + '_filename'] = read_metadata_file_for_dataset(ds_name=ds_name,
59
+ metadata_dir=md_results_dir,
60
+ json_url=md_results_url)
61
+
62
+
63
+ #%% Build up a list of URLs to test
64
+
65
+ url_to_source = {}
66
+
67
+ # The first image in a dataset is disproportionately likely to be human (and thus 404)
68
+ image_index = 1000
69
+
70
+ # ds_name = list(metadata_table.keys())[0]
71
+ for ds_name in metadata_table.keys():
72
+
73
+ if 'bbox' in ds_name:
74
+ print('Skipping bbox dataset {}'.format(ds_name))
75
+ continue
76
+
77
+ print('Processing dataset {}'.format(ds_name))
78
+
79
+ json_filename = metadata_table[ds_name]['json_filename']
80
+ with open(json_filename, 'r') as f:
81
+ data = json.load(f)
82
+
83
+ image_base_url = metadata_table[ds_name]['image_base_url']
84
+ assert not image_base_url.endswith('/')
85
+ # Download a test image
86
+ test_image_relative_path = data['images'][image_index]['file_name']
87
+ test_image_url = image_base_url + '/' + test_image_relative_path
88
+
89
+ url_to_source[test_image_url] = ds_name + ' metadata'
90
+
91
+ # k = md_results_keys[2]
92
+ for k in md_results_keys:
93
+ k_fn = k + '_filename'
94
+ if metadata_table[ds_name][k_fn] is not None:
95
+ with open(metadata_table[ds_name][k_fn],'r') as f:
96
+ md_results = json.load(f)
97
+ im = md_results['images'][image_index]
98
+ md_image_url = image_base_url + '/' + im['file']
99
+ url_to_source[md_image_url] = ds_name + ' ' + k
100
+
101
+ # ...for each dataset
102
+
103
+
104
+ #%% Test URLs
105
+
106
+ from md_utils.url_utils import test_urls
107
+
108
+ urls_to_test = sorted(url_to_source.keys())
109
+ urls_to_test = [fn.replace('\\','/') for fn in urls_to_test]
110
+
111
+ status_codes = test_urls(urls_to_test,error_on_failure=False)
112
+
113
+ for i_url,url in enumerate(urls_to_test):
114
+ if status_codes[i_url] != 200:
115
+ print('Status {} for {} ({})'.format(
116
+ status_codes[i_url],url,url_to_source[url]))
@@ -49,6 +49,8 @@ def resize_coco_dataset(input_folder,input_filename,
49
49
  of the way there, due to what appears to be a slight bias inherent to MD. If a box extends
50
50
  within [right_edge_quantization_threshold] (a small number, from 0 to 1, but probably around
51
51
  0.02) of the right edge of the image, it will be extended to the far right edge.
52
+
53
+ Returns the COCO database with resized images.
52
54
  """
53
55
 
54
56
  # Read input data
@@ -62,7 +64,9 @@ def resize_coco_dataset(input_folder,input_filename,
62
64
 
63
65
  # For each image
64
66
 
65
- # im = d['images'][1]
67
+ # TODO: this is trivially parallelizable
68
+ #
69
+ # im = d['images'][0]
66
70
  for im in tqdm(d['images']):
67
71
 
68
72
  input_fn_relative = im['file_name']
@@ -143,6 +147,8 @@ def resize_coco_dataset(input_folder,input_filename,
143
147
  with open(output_filename,'w') as f:
144
148
  json.dump(d,f,indent=1)
145
149
 
150
+ return d
151
+
146
152
  # ...def resize_coco_dataset(...)
147
153
 
148
154
 
@@ -153,17 +159,13 @@ if False:
153
159
  pass
154
160
 
155
161
  #%% Test resizing
156
-
157
- # input_filename = os.path.expanduser('~/tmp/labelme_to_coco_test.json')
158
- # input_folder = os.path.expanduser('~/data/labelme-json-test')
159
- # target_size = (600,-1)
160
-
161
- input_folder = os.path.expanduser('~/data/usgs-kissel-training')
162
- input_filename = os.path.expanduser('~/data/usgs-tegus.json')
162
+
163
+ input_folder = os.path.expanduser('~/data/usgs-tegus/usgs-kissel-training')
164
+ input_filename = os.path.expanduser('~/data/usgs-tegus/usgs-kissel-training.json')
163
165
  target_size = (1600,-1)
164
166
 
165
- output_filename = insert_before_extension(input_filename,'resized')
166
- output_folder = input_folder + '-resized'
167
+ output_filename = insert_before_extension(input_filename,'resized-test')
168
+ output_folder = input_folder + '-resized-test'
167
169
 
168
170
  correct_size_image_handling = 'rewrite'
169
171
 
@@ -61,21 +61,37 @@ from detection.run_detector import CONF_DIGITS, COORD_DIGITS
61
61
 
62
62
  def read_classes_from_yolo_dataset_file(fn):
63
63
  """
64
- Read a dictionary mapping integer class IDs to class names from a YOLOv5 dataset.yaml
65
- file.
64
+ Read a dictionary mapping integer class IDs to class names from a YOLOv5/YOLOv8
65
+ dataset.yaml file or a .json file. A .json file should contain a dictionary mapping
66
+ integer category IDs to string category names.
66
67
  """
67
68
 
68
- with open(fn,'r') as f:
69
- lines = f.readlines()
70
-
71
- category_id_to_name = {}
72
- pat = '\d+:.+'
73
- for s in lines:
74
- if re.search(pat,s) is not None:
75
- tokens = s.split(':')
76
- assert len(tokens) == 2, 'Invalid token in category file {}'.format(fn)
77
- category_id_to_name[int(tokens[0].strip())] = tokens[1].strip()
69
+ if fn.endswith('.yml') or fn.endswith('.yaml'):
70
+
71
+ with open(fn,'r') as f:
72
+ lines = f.readlines()
73
+
74
+ category_id_to_name = {}
75
+ pat = '\d+:.+'
76
+ for s in lines:
77
+ if re.search(pat,s) is not None:
78
+ tokens = s.split(':')
79
+ assert len(tokens) == 2, 'Invalid token in category file {}'.format(fn)
80
+ category_id_to_name[int(tokens[0].strip())] = tokens[1].strip()
81
+
82
+ elif fn.endswith('.json'):
83
+
84
+ with open(fn,'r') as f:
85
+ d_in = json.load(f)
86
+ category_id_to_name = {}
87
+ for k in d_in.keys():
88
+ category_id_to_name[int(k)] = d_in[k]
78
89
 
90
+ else:
91
+
92
+ raise ValueError('Unrecognized category file type: {}'.format(fn))
93
+
94
+ assert len(category_id_to_name) > 0, 'Failed to read class mappings from {}'.format(fn)
79
95
  return category_id_to_name
80
96
 
81
97
 
@@ -125,7 +141,8 @@ def yolo_json_output_to_md_output(yolo_json_file, image_folder,
125
141
  if image_id_to_error is None:
126
142
  image_id_to_error = {}
127
143
 
128
- print('Converting {} to MD format'.format(yolo_json_file))
144
+ print('Converting {} to MD format and writing results to {}'.format(
145
+ yolo_json_file,output_file))
129
146
 
130
147
  if isinstance(yolo_category_id_to_name,str):
131
148
  assert os.path.isfile(yolo_category_id_to_name), \
@@ -194,6 +211,16 @@ def yolo_json_output_to_md_output(yolo_json_file, image_folder,
194
211
 
195
212
  # ...if image IDs are formatted as integers in YOLO output
196
213
 
214
+ # In a modified version of val.py, we use negative category IDs to indicate an error
215
+ # that happened during inference (typically truncated images with valid headers,
216
+ # so corruption was not detected during val.py's initial corruption check pass.
217
+ for det in detections:
218
+ if det['category_id'] < 0:
219
+ assert 'error' in det, 'Negative category ID present with no error string'
220
+ error_string = det['error']
221
+ print('Caught inference-time failure {} for image {}'.format(error_string,det['image_id']))
222
+ image_id_to_error[det['image_id']] = error_string
223
+
197
224
  output_images = []
198
225
 
199
226
  # image_file_relative = image_files_relative[10]
@@ -18,6 +18,7 @@ from PIL import Image
18
18
  from tqdm import tqdm
19
19
 
20
20
  from md_utils.path_utils import find_images
21
+ from data_management.yolo_output_to_md_output import read_classes_from_yolo_dataset_file
21
22
 
22
23
 
23
24
  #%% Main conversion function
@@ -25,8 +26,10 @@ from md_utils.path_utils import find_images
25
26
  def yolo_to_coco(input_folder,class_name_file,output_file=None):
26
27
  """
27
28
  Convert the YOLO-formatted data in [input_folder] to a COCO-formatted dictionary,
28
- reading class names from the flat list [class_name_file]. Optionally writes the output
29
- dataset to [output_file].
29
+ reading class names from [class_name_file], which can be a flat list with a .txt
30
+ extension or a YOLO dataset.yml file. Optionally writes the output dataset to [output_file].
31
+
32
+ Returns a COCO-formatted dictionary.
30
33
  """
31
34
 
32
35
  # Validate input
@@ -35,29 +38,39 @@ def yolo_to_coco(input_folder,class_name_file,output_file=None):
35
38
  assert os.path.isfile(class_name_file)
36
39
 
37
40
 
38
- # Class names
41
+ # Read class names
39
42
 
40
- with open(class_name_file,'r') as f:
41
- lines = f.readlines()
42
- assert len(lines) > 0, 'Empty class name file {}'.format(class_name_file)
43
- lines = [s.strip() for s in lines]
44
- assert len(lines[0]) > 0, 'Empty class name file {} (empty first line)'.format(class_name_file)
43
+ ext = os.path.splitext(class_name_file)[1][1:]
44
+ assert ext in ('yml','txt','yaml'), 'Unrecognized class name file type {}'.format(
45
+ class_name_file)
45
46
 
46
- # Blank lines should only appear at the end
47
- b_found_blank = False
48
- for s in lines:
49
- if len(s) == 0:
50
- b_found_blank = True
51
- elif b_found_blank:
52
- raise ValueError('Invalid class name file {}, non-blank line after the last blank line'.format(
53
- class_name_file))
54
-
55
- category_id_to_name = {}
47
+ if ext == 'txt':
56
48
 
57
- for i_category_id,category_name in enumerate(lines):
58
- assert len(category_name) > 0
59
- category_id_to_name[i_category_id] = category_name
49
+ with open(class_name_file,'r') as f:
50
+ lines = f.readlines()
51
+ assert len(lines) > 0, 'Empty class name file {}'.format(class_name_file)
52
+ class_names = [s.strip() for s in lines]
53
+ assert len(lines[0]) > 0, 'Empty class name file {} (empty first line)'.format(class_name_file)
60
54
 
55
+ # Blank lines should only appear at the end
56
+ b_found_blank = False
57
+ for s in lines:
58
+ if len(s) == 0:
59
+ b_found_blank = True
60
+ elif b_found_blank:
61
+ raise ValueError('Invalid class name file {}, non-blank line after the last blank line'.format(
62
+ class_name_file))
63
+
64
+ category_id_to_name = {}
65
+ for i_category_id,category_name in enumerate(class_names):
66
+ assert len(category_name) > 0
67
+ category_id_to_name[i_category_id] = category_name
68
+
69
+ else:
70
+
71
+ assert ext in ('yml','yaml')
72
+ category_id_to_name = read_classes_from_yolo_dataset_file(class_name_file)
73
+
61
74
 
62
75
  # Enumerate images
63
76