megadetector 5.0.27__py3-none-any.whl → 5.0.29__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 (176) hide show
  1. megadetector/api/batch_processing/api_core/batch_service/score.py +4 -5
  2. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +1 -1
  3. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +1 -1
  4. megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +2 -2
  5. megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +1 -1
  6. megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +1 -1
  7. megadetector/api/synchronous/api_core/tests/load_test.py +2 -3
  8. megadetector/classification/aggregate_classifier_probs.py +3 -3
  9. megadetector/classification/analyze_failed_images.py +5 -5
  10. megadetector/classification/cache_batchapi_outputs.py +5 -5
  11. megadetector/classification/create_classification_dataset.py +11 -12
  12. megadetector/classification/crop_detections.py +10 -10
  13. megadetector/classification/csv_to_json.py +8 -8
  14. megadetector/classification/detect_and_crop.py +13 -15
  15. megadetector/classification/evaluate_model.py +7 -7
  16. megadetector/classification/identify_mislabeled_candidates.py +6 -6
  17. megadetector/classification/json_to_azcopy_list.py +1 -1
  18. megadetector/classification/json_validator.py +29 -32
  19. megadetector/classification/map_classification_categories.py +9 -9
  20. megadetector/classification/merge_classification_detection_output.py +12 -9
  21. megadetector/classification/prepare_classification_script.py +19 -19
  22. megadetector/classification/prepare_classification_script_mc.py +23 -23
  23. megadetector/classification/run_classifier.py +4 -4
  24. megadetector/classification/save_mislabeled.py +6 -6
  25. megadetector/classification/train_classifier.py +1 -1
  26. megadetector/classification/train_classifier_tf.py +9 -9
  27. megadetector/classification/train_utils.py +10 -10
  28. megadetector/data_management/annotations/annotation_constants.py +1 -1
  29. megadetector/data_management/camtrap_dp_to_coco.py +45 -45
  30. megadetector/data_management/cct_json_utils.py +101 -101
  31. megadetector/data_management/cct_to_md.py +49 -49
  32. megadetector/data_management/cct_to_wi.py +33 -33
  33. megadetector/data_management/coco_to_labelme.py +75 -75
  34. megadetector/data_management/coco_to_yolo.py +189 -189
  35. megadetector/data_management/databases/add_width_and_height_to_db.py +3 -2
  36. megadetector/data_management/databases/combine_coco_camera_traps_files.py +38 -38
  37. megadetector/data_management/databases/integrity_check_json_db.py +202 -188
  38. megadetector/data_management/databases/subset_json_db.py +33 -33
  39. megadetector/data_management/generate_crops_from_cct.py +38 -38
  40. megadetector/data_management/get_image_sizes.py +54 -49
  41. megadetector/data_management/labelme_to_coco.py +130 -124
  42. megadetector/data_management/labelme_to_yolo.py +78 -72
  43. megadetector/data_management/lila/create_lila_blank_set.py +81 -83
  44. megadetector/data_management/lila/create_lila_test_set.py +32 -31
  45. megadetector/data_management/lila/create_links_to_md_results_files.py +18 -18
  46. megadetector/data_management/lila/download_lila_subset.py +21 -24
  47. megadetector/data_management/lila/generate_lila_per_image_labels.py +91 -91
  48. megadetector/data_management/lila/get_lila_annotation_counts.py +30 -30
  49. megadetector/data_management/lila/get_lila_image_counts.py +22 -22
  50. megadetector/data_management/lila/lila_common.py +70 -70
  51. megadetector/data_management/lila/test_lila_metadata_urls.py +13 -14
  52. megadetector/data_management/mewc_to_md.py +339 -340
  53. megadetector/data_management/ocr_tools.py +258 -252
  54. megadetector/data_management/read_exif.py +232 -223
  55. megadetector/data_management/remap_coco_categories.py +26 -26
  56. megadetector/data_management/remove_exif.py +31 -20
  57. megadetector/data_management/rename_images.py +187 -187
  58. megadetector/data_management/resize_coco_dataset.py +41 -41
  59. megadetector/data_management/speciesnet_to_md.py +41 -41
  60. megadetector/data_management/wi_download_csv_to_coco.py +55 -55
  61. megadetector/data_management/yolo_output_to_md_output.py +117 -120
  62. megadetector/data_management/yolo_to_coco.py +195 -188
  63. megadetector/detection/change_detection.py +831 -0
  64. megadetector/detection/process_video.py +341 -338
  65. megadetector/detection/pytorch_detector.py +308 -266
  66. megadetector/detection/run_detector.py +186 -166
  67. megadetector/detection/run_detector_batch.py +366 -364
  68. megadetector/detection/run_inference_with_yolov5_val.py +328 -325
  69. megadetector/detection/run_tiled_inference.py +312 -253
  70. megadetector/detection/tf_detector.py +24 -24
  71. megadetector/detection/video_utils.py +291 -283
  72. megadetector/postprocessing/add_max_conf.py +15 -11
  73. megadetector/postprocessing/categorize_detections_by_size.py +44 -44
  74. megadetector/postprocessing/classification_postprocessing.py +808 -311
  75. megadetector/postprocessing/combine_batch_outputs.py +20 -21
  76. megadetector/postprocessing/compare_batch_results.py +528 -517
  77. megadetector/postprocessing/convert_output_format.py +97 -97
  78. megadetector/postprocessing/create_crop_folder.py +220 -147
  79. megadetector/postprocessing/detector_calibration.py +173 -168
  80. megadetector/postprocessing/generate_csv_report.py +508 -0
  81. megadetector/postprocessing/load_api_results.py +25 -22
  82. megadetector/postprocessing/md_to_coco.py +129 -98
  83. megadetector/postprocessing/md_to_labelme.py +89 -83
  84. megadetector/postprocessing/md_to_wi.py +40 -40
  85. megadetector/postprocessing/merge_detections.py +87 -114
  86. megadetector/postprocessing/postprocess_batch_results.py +319 -302
  87. megadetector/postprocessing/remap_detection_categories.py +36 -36
  88. megadetector/postprocessing/render_detection_confusion_matrix.py +205 -199
  89. megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +57 -57
  90. megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +27 -28
  91. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +702 -677
  92. megadetector/postprocessing/separate_detections_into_folders.py +226 -211
  93. megadetector/postprocessing/subset_json_detector_output.py +265 -262
  94. megadetector/postprocessing/top_folders_to_bottom.py +45 -45
  95. megadetector/postprocessing/validate_batch_results.py +70 -70
  96. megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +52 -52
  97. megadetector/taxonomy_mapping/map_new_lila_datasets.py +15 -15
  98. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +14 -14
  99. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +66 -69
  100. megadetector/taxonomy_mapping/retrieve_sample_image.py +16 -16
  101. megadetector/taxonomy_mapping/simple_image_download.py +8 -8
  102. megadetector/taxonomy_mapping/species_lookup.py +33 -33
  103. megadetector/taxonomy_mapping/taxonomy_csv_checker.py +14 -14
  104. megadetector/taxonomy_mapping/taxonomy_graph.py +11 -11
  105. megadetector/taxonomy_mapping/validate_lila_category_mappings.py +13 -13
  106. megadetector/utils/azure_utils.py +22 -22
  107. megadetector/utils/ct_utils.py +1019 -200
  108. megadetector/utils/directory_listing.py +21 -77
  109. megadetector/utils/gpu_test.py +22 -22
  110. megadetector/utils/md_tests.py +541 -518
  111. megadetector/utils/path_utils.py +1511 -406
  112. megadetector/utils/process_utils.py +41 -41
  113. megadetector/utils/sas_blob_utils.py +53 -49
  114. megadetector/utils/split_locations_into_train_val.py +73 -60
  115. megadetector/utils/string_utils.py +147 -26
  116. megadetector/utils/url_utils.py +463 -173
  117. megadetector/utils/wi_utils.py +2629 -2868
  118. megadetector/utils/write_html_image_list.py +137 -137
  119. megadetector/visualization/plot_utils.py +21 -21
  120. megadetector/visualization/render_images_with_thumbnails.py +37 -73
  121. megadetector/visualization/visualization_utils.py +424 -404
  122. megadetector/visualization/visualize_db.py +197 -190
  123. megadetector/visualization/visualize_detector_output.py +126 -98
  124. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/METADATA +6 -3
  125. megadetector-5.0.29.dist-info/RECORD +163 -0
  126. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
  127. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  128. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  129. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  130. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  131. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  132. megadetector/data_management/importers/awc_to_json.py +0 -191
  133. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  134. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  135. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  136. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  137. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  138. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  139. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  140. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  141. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  142. megadetector/data_management/importers/ena24_to_json.py +0 -276
  143. megadetector/data_management/importers/filenames_to_json.py +0 -386
  144. megadetector/data_management/importers/helena_to_cct.py +0 -283
  145. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  146. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  147. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  148. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  149. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  150. megadetector/data_management/importers/missouri_to_json.py +0 -490
  151. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  152. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  153. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  154. megadetector/data_management/importers/pc_to_json.py +0 -365
  155. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  156. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  157. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  158. megadetector/data_management/importers/rspb_to_json.py +0 -356
  159. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  160. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  161. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  162. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  163. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  164. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  165. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  166. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  167. megadetector/data_management/importers/ubc_to_json.py +0 -399
  168. megadetector/data_management/importers/umn_to_json.py +0 -507
  169. megadetector/data_management/importers/wellington_to_json.py +0 -263
  170. megadetector/data_management/importers/wi_to_json.py +0 -442
  171. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  172. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  173. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  174. megadetector-5.0.27.dist-info/RECORD +0 -208
  175. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
  176. {megadetector-5.0.27.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
@@ -10,8 +10,8 @@ https://github.com/agentmorris/MegaDetector/tree/main/megadetector/api/batch_pro
10
10
  This enables the results to be used in our post-processing pipeline; see postprocess_batch_results.py.
11
11
 
12
12
  This script can save results to checkpoints intermittently, in case disaster
13
- strikes. To enable this, set --checkpoint_frequency to n > 0, and results
14
- will be saved as a checkpoint every n images. Checkpoints will be written
13
+ strikes. To enable this, set --checkpoint_frequency to n > 0, and results
14
+ will be saved as a checkpoint every n images. Checkpoints will be written
15
15
  to a file in the same directory as the output_file, and after all images
16
16
  are processed and final results file written to output_file, the temporary
17
17
  checkpoint file will be deleted. If you want to resume from a checkpoint, set
@@ -26,10 +26,10 @@ run a gazillion MegaDetector images on multiple GPUs using this script, we just
26
26
  one GPU *per invocation of this script*. Dividing a big batch of images into one chunk
27
27
  per GPU happens outside of this script.
28
28
 
29
- Does not have a command-line option to bind the process to a particular GPU, but you can
29
+ Does not have a command-line option to bind the process to a particular GPU, but you can
30
30
  prepend with "CUDA_VISIBLE_DEVICES=0 ", for example, to bind to GPU 0, e.g.:
31
31
 
32
- CUDA_VISIBLE_DEVICES=0 python detection/run_detector_batch.py md_v4.1.0.pb ~/data ~/mdv4test.json
32
+ CUDA_VISIBLE_DEVICES=0 python detection/run_detector_batch.py md_v4.1.0.pb ~/data ~/mdv4test.json
33
33
 
34
34
  You can disable GPU processing entirely by setting CUDA_VISIBLE_DEVICES=''.
35
35
 
@@ -70,6 +70,7 @@ from megadetector.detection.run_detector import \
70
70
  get_detector_metadata_from_version_string
71
71
 
72
72
  from megadetector.utils import path_utils
73
+ from megadetector.utils import ct_utils
73
74
  from megadetector.utils.ct_utils import parse_kvp_list
74
75
  from megadetector.utils.ct_utils import split_list_into_n_chunks
75
76
  from megadetector.utils.ct_utils import sort_list_of_dicts_by_key
@@ -92,7 +93,7 @@ max_queue_size = 10
92
93
  # How often should we print progress when using the image queue?
93
94
  n_queue_print = 1000
94
95
 
95
- # TODO: it's a little sloppy that these are module-level globals, but in practice it
96
+ # TODO: it's a little sloppy that these are module-level globals, but in practice it
96
97
  # doesn't really matter, so I'm not in a big rush to move these to options until I do
97
98
  # a larger cleanup of all the long argument lists in this module.
98
99
  #
@@ -116,40 +117,40 @@ def _producer_func(q,
116
117
  verbose=False,
117
118
  image_size=None,
118
119
  augment=None):
119
- """
120
+ """
120
121
  Producer function; only used when using the (optional) image queue.
121
-
122
- Reads up to images from disk and puts them on the blocking queue for
123
- processing. Each image is queued as a tuple of [filename,Image]. Sends
122
+
123
+ Reads up to images from disk and puts them on the blocking queue for
124
+ processing. Each image is queued as a tuple of [filename,Image]. Sends
124
125
  "None" to the queue when finished.
125
-
126
+
126
127
  The "detector" argument is only used for preprocessing.
127
128
  """
128
-
129
+
129
130
  if verbose:
130
131
  print('Producer starting: ID {}, preprocessor {}'.format(producer_id,preprocessor))
131
132
  sys.stdout.flush()
132
-
133
+
133
134
  if preprocessor is not None:
134
135
  assert isinstance(preprocessor,str)
135
136
  detector_options = deepcopy(detector_options)
136
137
  detector_options['preprocess_only'] = True
137
138
  preprocessor = load_detector(preprocessor,detector_options=detector_options,verbose=verbose)
138
-
139
+
139
140
  for im_file in image_files:
140
-
141
+
141
142
  try:
142
143
  if verbose:
143
144
  print('Loading image {} on producer {}'.format(im_file,producer_id))
144
145
  sys.stdout.flush()
145
146
  image = vis_utils.load_image(im_file)
146
-
147
+
147
148
  if preprocessor is not None:
148
-
149
+
149
150
  image_info = preprocessor.generate_detections_one_image(
150
- image,
151
- im_file,
152
- detection_threshold=None,
151
+ image,
152
+ im_file,
153
+ detection_threshold=None,
153
154
  image_size=image_size,
154
155
  skip_image_resizing=False,
155
156
  augment=augment,
@@ -158,29 +159,29 @@ def _producer_func(q,
158
159
  if 'failure' in image_info:
159
160
  assert image_info['failure'] == run_detector.FAILURE_INFER
160
161
  raise
161
-
162
+
162
163
  image = image_info
163
-
164
- except Exception:
165
- print('Producer process: image {} cannot be loaded'.format(im_file))
166
- image = run_detector.FAILURE_IMAGE_OPEN
167
-
164
+
165
+ except Exception as e:
166
+ print('Producer process: image {} cannot be loaded:\n{}'.format(im_file,str(e)))
167
+ image = run_detector.FAILURE_IMAGE_OPEN
168
+
168
169
  if verbose:
169
170
  print('Queueing image {} from producer {}'.format(im_file,producer_id))
170
171
  sys.stdout.flush()
171
-
172
+
172
173
  q.put([im_file,image,producer_id])
173
-
174
+
174
175
  # This is a signal to the consumer function that a worker has finished
175
176
  q.put(None)
176
-
177
+
177
178
  if verbose:
178
179
  print('Loader worker {} finished'.format(producer_id))
179
180
  sys.stdout.flush()
180
181
 
181
182
  # ...def _producer_func(...)
182
-
183
-
183
+
184
+
184
185
  def _consumer_func(q,
185
186
  return_queue,
186
187
  model_file,
@@ -188,25 +189,25 @@ def _consumer_func(q,
188
189
  loader_workers,
189
190
  image_size=None,
190
191
  include_image_size=False,
191
- include_image_timestamp=False,
192
+ include_image_timestamp=False,
192
193
  include_exif_data=False,
193
194
  augment=False,
194
195
  detector_options=None,
195
196
  preprocess_on_image_queue=default_preprocess_on_image_queue,
196
197
  n_total_images=None
197
198
  ):
198
- """
199
+ """
199
200
  Consumer function; only used when using the (optional) image queue.
200
-
201
+
201
202
  Pulls images from a blocking queue and processes them. Returns when "None" has
202
203
  been read from each loader's queue.
203
204
  """
204
-
205
+
205
206
  if verbose:
206
207
  print('Consumer starting'); sys.stdout.flush()
207
208
 
208
209
  start_time = time.time()
209
-
210
+
210
211
  if isinstance(model_file,str):
211
212
  detector = load_detector(model_file,detector_options=detector_options,verbose=verbose)
212
213
  elapsed = time.time() - start_time
@@ -216,21 +217,21 @@ def _consumer_func(q,
216
217
  else:
217
218
  detector = model_file
218
219
  print('Detector of type {} passed to consumer function'.format(type(detector)))
219
-
220
+
220
221
  results = []
221
-
222
+
222
223
  n_images_processed = 0
223
224
  n_queues_finished = 0
224
-
225
+
225
226
  pbar = None
226
227
  if n_total_images is not None:
227
228
  # TODO: in principle I should close this pbar
228
229
  pbar = tqdm(total=n_total_images)
229
-
230
+
230
231
  while True:
231
-
232
+
232
233
  r = q.get()
233
-
234
+
234
235
  # Is this the last image in one of the producer queues?
235
236
  if r is None:
236
237
  n_queues_finished += 1
@@ -246,7 +247,7 @@ def _consumer_func(q,
246
247
  n_images_processed += 1
247
248
  im_file = r[0]
248
249
  image = r[1]
249
-
250
+
250
251
  """
251
252
  result['img_processed'] = img
252
253
  result['img_original'] = img_original
@@ -255,19 +256,19 @@ def _consumer_func(q,
255
256
  result['letterbox_ratio'] = letterbox_ratio
256
257
  result['letterbox_pad'] = letterbox_pad
257
258
  """
258
-
259
+
259
260
  if pbar is not None:
260
261
  pbar.update(1)
261
-
262
+
262
263
  if False:
263
264
  if verbose or ((n_images_processed % n_queue_print) == 1):
264
265
  elapsed = time.time() - start_time
265
266
  images_per_second = n_images_processed / elapsed
266
267
  print('De-queued image {} ({:.2f}/s) ({})'.format(n_images_processed,
267
268
  images_per_second,
268
- im_file));
269
+ im_file))
269
270
  sys.stdout.flush()
270
-
271
+
271
272
  if isinstance(image,str):
272
273
  # This is how the producer function communicates read errors
273
274
  results.append({'file': im_file,
@@ -276,7 +277,7 @@ def _consumer_func(q,
276
277
  print('Expected a dict, received an image of type {}'.format(type(image)))
277
278
  results.append({'file': im_file,
278
279
  'failure': 'illegal image type'})
279
-
280
+
280
281
  else:
281
282
  results.append(process_image(im_file=im_file,
282
283
  detector=detector,
@@ -285,14 +286,14 @@ def _consumer_func(q,
285
286
  quiet=True,
286
287
  image_size=image_size,
287
288
  include_image_size=include_image_size,
288
- include_image_timestamp=include_image_timestamp,
289
+ include_image_timestamp=include_image_timestamp,
289
290
  include_exif_data=include_exif_data,
290
291
  augment=augment,
291
292
  skip_image_resizing=preprocess_on_image_queue))
292
293
  if verbose:
293
294
  print('Processed image {}'.format(im_file)); sys.stdout.flush()
294
295
  q.task_done()
295
-
296
+
296
297
  # ...while True (consumer loop)
297
298
 
298
299
  # ...def _consumer_func(...)
@@ -303,7 +304,7 @@ def run_detector_with_image_queue(image_files,
303
304
  confidence_threshold,
304
305
  quiet=False,
305
306
  image_size=None,
306
- include_image_size=False,
307
+ include_image_size=False,
307
308
  include_image_timestamp=False,
308
309
  include_exif_data=False,
309
310
  augment=False,
@@ -311,11 +312,11 @@ def run_detector_with_image_queue(image_files,
311
312
  loader_workers=default_loaders,
312
313
  preprocess_on_image_queue=default_preprocess_on_image_queue):
313
314
  """
314
- Driver function for the (optional) multiprocessing-based image queue; only used
315
- when --use_image_queue is specified. Starts a reader process to read images from disk, but
315
+ Driver function for the (optional) multiprocessing-based image queue; only used
316
+ when --use_image_queue is specified. Starts a reader process to read images from disk, but
316
317
  processes images in the process from which this function is called (i.e., does not currently
317
318
  spawn a separate consumer process).
318
-
319
+
319
320
  Args:
320
321
  image_files (str): list of absolute paths to images
321
322
  model_file (str): filename or model identifier (e.g. "MDV5A")
@@ -329,35 +330,36 @@ def run_detector_with_image_queue(image_files,
329
330
  include_image_timestamp (bool, optional): should we include image timestamps in the output for each image?
330
331
  include_exif_data (bool, optional): should we include EXIF data in the output for each image?
331
332
  augment (bool, optional): enable image augmentation
332
- detector_options (dict, optional): key/value pairs that are interpreted differently
333
+ detector_options (dict, optional): key/value pairs that are interpreted differently
333
334
  by different detectors
334
335
  loader_workers (int, optional): number of loaders to use
335
-
336
+
336
337
  Returns:
337
338
  list: list of dicts in the format returned by process_image()
338
339
  """
339
-
340
+
340
341
  # Validate inputs
341
342
  assert isinstance(model_file,str)
342
-
343
+
343
344
  if loader_workers <= 0:
344
345
  loader_workers = 1
345
-
346
+
346
347
  q = multiprocessing.JoinableQueue(max_queue_size)
347
348
  return_queue = multiprocessing.Queue(1)
348
-
349
+
349
350
  producers = []
350
-
351
+
351
352
  worker_string = 'thread' if use_threads_for_queue else 'process'
352
353
  print('Starting a {} pool with {} workers'.format(worker_string,loader_workers))
353
-
354
+
354
355
  preprocessor = None
355
-
356
+
356
357
  if preprocess_on_image_queue:
358
+ print('Enabling image queue preprocessing')
357
359
  preprocessor = model_file
358
-
360
+
359
361
  n_total_images = len(image_files)
360
-
362
+
361
363
  chunks = split_list_into_n_chunks(image_files, loader_workers, chunk_strategy='greedy')
362
364
  for i_chunk,chunk in enumerate(chunks):
363
365
  if use_threads_for_queue:
@@ -378,11 +380,11 @@ def run_detector_with_image_queue(image_files,
378
380
  image_size,
379
381
  augment))
380
382
  producers.append(producer)
381
-
383
+
382
384
  for producer in producers:
383
385
  producer.daemon = False
384
386
  producer.start()
385
-
387
+
386
388
  if run_separate_consumer_process:
387
389
  if use_threads_for_queue:
388
390
  consumer = Thread(target=_consumer_func,args=(q,
@@ -392,7 +394,7 @@ def run_detector_with_image_queue(image_files,
392
394
  loader_workers,
393
395
  image_size,
394
396
  include_image_size,
395
- include_image_timestamp,
397
+ include_image_timestamp,
396
398
  include_exif_data,
397
399
  augment,
398
400
  detector_options,
@@ -406,7 +408,7 @@ def run_detector_with_image_queue(image_files,
406
408
  loader_workers,
407
409
  image_size,
408
410
  include_image_size,
409
- include_image_timestamp,
411
+ include_image_timestamp,
410
412
  include_exif_data,
411
413
  augment,
412
414
  detector_options,
@@ -422,7 +424,7 @@ def run_detector_with_image_queue(image_files,
422
424
  loader_workers,
423
425
  image_size,
424
426
  include_image_size,
425
- include_image_timestamp,
427
+ include_image_timestamp,
426
428
  include_exif_data,
427
429
  augment,
428
430
  detector_options,
@@ -433,21 +435,21 @@ def run_detector_with_image_queue(image_files,
433
435
  producer.join()
434
436
  if verbose:
435
437
  print('Producer {} finished'.format(i_producer))
436
-
438
+
437
439
  if verbose:
438
440
  print('All producers finished')
439
-
441
+
440
442
  if run_separate_consumer_process:
441
443
  consumer.join()
442
444
  if verbose:
443
445
  print('Consumer loop finished')
444
-
446
+
445
447
  q.join()
446
448
  if verbose:
447
449
  print('Queue joined')
448
450
 
449
451
  results = return_queue.get()
450
-
452
+
451
453
  return results
452
454
 
453
455
  # ...def run_detector_with_image_queue(...)
@@ -458,29 +460,29 @@ def run_detector_with_image_queue(image_files,
458
460
  def _chunks_by_number_of_chunks(ls, n):
459
461
  """
460
462
  Splits a list into n even chunks.
461
-
463
+
462
464
  External callers should use ct_utils.split_list_into_n_chunks().
463
465
 
464
466
  Args:
465
467
  ls (list): list to break up into chunks
466
468
  n (int): number of chunks
467
469
  """
468
-
470
+
469
471
  for i in range(0, n):
470
472
  yield ls[i::n]
471
473
 
472
474
 
473
475
  #%% Image processing functions
474
476
 
475
- def process_images(im_files,
476
- detector,
477
- confidence_threshold,
478
- use_image_queue=False,
479
- quiet=False,
480
- image_size=None,
481
- checkpoint_queue=None,
482
- include_image_size=False,
483
- include_image_timestamp=False,
477
+ def process_images(im_files,
478
+ detector,
479
+ confidence_threshold,
480
+ use_image_queue=False,
481
+ quiet=False,
482
+ image_size=None,
483
+ checkpoint_queue=None,
484
+ include_image_size=False,
485
+ include_image_timestamp=False,
484
486
  include_exif_data=False,
485
487
  augment=False,
486
488
  detector_options=None,
@@ -488,9 +490,9 @@ def process_images(im_files,
488
490
  preprocess_on_image_queue=default_preprocess_on_image_queue):
489
491
  """
490
492
  Runs a detector (typically MegaDetector) over a list of image files on a single thread.
491
-
493
+
492
494
  Args:
493
- im_files (list: paths to image files
495
+ im_files (list: paths to image files
494
496
  detector (str or detector object): loaded model or str; if this is a string, it can be a
495
497
  path to a .pb/.pt model file or a known model identifier (e.g. "MDV5A")
496
498
  confidence_threshold (float): only detections above this threshold are returned
@@ -504,7 +506,7 @@ def process_images(im_files,
504
506
  include_image_timestamp (bool, optional): should we include image timestamps in the output for each image?
505
507
  include_exif_data (bool, optional): should we include EXIF data in the output for each image?
506
508
  augment (bool, optional): enable image augmentation
507
- detector_options (dict, optional): key/value pairs that are interpreted differently
509
+ detector_options (dict, optional): key/value pairs that are interpreted differently
508
510
  by different detectors
509
511
  loader_workers (int, optional): number of loaders to use (only relevant when using image queue)
510
512
 
@@ -512,60 +514,60 @@ def process_images(im_files,
512
514
  list: list of dicts, in which each dict represents detections on one image,
513
515
  see the 'images' key in https://github.com/agentmorris/MegaDetector/tree/main/megadetector/api/batch_processing#batch-processing-api-output-format
514
516
  """
515
-
517
+
516
518
  if isinstance(detector, str):
517
-
519
+
518
520
  start_time = time.time()
519
521
  detector = load_detector(detector,detector_options=detector_options,verbose=verbose)
520
522
  elapsed = time.time() - start_time
521
523
  print('Loaded model (batch level) in {}'.format(humanfriendly.format_timespan(elapsed)))
522
524
 
523
525
  if use_image_queue:
524
-
525
- run_detector_with_image_queue(im_files,
526
- detector,
527
- confidence_threshold,
528
- quiet=quiet,
526
+
527
+ run_detector_with_image_queue(im_files,
528
+ detector,
529
+ confidence_threshold,
530
+ quiet=quiet,
529
531
  image_size=image_size,
530
- include_image_size=include_image_size,
532
+ include_image_size=include_image_size,
531
533
  include_image_timestamp=include_image_timestamp,
532
534
  include_exif_data=include_exif_data,
533
535
  augment=augment,
534
536
  detector_options=detector_options,
535
537
  loader_workers=loader_workers,
536
538
  preprocess_on_image_queue=preprocess_on_image_queue)
537
-
538
- else:
539
-
539
+
540
+ else:
541
+
540
542
  results = []
541
543
  for im_file in im_files:
542
- result = process_image(im_file,
543
- detector,
544
+ result = process_image(im_file,
545
+ detector,
544
546
  confidence_threshold,
545
- quiet=quiet,
546
- image_size=image_size,
547
- include_image_size=include_image_size,
547
+ quiet=quiet,
548
+ image_size=image_size,
549
+ include_image_size=include_image_size,
548
550
  include_image_timestamp=include_image_timestamp,
549
551
  include_exif_data=include_exif_data,
550
552
  augment=augment)
551
553
 
552
554
  if checkpoint_queue is not None:
553
555
  checkpoint_queue.put(result)
554
- results.append(result)
555
-
556
+ results.append(result)
557
+
556
558
  return results
557
559
 
558
560
  # ...def process_images(...)
559
561
 
560
562
 
561
- def process_image(im_file,
562
- detector,
563
- confidence_threshold,
564
- image=None,
565
- quiet=False,
566
- image_size=None,
563
+ def process_image(im_file,
564
+ detector,
565
+ confidence_threshold,
566
+ image=None,
567
+ quiet=False,
568
+ image_size=None,
567
569
  include_image_size=False,
568
- include_image_timestamp=False,
570
+ include_image_timestamp=False,
569
571
  include_exif_data=False,
570
572
  skip_image_resizing=False,
571
573
  augment=False):
@@ -574,7 +576,7 @@ def process_image(im_file,
574
576
 
575
577
  Args:
576
578
  im_file (str): path to image file
577
- detector (detector object): loaded model, this can no longer be a string by the time
579
+ detector (detector object): loaded model, this can no longer be a string by the time
578
580
  you get this far down the pipeline
579
581
  confidence_threshold (float): only detections above this threshold are returned
580
582
  image (Image, optional): previously-loaded image, if available, used when a worker
@@ -582,22 +584,22 @@ def process_image(im_file,
582
584
  quiet (bool, optional): suppress per-image printouts
583
585
  image_size (tuple, optional): image size to use for inference, only mess with this
584
586
  if (a) you're using a model other than MegaDetector or (b) you know what you're
585
- doing
587
+ doing
586
588
  include_image_size (bool, optional): should we include image size in the output for each image?
587
589
  include_image_timestamp (bool, optional): should we include image timestamps in the output for each image?
588
- include_exif_data (bool, optional): should we include EXIF data in the output for each image?
590
+ include_exif_data (bool, optional): should we include EXIF data in the output for each image?
589
591
  skip_image_resizing (bool, optional): whether to skip internal image resizing and rely on external resizing
590
592
  augment (bool, optional): enable image augmentation
591
593
 
592
594
  Returns:
593
595
  dict: dict representing detections on one image,
594
- see the 'images' key in
596
+ see the 'images' key in
595
597
  https://github.com/agentmorris/MegaDetector/tree/main/megadetector/api/batch_processing#batch-processing-api-output-format
596
598
  """
597
-
599
+
598
600
  if not quiet:
599
601
  print('Processing image {}'.format(im_file))
600
-
602
+
601
603
  if image is None:
602
604
  try:
603
605
  image = vis_utils.load_image(im_file)
@@ -611,11 +613,11 @@ def process_image(im_file,
611
613
  return result
612
614
 
613
615
  try:
614
-
616
+
615
617
  result = detector.generate_detections_one_image(
616
- image,
617
- im_file,
618
- detection_threshold=confidence_threshold,
618
+ image,
619
+ im_file,
620
+ detection_threshold=confidence_threshold,
619
621
  image_size=image_size,
620
622
  skip_image_resizing=skip_image_resizing,
621
623
  augment=augment)
@@ -631,7 +633,7 @@ def process_image(im_file,
631
633
  if isinstance(image,dict):
632
634
  image = image['img_original_pil']
633
635
 
634
- if include_image_size:
636
+ if include_image_size:
635
637
  result['width'] = image.width
636
638
  result['height'] = image.height
637
639
 
@@ -650,13 +652,13 @@ def _load_custom_class_mapping(class_mapping_filename):
650
652
  """
651
653
  This is an experimental hack to allow the use of non-MD YOLOv5 models through
652
654
  the same infrastructure; it disables the code that enforces MDv5-like class lists.
653
-
655
+
654
656
  Should be a .json file that maps int-strings to strings, or a YOLOv5 dataset.yaml file.
655
657
  """
656
-
658
+
657
659
  if class_mapping_filename is None:
658
660
  return
659
-
661
+
660
662
  run_detector.USE_MODEL_NATIVE_CLASSES = True
661
663
  if class_mapping_filename.endswith('.json'):
662
664
  with open(class_mapping_filename,'r') as f:
@@ -667,28 +669,28 @@ def _load_custom_class_mapping(class_mapping_filename):
667
669
  class_mapping = {str(k):v for k,v in class_mapping.items()}
668
670
  else:
669
671
  raise ValueError('Unrecognized class mapping file {}'.format(class_mapping_filename))
670
-
672
+
671
673
  print('Loaded custom class mapping:')
672
674
  print(class_mapping)
673
675
  run_detector.DEFAULT_DETECTOR_LABEL_MAP = class_mapping
674
676
  return class_mapping
675
-
676
-
677
+
678
+
677
679
  #%% Main function
678
680
 
679
- def load_and_run_detector_batch(model_file,
680
- image_file_names,
681
+ def load_and_run_detector_batch(model_file,
682
+ image_file_names,
681
683
  checkpoint_path=None,
682
684
  confidence_threshold=run_detector.DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD,
683
- checkpoint_frequency=-1,
684
- results=None,
685
+ checkpoint_frequency=-1,
686
+ results=None,
685
687
  n_cores=1,
686
- use_image_queue=False,
687
- quiet=False,
688
- image_size=None,
689
- class_mapping_filename=None,
690
- include_image_size=False,
691
- include_image_timestamp=False,
688
+ use_image_queue=False,
689
+ quiet=False,
690
+ image_size=None,
691
+ class_mapping_filename=None,
692
+ include_image_size=False,
693
+ include_image_timestamp=False,
692
694
  include_exif_data=False,
693
695
  augment=False,
694
696
  force_model_download=False,
@@ -697,19 +699,18 @@ def load_and_run_detector_batch(model_file,
697
699
  preprocess_on_image_queue=default_preprocess_on_image_queue):
698
700
  """
699
701
  Load a model file and run it on a list of images.
700
-
702
+
701
703
  Args:
702
-
703
704
  model_file (str): path to model file, or supported model string (e.g. "MDV5A")
704
- image_file_names (list or str): list of strings (image filenames), a single image filename,
705
- a folder to recursively search for images in, or a .json or .txt file containing a list
705
+ image_file_names (list or str): list of strings (image filenames), a single image filename,
706
+ a folder to recursively search for images in, or a .json or .txt file containing a list
706
707
  of images.
707
708
  checkpoint_path (str, optional), path to use for checkpoints (if None, checkpointing
708
709
  is disabled)
709
710
  confidence_threshold (float, optional): only detections above this threshold are returned
710
- checkpoint_frequency (int, optional): int, write results to JSON checkpoint file every N
711
+ checkpoint_frequency (int, optional): int, write results to JSON checkpoint file every N
711
712
  images, -1 disabled checkpointing
712
- results (list, optional): list of dicts, existing results loaded from checkpoint; generally
713
+ results (list, optional): list of dicts, existing results loaded from checkpoint; generally
713
714
  not useful if you're using this function outside of the CLI
714
715
  n_cores (int, optional): number of parallel worker to use, ignored if we're running on a GPU
715
716
  use_image_queue (bool, optional): use a dedicated worker for image loading
@@ -717,7 +718,7 @@ def load_and_run_detector_batch(model_file,
717
718
  image_size (tuple, optional): image size to use for inference, only mess with this
718
719
  if (a) you're using a model other than MegaDetector or (b) you know what you're
719
720
  doing
720
- class_mapping_filename (str, optional), use a non-default class mapping supplied in a .json
721
+ class_mapping_filename (str, optional), use a non-default class mapping supplied in a .json
721
722
  file or YOLOv5 dataset.yaml file
722
723
  include_image_size (bool, optional): should we include image size in the output for each image?
723
724
  include_image_timestamp (bool, optional): should we include image timestamps in the output for each image?
@@ -726,37 +727,37 @@ def load_and_run_detector_batch(model_file,
726
727
  force_model_download (bool, optional): force downloading the model file if
727
728
  a named model (e.g. "MDV5A") is supplied, even if the local file already
728
729
  exists
729
- detector_options (dict, optional): key/value pairs that are interpreted differently
730
+ detector_options (dict, optional): key/value pairs that are interpreted differently
730
731
  by different detectors
731
732
  loader_workers (int, optional): number of loaders to use, only relevant when use_image_queue is True
732
-
733
+
733
734
  Returns:
734
735
  results: list of dicts; each dict represents detections on one image
735
736
  """
736
-
737
+
737
738
  # Validate input arguments
738
739
  if n_cores is None or n_cores <= 0:
739
740
  n_cores = 1
740
-
741
+
741
742
  if confidence_threshold is None:
742
743
  confidence_threshold=run_detector.DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD
743
-
744
+
744
745
  # Disable checkpointing if checkpoint_path is None
745
746
  if checkpoint_frequency is None or checkpoint_path is None:
746
747
  checkpoint_frequency = -1
747
748
 
748
749
  if class_mapping_filename is not None:
749
750
  _load_custom_class_mapping(class_mapping_filename)
750
-
751
+
751
752
  # Handle the case where image_file_names is not yet actually a list
752
753
  if isinstance(image_file_names,str):
753
-
754
+
754
755
  # Find the images to score; images can be a directory, may need to recurse
755
756
  if os.path.isdir(image_file_names):
756
757
  image_dir = image_file_names
757
758
  image_file_names = path_utils.find_images(image_dir, True)
758
759
  print('{} image files found in folder {}'.format(len(image_file_names),image_dir))
759
-
760
+
760
761
  # A single file, or a list of image paths
761
762
  elif os.path.isfile(image_file_names):
762
763
  list_file = image_file_names
@@ -779,43 +780,43 @@ def load_and_run_detector_batch(model_file,
779
780
  'File {} supplied as [image_file_names] argument, but extension is neither .json nor .txt'\
780
781
  .format(
781
782
  list_file))
782
- else:
783
+ else:
783
784
  raise ValueError(
784
785
  '{} supplied as [image_file_names] argument, but it does not appear to be a file or folder'.format(
785
786
  image_file_names))
786
-
787
+
787
788
  if results is None:
788
789
  results = []
789
790
 
790
791
  already_processed = set([i['file'] for i in results])
791
792
 
792
793
  model_file = try_download_known_detector(model_file, force_download=force_model_download)
793
-
794
+
794
795
  print('GPU available: {}'.format(is_gpu_available(model_file)))
795
-
796
+
796
797
  if n_cores > 1 and is_gpu_available(model_file):
797
-
798
+
798
799
  print('Warning: multiple cores requested, but a GPU is available; parallelization across ' + \
799
800
  'GPUs is not currently supported, defaulting to one GPU')
800
801
  n_cores = 1
801
802
 
802
803
  if n_cores > 1 and use_image_queue:
803
-
804
+
804
805
  print('Warning: multiple cores requested, but the image queue is enabled; parallelization ' + \
805
806
  'with the image queue is not currently supported, defaulting to one worker')
806
807
  n_cores = 1
807
-
808
+
808
809
  if use_image_queue:
809
-
810
+
810
811
  assert checkpoint_frequency < 0, \
811
812
  'Using an image queue is not currently supported when checkpointing is enabled'
812
813
  assert len(results) == 0, \
813
814
  'Using an image queue with results loaded from a checkpoint is not currently supported'
814
815
  assert n_cores <= 1
815
- results = run_detector_with_image_queue(image_file_names,
816
- model_file,
817
- confidence_threshold,
818
- quiet,
816
+ results = run_detector_with_image_queue(image_file_names,
817
+ model_file,
818
+ confidence_threshold,
819
+ quiet,
819
820
  image_size=image_size,
820
821
  include_image_size=include_image_size,
821
822
  include_image_timestamp=include_image_timestamp,
@@ -824,7 +825,7 @@ def load_and_run_detector_batch(model_file,
824
825
  detector_options=detector_options,
825
826
  loader_workers=loader_workers,
826
827
  preprocess_on_image_queue=preprocess_on_image_queue)
827
-
828
+
828
829
  elif n_cores <= 1:
829
830
 
830
831
  # Load the detector
@@ -847,11 +848,11 @@ def load_and_run_detector_batch(model_file,
847
848
 
848
849
  count += 1
849
850
 
850
- result = process_image(im_file,
851
- detector,
852
- confidence_threshold,
853
- quiet=quiet,
854
- image_size=image_size,
851
+ result = process_image(im_file,
852
+ detector,
853
+ confidence_threshold,
854
+ quiet=quiet,
855
+ image_size=image_size,
855
856
  include_image_size=include_image_size,
856
857
  include_image_timestamp=include_image_timestamp,
857
858
  include_exif_data=include_exif_data,
@@ -860,97 +861,100 @@ def load_and_run_detector_batch(model_file,
860
861
 
861
862
  # Write a checkpoint if necessary
862
863
  if (checkpoint_frequency != -1) and ((count % checkpoint_frequency) == 0):
863
-
864
+
864
865
  print('Writing a new checkpoint after having processed {} images since '
865
866
  'last restart'.format(count))
866
-
867
+
867
868
  _write_checkpoint(checkpoint_path, results)
868
-
869
+
869
870
  else:
870
-
871
+
871
872
  # Multiprocessing is enabled at this point
872
-
873
+
873
874
  # When using multiprocessing, tell the workers to load the model on each
874
875
  # process, by passing the model_file string as the "model" argument to
875
876
  # process_images.
876
877
  detector = model_file
877
878
 
878
- print('Creating pool with {} cores'.format(n_cores))
879
+ print('Creating worker pool with {} cores'.format(n_cores))
879
880
 
880
881
  if len(already_processed) > 0:
881
882
  n_images_all = len(image_file_names)
882
883
  image_file_names = [fn for fn in image_file_names if fn not in already_processed]
883
884
  print('Loaded {} of {} images from checkpoint'.format(
884
885
  len(already_processed),n_images_all))
885
-
886
- # Divide images into chunks; we'll send one chunk to each worker process
886
+
887
+ # Divide images into chunks; we'll send one chunk to each worker process
887
888
  image_batches = list(_chunks_by_number_of_chunks(image_file_names, n_cores))
888
-
889
- pool = workerpool(n_cores)
890
-
891
- if checkpoint_path is not None:
892
-
893
- # Multiprocessing and checkpointing are both enabled at this point
894
-
895
- checkpoint_queue = Manager().Queue()
896
-
897
- # Pass the "results" array (which may already contain images loaded from an existing
898
- # checkpoint) to the checkpoint queue handler function, which will append results to
899
- # the list as they become available.
900
- checkpoint_thread = Thread(target=_checkpoint_queue_handler,
901
- args=(checkpoint_path, checkpoint_frequency,
902
- checkpoint_queue, results), daemon=True)
903
- checkpoint_thread.start()
904
-
905
- pool.map(partial(process_images,
906
- detector=detector,
907
- confidence_threshold=confidence_threshold,
908
- use_image_queue=False,
909
- quiet=quiet,
910
- image_size=image_size,
911
- checkpoint_queue=checkpoint_queue,
912
- include_image_size=include_image_size,
913
- include_image_timestamp=include_image_timestamp,
914
- include_exif_data=include_exif_data,
915
- augment=augment,
916
- detector_options=detector_options),
917
- image_batches)
918
-
919
- checkpoint_queue.put(None)
920
889
 
921
- else:
922
-
923
- # Multprocessing is enabled, but checkpointing is not
924
-
925
- new_results = pool.map(partial(process_images,
926
- detector=detector,
927
- confidence_threshold=confidence_threshold,
928
- use_image_queue=False,
929
- quiet=quiet,
930
- checkpoint_queue=None,
931
- image_size=image_size,
932
- include_image_size=include_image_size,
933
- include_image_timestamp=include_image_timestamp,
934
- include_exif_data=include_exif_data,
935
- augment=augment,
936
- detector_options=detector_options),
937
- image_batches)
938
-
939
- new_results = list(itertools.chain.from_iterable(new_results))
940
-
941
- # Append the results we just computed to "results", which is *usually* empty, but will
942
- # be non-empty if we resumed from a checkpoint
943
- results += new_results
944
-
945
- # ...if checkpointing is/isn't enabled
946
-
890
+ pool = None
947
891
  try:
948
- pool.close()
949
- except Exception as e:
950
- print('Warning: error closing multiprocessing pool:\n{}'.format(str(e)))
951
-
892
+ pool = workerpool(n_cores)
893
+
894
+ if checkpoint_path is not None:
895
+
896
+ # Multiprocessing and checkpointing are both enabled at this point
897
+
898
+ checkpoint_queue = Manager().Queue()
899
+
900
+ # Pass the "results" array (which may already contain images loaded from an existing
901
+ # checkpoint) to the checkpoint queue handler function, which will append results to
902
+ # the list as they become available.
903
+ checkpoint_thread = Thread(target=_checkpoint_queue_handler,
904
+ args=(checkpoint_path, checkpoint_frequency,
905
+ checkpoint_queue, results), daemon=True)
906
+ checkpoint_thread.start()
907
+
908
+ pool.map(partial(process_images,
909
+ detector=detector,
910
+ confidence_threshold=confidence_threshold,
911
+ use_image_queue=False,
912
+ quiet=quiet,
913
+ image_size=image_size,
914
+ checkpoint_queue=checkpoint_queue,
915
+ include_image_size=include_image_size,
916
+ include_image_timestamp=include_image_timestamp,
917
+ include_exif_data=include_exif_data,
918
+ augment=augment,
919
+ detector_options=detector_options),
920
+ image_batches)
921
+
922
+ checkpoint_queue.put(None)
923
+
924
+ else:
925
+
926
+ # Multprocessing is enabled, but checkpointing is not
927
+
928
+ new_results = pool.map(partial(process_images,
929
+ detector=detector,
930
+ confidence_threshold=confidence_threshold,
931
+ use_image_queue=False,
932
+ quiet=quiet,
933
+ checkpoint_queue=None,
934
+ image_size=image_size,
935
+ include_image_size=include_image_size,
936
+ include_image_timestamp=include_image_timestamp,
937
+ include_exif_data=include_exif_data,
938
+ augment=augment,
939
+ detector_options=detector_options),
940
+ image_batches)
941
+
942
+ new_results = list(itertools.chain.from_iterable(new_results))
943
+
944
+ # Append the results we just computed to "results", which is *usually* empty, but will
945
+ # be non-empty if we resumed from a checkpoint
946
+ results += new_results
947
+
948
+ # ...if checkpointing is/isn't enabled
949
+
950
+ finally:
951
+ if pool is not None:
952
+ pool.close()
953
+ pool.join()
954
+ print("Pool closed and joined for multi-core inference")
955
+
952
956
  # ...if we're running (1) with image queue, (2) on one core, or (3) on multiple cores
953
-
957
+
954
958
  # 'results' may have been modified in place, but we also return it for
955
959
  # backwards-compatibility.
956
960
  return results
@@ -963,21 +967,21 @@ def _checkpoint_queue_handler(checkpoint_path, checkpoint_frequency, checkpoint_
963
967
  Thread function to accumulate results and write checkpoints when checkpointing and
964
968
  multiprocessing are both enabled.
965
969
  """
966
-
970
+
967
971
  result_count = 0
968
972
  while True:
969
- result = checkpoint_queue.get()
970
- if result is None:
971
- break
972
-
973
+ result = checkpoint_queue.get()
974
+ if result is None:
975
+ break
976
+
973
977
  result_count +=1
974
978
  results.append(result)
975
979
 
976
980
  if (checkpoint_frequency != -1) and (result_count % checkpoint_frequency == 0):
977
-
981
+
978
982
  print('Writing a new checkpoint after having processed {} images since '
979
983
  'last restart'.format(result_count))
980
-
984
+
981
985
  _write_checkpoint(checkpoint_path, results)
982
986
 
983
987
 
@@ -985,20 +989,19 @@ def _write_checkpoint(checkpoint_path, results):
985
989
  """
986
990
  Writes the 'images' field in the dict 'results' to a json checkpoint file.
987
991
  """
988
-
989
- assert checkpoint_path is not None
990
-
992
+
993
+ assert checkpoint_path is not None
994
+
991
995
  # Back up any previous checkpoints, to protect against crashes while we're writing
992
996
  # the checkpoint file.
993
997
  checkpoint_tmp_path = None
994
998
  if os.path.isfile(checkpoint_path):
995
999
  checkpoint_tmp_path = checkpoint_path + '_tmp'
996
1000
  shutil.copyfile(checkpoint_path,checkpoint_tmp_path)
997
-
1001
+
998
1002
  # Write the new checkpoint
999
- with open(checkpoint_path, 'w') as f:
1000
- json.dump({'images': results}, f, indent=1, default=str)
1001
-
1003
+ ct_utils.write_json(checkpoint_path, {'images': results}, force_str=True)
1004
+
1002
1005
  # Remove the backup checkpoint if it exists
1003
1006
  if checkpoint_tmp_path is not None:
1004
1007
  os.remove(checkpoint_tmp_path)
@@ -1007,33 +1010,33 @@ def _write_checkpoint(checkpoint_path, results):
1007
1010
  def get_image_datetime(image):
1008
1011
  """
1009
1012
  Reads EXIF datetime from a PIL Image object.
1010
-
1013
+
1011
1014
  Args:
1012
1015
  image (Image): the PIL Image object from which we should read datetime information
1013
-
1016
+
1014
1017
  Returns:
1015
1018
  str: the EXIF datetime from [image] (a PIL Image object), if available, as a string;
1016
1019
  returns None if EXIF datetime is not available.
1017
1020
  """
1018
-
1021
+
1019
1022
  exif_tags = read_exif.read_pil_exif(image,exif_options)
1020
-
1023
+
1021
1024
  try:
1022
1025
  datetime_str = exif_tags['DateTimeOriginal']
1023
1026
  _ = time.strptime(datetime_str, '%Y:%m:%d %H:%M:%S')
1024
1027
  return datetime_str
1025
1028
 
1026
1029
  except Exception:
1027
- return None
1030
+ return None
1028
1031
 
1029
1032
 
1030
- def write_results_to_file(results,
1031
- output_file,
1032
- relative_path_base=None,
1033
- detector_file=None,
1034
- info=None,
1033
+ def write_results_to_file(results,
1034
+ output_file,
1035
+ relative_path_base=None,
1036
+ detector_file=None,
1037
+ info=None,
1035
1038
  include_max_conf=False,
1036
- custom_metadata=None,
1039
+ custom_metadata=None,
1037
1040
  force_forward_slashes=True):
1038
1041
  """
1039
1042
  Writes list of detection results to JSON output file. Format matches:
@@ -1055,11 +1058,11 @@ def write_results_to_file(results,
1055
1058
  a dictionary, but no type/format checks are performed
1056
1059
  force_forward_slashes (bool, optional): convert all slashes in filenames within [results] to
1057
1060
  forward slashes
1058
-
1061
+
1059
1062
  Returns:
1060
1063
  dict: the MD-formatted dictionary that was written to [output_file]
1061
1064
  """
1062
-
1065
+
1063
1066
  if relative_path_base is not None:
1064
1067
  results_relative = []
1065
1068
  for r in results:
@@ -1075,68 +1078,67 @@ def write_results_to_file(results,
1075
1078
  r_converted['file'] = r_converted['file'].replace('\\','/')
1076
1079
  results_converted.append(r_converted)
1077
1080
  results = results_converted
1078
-
1081
+
1079
1082
  # The typical case: we need to build the 'info' struct
1080
1083
  if info is None:
1081
-
1082
- info = {
1084
+
1085
+ info = {
1083
1086
  'detection_completion_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
1084
- 'format_version': '1.4'
1087
+ 'format_version': '1.4'
1085
1088
  }
1086
-
1089
+
1087
1090
  if detector_file is not None:
1088
1091
  detector_filename = os.path.basename(detector_file)
1089
1092
  detector_version = get_detector_version_from_filename(detector_filename)
1090
1093
  detector_metadata = get_detector_metadata_from_version_string(detector_version)
1091
- info['detector'] = detector_filename
1094
+ info['detector'] = detector_filename
1092
1095
  info['detector_metadata'] = detector_metadata
1093
1096
  else:
1094
1097
  info['detector'] = 'unknown'
1095
1098
  info['detector_metadata'] = get_detector_metadata_from_version_string('unknown')
1096
-
1099
+
1097
1100
  # If the caller supplied the entire "info" struct
1098
1101
  else:
1099
-
1100
- if detector_file is not None:
1102
+
1103
+ if detector_file is not None:
1101
1104
  print('Warning (write_results_to_file): info struct and detector file ' + \
1102
1105
  'supplied, ignoring detector file')
1103
1106
 
1104
1107
  if custom_metadata is not None:
1105
1108
  info['custom_metadata'] = custom_metadata
1106
-
1109
+
1107
1110
  # The 'max_detection_conf' field used to be included by default, and it caused all kinds
1108
1111
  # of headaches, so it's no longer included unless the user explicitly requests it.
1109
1112
  if not include_max_conf:
1110
1113
  for im in results:
1111
1114
  if 'max_detection_conf' in im:
1112
1115
  del im['max_detection_conf']
1113
-
1116
+
1114
1117
  # Sort results by filename; not required by the format, but convenient for consistency
1115
1118
  results = sort_list_of_dicts_by_key(results,'file')
1116
-
1119
+
1117
1120
  # Sort detections in descending order by confidence; not required by the format, but
1118
1121
  # convenient for consistency
1119
1122
  for r in results:
1120
1123
  if ('detections' in r) and (r['detections'] is not None):
1121
1124
  r['detections'] = sort_list_of_dicts_by_key(r['detections'], 'conf', reverse=True)
1122
-
1125
+
1123
1126
  final_output = {
1124
1127
  'images': results,
1125
1128
  'detection_categories': run_detector.DEFAULT_DETECTOR_LABEL_MAP,
1126
1129
  'info': info
1127
1130
  }
1128
-
1131
+
1129
1132
  # Create the folder where the output file belongs; this will fail if
1130
1133
  # this is a relative path with no folder component
1131
1134
  try:
1132
1135
  os.makedirs(os.path.dirname(output_file),exist_ok=True)
1133
1136
  except Exception:
1134
1137
  pass
1135
-
1136
- with open(output_file, 'w') as f:
1137
- json.dump(final_output, f, indent=1, default=str)
1138
+
1139
+ ct_utils.write_json(output_file, final_output, force_str=True)
1138
1140
  print('Output file saved at {}'.format(output_file))
1139
-
1141
+
1140
1142
  return final_output
1141
1143
 
1142
1144
  # ...def write_results_to_file(...)
@@ -1145,15 +1147,15 @@ def write_results_to_file(results,
1145
1147
  #%% Interactive driver
1146
1148
 
1147
1149
  if False:
1148
-
1150
+
1149
1151
  pass
1150
1152
 
1151
1153
  #%%
1152
-
1154
+
1153
1155
  model_file = 'MDV5A'
1154
1156
  image_dir = r'g:\camera_traps\camera_trap_images'
1155
1157
  output_file = r'g:\temp\md-test.json'
1156
-
1158
+
1157
1159
  recursive = True
1158
1160
  output_relative_filenames = True
1159
1161
  include_max_conf = False
@@ -1161,7 +1163,7 @@ if False:
1161
1163
  image_size = None
1162
1164
  use_image_queue = False
1163
1165
  confidence_threshold = 0.0001
1164
- checkpoint_frequency = 5
1166
+ checkpoint_frequency = 5
1165
1167
  checkpoint_path = None
1166
1168
  resume_from_checkpoint = 'auto'
1167
1169
  allow_checkpoint_overwrite = False
@@ -1171,11 +1173,11 @@ if False:
1171
1173
  include_image_timestamp = True
1172
1174
  include_exif_data = True
1173
1175
  overwrite_handling = None
1174
-
1176
+
1175
1177
  # Generate a command line
1176
1178
  cmd = 'python run_detector_batch.py "{}" "{}" "{}"'.format(
1177
1179
  model_file,image_dir,output_file)
1178
-
1180
+
1179
1181
  if recursive:
1180
1182
  cmd += ' --recursive'
1181
1183
  if output_relative_filenames:
@@ -1210,18 +1212,18 @@ if False:
1210
1212
  cmd += ' --include_exif_data'
1211
1213
  if overwrite_handling is not None:
1212
1214
  cmd += ' --overwrite_handling {}'.format(overwrite_handling)
1213
-
1215
+
1214
1216
  print(cmd)
1215
1217
  import clipboard; clipboard.copy(cmd)
1216
-
1217
-
1218
+
1219
+
1218
1220
  #%% Run inference interactively
1219
-
1220
- image_file_names = path_utils.find_images(image_dir, recursive=False)
1221
+
1222
+ image_file_names = path_utils.find_images(image_dir, recursive=False)
1221
1223
  results = None
1222
-
1224
+
1223
1225
  start_time = time.time()
1224
-
1226
+
1225
1227
  results = load_and_run_detector_batch(model_file=model_file,
1226
1228
  image_file_names=image_file_names,
1227
1229
  checkpoint_path=checkpoint_path,
@@ -1232,21 +1234,22 @@ if False:
1232
1234
  use_image_queue=use_image_queue,
1233
1235
  quiet=quiet,
1234
1236
  image_size=image_size)
1235
-
1237
+
1236
1238
  elapsed = time.time() - start_time
1237
-
1239
+
1238
1240
  print('Finished inference in {}'.format(humanfriendly.format_timespan(elapsed)))
1239
1241
 
1240
-
1242
+
1241
1243
  #%% Command-line driver
1242
1244
 
1243
- def main():
1244
-
1245
+ def main(): # noqa
1246
+
1245
1247
  parser = argparse.ArgumentParser(
1246
1248
  description='Module to run a TF/PT animal detection model on lots of images')
1247
1249
  parser.add_argument(
1248
1250
  'detector_file',
1249
- help='Path to detector model file (.pb or .pt). Can also be the strings "MDV4", "MDV5A", or "MDV5B" to request automatic download.')
1251
+ help='Path to detector model file (.pb or .pt). Can also be the strings "MDV4", ' + \
1252
+ '"MDV5A", or "MDV5B" to request automatic download.')
1250
1253
  parser.add_argument(
1251
1254
  'image_file',
1252
1255
  help=\
@@ -1278,7 +1281,7 @@ def main():
1278
1281
  '--image_size',
1279
1282
  type=int,
1280
1283
  default=None,
1281
- help=('Force image resizing to a specific integer size on the long axis (not recommended to change this)'))
1284
+ help=('Force image resizing to a specific integer size on the long axis (not recommended to change this)'))
1282
1285
  parser.add_argument(
1283
1286
  '--augment',
1284
1287
  action='store_true',
@@ -1315,7 +1318,7 @@ def main():
1315
1318
  type=str,
1316
1319
  default=None,
1317
1320
  help='File name to which checkpoints will be written if checkpoint_frequency is > 0, ' + \
1318
- 'defaults to md_checkpoint_[date].json in the same folder as the output file')
1321
+ 'defaults to md_checkpoint_[date].json in the same folder as the output file')
1319
1322
  parser.add_argument(
1320
1323
  '--resume_from_checkpoint',
1321
1324
  type=str,
@@ -1366,7 +1369,7 @@ def main():
1366
1369
  type=str,
1367
1370
  default='overwrite',
1368
1371
  help='What should we do if the output file exists? overwrite/skip/error (default overwrite)'
1369
- )
1372
+ )
1370
1373
  parser.add_argument(
1371
1374
  '--force_model_download',
1372
1375
  action='store_true',
@@ -1386,28 +1389,28 @@ def main():
1386
1389
  metavar='KEY=VALUE',
1387
1390
  default='',
1388
1391
  help='Detector-specific options, as a space-separated list of key-value pairs')
1389
-
1392
+
1390
1393
  if len(sys.argv[1:]) == 0:
1391
1394
  parser.print_help()
1392
1395
  parser.exit()
1393
1396
 
1394
1397
  args = parser.parse_args()
1395
-
1398
+
1396
1399
  global verbose
1397
1400
  global use_threads_for_queue
1398
-
1401
+
1399
1402
  if args.verbose:
1400
1403
  verbose = True
1401
1404
  if args.use_threads_for_queue:
1402
1405
  use_threads_for_queue = True
1403
-
1406
+
1404
1407
  detector_options = parse_kvp_list(args.detector_options)
1405
-
1406
- # If the specified detector file is really the name of a known model, find
1408
+
1409
+ # If the specified detector file is really the name of a known model, find
1407
1410
  # (and possibly download) that model
1408
- args.detector_file = try_download_known_detector(args.detector_file,
1411
+ args.detector_file = try_download_known_detector(args.detector_file,
1409
1412
  force_download=args.force_model_download)
1410
-
1413
+
1411
1414
  assert os.path.exists(args.detector_file), \
1412
1415
  'detector file {} does not exist'.format(args.detector_file)
1413
1416
  assert 0.0 <= args.threshold <= 1.0, 'Confidence threshold needs to be between 0 and 1'
@@ -1438,12 +1441,12 @@ def main():
1438
1441
 
1439
1442
  if len(output_dir) > 0:
1440
1443
  os.makedirs(output_dir,exist_ok=True)
1441
-
1444
+
1442
1445
  assert not os.path.isdir(args.output_file), 'Specified output file is a directory'
1443
-
1446
+
1444
1447
  if args.class_mapping_filename is not None:
1445
1448
  _load_custom_class_mapping(args.class_mapping_filename)
1446
-
1449
+
1447
1450
  # Load the checkpoint if available
1448
1451
  #
1449
1452
  # File paths in the checkpoint are always absolute paths; conversion to relative paths
@@ -1462,7 +1465,7 @@ def main():
1462
1465
  len(checkpoint_files),output_dir))
1463
1466
  checkpoint_files = sorted(checkpoint_files)
1464
1467
  checkpoint_file_relative = checkpoint_files[-1]
1465
- checkpoint_file = os.path.join(output_dir,checkpoint_file_relative)
1468
+ checkpoint_file = os.path.join(output_dir,checkpoint_file_relative)
1466
1469
  else:
1467
1470
  checkpoint_file = args.resume_from_checkpoint
1468
1471
  assert os.path.exists(checkpoint_file), \
@@ -1482,7 +1485,7 @@ def main():
1482
1485
  if os.path.isdir(args.image_file):
1483
1486
  image_file_names = path_utils.find_images(args.image_file, args.recursive)
1484
1487
  if len(image_file_names) > 0:
1485
- print('{} image files found in the input directory'.format(len(image_file_names)))
1488
+ print('{} image files found in the input directory'.format(len(image_file_names)))
1486
1489
  else:
1487
1490
  if (args.recursive):
1488
1491
  print('No image files found in directory {}, exiting'.format(args.image_file))
@@ -1491,14 +1494,14 @@ def main():
1491
1494
  '--recursive?'.format(
1492
1495
  args.image_file))
1493
1496
  return
1494
-
1497
+
1495
1498
  # A json list of image paths
1496
- elif os.path.isfile(args.image_file) and args.image_file.endswith('.json'):
1499
+ elif os.path.isfile(args.image_file) and args.image_file.endswith('.json'):
1497
1500
  with open(args.image_file) as f:
1498
1501
  image_file_names = json.load(f)
1499
1502
  print('Loaded {} image filenames from .json list file {}'.format(
1500
1503
  len(image_file_names),args.image_file))
1501
-
1504
+
1502
1505
  # A text list of image paths
1503
1506
  elif os.path.isfile(args.image_file) and args.image_file.endswith('.txt'):
1504
1507
  with open(args.image_file) as f:
@@ -1506,51 +1509,51 @@ def main():
1506
1509
  image_file_names = [fn.strip() for fn in image_file_names if len(fn.strip()) > 0]
1507
1510
  print('Loaded {} image filenames from .txt list file {}'.format(
1508
1511
  len(image_file_names),args.image_file))
1509
-
1512
+
1510
1513
  # A single image file
1511
1514
  elif os.path.isfile(args.image_file) and path_utils.is_image_file(args.image_file):
1512
1515
  image_file_names = [args.image_file]
1513
1516
  print('Processing image {}'.format(args.image_file))
1514
-
1515
- else:
1517
+
1518
+ else:
1516
1519
  raise ValueError('image_file specified is not a directory, a json list, or an image file, '
1517
1520
  '(or does not have recognizable extensions).')
1518
1521
 
1519
- # At this point, regardless of how they were specified, [image_file_names] is a list of
1522
+ # At this point, regardless of how they were specified, [image_file_names] is a list of
1520
1523
  # absolute image paths.
1521
1524
  assert len(image_file_names) > 0, 'Specified image_file does not point to valid image files'
1522
-
1525
+
1523
1526
  # Convert to forward slashes to facilitate comparison with previous results
1524
1527
  image_file_names = [fn.replace('\\','/') for fn in image_file_names]
1525
-
1528
+
1526
1529
  # We can head off many problems related to incorrect command line formulation if we confirm
1527
- # that one image exists before proceeding. The use of the first image for this test is
1530
+ # that one image exists before proceeding. The use of the first image for this test is
1528
1531
  # arbitrary.
1529
1532
  assert os.path.exists(image_file_names[0]), \
1530
1533
  'The first image to be processed does not exist at {}'.format(image_file_names[0])
1531
1534
 
1532
1535
  # Possibly load results from a previous pass
1533
1536
  previous_results = None
1534
-
1537
+
1535
1538
  if args.previous_results_file is not None:
1536
-
1539
+
1537
1540
  assert os.path.isfile(args.previous_results_file), \
1538
1541
  'Could not find previous results file {}'.format(args.previous_results_file)
1539
1542
  with open(args.previous_results_file,'r') as f:
1540
1543
  previous_results = json.load(f)
1541
-
1544
+
1542
1545
  assert previous_results['detection_categories'] == run_detector.DEFAULT_DETECTOR_LABEL_MAP, \
1543
1546
  "Can't merge previous results when those results use a different set of detection categories"
1544
-
1547
+
1545
1548
  print('Loaded previous results for {} images from {}'.format(
1546
1549
  len(previous_results['images']), args.previous_results_file))
1547
-
1548
- # Convert previous result filenames to absolute paths if necessary
1550
+
1551
+ # Convert previous result filenames to absolute paths if necessary
1549
1552
  #
1550
- # We asserted above to make sure that we are using relative paths and processing a
1553
+ # We asserted above to make sure that we are using relative paths and processing a
1551
1554
  # folder, but just to be super-clear...
1552
1555
  assert os.path.isdir(args.image_file)
1553
-
1556
+
1554
1557
  previous_image_files_set = set()
1555
1558
  for im in previous_results['images']:
1556
1559
  assert not os.path.isabs(im['file']), \
@@ -1558,54 +1561,53 @@ def main():
1558
1561
  fn_abs = os.path.join(args.image_file,im['file']).replace('\\','/')
1559
1562
  # Absolute paths are expected at the final output stage below
1560
1563
  im['file'] = fn_abs
1561
- previous_image_files_set.add(fn_abs)
1562
-
1564
+ previous_image_files_set.add(fn_abs)
1565
+
1563
1566
  image_file_names_to_keep = []
1564
1567
  for fn_abs in image_file_names:
1565
1568
  if fn_abs not in previous_image_files_set:
1566
1569
  image_file_names_to_keep.append(fn_abs)
1567
-
1570
+
1568
1571
  print('Based on previous results file, processing {} of {} images'.format(
1569
1572
  len(image_file_names_to_keep), len(image_file_names)))
1570
-
1573
+
1571
1574
  image_file_names = image_file_names_to_keep
1572
-
1575
+
1573
1576
  # ...if we're handling previous results
1574
-
1577
+
1575
1578
  # Test that we can write to the output_file's dir if checkpointing requested
1576
1579
  if args.checkpoint_frequency != -1:
1577
-
1580
+
1578
1581
  if args.checkpoint_path is not None:
1579
1582
  checkpoint_path = args.checkpoint_path
1580
1583
  else:
1581
1584
  checkpoint_path = os.path.join(output_dir,
1582
1585
  'md_checkpoint_{}.json'.format(
1583
1586
  datetime.now().strftime("%Y%m%d%H%M%S")))
1584
-
1587
+
1585
1588
  # Don't overwrite existing checkpoint files, this is a sure-fire way to eventually
1586
1589
  # erase someone's checkpoint.
1587
1590
  if (checkpoint_path is not None) and (not args.allow_checkpoint_overwrite) \
1588
1591
  and (args.resume_from_checkpoint is None):
1589
-
1592
+
1590
1593
  assert not os.path.isfile(checkpoint_path), \
1591
1594
  f'Checkpoint path {checkpoint_path} already exists, delete or move it before ' + \
1592
1595
  're-using the same checkpoint path, or specify --allow_checkpoint_overwrite'
1593
1596
 
1594
-
1597
+
1595
1598
  # Confirm that we can write to the checkpoint path; this avoids issues where
1596
1599
  # we crash after several thousand images.
1597
1600
  #
1598
- # But actually, commenting this out for now... the scenario where we are resuming from a
1601
+ # But actually, commenting this out for now... the scenario where we are resuming from a
1599
1602
  # checkpoint, then immediately overwrite that checkpoint with empty data is higher-risk
1600
1603
  # than the annoyance of crashing a few minutes after starting a job.
1601
1604
  if False:
1602
- with open(checkpoint_path, 'w') as f:
1603
- json.dump({'images': []}, f)
1604
-
1605
+ ct_utils.write_json(checkpoint_path, {'images': []}, indent=None)
1606
+
1605
1607
  print('The checkpoint file will be written to {}'.format(checkpoint_path))
1606
-
1608
+
1607
1609
  else:
1608
-
1610
+
1609
1611
  if args.checkpoint_path is not None:
1610
1612
  print('Warning: checkpointing disabled because checkpoint_frequency is -1, ' + \
1611
1613
  'but a checkpoint path was specified')
@@ -1640,23 +1642,23 @@ def main():
1640
1642
  len(results),humanfriendly.format_timespan(elapsed),images_per_second))
1641
1643
 
1642
1644
  relative_path_base = None
1643
-
1644
- # We asserted above to make sure that if output_relative_filenames is set,
1645
+
1646
+ # We asserted above to make sure that if output_relative_filenames is set,
1645
1647
  # args.image_file is a folder, but we'll double-check for clarity.
1646
1648
  if args.output_relative_filenames:
1647
1649
  assert os.path.isdir(args.image_file)
1648
1650
  relative_path_base = args.image_file
1649
-
1651
+
1650
1652
  # Merge results from a previous file if necessary
1651
1653
  if previous_results is not None:
1652
1654
  previous_filenames_set = set([im['file'] for im in previous_results['images']])
1653
1655
  new_filenames_set = set([im['file'] for im in results])
1654
1656
  assert len(previous_filenames_set.intersection(new_filenames_set)) == 0, \
1655
1657
  'Previous results handling error: redundant image filenames'
1656
- results.extend(previous_results['images'])
1657
-
1658
- write_results_to_file(results,
1659
- args.output_file,
1658
+ results.extend(previous_results['images'])
1659
+
1660
+ write_results_to_file(results,
1661
+ args.output_file,
1660
1662
  relative_path_base=relative_path_base,
1661
1663
  detector_file=args.detector_file,
1662
1664
  include_max_conf=args.include_max_conf)