megadetector 5.0.8__py3-none-any.whl → 5.0.10__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 (190) hide show
  1. api/__init__.py +0 -0
  2. api/batch_processing/__init__.py +0 -0
  3. api/batch_processing/api_core/__init__.py +0 -0
  4. api/batch_processing/api_core/batch_service/__init__.py +0 -0
  5. api/batch_processing/api_core/batch_service/score.py +0 -1
  6. api/batch_processing/api_core/server_job_status_table.py +0 -1
  7. api/batch_processing/api_core_support/__init__.py +0 -0
  8. api/batch_processing/api_core_support/aggregate_results_manually.py +0 -1
  9. api/batch_processing/api_support/__init__.py +0 -0
  10. api/batch_processing/api_support/summarize_daily_activity.py +0 -1
  11. api/batch_processing/data_preparation/__init__.py +0 -0
  12. api/batch_processing/data_preparation/manage_local_batch.py +65 -65
  13. api/batch_processing/data_preparation/manage_video_batch.py +8 -8
  14. api/batch_processing/integration/digiKam/xmp_integration.py +0 -1
  15. api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +0 -1
  16. api/batch_processing/postprocessing/__init__.py +0 -0
  17. api/batch_processing/postprocessing/add_max_conf.py +12 -12
  18. api/batch_processing/postprocessing/categorize_detections_by_size.py +32 -14
  19. api/batch_processing/postprocessing/combine_api_outputs.py +68 -54
  20. api/batch_processing/postprocessing/compare_batch_results.py +113 -43
  21. api/batch_processing/postprocessing/convert_output_format.py +41 -16
  22. api/batch_processing/postprocessing/load_api_results.py +16 -17
  23. api/batch_processing/postprocessing/md_to_coco.py +31 -21
  24. api/batch_processing/postprocessing/md_to_labelme.py +52 -22
  25. api/batch_processing/postprocessing/merge_detections.py +14 -14
  26. api/batch_processing/postprocessing/postprocess_batch_results.py +246 -174
  27. api/batch_processing/postprocessing/remap_detection_categories.py +32 -25
  28. api/batch_processing/postprocessing/render_detection_confusion_matrix.py +60 -27
  29. api/batch_processing/postprocessing/repeat_detection_elimination/find_repeat_detections.py +53 -44
  30. api/batch_processing/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +25 -14
  31. api/batch_processing/postprocessing/repeat_detection_elimination/repeat_detections_core.py +242 -158
  32. api/batch_processing/postprocessing/separate_detections_into_folders.py +159 -114
  33. api/batch_processing/postprocessing/subset_json_detector_output.py +146 -169
  34. api/batch_processing/postprocessing/top_folders_to_bottom.py +77 -43
  35. api/synchronous/__init__.py +0 -0
  36. api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  37. api/synchronous/api_core/animal_detection_api/api_backend.py +0 -2
  38. api/synchronous/api_core/animal_detection_api/api_frontend.py +266 -268
  39. api/synchronous/api_core/animal_detection_api/config.py +35 -35
  40. api/synchronous/api_core/tests/__init__.py +0 -0
  41. api/synchronous/api_core/tests/load_test.py +109 -109
  42. classification/__init__.py +0 -0
  43. classification/aggregate_classifier_probs.py +21 -24
  44. classification/analyze_failed_images.py +11 -13
  45. classification/cache_batchapi_outputs.py +51 -51
  46. classification/create_classification_dataset.py +69 -68
  47. classification/crop_detections.py +54 -53
  48. classification/csv_to_json.py +97 -100
  49. classification/detect_and_crop.py +105 -105
  50. classification/evaluate_model.py +43 -42
  51. classification/identify_mislabeled_candidates.py +47 -46
  52. classification/json_to_azcopy_list.py +10 -10
  53. classification/json_validator.py +72 -71
  54. classification/map_classification_categories.py +44 -43
  55. classification/merge_classification_detection_output.py +68 -68
  56. classification/prepare_classification_script.py +157 -154
  57. classification/prepare_classification_script_mc.py +228 -228
  58. classification/run_classifier.py +27 -26
  59. classification/save_mislabeled.py +30 -30
  60. classification/train_classifier.py +20 -20
  61. classification/train_classifier_tf.py +21 -22
  62. classification/train_utils.py +10 -10
  63. data_management/__init__.py +0 -0
  64. data_management/annotations/__init__.py +0 -0
  65. data_management/annotations/annotation_constants.py +18 -31
  66. data_management/camtrap_dp_to_coco.py +238 -0
  67. data_management/cct_json_utils.py +102 -59
  68. data_management/cct_to_md.py +176 -158
  69. data_management/cct_to_wi.py +247 -219
  70. data_management/coco_to_labelme.py +272 -263
  71. data_management/coco_to_yolo.py +79 -58
  72. data_management/databases/__init__.py +0 -0
  73. data_management/databases/add_width_and_height_to_db.py +20 -16
  74. data_management/databases/combine_coco_camera_traps_files.py +35 -31
  75. data_management/databases/integrity_check_json_db.py +62 -24
  76. data_management/databases/subset_json_db.py +24 -15
  77. data_management/generate_crops_from_cct.py +27 -45
  78. data_management/get_image_sizes.py +188 -162
  79. data_management/importers/add_nacti_sizes.py +8 -8
  80. data_management/importers/add_timestamps_to_icct.py +78 -78
  81. data_management/importers/animl_results_to_md_results.py +158 -158
  82. data_management/importers/auckland_doc_test_to_json.py +9 -9
  83. data_management/importers/auckland_doc_to_json.py +8 -8
  84. data_management/importers/awc_to_json.py +7 -7
  85. data_management/importers/bellevue_to_json.py +15 -15
  86. data_management/importers/cacophony-thermal-importer.py +13 -13
  87. data_management/importers/carrizo_shrubfree_2018.py +8 -8
  88. data_management/importers/carrizo_trail_cam_2017.py +8 -8
  89. data_management/importers/cct_field_adjustments.py +9 -9
  90. data_management/importers/channel_islands_to_cct.py +10 -10
  91. data_management/importers/eMammal/copy_and_unzip_emammal.py +1 -0
  92. data_management/importers/ena24_to_json.py +7 -7
  93. data_management/importers/filenames_to_json.py +8 -8
  94. data_management/importers/helena_to_cct.py +7 -7
  95. data_management/importers/idaho-camera-traps.py +7 -7
  96. data_management/importers/idfg_iwildcam_lila_prep.py +10 -10
  97. data_management/importers/jb_csv_to_json.py +9 -9
  98. data_management/importers/mcgill_to_json.py +8 -8
  99. data_management/importers/missouri_to_json.py +18 -18
  100. data_management/importers/nacti_fieldname_adjustments.py +10 -10
  101. data_management/importers/noaa_seals_2019.py +7 -7
  102. data_management/importers/pc_to_json.py +7 -7
  103. data_management/importers/plot_wni_giraffes.py +7 -7
  104. data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -359
  105. data_management/importers/prepare_zsl_imerit.py +7 -7
  106. data_management/importers/rspb_to_json.py +8 -8
  107. data_management/importers/save_the_elephants_survey_A.py +8 -8
  108. data_management/importers/save_the_elephants_survey_B.py +9 -9
  109. data_management/importers/snapshot_safari_importer.py +26 -26
  110. data_management/importers/snapshot_safari_importer_reprise.py +665 -665
  111. data_management/importers/snapshot_serengeti_lila.py +14 -14
  112. data_management/importers/sulross_get_exif.py +8 -9
  113. data_management/importers/timelapse_csv_set_to_json.py +11 -11
  114. data_management/importers/ubc_to_json.py +13 -13
  115. data_management/importers/umn_to_json.py +7 -7
  116. data_management/importers/wellington_to_json.py +8 -8
  117. data_management/importers/wi_to_json.py +9 -9
  118. data_management/importers/zamba_results_to_md_results.py +181 -181
  119. data_management/labelme_to_coco.py +65 -24
  120. data_management/labelme_to_yolo.py +8 -8
  121. data_management/lila/__init__.py +0 -0
  122. data_management/lila/add_locations_to_island_camera_traps.py +9 -9
  123. data_management/lila/add_locations_to_nacti.py +147 -147
  124. data_management/lila/create_lila_blank_set.py +13 -13
  125. data_management/lila/create_lila_test_set.py +8 -8
  126. data_management/lila/create_links_to_md_results_files.py +106 -106
  127. data_management/lila/download_lila_subset.py +44 -110
  128. data_management/lila/generate_lila_per_image_labels.py +55 -42
  129. data_management/lila/get_lila_annotation_counts.py +18 -15
  130. data_management/lila/get_lila_image_counts.py +11 -11
  131. data_management/lila/lila_common.py +96 -33
  132. data_management/lila/test_lila_metadata_urls.py +132 -116
  133. data_management/ocr_tools.py +173 -128
  134. data_management/read_exif.py +110 -97
  135. data_management/remap_coco_categories.py +83 -83
  136. data_management/remove_exif.py +58 -62
  137. data_management/resize_coco_dataset.py +30 -23
  138. data_management/wi_download_csv_to_coco.py +246 -239
  139. data_management/yolo_output_to_md_output.py +86 -73
  140. data_management/yolo_to_coco.py +300 -60
  141. detection/__init__.py +0 -0
  142. detection/detector_training/__init__.py +0 -0
  143. detection/process_video.py +85 -33
  144. detection/pytorch_detector.py +43 -25
  145. detection/run_detector.py +157 -72
  146. detection/run_detector_batch.py +179 -113
  147. detection/run_inference_with_yolov5_val.py +108 -48
  148. detection/run_tiled_inference.py +111 -40
  149. detection/tf_detector.py +51 -29
  150. detection/video_utils.py +606 -521
  151. docs/source/conf.py +43 -0
  152. md_utils/__init__.py +0 -0
  153. md_utils/azure_utils.py +9 -9
  154. md_utils/ct_utils.py +228 -68
  155. md_utils/directory_listing.py +59 -64
  156. md_utils/md_tests.py +968 -871
  157. md_utils/path_utils.py +460 -134
  158. md_utils/process_utils.py +157 -133
  159. md_utils/sas_blob_utils.py +20 -20
  160. md_utils/split_locations_into_train_val.py +45 -32
  161. md_utils/string_utils.py +33 -10
  162. md_utils/url_utils.py +176 -60
  163. md_utils/write_html_image_list.py +40 -33
  164. md_visualization/__init__.py +0 -0
  165. md_visualization/plot_utils.py +102 -109
  166. md_visualization/render_images_with_thumbnails.py +34 -34
  167. md_visualization/visualization_utils.py +597 -291
  168. md_visualization/visualize_db.py +76 -48
  169. md_visualization/visualize_detector_output.py +61 -42
  170. {megadetector-5.0.8.dist-info → megadetector-5.0.10.dist-info}/METADATA +13 -7
  171. megadetector-5.0.10.dist-info/RECORD +224 -0
  172. {megadetector-5.0.8.dist-info → megadetector-5.0.10.dist-info}/top_level.txt +1 -0
  173. taxonomy_mapping/__init__.py +0 -0
  174. taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +342 -335
  175. taxonomy_mapping/map_new_lila_datasets.py +154 -154
  176. taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -134
  177. taxonomy_mapping/preview_lila_taxonomy.py +591 -591
  178. taxonomy_mapping/retrieve_sample_image.py +12 -12
  179. taxonomy_mapping/simple_image_download.py +11 -11
  180. taxonomy_mapping/species_lookup.py +10 -10
  181. taxonomy_mapping/taxonomy_csv_checker.py +18 -18
  182. taxonomy_mapping/taxonomy_graph.py +47 -47
  183. taxonomy_mapping/validate_lila_category_mappings.py +83 -76
  184. data_management/cct_json_to_filename_json.py +0 -89
  185. data_management/cct_to_csv.py +0 -140
  186. data_management/databases/remove_corrupted_images_from_db.py +0 -191
  187. detection/detector_training/copy_checkpoints.py +0 -43
  188. megadetector-5.0.8.dist-info/RECORD +0 -205
  189. {megadetector-5.0.8.dist-info → megadetector-5.0.10.dist-info}/LICENSE +0 -0
  190. {megadetector-5.0.8.dist-info → megadetector-5.0.10.dist-info}/WHEEL +0 -0
