megadetector 10.0.15__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.
Files changed (147) hide show
  1. megadetector/__init__.py +0 -0
  2. megadetector/api/__init__.py +0 -0
  3. megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
  4. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
  6. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
  7. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
  8. megadetector/classification/__init__.py +0 -0
  9. megadetector/classification/aggregate_classifier_probs.py +108 -0
  10. megadetector/classification/analyze_failed_images.py +227 -0
  11. megadetector/classification/cache_batchapi_outputs.py +198 -0
  12. megadetector/classification/create_classification_dataset.py +626 -0
  13. megadetector/classification/crop_detections.py +516 -0
  14. megadetector/classification/csv_to_json.py +226 -0
  15. megadetector/classification/detect_and_crop.py +853 -0
  16. megadetector/classification/efficientnet/__init__.py +9 -0
  17. megadetector/classification/efficientnet/model.py +415 -0
  18. megadetector/classification/efficientnet/utils.py +608 -0
  19. megadetector/classification/evaluate_model.py +520 -0
  20. megadetector/classification/identify_mislabeled_candidates.py +152 -0
  21. megadetector/classification/json_to_azcopy_list.py +63 -0
  22. megadetector/classification/json_validator.py +696 -0
  23. megadetector/classification/map_classification_categories.py +276 -0
  24. megadetector/classification/merge_classification_detection_output.py +509 -0
  25. megadetector/classification/prepare_classification_script.py +194 -0
  26. megadetector/classification/prepare_classification_script_mc.py +228 -0
  27. megadetector/classification/run_classifier.py +287 -0
  28. megadetector/classification/save_mislabeled.py +110 -0
  29. megadetector/classification/train_classifier.py +827 -0
  30. megadetector/classification/train_classifier_tf.py +725 -0
  31. megadetector/classification/train_utils.py +323 -0
  32. megadetector/data_management/__init__.py +0 -0
  33. megadetector/data_management/animl_to_md.py +161 -0
  34. megadetector/data_management/annotations/__init__.py +0 -0
  35. megadetector/data_management/annotations/annotation_constants.py +33 -0
  36. megadetector/data_management/camtrap_dp_to_coco.py +270 -0
  37. megadetector/data_management/cct_json_utils.py +566 -0
  38. megadetector/data_management/cct_to_md.py +184 -0
  39. megadetector/data_management/cct_to_wi.py +293 -0
  40. megadetector/data_management/coco_to_labelme.py +284 -0
  41. megadetector/data_management/coco_to_yolo.py +701 -0
  42. megadetector/data_management/databases/__init__.py +0 -0
  43. megadetector/data_management/databases/add_width_and_height_to_db.py +107 -0
  44. megadetector/data_management/databases/combine_coco_camera_traps_files.py +210 -0
  45. megadetector/data_management/databases/integrity_check_json_db.py +563 -0
  46. megadetector/data_management/databases/subset_json_db.py +195 -0
  47. megadetector/data_management/generate_crops_from_cct.py +200 -0
  48. megadetector/data_management/get_image_sizes.py +164 -0
  49. megadetector/data_management/labelme_to_coco.py +559 -0
  50. megadetector/data_management/labelme_to_yolo.py +349 -0
  51. megadetector/data_management/lila/__init__.py +0 -0
  52. megadetector/data_management/lila/create_lila_blank_set.py +556 -0
  53. megadetector/data_management/lila/create_lila_test_set.py +192 -0
  54. megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
  55. megadetector/data_management/lila/download_lila_subset.py +182 -0
  56. megadetector/data_management/lila/generate_lila_per_image_labels.py +777 -0
  57. megadetector/data_management/lila/get_lila_annotation_counts.py +174 -0
  58. megadetector/data_management/lila/get_lila_image_counts.py +112 -0
  59. megadetector/data_management/lila/lila_common.py +319 -0
  60. megadetector/data_management/lila/test_lila_metadata_urls.py +164 -0
  61. megadetector/data_management/mewc_to_md.py +344 -0
  62. megadetector/data_management/ocr_tools.py +873 -0
  63. megadetector/data_management/read_exif.py +964 -0
  64. megadetector/data_management/remap_coco_categories.py +195 -0
  65. megadetector/data_management/remove_exif.py +156 -0
  66. megadetector/data_management/rename_images.py +194 -0
  67. megadetector/data_management/resize_coco_dataset.py +665 -0
  68. megadetector/data_management/speciesnet_to_md.py +41 -0
  69. megadetector/data_management/wi_download_csv_to_coco.py +247 -0
  70. megadetector/data_management/yolo_output_to_md_output.py +594 -0
  71. megadetector/data_management/yolo_to_coco.py +984 -0
  72. megadetector/data_management/zamba_to_md.py +188 -0
  73. megadetector/detection/__init__.py +0 -0
  74. megadetector/detection/change_detection.py +840 -0
  75. megadetector/detection/process_video.py +479 -0
  76. megadetector/detection/pytorch_detector.py +1451 -0
  77. megadetector/detection/run_detector.py +1267 -0
  78. megadetector/detection/run_detector_batch.py +2172 -0
  79. megadetector/detection/run_inference_with_yolov5_val.py +1314 -0
  80. megadetector/detection/run_md_and_speciesnet.py +1604 -0
  81. megadetector/detection/run_tiled_inference.py +1044 -0
  82. megadetector/detection/tf_detector.py +209 -0
  83. megadetector/detection/video_utils.py +1379 -0
  84. megadetector/postprocessing/__init__.py +0 -0
  85. megadetector/postprocessing/add_max_conf.py +72 -0
  86. megadetector/postprocessing/categorize_detections_by_size.py +166 -0
  87. megadetector/postprocessing/classification_postprocessing.py +1943 -0
  88. megadetector/postprocessing/combine_batch_outputs.py +249 -0
  89. megadetector/postprocessing/compare_batch_results.py +2110 -0
  90. megadetector/postprocessing/convert_output_format.py +403 -0
  91. megadetector/postprocessing/create_crop_folder.py +629 -0
  92. megadetector/postprocessing/detector_calibration.py +570 -0
  93. megadetector/postprocessing/generate_csv_report.py +522 -0
  94. megadetector/postprocessing/load_api_results.py +223 -0
  95. megadetector/postprocessing/md_to_coco.py +428 -0
  96. megadetector/postprocessing/md_to_labelme.py +351 -0
  97. megadetector/postprocessing/md_to_wi.py +41 -0
  98. megadetector/postprocessing/merge_detections.py +392 -0
  99. megadetector/postprocessing/postprocess_batch_results.py +2140 -0
  100. megadetector/postprocessing/remap_detection_categories.py +226 -0
  101. megadetector/postprocessing/render_detection_confusion_matrix.py +677 -0
  102. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +206 -0
  103. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +82 -0
  104. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1665 -0
  105. megadetector/postprocessing/separate_detections_into_folders.py +795 -0
  106. megadetector/postprocessing/subset_json_detector_output.py +964 -0
  107. megadetector/postprocessing/top_folders_to_bottom.py +238 -0
  108. megadetector/postprocessing/validate_batch_results.py +332 -0
  109. megadetector/taxonomy_mapping/__init__.py +0 -0
  110. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
  111. megadetector/taxonomy_mapping/map_new_lila_datasets.py +211 -0
  112. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +165 -0
  113. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +543 -0
  114. megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
  115. megadetector/taxonomy_mapping/simple_image_download.py +231 -0
  116. megadetector/taxonomy_mapping/species_lookup.py +1008 -0
  117. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
  118. megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
  119. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
  120. megadetector/tests/__init__.py +0 -0
  121. megadetector/tests/test_nms_synthetic.py +335 -0
  122. megadetector/utils/__init__.py +0 -0
  123. megadetector/utils/ct_utils.py +1857 -0
  124. megadetector/utils/directory_listing.py +199 -0
  125. megadetector/utils/extract_frames_from_video.py +307 -0
  126. megadetector/utils/gpu_test.py +125 -0
  127. megadetector/utils/md_tests.py +2072 -0
  128. megadetector/utils/path_utils.py +2872 -0
  129. megadetector/utils/process_utils.py +172 -0
  130. megadetector/utils/split_locations_into_train_val.py +237 -0
  131. megadetector/utils/string_utils.py +234 -0
  132. megadetector/utils/url_utils.py +825 -0
  133. megadetector/utils/wi_platform_utils.py +968 -0
  134. megadetector/utils/wi_taxonomy_utils.py +1766 -0
  135. megadetector/utils/write_html_image_list.py +239 -0
  136. megadetector/visualization/__init__.py +0 -0
  137. megadetector/visualization/plot_utils.py +309 -0
  138. megadetector/visualization/render_images_with_thumbnails.py +243 -0
  139. megadetector/visualization/visualization_utils.py +1973 -0
  140. megadetector/visualization/visualize_db.py +630 -0
  141. megadetector/visualization/visualize_detector_output.py +498 -0
  142. megadetector/visualization/visualize_video_output.py +705 -0
  143. megadetector-10.0.15.dist-info/METADATA +115 -0
  144. megadetector-10.0.15.dist-info/RECORD +147 -0
  145. megadetector-10.0.15.dist-info/WHEEL +5 -0
  146. megadetector-10.0.15.dist-info/licenses/LICENSE +19 -0
  147. megadetector-10.0.15.dist-info/top_level.txt +1 -0
