megadetector 5.0.10__py3-none-any.whl → 5.0.11__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 (226) hide show
  1. {megadetector-5.0.10.dist-info → megadetector-5.0.11.dist-info}/LICENSE +0 -0
  2. {megadetector-5.0.10.dist-info → megadetector-5.0.11.dist-info}/METADATA +12 -11
  3. megadetector-5.0.11.dist-info/RECORD +5 -0
  4. megadetector-5.0.11.dist-info/top_level.txt +1 -0
  5. api/__init__.py +0 -0
  6. api/batch_processing/__init__.py +0 -0
  7. api/batch_processing/api_core/__init__.py +0 -0
  8. api/batch_processing/api_core/batch_service/__init__.py +0 -0
  9. api/batch_processing/api_core/batch_service/score.py +0 -439
  10. api/batch_processing/api_core/server.py +0 -294
  11. api/batch_processing/api_core/server_api_config.py +0 -98
  12. api/batch_processing/api_core/server_app_config.py +0 -55
  13. api/batch_processing/api_core/server_batch_job_manager.py +0 -220
  14. api/batch_processing/api_core/server_job_status_table.py +0 -152
  15. api/batch_processing/api_core/server_orchestration.py +0 -360
  16. api/batch_processing/api_core/server_utils.py +0 -92
  17. api/batch_processing/api_core_support/__init__.py +0 -0
  18. api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
  19. api/batch_processing/api_support/__init__.py +0 -0
  20. api/batch_processing/api_support/summarize_daily_activity.py +0 -152
  21. api/batch_processing/data_preparation/__init__.py +0 -0
  22. api/batch_processing/data_preparation/manage_local_batch.py +0 -2391
  23. api/batch_processing/data_preparation/manage_video_batch.py +0 -327
  24. api/batch_processing/integration/digiKam/setup.py +0 -6
  25. api/batch_processing/integration/digiKam/xmp_integration.py +0 -465
  26. api/batch_processing/integration/eMammal/test_scripts/config_template.py +0 -5
  27. api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -126
  28. api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +0 -55
  29. api/batch_processing/postprocessing/__init__.py +0 -0
  30. api/batch_processing/postprocessing/add_max_conf.py +0 -64
  31. api/batch_processing/postprocessing/categorize_detections_by_size.py +0 -163
  32. api/batch_processing/postprocessing/combine_api_outputs.py +0 -249
  33. api/batch_processing/postprocessing/compare_batch_results.py +0 -958
  34. api/batch_processing/postprocessing/convert_output_format.py +0 -397
  35. api/batch_processing/postprocessing/load_api_results.py +0 -195
  36. api/batch_processing/postprocessing/md_to_coco.py +0 -310
  37. api/batch_processing/postprocessing/md_to_labelme.py +0 -330
  38. api/batch_processing/postprocessing/merge_detections.py +0 -401
  39. api/batch_processing/postprocessing/postprocess_batch_results.py +0 -1904
  40. api/batch_processing/postprocessing/remap_detection_categories.py +0 -170
  41. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +0 -661
  42. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +0 -211
  43. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +0 -82
  44. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +0 -1631
  45. api/batch_processing/postprocessing/separate_detections_into_folders.py +0 -731
  46. api/batch_processing/postprocessing/subset_json_detector_output.py +0 -696
  47. api/batch_processing/postprocessing/top_folders_to_bottom.py +0 -223
  48. api/synchronous/__init__.py +0 -0
  49. api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  50. api/synchronous/api_core/animal_detection_api/api_backend.py +0 -152
  51. api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -266
  52. api/synchronous/api_core/animal_detection_api/config.py +0 -35
  53. api/synchronous/api_core/animal_detection_api/data_management/annotations/annotation_constants.py +0 -47
  54. api/synchronous/api_core/animal_detection_api/detection/detector_training/copy_checkpoints.py +0 -43
  55. api/synchronous/api_core/animal_detection_api/detection/detector_training/model_main_tf2.py +0 -114
  56. api/synchronous/api_core/animal_detection_api/detection/process_video.py +0 -543
  57. api/synchronous/api_core/animal_detection_api/detection/pytorch_detector.py +0 -304
  58. api/synchronous/api_core/animal_detection_api/detection/run_detector.py +0 -627
  59. api/synchronous/api_core/animal_detection_api/detection/run_detector_batch.py +0 -1029
  60. api/synchronous/api_core/animal_detection_api/detection/run_inference_with_yolov5_val.py +0 -581
  61. api/synchronous/api_core/animal_detection_api/detection/run_tiled_inference.py +0 -754
  62. api/synchronous/api_core/animal_detection_api/detection/tf_detector.py +0 -165
  63. api/synchronous/api_core/animal_detection_api/detection/video_utils.py +0 -495
  64. api/synchronous/api_core/animal_detection_api/md_utils/azure_utils.py +0 -174
  65. api/synchronous/api_core/animal_detection_api/md_utils/ct_utils.py +0 -262
  66. api/synchronous/api_core/animal_detection_api/md_utils/directory_listing.py +0 -251
  67. api/synchronous/api_core/animal_detection_api/md_utils/matlab_porting_tools.py +0 -97
  68. api/synchronous/api_core/animal_detection_api/md_utils/path_utils.py +0 -416
  69. api/synchronous/api_core/animal_detection_api/md_utils/process_utils.py +0 -110
  70. api/synchronous/api_core/animal_detection_api/md_utils/sas_blob_utils.py +0 -509
  71. api/synchronous/api_core/animal_detection_api/md_utils/string_utils.py +0 -59
  72. api/synchronous/api_core/animal_detection_api/md_utils/url_utils.py +0 -144
  73. api/synchronous/api_core/animal_detection_api/md_utils/write_html_image_list.py +0 -226
  74. api/synchronous/api_core/animal_detection_api/md_visualization/visualization_utils.py +0 -841
  75. api/synchronous/api_core/tests/__init__.py +0 -0
  76. api/synchronous/api_core/tests/load_test.py +0 -110
  77. classification/__init__.py +0 -0
  78. classification/aggregate_classifier_probs.py +0 -108
  79. classification/analyze_failed_images.py +0 -227
  80. classification/cache_batchapi_outputs.py +0 -198
  81. classification/create_classification_dataset.py +0 -627
  82. classification/crop_detections.py +0 -516
  83. classification/csv_to_json.py +0 -226
  84. classification/detect_and_crop.py +0 -855
  85. classification/efficientnet/__init__.py +0 -9
  86. classification/efficientnet/model.py +0 -415
  87. classification/efficientnet/utils.py +0 -610
  88. classification/evaluate_model.py +0 -520
  89. classification/identify_mislabeled_candidates.py +0 -152
  90. classification/json_to_azcopy_list.py +0 -63
  91. classification/json_validator.py +0 -695
  92. classification/map_classification_categories.py +0 -276
  93. classification/merge_classification_detection_output.py +0 -506
  94. classification/prepare_classification_script.py +0 -194
  95. classification/prepare_classification_script_mc.py +0 -228
  96. classification/run_classifier.py +0 -286
  97. classification/save_mislabeled.py +0 -110
  98. classification/train_classifier.py +0 -825
  99. classification/train_classifier_tf.py +0 -724
  100. classification/train_utils.py +0 -322
  101. data_management/__init__.py +0 -0
  102. data_management/annotations/__init__.py +0 -0
  103. data_management/annotations/annotation_constants.py +0 -34
  104. data_management/camtrap_dp_to_coco.py +0 -238
  105. data_management/cct_json_utils.py +0 -395
  106. data_management/cct_to_md.py +0 -176
  107. data_management/cct_to_wi.py +0 -289
  108. data_management/coco_to_labelme.py +0 -272
  109. data_management/coco_to_yolo.py +0 -662
  110. data_management/databases/__init__.py +0 -0
  111. data_management/databases/add_width_and_height_to_db.py +0 -33
  112. data_management/databases/combine_coco_camera_traps_files.py +0 -206
  113. data_management/databases/integrity_check_json_db.py +0 -477
  114. data_management/databases/subset_json_db.py +0 -115
  115. data_management/generate_crops_from_cct.py +0 -149
  116. data_management/get_image_sizes.py +0 -188
  117. data_management/importers/add_nacti_sizes.py +0 -52
  118. data_management/importers/add_timestamps_to_icct.py +0 -79
  119. data_management/importers/animl_results_to_md_results.py +0 -158
  120. data_management/importers/auckland_doc_test_to_json.py +0 -372
  121. data_management/importers/auckland_doc_to_json.py +0 -200
  122. data_management/importers/awc_to_json.py +0 -189
  123. data_management/importers/bellevue_to_json.py +0 -273
  124. data_management/importers/cacophony-thermal-importer.py +0 -796
  125. data_management/importers/carrizo_shrubfree_2018.py +0 -268
  126. data_management/importers/carrizo_trail_cam_2017.py +0 -287
  127. data_management/importers/cct_field_adjustments.py +0 -57
  128. data_management/importers/channel_islands_to_cct.py +0 -913
  129. data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  130. data_management/importers/eMammal/eMammal_helpers.py +0 -249
  131. data_management/importers/eMammal/make_eMammal_json.py +0 -223
  132. data_management/importers/ena24_to_json.py +0 -275
  133. data_management/importers/filenames_to_json.py +0 -385
  134. data_management/importers/helena_to_cct.py +0 -282
  135. data_management/importers/idaho-camera-traps.py +0 -1407
  136. data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  137. data_management/importers/jb_csv_to_json.py +0 -150
  138. data_management/importers/mcgill_to_json.py +0 -250
  139. data_management/importers/missouri_to_json.py +0 -489
  140. data_management/importers/nacti_fieldname_adjustments.py +0 -79
  141. data_management/importers/noaa_seals_2019.py +0 -181
  142. data_management/importers/pc_to_json.py +0 -365
  143. data_management/importers/plot_wni_giraffes.py +0 -123
  144. data_management/importers/prepare-noaa-fish-data-for-lila.py +0 -359
  145. data_management/importers/prepare_zsl_imerit.py +0 -131
  146. data_management/importers/rspb_to_json.py +0 -356
  147. data_management/importers/save_the_elephants_survey_A.py +0 -320
  148. data_management/importers/save_the_elephants_survey_B.py +0 -332
  149. data_management/importers/snapshot_safari_importer.py +0 -758
  150. data_management/importers/snapshot_safari_importer_reprise.py +0 -665
  151. data_management/importers/snapshot_serengeti_lila.py +0 -1067
  152. data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  153. data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  154. data_management/importers/sulross_get_exif.py +0 -65
  155. data_management/importers/timelapse_csv_set_to_json.py +0 -490
  156. data_management/importers/ubc_to_json.py +0 -399
  157. data_management/importers/umn_to_json.py +0 -507
  158. data_management/importers/wellington_to_json.py +0 -263
  159. data_management/importers/wi_to_json.py +0 -441
  160. data_management/importers/zamba_results_to_md_results.py +0 -181
  161. data_management/labelme_to_coco.py +0 -548
  162. data_management/labelme_to_yolo.py +0 -272
  163. data_management/lila/__init__.py +0 -0
  164. data_management/lila/add_locations_to_island_camera_traps.py +0 -97
  165. data_management/lila/add_locations_to_nacti.py +0 -147
  166. data_management/lila/create_lila_blank_set.py +0 -557
  167. data_management/lila/create_lila_test_set.py +0 -151
  168. data_management/lila/create_links_to_md_results_files.py +0 -106
  169. data_management/lila/download_lila_subset.py +0 -177
  170. data_management/lila/generate_lila_per_image_labels.py +0 -515
  171. data_management/lila/get_lila_annotation_counts.py +0 -170
  172. data_management/lila/get_lila_image_counts.py +0 -111
  173. data_management/lila/lila_common.py +0 -300
  174. data_management/lila/test_lila_metadata_urls.py +0 -132
  175. data_management/ocr_tools.py +0 -874
  176. data_management/read_exif.py +0 -681
  177. data_management/remap_coco_categories.py +0 -84
  178. data_management/remove_exif.py +0 -66
  179. data_management/resize_coco_dataset.py +0 -189
  180. data_management/wi_download_csv_to_coco.py +0 -246
  181. data_management/yolo_output_to_md_output.py +0 -441
  182. data_management/yolo_to_coco.py +0 -676
  183. detection/__init__.py +0 -0
  184. detection/detector_training/__init__.py +0 -0
  185. detection/detector_training/model_main_tf2.py +0 -114
  186. detection/process_video.py +0 -703
  187. detection/pytorch_detector.py +0 -337
  188. detection/run_detector.py +0 -779
  189. detection/run_detector_batch.py +0 -1219
  190. detection/run_inference_with_yolov5_val.py +0 -917
  191. detection/run_tiled_inference.py +0 -935
  192. detection/tf_detector.py +0 -188
  193. detection/video_utils.py +0 -606
  194. docs/source/conf.py +0 -43
  195. md_utils/__init__.py +0 -0
  196. md_utils/azure_utils.py +0 -174
  197. md_utils/ct_utils.py +0 -612
  198. md_utils/directory_listing.py +0 -246
  199. md_utils/md_tests.py +0 -968
  200. md_utils/path_utils.py +0 -1044
  201. md_utils/process_utils.py +0 -157
  202. md_utils/sas_blob_utils.py +0 -509
  203. md_utils/split_locations_into_train_val.py +0 -228
  204. md_utils/string_utils.py +0 -92
  205. md_utils/url_utils.py +0 -323
  206. md_utils/write_html_image_list.py +0 -225
  207. md_visualization/__init__.py +0 -0
  208. md_visualization/plot_utils.py +0 -293
  209. md_visualization/render_images_with_thumbnails.py +0 -275
  210. md_visualization/visualization_utils.py +0 -1537
  211. md_visualization/visualize_db.py +0 -551
  212. md_visualization/visualize_detector_output.py +0 -406
  213. megadetector-5.0.10.dist-info/RECORD +0 -224
  214. megadetector-5.0.10.dist-info/top_level.txt +0 -8
  215. taxonomy_mapping/__init__.py +0 -0
  216. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +0 -491
  217. taxonomy_mapping/map_new_lila_datasets.py +0 -154
  218. taxonomy_mapping/prepare_lila_taxonomy_release.py +0 -142
  219. taxonomy_mapping/preview_lila_taxonomy.py +0 -591
  220. taxonomy_mapping/retrieve_sample_image.py +0 -71
  221. taxonomy_mapping/simple_image_download.py +0 -218
  222. taxonomy_mapping/species_lookup.py +0 -834
  223. taxonomy_mapping/taxonomy_csv_checker.py +0 -159
  224. taxonomy_mapping/taxonomy_graph.py +0 -346
  225. taxonomy_mapping/validate_lila_category_mappings.py +0 -83
  226. {megadetector-5.0.10.dist-info → megadetector-5.0.11.dist-info}/WHEEL +0 -0