@@ -1,33 +1,33 @@
1
- ########
2
- #
3
- # ocr_tools.py
4
- #
5
- # Use OCR (via the Tesseract package) to pull metadata (particularly times and
6
- # dates from camera trap images).
7
- #
8
- # The general approach is:
9
- #
10
- # * Crop a fixed percentage from the top and bottom of an image, slightly larger
11
- # than the largest examples we've seen of how much space is used for metadata.
12
- #
13
- # * Define the background color as the median pixel value, and find rows that are
14
- # mostly that color to refine the crop.
15
- #
16
- # * Crop to the refined crop, then run pytesseract to extract text.
17
- #
18
- # * Use regular expressions to find time and date.
19
- #
20
- # Prior to using this module:
21
- #
22
- # * Install Tesseract from https://tesseract-ocr.github.io/tessdoc/Installation.html
23
- #
24
- # * pip install pytesseract
25
- #
26
- # Known limitations:
27
- #
28
- # * Semi-transparent overlays (which I've only seen on consumer cameras) usually fail.
29
- #
30
- ########
1
+ """
2
+
3
+ ocr_tools.py
4
+
5
+ Use OCR (via the Tesseract package) to pull metadata (particularly times and
6
+ dates from camera trap images).
7
+
8
+ The general approach is:
9
+
10
+ * Crop a fixed percentage from the top and bottom of an image, slightly larger
11
+ than the largest examples we've seen of how much space is used for metadata.
12
+
13
+ * Define the background color as the median pixel value, and find rows that are
14
+ mostly that color to refine the crop.
15
+
16
+ * Crop to the refined crop, then run pytesseract to extract text.
17
+
18
+ * Use regular expressions to find time and date.
19
+
20
+ Prior to using this module:
21
+
22
+ * Install Tesseract from https://tesseract-ocr.github.io/tessdoc/Installation.html
23
+
24
+ * pip install pytesseract
25
+
26
+ Known limitations:
27
+
28
+ * Semi-transparent overlays (which I've only seen on consumer cameras) usually fail.
29
+
30
+ """
31
31
 