@@ -0,0 +1,195 @@
1
+ """
2
+
3
+ remap_coco_categories.py
4
+
5
+ Given a COCO-formatted dataset, remap the categories to a new mapping. A common use
6
+ case is to take a fine-grained dataset (e.g. with species-level categories) and
7
+ map them to coarse categories (typically MD categories).
8
+
9
+ """
10
+
11
+ #%% Imports and constants
12
+
13
+ import os
14
+ import json
15
+ import argparse
16
+
17
+ from copy import deepcopy
18
+ from megadetector.utils.ct_utils import invert_dictionary
19
+
20
+
21
+ #%% Main function
22
+
23
+ def remap_coco_categories(input_data,
24
+ output_category_name_to_id,
25
+ input_category_name_to_output_category_name,
26
+ output_file=None,
27
+ allow_unused_categories=False):
28
+ """
29
+ Given a COCO-formatted dataset, remap the categories to a new categories mapping, optionally
30
+ writing the results to a new file.
31
+
32
+ Args:
33
+ input_data (str or dict): a COCO-formatted dict or a filename. If it's a dict, it will
34
+ be copied, not modified in place.
35
+ output_category_name_to_id (dict): a dict mapping strings to ints. Categories not in
36
+ this dict will be ignored or will result in errors, depending on allow_unused_categories.
37
+ input_category_name_to_output_category_name (dict): a dict mapping strings to strings.
38
+ Annotations using categories not in this dict will be omitted or will result in
39
+ errors, depending on allow_unused_categories.
40
+ output_file (str, optional): output file to which we should write remapped COCO data
41
+ allow_unused_categories (bool, optional): should we ignore categories not present in the
42
+ input/output mappings? If this is False and we encounter an unmapped category, we'll
43
+ error.
44
+
45
+ Returns:
46
+ dict: COCO-formatted dict
47
+ """
48
+
49
+ if isinstance(input_data,str):
50
+ assert os.path.isfile(input_data), "Can't find file {}".format(input_data)
51
+ with open(input_data,'r') as f:
52
+ input_data = json.load(f)
53
+ assert isinstance(input_data,dict), 'Illegal COCO input data'
54
+ else:
55
+ assert isinstance(input_data,dict), 'Illegal COCO input data'
56
+ input_data = deepcopy(input_data)
57
+
58
+ # It's safe to modify in-place now
59
+ output_data = input_data
60
+
61
+ # Read input name --> ID mapping
62
+ input_category_name_to_input_category_id = {}
63
+ for c in input_data['categories']:
64
+ input_category_name_to_input_category_id[c['name']] = c['id']
65
+ input_category_id_to_input_category_name = \
66
+ invert_dictionary(input_category_name_to_input_category_id)
67
+
68
+ # Map input IDs --> output IDs
69
+ input_category_id_to_output_category_id = {}
70
+ input_category_names = list(input_category_name_to_output_category_name.keys())
71
+
72
+ # input_name = input_category_names[0]
73
+ for input_name in input_category_names:
74
+
75
+ output_name = input_category_name_to_output_category_name[input_name]
76
+ assert output_name in output_category_name_to_id, \
77
+ 'No output ID for {} --> {}'.format(input_name,output_name)
78
+ input_id = input_category_name_to_input_category_id[input_name]
79
+ output_id = output_category_name_to_id[output_name]
80
+ input_category_id_to_output_category_id[input_id] = output_id
81
+
82
+ # ...for each category we want to keep
83
+
84
+ printed_unused_category_warnings = set()
85
+
86
+ valid_annotations = []
87
+
88
+ # Map annotations
89
+ for ann in output_data['annotations']:
90
+
91
+ input_category_id = ann['category_id']
92
+ if input_category_id not in input_category_id_to_output_category_id:
93
+ if allow_unused_categories:
94
+ if input_category_id not in printed_unused_category_warnings:
95
+ printed_unused_category_warnings.add(input_category_id)
96
+ input_category_name = \
97
+ input_category_id_to_input_category_name[input_category_id]
98
+ s = 'Skipping unmapped category ID {} ({})'.format(
99
+ input_category_id,input_category_name)
100
+ print(s)
101
+ continue
102
+ else:
103
+ s = 'Unmapped category ID {}'.format(input_category_id)
104
+ raise ValueError(s)
105
+ output_category_id = input_category_id_to_output_category_id[input_category_id]
106
+ ann['category_id'] = output_category_id
107
+ valid_annotations.append(ann)
108
+
109
+ # ...for each annotation
110
+
111
+ # The only reason annotations should get excluded is the case where we allow
112
+ # unused categories
113
+ if not allow_unused_categories:
114
+ assert len(valid_annotations) == len(output_data['annotations'])
115
+
116
+ output_data['annotations'] = valid_annotations
117
+
118
+ # Update the category list
119
+ output_categories = []
120
+ for output_name in output_category_name_to_id:
121
+ category = {'name':output_name,'id':output_category_name_to_id[output_name]}
122
+ output_categories.append(category)
123
+ output_data['categories'] = output_categories
124
+
125
+ if output_file is not None:
126
+ with open(output_file,'w') as f:
127
+ json.dump(output_data,f,indent=1)
128
+
129
+ return output_data
130
+
131
+ # ...def remap_coco_categories(...)
132
+
133
+
134
+ #%% Command-line driver
135
+
136
+ def main():
137
+ """
138
+ Command-line interface to remap COCO categories.
139
+ """
140
+
141
+ parser = argparse.ArgumentParser(
142
+ description='Remap categories in a COCO-formatted dataset'
143
+ )
144
+ parser.add_argument(
145
+ 'input_coco_file',
146
+ type=str,
147
+ help='Path to the input COCO .json file'
148
+ )
149
+ parser.add_argument(
150
+ 'output_category_map_file',
151
+ type=str,
152
+ help="Path to a .json file mapping output category names to integer IDs (e.g., {'cat':0, 'dog':1})"
153
+ )
154
+ parser.add_argument(
155
+ 'input_to_output_category_map_file',
156
+ type=str,
157
+ help="Path to a .json file mapping input category names to output category names" + \
158
+ " (e.g., {'old_cat_name':'cat', 'old_dog_name':'dog'})"
159
+ )
160
+ parser.add_argument(
161
+ 'output_coco_file',
162
+ type=str,
163
+ help='Path to save the remapped COCO .json file'
164
+ )
165
+ parser.add_argument(
166
+ '--allow_unused_categories',
167
+ action='store_true',
168
+ help='Allow unmapped categories (by default, errors on unmapped categories)'
169
+ )
170
+
171
+ args = parser.parse_args()
172
+
173
+ # Load category mappings
174
+ with open(args.output_category_map_file, 'r') as f:
175
+ output_category_name_to_id = json.load(f)
176
+
177
+ with open(args.input_to_output_category_map_file, 'r') as f:
178
+ input_category_name_to_output_category_name = json.load(f)
179
+
180
+ # Load COCO data
181
+ with open(args.input_coco_file, 'r') as f:
182
+ input_data = json.load(f)
183
+
184
+ remap_coco_categories(
185
+ input_data=input_data,
186
+ output_category_name_to_id=output_category_name_to_id,
187
+ input_category_name_to_output_category_name=input_category_name_to_output_category_name,
188
+ output_file=args.output_coco_file,
189
+ allow_unused_categories=args.allow_unused_categories
190
+ )
191
+
192
+ print(f'Successfully remapped categories and saved to {args.output_coco_file}')
193
+
194
+ if __name__ == '__main__':
195
+ main()
@@ -0,0 +1,156 @@
1
+ """
2
+
3
+ remove_exif.py
4
+
5
+ Removes all EXIF/IPTC/XMP metadata from a folder of images, without making
6
+ backup copies, using pyexiv2. Ignores non-jpeg images.
7
+
8
+ This module is rarely used, and pyexiv2 is not thread-safe, so pyexiv2 is not
9
+ included in package-level dependency lists. YMMV.
10
+
11
+ """
12
+
13
+ #%% Imports and constants
14
+
15
+ import os
16
+ import argparse
17
+
18
+ from megadetector.utils.path_utils import recursive_file_list
19
+
20
+ from multiprocessing.pool import Pool as Pool
21
+ from tqdm import tqdm
22
+
23
+
24
+ #%% Support functions
25
+
26
+ def remove_exif_from_image(fn):
27
+ """
28
+ Remove EXIF information from a single image
29
+
30
+ pyexiv2 is not thread safe, do not call this function in parallel within a process.
31
+
32
+ Parallelizing across processes is fine.
33
+
34
+ Args:
35
+ fn (str): image file from which we should remove EXIF information
36
+
37
+ Returns:
38
+ bool: whether EXIF removal succeeded
39
+ """
40
+
41
+ import pyexiv2 # type: ignore
42
+
43
+ try:
44
+ img = pyexiv2.Image(fn)
45
+ img.clear_exif()
46
+ img.clear_iptc()
47
+ img.clear_xmp()
48
+ img.close()
49
+ except Exception as e:
50
+ print('EXIF error on {}: {}'.format(fn,str(e)))
51
+ return False
52
+
53
+ return True
54
+
55
+
56
+ #%% Remove EXIF data
57
+
58
+ def remove_exif(image_base_folder,recursive=True,n_processes=1):
59
+ """
60
+ Removes all EXIF/IPTC/XMP metadata from a folder of images, without making
61
+ backup copies, using pyexiv2. Ignores non-jpeg images.
62
+
63
+ Args:
64
+ image_base_folder (str): the folder from which we should remove EXIF data
65
+ recursive (bool, optional): whether to process [image_base_folder] recursively
66
+ n_processes (int, optional): number of concurrent workers. Because pyexiv2 is not
67
+ thread-safe, only process-based parallelism is supported.
68
+ """
69
+
70
+ try:
71
+ import pyexiv2 # type: ignore #noqa
72
+ except:
73
+ print('pyexiv2 not available; try "pip install pyexiv2"')
74
+ raise
75
+
76
+
77
+ ##%% List files
78
+
79
+ assert os.path.isdir(image_base_folder), \
80
+ 'Could not find folder {}'.format(image_base_folder)
81
+ all_files = recursive_file_list(image_base_folder,
82
+ recursive=True,
83
+ return_relative_paths=False,
84
+ convert_slashes=True)
85
+ image_files = [s for s in all_files if \
86
+ (s.lower().endswith('.jpg') or s.lower().endswith('.jpeg'))]
87
+
88
+
89
+ ##%% Remove EXIF data (execution)
90
+
91
+ if n_processes == 1:
92
+
93
+ # fn = image_files[0]
94
+ for fn in tqdm(image_files):
95
+ remove_exif_from_image(fn)
96
+
97
+ else:
98
+ # pyexiv2 is not thread-safe, so we need to use processes
99
+ pool = None
100
+ try:
101
+ print('Starting parallel process pool with {} workers'.format(n_processes))
102
+ pool = Pool(n_processes)
103
+ _ = list(tqdm(pool.imap(remove_exif_from_image,image_files),total=len(image_files)))
104
+ finally:
105
+ if pool is not None:
106
+ pool.close()
107
+ pool.join()
108
+ print('Pool closed and joined for EXIF removal')
109
+
110
+ # ...remove_exif(...)
111
+
112
+
113
+ #%% Command-line driver
114
+
115
+ def main():
116
+ """
117
+ Command-line interface to remove EXIF data from images.
118
+ """
119
+
120
+ parser = argparse.ArgumentParser(
121
+ description='Removes EXIF/IPTC/XMP metadata from images in a folder'
122
+ )
123
+ parser.add_argument(
124
+ 'image_base_folder',
125
+ type=str,
126
+ help='Folder to process for EXIF removal'
127
+ )
128
+ parser.add_argument(
129
+ '--nonrecursive',
130
+ action='store_true',
131
+ help="Don't recurse into [image_base_folder] (default is recursive)"
132
+ )
133
+ parser.add_argument(
134
+ '--n_processes',
135
+ type=int,
136
+ default=1,
137
+ help='Number of concurrent processes for EXIF removal (default: 1)'
138
+ )
139
+
140
+ args = parser.parse_args()
141
+
142
+ recursive = (not args.nonrecursive)
143
+
144
+ print('Processing folder: {}'.format(args.image_base_folder))
145
+ if not os.path.isdir(args.image_base_folder):
146
+ raise ValueError('Folder not found at {}'.format(args.image_base_folder))
147
+
148
+ remove_exif(
149
+ image_base_folder=args.image_base_folder,
150
+ recursive=recursive,
151
+ n_processes=args.n_processes
152
+ )
153
+ print('Finished removing EXIF data')
154
+
155
+ if __name__ == '__main__':
156
+ main()
@@ -0,0 +1,194 @@
1
+ """
2
+
3
+ rename_images.py
4
+
5
+ Copies images from a possibly-nested folder structure to a flat folder structure, including EXIF
6
+ timestamps in each filename. Loosely equivalent to camtrapR's imageRename() function.
7
+
8
+ """
9
+
10
+ #%% Imports and constants
11
+
12
+ import os
13
+ import sys
14
+ import argparse
15
+
16
+ from megadetector.utils.path_utils import \
17
+ find_images, insert_before_extension, parallel_copy_files
18
+ from megadetector.data_management.read_exif import \
19
+ ReadExifOptions, read_exif_from_folder
20
+
21
+
22
+ #%% Functions
23
+
24
+ def rename_images(input_folder,
25
+ output_folder,
26
+ dry_run=False,
27
+ verbose=False,
28
+ read_exif_options=None,
29
+ n_copy_workers=8):
30
+ """
31
+ Copies images from a possibly-nested folder structure to a flat folder structure,
32
+ including EXIF timestamps in each filename. Loosely equivalent to camtrapR's
33
+ imageRename() function.
34
+
35
+ Args:
36
+ input_folder (str): the folder to search for images, always recursive
37
+ output_folder (str): the folder to which we will copy images; cannot be the
38
+ same as [input_folder]
39
+ dry_run (bool, optional): only map images, don't actually copy
40
+ verbose (bool, optional): enable additional debug output
41
+ read_exif_options (ReadExifOptions, optional): parameters controlling the reading of
42
+ EXIF information
43
+ n_copy_workers (int, optional): number of parallel threads to use for copying
44
+
45
+ Returns:
46
+ dict: a dict mapping relative filenames in the input folder to relative filenames in the output
47
+ folder
48
+ """
49
+
50
+ assert os.path.isdir(input_folder), 'Input folder {} does not exist'.format(
51
+ input_folder)
52
+
53
+ if not dry_run:
54
+ os.makedirs(output_folder,exist_ok=True)
55
+
56
+ # Read exif information
57
+ if read_exif_options is None:
58
+ read_exif_options = ReadExifOptions()
59
+
60
+ read_exif_options.tags_to_include = ['DateTime','Model',
61
+ 'Make','ExifImageWidth',
62
+ 'ExifImageHeight','DateTimeOriginal']
63
+ read_exif_options.verbose = False
64
+
65
+ exif_info = read_exif_from_folder(input_folder=input_folder,
66
+ output_file=None,
67
+ options=read_exif_options,
68
+ filenames=None,recursive=True)
69
+
70
+ print('Read EXIF information for {} images'.format(len(exif_info)))
71
+
72
+ filename_to_exif_info = {info['file_name']:info for info in exif_info}
73
+
74
+ image_files = find_images(input_folder,return_relative_paths=True,convert_slashes=True,recursive=True)
75
+
76
+ for fn in image_files:
77
+ assert fn in filename_to_exif_info, 'No EXIF info available for {}'.format(fn)
78
+
79
+ input_fn_relative_to_output_fn_relative = {}
80
+
81
+ # fn_relative = image_files[0]
82
+ for fn_relative in image_files:
83
+
84
+ input_fn_abs = os.path.join(input_folder,fn_relative)
85
+ image_exif_info = filename_to_exif_info[fn_relative]
86
+ if 'exif_tags' in image_exif_info:
87
+ image_exif_info = image_exif_info['exif_tags']
88
+
89
+ if image_exif_info is None or \
90
+ 'DateTimeOriginal' not in image_exif_info or \
91
+ image_exif_info['DateTimeOriginal'] is None:
92
+
93
+ dt_tag = 'unknown_datetime'
94
+ print('Warning: no datetime for {}'.format(fn_relative))
95
+
96
+ else:
97
+
98
+ dt_tag = str(image_exif_info['DateTimeOriginal']).replace(':','-').replace(' ','_').strip()
99
+
100
+ flat_filename = fn_relative.replace('\\','/').replace('/','_')
101
+
102
+ output_fn_relative = insert_before_extension(flat_filename,dt_tag)
103
+
104
+ input_fn_relative_to_output_fn_relative[fn_relative] = output_fn_relative
105
+
106
+ if not dry_run:
107
+
108
+ input_fn_abs_to_output_fn_abs = {}
109
+
110
+ for input_fn_relative in input_fn_relative_to_output_fn_relative:
111
+
112
+ output_fn_relative = input_fn_relative_to_output_fn_relative[input_fn_relative]
113
+ input_fn_abs = os.path.join(input_folder,input_fn_relative)
114
+ output_fn_abs = os.path.join(output_folder,output_fn_relative)
115
+ input_fn_abs_to_output_fn_abs[input_fn_abs] = output_fn_abs
116
+
117
+ parallel_copy_files(input_file_to_output_file=input_fn_abs_to_output_fn_abs,
118
+ max_workers=n_copy_workers,
119
+ use_threads=True,
120
+ overwrite=True,
121
+ verbose=verbose)
122
+
123
+ # ...if this is not a dry run
124
+
125
+ return input_fn_relative_to_output_fn_relative
126
+
127
+ # ...def rename_images()
128
+
129
+
130
+ #%% Interactive driver
131
+
132
+ if False:
133
+
134
+ pass
135
+
136
+ #%% Configure options
137
+
138
+ input_folder = r'G:\camera_traps\camera_trap_images\2018.05.04'
139
+ output_folder = r'g:\temp\rename-test-out'
140
+ dry_run = False
141
+ verbose = True
142
+ read_exif_options = ReadExifOptions()
143
+ read_exif_options.tags_to_include = ['DateTime','Model','Make',
144
+ 'ExifImageWidth','ExifImageHeight',
145
+ 'DateTimeOriginal']
146
+ read_exif_options.n_workers = 8
147
+ read_exif_options.verbose = verbose
148
+ n_copy_workers = 8
149
+
150
+
151
+ #%% Programmatic execution
152
+
153
+ input_fn_relative_to_output_fn_relative = rename_images(input_folder,
154
+ output_folder,
155
+ dry_run=dry_run,
156
+ verbose=verbose,
157
+ read_exif_options=read_exif_options,
158
+ n_copy_workers=n_copy_workers)
159
+
160
+
161
+ #%% Command-line driver
162
+
163
+ def main(): # noqa
164
+
165
+ parser = argparse.ArgumentParser(
166
+ description='Copies images from a possibly-nested folder structure to a flat folder structure, ' + \
167
+ 'adding datetime information from EXIF to each filename')
168
+
169
+ parser.add_argument(
170
+ 'input_folder',
171
+ type=str,
172
+ help='The folder to search for images, always recursive')
173
+
174
+ parser.add_argument(
175
+ 'output_folder',
176
+ type=str,
177
+ help='The folder to which we should write the flattened image structure')
178
+
179
+ parser.add_argument(
180
+ '--dry_run',
181
+ action='store_true',
182
+ help="Only map images, don't actually copy")
183
+
184
+ if len(sys.argv[1:]) == 0:
185
+ parser.print_help()
186
+ parser.exit()
187
+
188
+ args = parser.parse_args()
189
+
190
+ rename_images(args.input_folder,args.output_folder,dry_run=args.dry_run,
191
+ verbose=True,read_exif_options=None)
192
+
193
+ if __name__ == '__main__':
194
+ main()