@@ -1,913 +0,0 @@
1
- """
2
-
3
- channel_islands_to_cct.py
4
-
5
- Convert the Channel Islands data set to a COCO-camera-traps .json file
6
-
7
- Uses the command-line tool ExifTool (exiftool.org) to pull EXIF tags from images,
8
- because every Python package we tried failed to pull the "Maker Notes" field properly.
9
-
10
- """
11
-
12
- #%% Imports, constants, paths
13
-
14
- ## Imports ##
15
-
16
- import os
17
- import json
18
- import uuid
19
- import datetime
20
- import glob
21
- import subprocess
22
- import requests
23
- import shutil
24
-
25
- from multiprocessing.pool import ThreadPool
26
- from collections import defaultdict
27
- from urllib.parse import urlparse
28
- from tqdm import tqdm
29
- from PIL import Image
30
-
31
-
32
- ## Constants ##
33
-
34
- required_input_annotation_fields = set([
35
- 'task_id','batch_id','name','url','Object','Output','teams','task_url','Step A Agent'
36
- ])
37
-
38
- n_download_threads = 10
39
- n_exif_threads = 20
40
- n_copy_threads = n_exif_threads
41
-
42
-
43
- ## Paths ##
44
-
45
- input_base = r'e:\channel-islands-in'
46
- output_base = r'g:\channel-islands-out'
47
- exiftool_command_name = r'c:\exiftool-12.13\exiftool(-k).exe'
48
-
49
- input_annotation_folder = os.path.join(input_base,'SCI Cameratrap Samasource Labels')
50
- input_image_folder = os.path.join(input_base,'images')
51
-
52
- assert os.path.isdir(input_base)
53
- assert os.path.isdir(input_annotation_folder)
54
- assert os.path.isdir(input_image_folder)
55
- assert not input_annotation_folder.endswith('/')
56
-
57
- output_file = os.path.join(output_base,'channel_islands_camera_traps.json')
58
- output_image_folder = os.path.join(output_base,'images')
59
- output_image_folder_humans = os.path.join(output_base,'human_images')
60
-
61
- os.makedirs(output_base,exist_ok=True)
62
- os.makedirs(output_image_folder,exist_ok=True)
63
- os.makedirs(output_image_folder_humans,exist_ok=True)
64
-
65
- # Confirm that exiftool is available
66
- # assert which(exiftool_command_name) is not None, 'Could not locate the ExifTool executable'
67
- assert os.path.isfile(exiftool_command_name), 'Could not locate the ExifTool executable'
68
-
69
- parsed_input_file = os.path.join(output_base,'parsed_input.json')
70
- exif_load_results_file = os.path.join(output_base,'exif_load_results.json')
71
- sequence_info_results_file = os.path.join(output_base,'sequence_info_results.json')
72
-
73
-
74
- #%% Load information from every .json file
75
-
76
- json_files = glob.glob(input_annotation_folder+'/**/*.json', recursive=True)
77
- print('Found {} .json files'.format(len(json_files)))
78
-
79
- # Ignore the sample file... actually, first make sure there is a sample file
80
- sample_files = [fn for fn in json_files if 'sample' in fn]
81
- assert len(sample_files) == 1
82
-
83
- # ...and now ignore that sample file.
84
- json_files = [fn for fn in json_files if 'sample' not in fn]
85
- input_images = []
86
-
87
- json_basenames = set()
88
-
89
- # json_file = json_files[0]
90
- for json_file in tqdm(json_files):
91
-
92
- json_filename = os.path.basename(json_file)
93
- assert json_filename not in json_basenames
94
- json_basenames.add(json_filename)
95
-
96
- with open(json_file,'r') as f:
97
- annotations = json.load(f)
98
-
99
- # ann = annotations[0]
100
- for ann in annotations:
101
-
102
- assert isinstance(ann,dict)
103
- ann_keys = set(ann.keys())
104
- assert required_input_annotation_fields == ann_keys
105
- ann['json_filename'] = json_filename
106
- input_images.append(ann)
107
-
108
- # ...for each annotation in this file
109
-
110
- # ...for each .json file
111
-
112
- print('\nLoaded {} image records from {} .json files'.format(len(input_images),len(json_files)))
113
-
114
- image_urls = [ann['url'] for ann in input_images]
115
-
116
-
117
- #%% Confirm URL uniqueness, handle redundant tags
118
-
119
- output_images = []
120
-
121
- urls = set()
122
- for im in tqdm(input_images):
123
-
124
- url = im['url']
125
-
126
- if url in urls:
127
-
128
- for existing_im in input_images:
129
-
130
- # Have we already added this image?
131
- if url == existing_im['url']:
132
-
133
- # One .json file was basically duplicated, but as:
134
- #
135
- # Ellie_2016-2017 SC12.json
136
- # Ellie_2016-2017-SC12.json
137
- assert im['json_filename'].replace('-',' ') == existing_im['json_filename'].replace('-',' ')
138
-
139
- # If the new image has no output, just leave the old one there
140
- if im['Output'] is None:
141
- print('Warning: duplicate URL {}, keeping existing output'.format(url))
142
- break
143
-
144
- # If the old image has no output, and the new one has output, default to the one with output
145
- if (existing_im['Output'] is None) and (im['Output'] is not None):
146
- print('Warning: duplicate URL {}, adding new output'.format(url))
147
- existing_im['Output'] = im['Output']
148
- break
149
-
150
- else:
151
- # Don't worry about the cases where someone tagged 'fox' and someone tagged 'fox_partial'
152
- obj1 = im['Output'][0]['tags']['Object'].replace('_partial','')
153
- obj2 = existing_im['Output'][0]['tags']['Object'].replace('_partial','')
154
- if obj1 != obj2:
155
- print('Warning: image {} tagged with {} and {}'.format(url,obj1,obj2))
156
-
157
- # ...for each image we've already added
158
-
159
- else:
160
-
161
- urls.add(url)
162
- output_images.append(im)
163
-
164
- # ...if this URL is/isn't in the list of URLs we've already processed
165
-
166
- # ...for each image
167
-
168
- print('Kept {} of {} annotation records'.format(len(output_images),len(input_images)))
169
-
170
- images = output_images
171
-
172
-
173
- #%% Save progress
174
-
175
- with open(parsed_input_file,'w') as f:
176
- json.dump(images,f,indent=1)
177
-
178
- #%%
179
-
180
- if False:
181
-
182
- #%%
183
-
184
- with open(parsed_input_file,'r') as f:
185
- images = json.load(f)
186
- assert not any(['exif_tags' in im for im in images])
187
-
188
-
189
- #%% Download files (functions)
190
-
191
- # https://www.quickprogrammingtips.com/python/how-to-download-multiple-files-concurrently-in-python.html
192
-
193
- def download_relative_url(url,overwrite=False):
194
- """
195
- Download:
196
-
197
- https://somestuff.com/my/relative/path/image.jpg
198
-
199
- ...to:
200
-
201
- [input_image_folder]/my/relative/path/image.jpg
202
- """
203
-
204
- parsed_url = urlparse(url)
205
- relative_path = parsed_url.path
206
-
207
- # This is returned with a leading slash, remove it
208
- relative_path = relative_path[1:]
209
-
210
- target_file = os.path.join(input_image_folder,relative_path).replace('\\','/')
211
-
212
- if os.path.isfile(target_file and not overwrite):
213
- print('{} exists, skipping'.format(target_file))
214
- return url
215
-
216
- os.makedirs(os.path.dirname(target_file),exist_ok=True)
217
-
218
- print('Downloading {} to {}'.format(url, target_file))
219
-
220
- r = requests.get(url, stream=True)
221
- if r.status_code == requests.codes.ok:
222
- with open(target_file, 'wb') as f:
223
- for data in r:
224
- f.write(data)
225
- else:
226
- print('Warning: failed to download {}'.format(url))
227
-
228
- return url
229
-
230
-
231
- def download_relative_urls(urls,n_threads = n_download_threads):
232
- """
233
- Download all URLs in [urls]
234
- """
235
- if n_threads == 1:
236
- results = []
237
- for url in urls:
238
- results.append(download_relative_url(url))
239
- else:
240
- results = ThreadPool(n_threads).map(download_relative_url, urls)
241
- return results
242
-
243
-
244
- #%% Download files (execution)
245
-
246
- download_relative_urls(image_urls)
247
-
248
-
249
- #%% Read required fields from EXIF data (functions)
250
-
251
- def process_exif(file_path):
252
- """
253
- Get relevant fields from EXIF data for an image
254
- """
255
-
256
- # -G means "Print group name for each tag", e.g. print:
257
- #
258
- # [File] Bits Per Sample : 8
259
- #
260
- # ...instead of:
261
- #
262
- # Bits Per Sample : 8
263
-
264
- proc = subprocess.Popen([exiftool_command_name, '-G', file_path], stdout=subprocess.PIPE, encoding='utf8')
265
- exif_lines = proc.stdout.readlines()
266
- exif_lines = [s.strip() for s in exif_lines]
267
- assert exif_lines is not None and len(exif_lines) > 0, 'Failed to read EXIF data from {}'.format(file_path)
268
-
269
- # If we don't get any EXIF information, this probably isn't an image
270
- assert any([s.lower().startswith('[exif]') for s in exif_lines])
271
-
272
- exif_tags = {}
273
-
274
- found_makernotes = False
275
-
276
- # line_raw = exif_lines[0]
277
- for line_raw in exif_lines:
278
-
279
- line = line_raw.lower()
280
-
281
- # Split on the first occurrence of ":"
282
- tokens = line.split(':',1)
283
-
284
- assert(len(tokens) == 2)
285
- field_name = tokens[0].strip()
286
- field_value = tokens[1].strip()
287
-
288
- if field_name.startswith('[makernotes]'):
289
-
290
- found_makernotes = True
291
-
292
- if 'sequence' in field_name:
293
- # Typically:
294
- #
295
- # '[MakerNotes] Sequence ', '1 of 3']
296
- frame_num, seq_num_frames = field_value.split('of')
297
- exif_tags['frame_num'] = int(frame_num.strip())
298
- exif_tags['seq_num_frames'] = int(seq_num_frames.strip())
299
-
300
- # Not a typo; we are using serial number as a location
301
- elif 'serial number' in line:
302
- exif_tags['location'] = field_value
303
-
304
- elif ('date/time original' in line and '[file]' not in line and '[composite]' not in line):
305
-
306
- previous_dt = None
307
-
308
- if 'datetime' in exif_tags:
309
- previous_dt = exif_tags['datetime']
310
- dt = datetime.datetime.strptime(field_value,'%Y:%m:%d %H:%M:%S')
311
-
312
- # If there are multiple timestamps, make sure they're *almost* the same
313
- if previous_dt is not None:
314
- delta = abs((dt-previous_dt).total_seconds())
315
- assert delta < 1.0
316
-
317
- exif_tags['datetime'] = dt
318
-
319
- if False:
320
- if 'time' in line:
321
- assert 'datetime' not in exif_tags
322
- exif_tags['datetime'] = field_value
323
-
324
- if (('datetime original' in line) or ('create date' in line) or ('date/time created' in line) or ('date/time original' in line)) \
325
- and ('[file]' not in line) and ('[composite]' not in line):
326
-
327
- previous_dt = None
328
-
329
- if 'datetime' in exif_tags:
330
- previous_dt = exif_tags['datetime']
331
- dt = datetime.datetime.strptime(field_value,'%Y:%m:%d %H:%M:%S')
332
-
333
- # If there are multiple timestamps, make sure they're *almost* the same
334
- if previous_dt is not None:
335
- delta = abs((dt-previous_dt).total_seconds())
336
- assert delta < 1.0
337
-
338
- exif_tags['datetime'] = dt
339
-
340
- if 'image width' in line:
341
- exif_tags['width'] = int(field_value)
342
-
343
- if 'image height' in line:
344
- exif_tags['height'] = int(field_value)
345
-
346
- if 'temperature' in line and not 'fahrenheit' in line:
347
- exif_tags['temperature'] = field_value
348
-
349
- # ...for each line in the exiftool output
350
-
351
- makernotes_fields = ['frame_num', 'seq_num_frames', 'location', 'temperature']
352
-
353
- if not found_makernotes:
354
-
355
- print('Warning: could not find maker notes in {}'.format(file_path))
356
-
357
- # This isn't directly related to the lack of maker notes, but it happens that files that are missing
358
- # maker notes also happen to be missing EXIF date information
359
- if not 'datetime' in exif_tags:
360
- print('Warning: could not find datetime information in {}'.format(file_path))
361
-
362
- for field_name in makernotes_fields:
363
- assert field_name not in exif_tags
364
- exif_tags[field_name] = 'unknown'
365
-
366
- else:
367
-
368
- assert 'datetime' in exif_tags, 'Could not find datetime information in {}'.format(file_path)
369
- for field_name in makernotes_fields:
370
- assert field_name in exif_tags, 'Could not find {} in {}'.format(field_name,file_path)
371
-
372
- return exif_tags
373
-
374
- # ...process_exif()
375
-
376
-
377
- def get_image_local_path(im):
378
-
379
- url = im['url']
380
- parsed_url = urlparse(url)
381
- relative_path = parsed_url.path
382
-
383
- # This is returned with a leading slash, remove it
384
- relative_path = relative_path[1:]
385
-
386
- absolute_path = os.path.join(input_image_folder,relative_path).replace('\\','/')
387
- return absolute_path
388
-
389
-
390
- def add_exif_data(im, overwrite=False):
391
-
392
- if ('exif_tags' in im) and (overwrite==False):
393
- return None
394
-
395
- url = im['url']
396
-
397
- # Ignore non-image files
398
- if url.lower().endswith('ds_store') or ('dropbox.device' in url.lower()):
399
- im['exif_tags'] = None
400
- return
401
-
402
- try:
403
- input_image_path = get_image_local_path(im)
404
- assert os.path.isfile(input_image_path)
405
- exif_tags = process_exif(input_image_path)
406
- im['exif_tags'] = exif_tags
407
- except Exception as e:
408
- s = 'Error on {}: {}'.format(url,str(e))
409
- print(s)
410
- return s
411
- return None
412
-
413
-
414
- #%% Read EXIF data (execution)
415
-
416
- if n_exif_threads == 1:
417
- # ann = images[0]
418
- for im in tqdm(images):
419
- add_exif_data(im)
420
- else:
421
- pool = ThreadPool(n_exif_threads)
422
- exif_read_results = list(tqdm(pool.imap(add_exif_data, images), total=len(images)))
423
-
424
-
425
- #%% Save progress
426
-
427
- with open(exif_load_results_file,'w') as f:
428
-
429
- # Use default=str to handle datetime objects
430
- json.dump(images, f, indent=1, default=str)
431
-
432
- #%%
433
-
434
- if False:
435
-
436
- #%%
437
-
438
- with open(exif_load_results_file,'r') as f:
439
- # Not deserializing datetimes yet, will do this if I actually need to run this
440
- images = json.load(f)
441
-
442
-
443
- #%% Check for EXIF read errors
444
-
445
- for i_result,result in enumerate(exif_read_results):
446
-
447
- if result is not None:
448
-
449
- print('\nError found on image {}: {}'.format(i_result,result))
450
- im = images[i_result]
451
- file_path = get_image_local_path(im)
452
- assert images[i_result] == im
453
- result = add_exif_data(im)
454
- assert result is None
455
- print('\nFixed!\n')
456
- exif_read_results[i_result] = result
457
-
458
-
459
- #%% Remove junk
460
-
461
- images_out = []
462
- for im in images:
463
-
464
- url = im['url']
465
-
466
- # Ignore non-image files
467
- if ('ds_store' in url.lower()) or ('dropbox.device' in url.lower()):
468
- continue
469
- images_out.append(im)
470
-
471
- images = images_out
472
-
473
-
474
- #%% Fill in some None values
475
-
476
- # ...so we can sort by datetime later, and let None's be sorted arbitrarily
477
-
478
- for im in images:
479
- if 'exif_tags' not in im:
480
- im['exif_tags'] = None
481
- if 'datetime' not in im['exif_tags']:
482
- im['exif_tags']['datetime'] = None
483
-
484
- images = sorted(images, key = lambda im: im['url'])
485
-
486
-
487
- #%% Find unique locations
488
-
489
- locations = set()
490
-
491
- for ann in tqdm(images):
492
-
493
- assert 'exif_tags' in ann
494
- location = ann['exif_tags']['location']
495
- assert location is not None and len(location) > 0
496
- locations.add(location)
497
-
498
-
499
- #%% Synthesize sequence information
500
-
501
- print('Found {} locations'.format(len(locations)))
502
-
503
- locations = list(locations)
504
-
505
- sequences = set()
506
- sequence_to_images = defaultdict(list)
507
- images = images
508
- max_seconds_within_sequence = 10
509
-
510
- # Sort images by time within each location
511
- # i_location=0; location = locations[i_location]
512
- for i_location,location in enumerate(locations):
513
-
514
- images_this_location = [im for im in images if im['exif_tags']['location'] == location]
515
- sorted_images_this_location = sorted(images_this_location, key = lambda im: im['exif_tags']['datetime'])
516
-
517
- current_sequence_id = None
518
- next_frame_number = 0
519
- previous_datetime = None
520
-
521
- # previous_datetime = sorted_images_this_location[0]['datetime']
522
- # im = sorted_images_this_camera[1]
523
- for i_image,im in enumerate(sorted_images_this_location):
524
-
525
- # Timestamp for this image, may be None
526
- dt = im['exif_tags']['datetime']
527
-
528
- # Start a new sequence if:
529
- #
530
- # * This image has no timestamp
531
- # * This image has a frame number of zero
532
- # * We have no previous image timestamp
533
- #
534
- if dt is None:
535
- delta = None
536
- elif previous_datetime is None:
537
- delta = None
538
- else:
539
- assert isinstance(dt,datetime.datetime)
540
- delta = (dt - previous_datetime).total_seconds()
541
-
542
- # Start a new sequence if necessary
543
- if delta is None or delta > max_seconds_within_sequence:
544
- next_frame_number = 0
545
- current_sequence_id = str(uuid.uuid1())
546
- sequences.add(current_sequence_id)
547
- assert current_sequence_id is not None
548
-
549
- im['seq_id'] = current_sequence_id
550
- im['synthetic_frame_number'] = next_frame_number
551
- next_frame_number = next_frame_number + 1
552
- previous_datetime = dt
553
- sequence_to_images[im['seq_id']].append(im)
554
-
555
- # ...for each image in this location
556
-
557
- # ...for each location
558
-
559
-
560
- #%% Count frames in each sequence
561
-
562
- print('Created {} sequences from {} images'.format(len(sequences),len(images)))
563
-
564
- num_frames_per_sequence = {}
565
- for seq_id in sequences:
566
- # images_this_sequence = [im for im in images if im['seq_id'] == seq_id]
567
- images_this_sequence = sequence_to_images[seq_id]
568
- num_frames_per_sequence[seq_id] = len(images_this_sequence)
569
- for im in images_this_sequence:
570
- im['synthetic_seq_num_frames'] = len(images_this_sequence)
571
-
572
-
573
- #%% Create output filenames for each image, store original filenames
574
-
575
- images_per_folder = 1000
576
- output_paths = set()
577
-
578
- # i_location = 0; location = locations[i_location]
579
- for i_location,location in enumerate(locations):
580
-
581
- images_this_location = [im for im in images if im['exif_tags']['location'] == location]
582
- sorted_images_this_location = sorted(images_this_location, key = lambda im: im['exif_tags']['datetime'])
583
-
584
- # i_image = 0; im = sorted_images_this_location[i_image]
585
- for i_image,im in enumerate(sorted_images_this_location):
586
-
587
- url = im['url']
588
- parsed_url = urlparse(url)
589
- relative_path = parsed_url.path
590
- relative_path = relative_path[1:]
591
- im['original_relative_path'] = relative_path
592
- image_id = uuid.uuid1()
593
- im['id'] = image_id
594
- folder_number = i_image // images_per_folder
595
- image_number = i_image % images_per_folder
596
- output_relative_path = 'loc-' + location + '/' + '{0:03d}'.format(folder_number) + '/' + '{0:03d}'.format(image_number) + '.jpg'
597
- im['output_relative_path'] = output_relative_path
598
- assert output_relative_path not in output_paths
599
- output_paths.add(output_relative_path)
600
-
601
- assert len(output_paths) == len(images)
602
-
603
-
604
- #%% Save progress
605
-
606
- with open(sequence_info_results_file,'w') as f:
607
-
608
- # Use default=str to handle datetime objects
609
- json.dump(images, f, indent=1, default=str)
610
-
611
- #%%
612
-
613
- if False:
614
-
615
- #%%
616
-
617
- with open(sequence_info_results_file,'r') as f:
618
- images = json.load(f)
619
-
620
-
621
- #%% Copy images to their output files (functions)
622
-
623
- def copy_image_to_output(im):
624
-
625
- source_path = os.path.join(input_image_folder,im['original_relative_path'])
626
- assert(os.path.isfile(source_path))
627
- dest_path = os.path.join(output_image_folder,im['output_relative_path'])
628
- os.makedirs(os.path.dirname(dest_path),exist_ok=True)
629
- shutil.copyfile(source_path,dest_path)
630
- print('Copying {} to {}'.format(source_path,dest_path))
631
- return None
632
-
633
-
634
- #%% Copy images to output files (execution)
635
-
636
- if n_copy_threads == 1:
637
- for im in tqdm(images):
638
- copy_image_to_output(im)
639
- else:
640
- pool = ThreadPool(n_copy_threads)
641
- copy_image_results = list(tqdm(pool.imap(copy_image_to_output, images), total=len(images)))
642
-
643
-
644
- #%% Rename the main image list for consistency with other scripts
645
-
646
- all_image_info = images
647
-
648
-
649
- #%% Create CCT dictionaries
650
-
651
- def transform_bbox(coords):
652
- """
653
- Derive CCT-formatted bounding boxes from the SamaSource coordinate system.
654
-
655
- SamaSource provides a list of four points (x,y) that should make a box.
656
-
657
- CCT coordinates are absolute, with the origin at the upper-left, as x,y,w,h.
658
- """
659
-
660
- # Make sure this is really a box
661
- assert len(coords) == 4
662
- assert all(len(coord) == 2 for coord in coords)
663
- assert coords[0][1] == coords[1][1]
664
- assert coords[2][1] == coords[3][1]
665
- assert coords[0][0] == coords[2][0]
666
- assert coords[1][0] == coords[3][0]
667
-
668
- # Transform to CCT format
669
- x = coords[0][0]
670
- y = coords[0][1]
671
- h = coords[2][1] - coords[0][1]
672
- w = coords[1][0] - coords[0][0]
673
- return [x, y, w, h]
674
-
675
- annotations = []
676
- image_ids_to_images = {}
677
- category_name_to_category = {}
678
-
679
- # Force the empty category to be ID 0
680
- empty_category = {}
681
- empty_category['id'] = 0
682
- empty_category['name'] = 'empty'
683
- category_name_to_category['empty'] = empty_category
684
- next_category_id = 1
685
-
686
- default_annotation = {}
687
- default_annotation['tags'] = {}
688
- default_annotation['tags']['Object'] = None
689
-
690
- # i_image = 0; input_im = all_image_info[0]
691
- for i_image,input_im in tqdm(enumerate(all_image_info),total=len(all_image_info)):
692
-
693
- output_im = {}
694
- output_im['id'] = input_im['id']
695
- output_im['file_name'] = input_im['output_relative_path']
696
- output_im['seq_id'] = input_im['seq_id']
697
- output_im['seq_num_frames'] = input_im['synthetic_seq_num_frames']
698
- output_im['frame_num'] = input_im['synthetic_frame_number']
699
- output_im['original_relative_path'] = input_im['original_relative_path']
700
-
701
- # This issue only impacted one image that wasn't a real image, it was just a screenshot
702
- # showing "no images available for this camera"
703
- if 'location' not in input_im['exif_tags'] or input_im['exif_tags']['location'] == 'unknown':
704
- print('Warning: no location for image {}, skipping'.format(
705
- input_im['url']))
706
- continue
707
- output_im['location'] = input_im['exif_tags']['location']
708
-
709
- assert output_im['id'] not in image_ids_to_images
710
- image_ids_to_images[output_im['id']] = output_im
711
-
712
- exif_tags = input_im['exif_tags']
713
-
714
- # Convert datetime if necessary
715
- dt = exif_tags['datetime']
716
- if dt is not None and isinstance(dt,str):
717
- dt = datetime.datetime.strptime(dt, '%Y-%m-%d %H:%M:%S')
718
-
719
- # Process temperature if available
720
- output_im['temperature'] = exif_tags['temperature'] if 'temperature' in exif_tags else None
721
-
722
- # Read width and height if necessary
723
- w = None
724
- h = None
725
-
726
- if 'width' in exif_tags:
727
- w = exif_tags['width']
728
- if 'height' in exif_tags:
729
- h = exif_tags['height']
730
-
731
- output_image_full_path = os.path.join(output_image_folder,input_im['output_relative_path'])
732
-
733
- if w is None or h is None:
734
- pil_image = Image.open(output_image_full_path)
735
- w, h = pil_image.size
736
-
737
- output_im['width'] = w
738
- output_im['height'] = h
739
-
740
- # I don't know what this field is; confirming that it's always None
741
- assert input_im['Object'] is None
742
-
743
- # Process object and bbox
744
- input_annotations = input_im['Output']
745
-
746
- if input_annotations is None:
747
- input_annotations = [default_annotation]
748
-
749
- # os.startfile(output_image_full_path)
750
-
751
- for i_ann,input_annotation in enumerate(input_annotations):
752
-
753
- bbox = None
754
-
755
- assert isinstance(input_annotation,dict)
756
-
757
- if input_annotation['tags']['Object'] is None:
758
-
759
- # Zero is hard-coded as the empty category, but check to be safe
760
- category_id = 0
761
- assert category_name_to_category['empty']['id'] == category_id
762
-
763
- else:
764
-
765
- # I can't figure out the 'index' field, but I'm not losing sleep about it
766
- # assert input_annotation['index'] == 1+i_ann
767
-
768
- points = input_annotation['points']
769
- assert points is not None and len(points) == 4
770
- bbox = transform_bbox(points)
771
- assert len(input_annotation['tags']) == 1 and 'Object' in input_annotation['tags']
772
-
773
- # Some annotators (but not all) included "_partial" when animals were partially obscured
774
- category_name = input_annotation['tags']['Object'].replace('_partial','').lower().strip()
775
-
776
- # Annotators *mostly* used 'none', but sometimes 'empty'. 'empty' is CCT-correct.
777
- if category_name == 'none':
778
- category_name = 'empty'
779
-
780
- category_id = None
781
-
782
- # If we've seen this category before...
783
- if category_name in category_name_to_category:
784
-
785
- category = category_name_to_category[category_name]
786
- category_id = category['id']
787
-
788
- # If this is a new category...
789
- else:
790
-
791
- category_id = next_category_id
792
- category = {}
793
- category['id'] = category_id
794
- category['name'] = category_name
795
- category_name_to_category[category_name] = category
796
- next_category_id += 1
797
-
798
- # ...if this is an empty/non-empty annotation
799
-
800
- # Create an annotation
801
- annotation = {}
802
- annotation['id'] = str(uuid.uuid1())
803
- annotation['image_id'] = output_im['id']
804
- annotation['category_id'] = category_id
805
- annotation['sequence_level_annotation'] = False
806
- if bbox is not None:
807
- annotation['bbox'] = bbox
808
-
809
- annotations.append(annotation)
810
-
811
- # ...for each annotation on this image
812
-
813
- # ...for each image
814
-
815
- images = list(image_ids_to_images.values())
816
- categories = list(category_name_to_category.values())
817
- print('Loaded {} annotations in {} categories for {} images'.format(
818
- len(annotations),len(categories),len(images)))
819
-
820
-
821
- #%% Change *two* annotations on images that I discovered contains a human after running MDv4
822
-
823
- manual_human_ids = ['a07fc88a-6dd8-4d66-b552-d21d50fa39d0','285363f9-d76d-4727-b530-a6bd401bb4c7']
824
- human_id = [cat['id'] for cat in categories if cat['name'] == 'human'][0]
825
- for ann in tqdm(annotations):
826
- if ann['image_id'] in manual_human_ids:
827
- old_cat_id = ann['category_id']
828
- print('Changing annotation for image {} from {} to {}'.format(
829
- ann['image_id'],old_cat_id,human_id))
830
- ann['category_id'] = human_id
831
-
832
-
833
- #%% Move human images
834
-
835
- human_image_ids = set()
836
- human_id = [cat['id'] for cat in categories if cat['name'] == 'human'][0]
837
-
838
- # ann = annotations[0]
839
- for ann in tqdm(annotations):
840
- if ann['category_id'] == human_id:
841
- human_image_ids.add(ann['image_id'])
842
-
843
- print('\nFound {} human images'.format(len(human_image_ids)))
844
-
845
- for im in tqdm(images):
846
- if im['id'] not in human_image_ids:
847
- continue
848
- source_path = os.path.join(output_image_folder,im['file_name'])
849
- if not os.path.isfile(source_path):
850
- continue
851
- target_path = os.path.join(output_image_folder_humans,im['file_name'])
852
- print('Moving {} to {}'.format(source_path,target_path))
853
- os.makedirs(os.path.dirname(target_path),exist_ok=True)
854
- shutil.move(source_path,target_path)
855
-
856
-
857
- #%% Count images by location
858
-
859
- locations_to_images = defaultdict(list)
860
- for im in tqdm(images):
861
- locations_to_images[im['location']].append(im)
862
-
863
-
864
- #%% Write output
865
-
866
- info = {}
867
- info['year'] = 2020
868
- info['version'] = 1.0
869
- info['description'] = 'Camera trap data collected from the Channel Islands, California'
870
- info['contributor'] = 'The Nature Conservancy of California'
871
-
872
- json_data = {}
873
- json_data['images'] = images
874
- json_data['annotations'] = annotations
875
- json_data['categories'] = categories
876
- json_data['info'] = info
877
- json.dump(json_data, open(output_file, 'w'), indent=1)
878
-
879
- print('Finished writing .json file with {} images, {} annotations, and {} categories'.format(
880
- len(images),len(annotations),len(categories)))
881
-
882
-
883
- #%% Validate output
884
-
885
- from data_management.databases import integrity_check_json_db
886
-
887
- fn = output_file
888
- options = integrity_check_json_db.IntegrityCheckOptions()
889
- options.baseDir = output_image_folder
890
- options.bCheckImageSizes = False
891
- options.bCheckImageExistence = False
892
- options.bFindUnusedImages = False
893
-
894
- sortedCategories, data, error = integrity_check_json_db.integrity_check_json_db(fn,options)
895
-
896
-
897
- #%% Preview labels
898
-
899
- from md_visualization import visualize_db
900
-
901
- viz_options = visualize_db.DbVizOptions()
902
- viz_options.num_to_visualize = 159
903
- # viz_options.classes_to_exclude = [0]
904
- viz_options.classes_to_include = ['other']
905
- viz_options.trim_to_images_with_bboxes = False
906
- viz_options.add_search_links = False
907
- viz_options.sort_by_filename = False
908
- viz_options.parallelize_rendering = True
909
- html_output_file,image_db = visualize_db.visualize_db(db_path=output_file,
910
- output_dir=os.path.join(output_base,'preview'),
911
- image_base_dir=output_image_folder,
912
- options=viz_options)
913
- os.startfile(html_output_file)