32
32
  #%% Notes to self
33
33
 
@@ -55,9 +55,10 @@ from PIL import Image, ImageFilter
55
55
  from tqdm import tqdm
56
56
 
57
57
  from md_utils.path_utils import find_images
58
- from md_visualization import visualization_utils as vis_utils
59
- from md_utils import write_html_image_list
60
58
  from md_utils.path_utils import open_file
59
+ from md_utils import write_html_image_list
60
+ from md_utils.ct_utils import is_iterable
61
+ from md_visualization import visualization_utils as vis_utils
61
62
 
62
63
  # pip install pytesseract
63
64
  #
@@ -69,58 +70,64 @@ import pytesseract
69
70
  #%% Extraction options
70
71
 
71
72
  class DatetimeExtractionOptions:
72
-
73
+ """
74
+ Options used to parameterize datetime extraction in most functions in this module.
75
+ """
76
+
73
77
  def __init__(self):
74
78
 
75
- # Using a semi-arbitrary metric of how much it feels like we found the
76
- # text-containing region, discard regions that appear to be extraction failures
79
+ #: Using a semi-arbitrary metric of how much it feels like we found the
80
+ #: text-containing region, discard regions that appear to be extraction failures
77
81
  self.p_crop_success_threshold = 0.5
78
82
 
79
- # Pad each crop with a few pixels to make tesseract happy
83
+ #: Pad each crop with a few pixels to make tesseract happy
80
84
  self.crop_padding = 10
81
85
 
82
- # Discard short text, typically text from the top of the image
86
+ #: Discard short text, typically text from the top of the image
83
87
  self.min_text_length = 4
84
88
 
85
- # When we're looking for pixels that match the background color, allow some
86
- # tolerance around the dominant color
89
+ #: When we're looking for pixels that match the background color, allow some
90
+ #: tolerance around the dominant color
87
91
  self.background_tolerance = 2
88
92
 
89
- # We need to see a consistent color in at least this fraction of pixels in our rough
90
- # crop to believe that we actually found a candidate metadata region.
93
+ #: We need to see a consistent color in at least this fraction of pixels in our rough
94
+ #: crop to believe that we actually found a candidate metadata region.
91
95
  self.min_background_fraction = 0.3
92
96
 
93
- # What fraction of the [top,bottom] of the image should we use for our rough crop?
97
+ #: What fraction of the [top,bottom] of the image should we use for our rough crop?
94
98
  self.image_crop_fraction = [0.045 , 0.045]
95
99
  # self.image_crop_fraction = [0.08 , 0.08]
96
100
 
97
- # Within that rough crop, how much should we use for determining the background color?
101
+ #: Within that rough crop, how much should we use for determining the background color?
98
102
  self.background_crop_fraction_of_rough_crop = 0.5
99
103
 
100
- # A row is considered a probable metadata row if it contains at least this fraction
101
- # of the background color. This is used only to find the top and bottom of the crop area,
102
- # so it's not that *every* row needs to hit this criteria, only the rows that are generally
103
- # above and below the text.
104
+ #: A row is considered a probable metadata row if it contains at least this fraction
105
+ #: of the background color. This is used only to find the top and bottom of the crop area,
106
+ #: so it's not that *every* row needs to hit this criteria, only the rows that are generally
107
+ #: above and below the text.
104
108
  self.min_background_fraction_for_background_row = 0.5
105
109
 
106
- # psm 6: "assume a single uniform block of text"
107
- # psm 13: raw line
108
- # oem: 0 == legacy, 1 == lstm
109
- # tesseract_config_string = '--oem 0 --psm 6'
110
- #
111
- # Try these configuration strings in order until we find a valid datetime
110
+ #: psm 6: "assume a single uniform block of text"
111
+ #: psm 13: raw line
112
+ #: oem: 0 == legacy, 1 == lstm
113
+ #: tesseract_config_string = '--oem 0 --psm 6'
114
+ #:
115
+ #: Try these configuration strings in order until we find a valid datetime
112
116
  self.tesseract_config_strings = ['--oem 1 --psm 13','--oem 0 --psm 13',
113
117
  '--oem 1 --psm 6','--oem 0 --psm 6']
114
118
 
119
+ #: If this is False, and one set of options appears to succeed for an image, we'll
120
+ #: stop there. If this is True, we always run all option sets on every image.
115
121
  self.force_all_ocr_options = False
116
122
 
123
+ #: Whether to apply PIL's ImageFilter.SHARPEN prior to OCR
117
124
  self.apply_sharpening_filter = True
118
125
 
119
- # Tesseract should be on your system path, but you can also specify the
120
- # path explicitly.
121
- #
122
- # os.environ['PATH'] += r';C:\Program Files\Tesseract-OCR'
123
- # self.tesseract_cmd = 'r"C:\Program Files\Tesseract-OCR\tesseract.exe"'
126
+ #: Tesseract should be on your system path, but you can also specify the
127
+ #: path explicitly, e.g. you can do either of these:
128
+ #:
129
+ #: * os.environ['PATH'] += r';C:\Program Files\Tesseract-OCR'
130
+ #: * self.tesseract_cmd = 'r"C:\Program Files\Tesseract-OCR\tesseract.exe"'
124
131
  self.tesseract_cmd = 'tesseract.exe'
125
132
 
126
133
 
@@ -128,10 +135,14 @@ class DatetimeExtractionOptions:
128
135
 
129
136
  def make_rough_crops(image,options=None):
130
137
  """
131
- Crops the top and bottom regions out of an image, returns a dict with fields
132
- 'top' and 'bottom', each pointing to a PIL image.
138
+ Crops the top and bottom regions out of an image.
133
139
 
134
- [image] can be a PIL image or a file name.
140
+ Args:
141
+ image (Image or str): a PIL Image or file name
142
+ options (DatetimeExtractionOptions, optional): OCR parameters
143
+
144
+ Returns:
145
+ dict: a dict with fields 'top' and 'bottom', each pointing to a new PIL Image
135
146
  """
136
147
 
137
148
  if options is None:
@@ -158,10 +169,8 @@ def make_rough_crops(image,options=None):
158
169
 
159
170
  def crop_to_solid_region(rough_crop,crop_location,options=None):
160
171
  """
161
- Given a rough crop from the top or bottom of an imaeg, find the background color
162
- and crop to the metadata region.
163
-
164
- rough_crop should be PIL Image, crop_location should be 'top' or 'bottom'.
172
+ Given a rough crop from the top or bottom of an image, finds the background color
173
+ and crops to the metadata region.
165
174
 
166
175
  Within a region of an image (typically a crop from the top-ish or bottom-ish part of
167
176
  an image), tightly crop to the solid portion (typically a region with a black background).
@@ -169,7 +178,13 @@ def crop_to_solid_region(rough_crop,crop_location,options=None):
169
178
  The success metric is just a binary indicator right now: 1.0 if we found a region we believe
170
179
  contains a solid background, 0.0 otherwise.
171
180
 
172
- Returns cropped_image,p_success,padded_image
181
+ Args:
182
+ rough_crop (Image): the PIL Image to crop
183
+ crop_location (str): 'top' or 'bottom'
184
+ options (DatetimeExtractionOptions, optional): OCR parameters
185
+
186
+ Returns:
187
+ tuple: a tuple containing (a cropped_image (Image), p_success (float), padded_image (Image))
173
188
  """
174
189
 
175
190
  if options is None:
@@ -283,8 +298,17 @@ def crop_to_solid_region(rough_crop,crop_location,options=None):
283
298
 
284
299
  def find_text_in_crops(rough_crops,options=None,tesseract_config_string=None):
285
300
  """
286
- Find all text in each Image in the dict [rough_crops]; those images should be pretty small
301
+ Finds all text in each Image in the dict [rough_crops]; those images should be pretty small
287
302
  regions by the time they get to this function, roughly the top or bottom 20% of an image.
303
+
304
+ Args:
305
+ rough_crops (list): list of Image objects that have been cropped close to text
306
+ options (DatetimeExtractionOptions, optional): OCR parameters
307
+ tesseract_config_string (str, optional): optional CLI argument to pass to tesseract.exe
308
+
309
+ Returns:
310
+ dict: a dict with keys "top" and "bottom", where each value is a dict with keys
311
+ 'text' (text found, if any) and 'crop_to_solid_region_results' (metadata about the OCR pass)
288
312
  """
289
313
 
290
314
  if options is None:
@@ -338,7 +362,7 @@ def find_text_in_crops(rough_crops,options=None,tesseract_config_string=None):
338
362
  # ...def find_text_in_crops(...)
339
363
 
340
364
 
341
- def datetime_string_to_datetime(matched_string):
365
+ def _datetime_string_to_datetime(matched_string):
342
366
  """
343
367
  Takes an OCR-matched datetime string, does a little cleanup, and parses a date
344
368
  from it.
@@ -358,7 +382,7 @@ def datetime_string_to_datetime(matched_string):
358
382
  return extracted_datetime
359
383
 
360
384
 
361
- def get_datetime_from_strings(strings,options=None):
385
+ def _get_datetime_from_strings(strings,options=None):
362
386
  """
363
387
  Given a string or list of strings, search for exactly one datetime in those strings.
364
388
  using a series of regular expressions.
@@ -381,72 +405,76 @@ def get_datetime_from_strings(strings,options=None):
381
405
  # 2013-10-02 11:40:50 AM
382
406
  m = re.search('(\d\d\d\d)\s?-\s?(\d\d)\s?-\s?(\d\d)\s+(\d+)\s?:?\s?(\d\d)\s?:\s?(\d\d)\s*([a|p]m)',s)
383
407
  if m is not None:
384
- return datetime_string_to_datetime(m.group(0))
408
+ return _datetime_string_to_datetime(m.group(0))
385
409
 
386
410
  # 04/01/2017 08:54:00AM
387
411
  m = re.search('(\d\d)\s?/\s?(\d\d)\s?/\s?(\d\d\d\d)\s+(\d+)\s?:\s?(\d\d)\s?:\s?(\d\d)\s*([a|p]m)',s)
388
412
  if m is not None:
389
- return datetime_string_to_datetime(m.group(0))
413
+ return _datetime_string_to_datetime(m.group(0))
390
414
 
391
415
  # 2017/04/01 08:54:00AM
392
416
  m = re.search('(\d\d\d\d)\s?/\s?(\d\d)\s?/\s?(\d\d)\s+(\d+)\s?:\s?(\d\d)\s?:\s?(\d\d)\s*([a|p]m)',s)
393
417
  if m is not None:
394
- return datetime_string_to_datetime(m.group(0))
418
+ return _datetime_string_to_datetime(m.group(0))
395
419
 
396
420
  # 04/01/2017 08:54AM
397
421
  m = re.search('(\d\d)\s?/\s?(\d\d)\s?/\s?(\d\d\d\d)\s+(\d+)\s?:\s?(\d\d)\s*([a|p]m)',s)
398
422
  if m is not None:
399
- return datetime_string_to_datetime(m.group(0))
423
+ return _datetime_string_to_datetime(m.group(0))
400
424
 
401
425
  # 2017/04/01 08:54AM
402
426
  m = re.search('(\d\d\d\d)\s?/\s?(\d\d)\s?/\s?(\d\d)\s+(\d+)\s?:\s?(\d\d)\s*([a|p]m)',s)
403
427
  if m is not None:
404
- return datetime_string_to_datetime(m.group(0))
428
+ return _datetime_string_to_datetime(m.group(0))
405
429
 
406
430
  ### No AM/PM
407
431
 
408
432
  # 2013-07-27 04:56:35
409
433
  m = re.search('(\d\d\d\d)\s?-\s?(\d\d)\s?-\s?(\d\d)\s*(\d\d)\s?:\s?(\d\d)\s?:\s?(\d\d)',s)
410
434
  if m is not None:
411
- return datetime_string_to_datetime(m.group(0))
435
+ return _datetime_string_to_datetime(m.group(0))
412
436
 
413
437
  # 07-27-2013 04:56:35
414
438
  m = re.search('(\d\d)\s?-\s?(\d\d)\s?-\s?(\d\d\d\d)\s*(\d\d)\s?:\s?(\d\d)\s?:\s?(\d\d)',s)
415
439
  if m is not None:
416
- return datetime_string_to_datetime(m.group(0))
440
+ return _datetime_string_to_datetime(m.group(0))
417
441
 
418
442
  # 2013/07/27 04:56:35
419
443
  m = re.search('(\d\d\d\d)\s?/\s?(\d\d)\s?/\s?(\d\d)\s*(\d\d)\s?:\s?(\d\d)\s?:\s?(\d\d)',s)
420
444
  if m is not None:
421
- return datetime_string_to_datetime(m.group(0))
445
+ return _datetime_string_to_datetime(m.group(0))
422
446
 
423
447
  # 07/27/2013 04:56:35
424
448
  m = re.search('(\d\d)\s?/\s?(\d\d)\s?/\s?(\d\d\d\d)\s*(\d\d)\s?:\s?(\d\d)\s?:\s?(\d\d)',s)
425
449
  if m is not None:
426
- return datetime_string_to_datetime(m.group(0))
450
+ return _datetime_string_to_datetime(m.group(0))
427
451
 
428
452
  return None
429
453
 
430
- # ...def get_datetime_from_strings(...)
454
+ # ...def _get_datetime_from_strings(...)
431
455
 
432
456
 
433
457
  def get_datetime_from_image(image,include_crops=True,options=None):
434
458
  """
435
- Find the datetime string (if present) in [image], which can be a PIL image or a
436
- filename. Returns a dict:
437
-
438
- datetime: Python datetime object, or None
439
-
440
- text_results: length-2 list of strings
441
-
442
- all_extracted_datetimes: if we ran multiple option sets, this will contain the
443
- datetimes extracted for each option set
444
-
445
- ocr_results: detailed results from the OCR process, including crops as PIL images;
446
- only included if include_crops is True.
447
-
448
- [options] can be None, a DatetimeExtractionOptions object, or a list of
449
- DatetimeExtractionOptions objects to try for each image.
459
+ Tries to find the datetime string (if present) in an image.
460
+
461
+ Args:
462
+ image (Image or str): the PIL Image object or image filename in which we should look for
463
+ datetime information.
464
+ include_crops (bool, optional): whether to include cropped images in the return dict (set
465
+ this to False if you're worried about size and you're processing a zillion images)
466
+ options (DatetimeExtractionOptions or list, optional): OCR parameters, either one
467
+ DatetimeExtractionOptions object or a list of options to try
468
+
469
+ Returns:
470
+ dict: a dict with fields:
471
+
472
+ - datetime: Python datetime object, or None
473
+ - text_results: length-2 list of strings
474
+ - all_extracted_datetimes: if we ran multiple option sets, this will contain the
475
+ datetimes extracted for each option set
476
+ - ocr_results: detailed results from the OCR process, including crops as PIL images;
477
+ only included if include_crops is True
450
478
  """
451
479
 
452
480
  if options is None:
@@ -478,7 +506,7 @@ def get_datetime_from_image(image,include_crops=True,options=None):
478
506
  all_text_results.append(text_results)
479
507
 
480
508
  # Find datetime
481
- extracted_datetime_this_option_set = get_datetime_from_strings(text_results,options)
509
+ extracted_datetime_this_option_set = _get_datetime_from_strings(text_results,options)
482
510
  assert isinstance(extracted_datetime_this_option_set,datetime.datetime) or \
483
511
  (extracted_datetime_this_option_set is None)
484
512
 
@@ -512,18 +540,27 @@ def get_datetime_from_image(image,include_crops=True,options=None):
512
540
  # ...def get_datetime_from_image(...)
513
541
 
514
542
 
515
- def is_iterable(x):
516
- try:
517
- _ = iter(x)
518
- except:
519
- return False
520
- return True
521
-
522
-
523
543
  def try_get_datetime_from_image(filename,include_crops=False,options=None):
524
544
  """
525
545
  Try/catch wrapper for get_datetime_from_image, optionally trying multiple option sets
526
546
  until we find a datetime.
547
+
548
+ Args:
549
+ image (Image or str): the PIL Image object or image filename in which we should look for
550
+ datetime information.
551
+ include_crops (bool, optional): whether to include cropped images in the return dict (set
552
+ this to False if you're worried about size and you're processing a zillion images)
553
+ options (DatetimeExtractionOptions or list, optional): OCR parameters, either one
554
+ DatetimeExtractionOptions object or a list of options to try
555
+
556
+ Returns:
557
+ dict: A dict with fields:
558
+ - datetime: Python datetime object, or None
559
+ - text_results: length-2 list of strings
560
+ - all_extracted_datetimes: if we ran multiple option sets, this will contain the
561
+ datetimes extracted for each option set
562
+ - ocr_results: detailed results from the OCR process, including crops as PIL images;
563
+ only included if include_crops is True
527
564
  """
528
565
 
529
566
  if options is None:
@@ -547,16 +584,28 @@ def try_get_datetime_from_image(filename,include_crops=False,options=None):
547
584
  return result
548
585
 
549
586
 
550
- def get_datetimes_for_folder(folder_name,output_file=None,n_to_sample=-1,options=None):
587
+ def get_datetimes_for_folder(folder_name,output_file=None,n_to_sample=-1,options=None,
588
+ n_workers=16,use_threads=False):
551
589
  """
552
- Retrieve metadata from every image in [folder_name], and
553
- write the results to the .json file [output_file].
554
-
555
- [options] can be None, a DatetimeExtractionOptions object, or a list of
556
- DatetimeExtractionOptions objects to try for each image.
557
-
558
- Returns a dict mapping filenames to datetime extraction results. Optionally writes
559
- results to the .json file [output_file].
590
+ The main entry point for this module. Tries to retrieve metadata from pixels for every
591
+ image in [folder_name], optionally the results to the .json file [output_file].
592
+
593
+ Args:
594
+ folder_name (str): the folder of images to process recursively
595
+ output_file (str, optional): the .json file to which we should write results; if None,
596
+ just returns the results
597
+ n_to_sample (int, optional): for debugging only, used to limit the number of images
598
+ we process
599
+ options (DatetimeExtractionOptions or list, optional): OCR parameters, either one
600
+ DatetimeExtractionOptions object or a list of options to try for each image
601
+ n_workers (int, optional): the number of parallel workers to use; set to <= 1 to disable
602
+ parallelization
603
+ use_threads (bool, optional): whether to use threads (True) or processes (False) for
604
+ parallelization; not relevant if n_workers <= 1
605
+
606
+ Returns:
607
+ dict: a dict mapping filenames to datetime extraction results, see try_get_datetime_from_images
608
+ for the format of each value in the dict.
560
609
  """
561
610
 
562
611
  if options is None:
@@ -570,11 +619,8 @@ def get_datetimes_for_folder(folder_name,output_file=None,n_to_sample=-1,options
570
619
  import random
571
620
  random.seed(0)
572
621
  image_file_names = random.sample(image_file_names,n_to_sample)
573
-
574
- n_cores = 16
575
- use_threads = False
576
-
577
- if n_cores <= 1:
622
+
623
+ if n_workers <= 1:
578
624
 
579
625
  all_results = []
580
626
  for fn_abs in tqdm(image_file_names):
@@ -583,19 +629,19 @@ def get_datetimes_for_folder(folder_name,output_file=None,n_to_sample=-1,options
583
629
  else:
584
630
 
585
631
  # Don't spawn more than one worker per image
586
- if n_cores > len(image_file_names):
587
- n_cores = len(image_file_names)
632
+ if n_workers > len(image_file_names):
633
+ n_workers = len(image_file_names)
588
634
 
589
635
  if use_threads:
590
636
  from multiprocessing.pool import ThreadPool
591
- pool = ThreadPool(n_cores)
637
+ pool = ThreadPool(n_workers)
592
638
  worker_string = 'threads'
593
639
  else:
594
640
  from multiprocessing.pool import Pool
595
- pool = Pool(n_cores)
641
+ pool = Pool(n_workers)
596
642
  worker_string = 'processes'
597
643
 
598
- print('Starting a pool of {} {}'.format(n_cores,worker_string))
644
+ print('Starting a pool of {} {}'.format(n_workers,worker_string))
599
645
 
600
646
  all_results = list(tqdm(pool.imap(
601
647
  partial(try_get_datetime_from_image,options=options),image_file_names),
@@ -621,7 +667,6 @@ if False:
621
667
  #%% Process images
622
668
 
623
669
  folder_name = r'g:\temp\island_conservation_camera_traps'
624
- # folder_name = r'g:\camera_traps\camera_trap_images'
625
670
  output_file = r'g:\temp\ocr_results.json'
626
671
  from md_utils.path_utils import insert_before_extension
627
672
  output_file = insert_before_extension(output_file)
@@ -650,11 +695,6 @@ if False:
650
695
  #%% Scrap cell
651
696
 
652
697
  fn = 'g:/camera_traps/camera_trap_images/2018.07.02/newcam/people/DSCF0273.JPG'
653
- # fn = r'g:\camera_traps\camera_trap_images\2022.01.29\cam0\coyote\DSCF0057.JPG'
654
- # fn = 'g:/temp/island_conservation_camera_traps/chile/frances01/frances012013/chile_frances012013_02012013105658.jpg'
655
- # fn = 'g:/temp/island_conservation_camera_traps/dominicanrepublic/camara06/cam0618junio2016/dominicanrepublic_cam0618junio2016_20160614_114115_img_0013.jpg'
656
- # fn = os.path.join(folder_name,r'dominicanrepublic\camara22\cam228noviembre2015\dominicanrepublic_cam228noviembre2015_20151105_071226_img_0132.jpg')
657
- # fn = 'g:/camera_traps/camera_trap_images/2021.06.06/camera01/empty/DSCF0873.JPG'
658
698
  include_crops = False
659
699
  options_a = DatetimeExtractionOptions()
660
700
  options_b = DatetimeExtractionOptions()
@@ -827,3 +867,8 @@ if False:
827
867
 
828
868
  if extracted_datetime is not None:
829
869
  assert extracted_datetime.year <= 2023 and extracted_datetime.year >= 1990
870
+
871
+
872
+ #%% Command-line driver
873
+
874
+ # TODO