megadetector 5.0.28__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 +231 -224
  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 +340 -337
  65. megadetector/detection/pytorch_detector.py +304 -262
  66. megadetector/detection/run_detector.py +177 -164
  67. megadetector/detection/run_detector_batch.py +364 -363
  68. megadetector/detection/run_inference_with_yolov5_val.py +328 -325
  69. megadetector/detection/run_tiled_inference.py +256 -249
  70. megadetector/detection/tf_detector.py +24 -24
  71. megadetector/detection/video_utils.py +290 -282
  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 +415 -415
  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 +219 -146
  79. megadetector/postprocessing/detector_calibration.py +173 -168
  80. megadetector/postprocessing/generate_csv_report.py +508 -499
  81. megadetector/postprocessing/load_api_results.py +23 -20
  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 +313 -298
  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 -66
  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 +10 -10
  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 +1018 -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 +1457 -398
  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 +61 -61
  115. megadetector/utils/string_utils.py +147 -26
  116. megadetector/utils/url_utils.py +463 -173
  117. megadetector/utils/wi_utils.py +2629 -2526
  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 +401 -397
  122. megadetector/visualization/visualize_db.py +197 -190
  123. megadetector/visualization/visualize_detector_output.py +79 -73
  124. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/METADATA +135 -132
  125. megadetector-5.0.29.dist-info/RECORD +163 -0
  126. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/WHEEL +1 -1
  127. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/licenses/LICENSE +0 -0
  128. {megadetector-5.0.28.dist-info → megadetector-5.0.29.dist-info}/top_level.txt +0 -0
  129. megadetector/data_management/importers/add_nacti_sizes.py +0 -52
  130. megadetector/data_management/importers/add_timestamps_to_icct.py +0 -79
  131. megadetector/data_management/importers/animl_results_to_md_results.py +0 -158
  132. megadetector/data_management/importers/auckland_doc_test_to_json.py +0 -373
  133. megadetector/data_management/importers/auckland_doc_to_json.py +0 -201
  134. megadetector/data_management/importers/awc_to_json.py +0 -191
  135. megadetector/data_management/importers/bellevue_to_json.py +0 -272
  136. megadetector/data_management/importers/cacophony-thermal-importer.py +0 -793
  137. megadetector/data_management/importers/carrizo_shrubfree_2018.py +0 -269
  138. megadetector/data_management/importers/carrizo_trail_cam_2017.py +0 -289
  139. megadetector/data_management/importers/cct_field_adjustments.py +0 -58
  140. megadetector/data_management/importers/channel_islands_to_cct.py +0 -913
  141. megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +0 -180
  142. megadetector/data_management/importers/eMammal/eMammal_helpers.py +0 -249
  143. megadetector/data_management/importers/eMammal/make_eMammal_json.py +0 -223
  144. megadetector/data_management/importers/ena24_to_json.py +0 -276
  145. megadetector/data_management/importers/filenames_to_json.py +0 -386
  146. megadetector/data_management/importers/helena_to_cct.py +0 -283
  147. megadetector/data_management/importers/idaho-camera-traps.py +0 -1407
  148. megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +0 -294
  149. megadetector/data_management/importers/import_desert_lion_conservation_camera_traps.py +0 -387
  150. megadetector/data_management/importers/jb_csv_to_json.py +0 -150
  151. megadetector/data_management/importers/mcgill_to_json.py +0 -250
  152. megadetector/data_management/importers/missouri_to_json.py +0 -490
  153. megadetector/data_management/importers/nacti_fieldname_adjustments.py +0 -79
  154. megadetector/data_management/importers/noaa_seals_2019.py +0 -181
  155. megadetector/data_management/importers/osu-small-animals-to-json.py +0 -364
  156. megadetector/data_management/importers/pc_to_json.py +0 -365
  157. megadetector/data_management/importers/plot_wni_giraffes.py +0 -123
  158. megadetector/data_management/importers/prepare_zsl_imerit.py +0 -131
  159. megadetector/data_management/importers/raic_csv_to_md_results.py +0 -416
  160. megadetector/data_management/importers/rspb_to_json.py +0 -356
  161. megadetector/data_management/importers/save_the_elephants_survey_A.py +0 -320
  162. megadetector/data_management/importers/save_the_elephants_survey_B.py +0 -329
  163. megadetector/data_management/importers/snapshot_safari_importer.py +0 -758
  164. megadetector/data_management/importers/snapshot_serengeti_lila.py +0 -1067
  165. megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +0 -150
  166. megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +0 -153
  167. megadetector/data_management/importers/sulross_get_exif.py +0 -65
  168. megadetector/data_management/importers/timelapse_csv_set_to_json.py +0 -490
  169. megadetector/data_management/importers/ubc_to_json.py +0 -399
  170. megadetector/data_management/importers/umn_to_json.py +0 -507
  171. megadetector/data_management/importers/wellington_to_json.py +0 -263
  172. megadetector/data_management/importers/wi_to_json.py +0 -442
  173. megadetector/data_management/importers/zamba_results_to_md_results.py +0 -180
  174. megadetector/data_management/lila/add_locations_to_island_camera_traps.py +0 -101
  175. megadetector/data_management/lila/add_locations_to_nacti.py +0 -151
  176. megadetector-5.0.28.dist-info/RECORD +0 -209
@@ -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
- if preprocessor is not None:
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
+
164
165
  except Exception as e:
165
166
  print('Producer process: image {} cannot be loaded:\n{}'.format(im_file,str(e)))
166
- image = run_detector.FAILURE_IMAGE_OPEN
167
-
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,36 +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:
357
358
  print('Enabling image queue preprocessing')
358
359
  preprocessor = model_file
359
-
360
+
360
361
  n_total_images = len(image_files)
361
-
362
+
362
363
  chunks = split_list_into_n_chunks(image_files, loader_workers, chunk_strategy='greedy')
363
364
  for i_chunk,chunk in enumerate(chunks):
364
365
  if use_threads_for_queue:
@@ -379,11 +380,11 @@ def run_detector_with_image_queue(image_files,
379
380
  image_size,
380
381
  augment))
381
382
  producers.append(producer)
382
-
383
+
383
384
  for producer in producers:
384
385
  producer.daemon = False
385
386
  producer.start()
386
-
387
+
387
388
  if run_separate_consumer_process:
388
389
  if use_threads_for_queue:
389
390
  consumer = Thread(target=_consumer_func,args=(q,
@@ -393,7 +394,7 @@ def run_detector_with_image_queue(image_files,
393
394
  loader_workers,
394
395
  image_size,
395
396
  include_image_size,
396
- include_image_timestamp,
397
+ include_image_timestamp,
397
398
  include_exif_data,
398
399
  augment,
399
400
  detector_options,
@@ -407,7 +408,7 @@ def run_detector_with_image_queue(image_files,
407
408
  loader_workers,
408
409
  image_size,
409
410
  include_image_size,
410
- include_image_timestamp,
411
+ include_image_timestamp,
411
412
  include_exif_data,
412
413
  augment,
413
414
  detector_options,
@@ -423,7 +424,7 @@ def run_detector_with_image_queue(image_files,
423
424
  loader_workers,
424
425
  image_size,
425
426
  include_image_size,
426
- include_image_timestamp,
427
+ include_image_timestamp,
427
428
  include_exif_data,
428
429
  augment,
429
430
  detector_options,
@@ -434,21 +435,21 @@ def run_detector_with_image_queue(image_files,
434
435
  producer.join()
435
436
  if verbose:
436
437
  print('Producer {} finished'.format(i_producer))
437
-
438
+
438
439
  if verbose:
439
440
  print('All producers finished')
440
-
441
+
441
442
  if run_separate_consumer_process:
442
443
  consumer.join()
443
444
  if verbose:
444
445
  print('Consumer loop finished')
445
-
446
+
446
447
  q.join()
447
448
  if verbose:
448
449
  print('Queue joined')
449
450
 
450
451
  results = return_queue.get()
451
-
452
+
452
453
  return results
453
454
 
454
455
  # ...def run_detector_with_image_queue(...)
@@ -459,29 +460,29 @@ def run_detector_with_image_queue(image_files,
459
460
  def _chunks_by_number_of_chunks(ls, n):
460
461
  """
461
462
  Splits a list into n even chunks.
462
-
463
+
463
464
  External callers should use ct_utils.split_list_into_n_chunks().
464
465
 
465
466
  Args:
466
467
  ls (list): list to break up into chunks
467
468
  n (int): number of chunks
468
469
  """
469
-
470
+
470
471
  for i in range(0, n):
471
472
  yield ls[i::n]
472
473
 
473
474
 
474
475
  #%% Image processing functions
475
476
 
476
- def process_images(im_files,
477
- detector,
478
- confidence_threshold,
479
- use_image_queue=False,
480
- quiet=False,
481
- image_size=None,
482
- checkpoint_queue=None,
483
- include_image_size=False,
484
- 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,
485
486
  include_exif_data=False,
486
487
  augment=False,
487
488
  detector_options=None,
@@ -489,9 +490,9 @@ def process_images(im_files,
489
490
  preprocess_on_image_queue=default_preprocess_on_image_queue):
490
491
  """
491
492
  Runs a detector (typically MegaDetector) over a list of image files on a single thread.
492
-
493
+
493
494
  Args:
494
- im_files (list: paths to image files
495
+ im_files (list: paths to image files
495
496
  detector (str or detector object): loaded model or str; if this is a string, it can be a
496
497
  path to a .pb/.pt model file or a known model identifier (e.g. "MDV5A")
497
498
  confidence_threshold (float): only detections above this threshold are returned
@@ -505,7 +506,7 @@ def process_images(im_files,
505
506
  include_image_timestamp (bool, optional): should we include image timestamps in the output for each image?
506
507
  include_exif_data (bool, optional): should we include EXIF data in the output for each image?
507
508
  augment (bool, optional): enable image augmentation
508
- detector_options (dict, optional): key/value pairs that are interpreted differently
509
+ detector_options (dict, optional): key/value pairs that are interpreted differently
509
510
  by different detectors
510
511
  loader_workers (int, optional): number of loaders to use (only relevant when using image queue)
511
512
 
@@ -513,60 +514,60 @@ def process_images(im_files,
513
514
  list: list of dicts, in which each dict represents detections on one image,
514
515
  see the 'images' key in https://github.com/agentmorris/MegaDetector/tree/main/megadetector/api/batch_processing#batch-processing-api-output-format
515
516
  """
516
-
517
+
517
518
  if isinstance(detector, str):
518
-
519
+
519
520
  start_time = time.time()
520
521
  detector = load_detector(detector,detector_options=detector_options,verbose=verbose)
521
522
  elapsed = time.time() - start_time
522
523
  print('Loaded model (batch level) in {}'.format(humanfriendly.format_timespan(elapsed)))
523
524
 
524
525
  if use_image_queue:
525
-
526
- run_detector_with_image_queue(im_files,
527
- detector,
528
- confidence_threshold,
529
- quiet=quiet,
526
+
527
+ run_detector_with_image_queue(im_files,
528
+ detector,
529
+ confidence_threshold,
530
+ quiet=quiet,
530
531
  image_size=image_size,
531
- include_image_size=include_image_size,
532
+ include_image_size=include_image_size,
532
533
  include_image_timestamp=include_image_timestamp,
533
534
  include_exif_data=include_exif_data,
534
535
  augment=augment,
535
536
  detector_options=detector_options,
536
537
  loader_workers=loader_workers,
537
538
  preprocess_on_image_queue=preprocess_on_image_queue)
538
-
539
- else:
540
-
539
+
540
+ else:
541
+
541
542
  results = []
542
543
  for im_file in im_files:
543
- result = process_image(im_file,
544
- detector,
544
+ result = process_image(im_file,
545
+ detector,
545
546
  confidence_threshold,
546
- quiet=quiet,
547
- image_size=image_size,
548
- include_image_size=include_image_size,
547
+ quiet=quiet,
548
+ image_size=image_size,
549
+ include_image_size=include_image_size,
549
550
  include_image_timestamp=include_image_timestamp,
550
551
  include_exif_data=include_exif_data,
551
552
  augment=augment)
552
553
 
553
554
  if checkpoint_queue is not None:
554
555
  checkpoint_queue.put(result)
555
- results.append(result)
556
-
556
+ results.append(result)
557
+
557
558
  return results
558
559
 
559
560
  # ...def process_images(...)
560
561
 
561
562
 
562
- def process_image(im_file,
563
- detector,
564
- confidence_threshold,
565
- image=None,
566
- quiet=False,
567
- image_size=None,
563
+ def process_image(im_file,
564
+ detector,
565
+ confidence_threshold,
566
+ image=None,
567
+ quiet=False,
568
+ image_size=None,
568
569
  include_image_size=False,
569
- include_image_timestamp=False,
570
+ include_image_timestamp=False,
570
571
  include_exif_data=False,
571
572
  skip_image_resizing=False,
572
573
  augment=False):
@@ -575,7 +576,7 @@ def process_image(im_file,
575
576
 
576
577
  Args:
577
578
  im_file (str): path to image file
578
- 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
579
580
  you get this far down the pipeline
580
581
  confidence_threshold (float): only detections above this threshold are returned
581
582
  image (Image, optional): previously-loaded image, if available, used when a worker
@@ -583,22 +584,22 @@ def process_image(im_file,
583
584
  quiet (bool, optional): suppress per-image printouts
584
585
  image_size (tuple, optional): image size to use for inference, only mess with this
585
586
  if (a) you're using a model other than MegaDetector or (b) you know what you're
586
- doing
587
+ doing
587
588
  include_image_size (bool, optional): should we include image size in the output for each image?
588
589
  include_image_timestamp (bool, optional): should we include image timestamps in the output for each image?
589
- 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?
590
591
  skip_image_resizing (bool, optional): whether to skip internal image resizing and rely on external resizing
591
592
  augment (bool, optional): enable image augmentation
592
593
 
593
594
  Returns:
594
595
  dict: dict representing detections on one image,
595
- see the 'images' key in
596
+ see the 'images' key in
596
597
  https://github.com/agentmorris/MegaDetector/tree/main/megadetector/api/batch_processing#batch-processing-api-output-format
597
598
  """
598
-
599
+
599
600
  if not quiet:
600
601
  print('Processing image {}'.format(im_file))
601
-
602
+
602
603
  if image is None:
603
604
  try:
604
605
  image = vis_utils.load_image(im_file)
@@ -612,11 +613,11 @@ def process_image(im_file,
612
613
  return result
613
614
 
614
615
  try:
615
-
616
+
616
617
  result = detector.generate_detections_one_image(
617
- image,
618
- im_file,
619
- detection_threshold=confidence_threshold,
618
+ image,
619
+ im_file,
620
+ detection_threshold=confidence_threshold,
620
621
  image_size=image_size,
621
622
  skip_image_resizing=skip_image_resizing,
622
623
  augment=augment)
@@ -632,7 +633,7 @@ def process_image(im_file,
632
633
  if isinstance(image,dict):
633
634
  image = image['img_original_pil']
634
635
 
635
- if include_image_size:
636
+ if include_image_size:
636
637
  result['width'] = image.width
637
638
  result['height'] = image.height
638
639
 
@@ -651,13 +652,13 @@ def _load_custom_class_mapping(class_mapping_filename):
651
652
  """
652
653
  This is an experimental hack to allow the use of non-MD YOLOv5 models through
653
654
  the same infrastructure; it disables the code that enforces MDv5-like class lists.
654
-
655
+
655
656
  Should be a .json file that maps int-strings to strings, or a YOLOv5 dataset.yaml file.
656
657
  """
657
-
658
+
658
659
  if class_mapping_filename is None:
659
660
  return
660
-
661
+
661
662
  run_detector.USE_MODEL_NATIVE_CLASSES = True
662
663
  if class_mapping_filename.endswith('.json'):
663
664
  with open(class_mapping_filename,'r') as f:
@@ -668,28 +669,28 @@ def _load_custom_class_mapping(class_mapping_filename):
668
669
  class_mapping = {str(k):v for k,v in class_mapping.items()}
669
670
  else:
670
671
  raise ValueError('Unrecognized class mapping file {}'.format(class_mapping_filename))
671
-
672
+
672
673
  print('Loaded custom class mapping:')
673
674
  print(class_mapping)
674
675
  run_detector.DEFAULT_DETECTOR_LABEL_MAP = class_mapping
675
676
  return class_mapping
676
-
677
-
677
+
678
+
678
679
  #%% Main function
679
680
 
680
- def load_and_run_detector_batch(model_file,
681
- image_file_names,
681
+ def load_and_run_detector_batch(model_file,
682
+ image_file_names,
682
683
  checkpoint_path=None,
683
684
  confidence_threshold=run_detector.DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD,
684
- checkpoint_frequency=-1,
685
- results=None,
685
+ checkpoint_frequency=-1,
686
+ results=None,
686
687
  n_cores=1,
687
- use_image_queue=False,
688
- quiet=False,
689
- image_size=None,
690
- class_mapping_filename=None,
691
- include_image_size=False,
692
- 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,
693
694
  include_exif_data=False,
694
695
  augment=False,
695
696
  force_model_download=False,
@@ -698,19 +699,18 @@ def load_and_run_detector_batch(model_file,
698
699
  preprocess_on_image_queue=default_preprocess_on_image_queue):
699
700
  """
700
701
  Load a model file and run it on a list of images.
701
-
702
+
702
703
  Args:
703
-
704
704
  model_file (str): path to model file, or supported model string (e.g. "MDV5A")
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
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
707
707
  of images.
708
708
  checkpoint_path (str, optional), path to use for checkpoints (if None, checkpointing
709
709
  is disabled)
710
710
  confidence_threshold (float, optional): only detections above this threshold are returned
711
- 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
712
712
  images, -1 disabled checkpointing
713
- 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
714
714
  not useful if you're using this function outside of the CLI
715
715
  n_cores (int, optional): number of parallel worker to use, ignored if we're running on a GPU
716
716
  use_image_queue (bool, optional): use a dedicated worker for image loading
@@ -718,7 +718,7 @@ def load_and_run_detector_batch(model_file,
718
718
  image_size (tuple, optional): image size to use for inference, only mess with this
719
719
  if (a) you're using a model other than MegaDetector or (b) you know what you're
720
720
  doing
721
- 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
722
722
  file or YOLOv5 dataset.yaml file
723
723
  include_image_size (bool, optional): should we include image size in the output for each image?
724
724
  include_image_timestamp (bool, optional): should we include image timestamps in the output for each image?
@@ -727,37 +727,37 @@ def load_and_run_detector_batch(model_file,
727
727
  force_model_download (bool, optional): force downloading the model file if
728
728
  a named model (e.g. "MDV5A") is supplied, even if the local file already
729
729
  exists
730
- detector_options (dict, optional): key/value pairs that are interpreted differently
730
+ detector_options (dict, optional): key/value pairs that are interpreted differently
731
731
  by different detectors
732
732
  loader_workers (int, optional): number of loaders to use, only relevant when use_image_queue is True
733
-
733
+
734
734
  Returns:
735
735
  results: list of dicts; each dict represents detections on one image
736
736
  """
737
-
737
+
738
738
  # Validate input arguments
739
739
  if n_cores is None or n_cores <= 0:
740
740
  n_cores = 1
741
-
741
+
742
742
  if confidence_threshold is None:
743
743
  confidence_threshold=run_detector.DEFAULT_OUTPUT_CONFIDENCE_THRESHOLD
744
-
744
+
745
745
  # Disable checkpointing if checkpoint_path is None
746
746
  if checkpoint_frequency is None or checkpoint_path is None:
747
747
  checkpoint_frequency = -1
748
748
 
749
749
  if class_mapping_filename is not None:
750
750
  _load_custom_class_mapping(class_mapping_filename)
751
-
751
+
752
752
  # Handle the case where image_file_names is not yet actually a list
753
753
  if isinstance(image_file_names,str):
754
-
754
+
755
755
  # Find the images to score; images can be a directory, may need to recurse
756
756
  if os.path.isdir(image_file_names):
757
757
  image_dir = image_file_names
758
758
  image_file_names = path_utils.find_images(image_dir, True)
759
759
  print('{} image files found in folder {}'.format(len(image_file_names),image_dir))
760
-
760
+
761
761
  # A single file, or a list of image paths
762
762
  elif os.path.isfile(image_file_names):
763
763
  list_file = image_file_names
@@ -780,43 +780,43 @@ def load_and_run_detector_batch(model_file,
780
780
  'File {} supplied as [image_file_names] argument, but extension is neither .json nor .txt'\
781
781
  .format(
782
782
  list_file))
783
- else:
783
+ else:
784
784
  raise ValueError(
785
785
  '{} supplied as [image_file_names] argument, but it does not appear to be a file or folder'.format(
786
786
  image_file_names))
787
-
787
+
788
788
  if results is None:
789
789
  results = []
790
790
 
791
791
  already_processed = set([i['file'] for i in results])
792
792
 
793
793
  model_file = try_download_known_detector(model_file, force_download=force_model_download)
794
-
794
+
795
795
  print('GPU available: {}'.format(is_gpu_available(model_file)))
796
-
796
+
797
797
  if n_cores > 1 and is_gpu_available(model_file):
798
-
798
+
799
799
  print('Warning: multiple cores requested, but a GPU is available; parallelization across ' + \
800
800
  'GPUs is not currently supported, defaulting to one GPU')
801
801
  n_cores = 1
802
802
 
803
803
  if n_cores > 1 and use_image_queue:
804
-
804
+
805
805
  print('Warning: multiple cores requested, but the image queue is enabled; parallelization ' + \
806
806
  'with the image queue is not currently supported, defaulting to one worker')
807
807
  n_cores = 1
808
-
808
+
809
809
  if use_image_queue:
810
-
810
+
811
811
  assert checkpoint_frequency < 0, \
812
812
  'Using an image queue is not currently supported when checkpointing is enabled'
813
813
  assert len(results) == 0, \
814
814
  'Using an image queue with results loaded from a checkpoint is not currently supported'
815
815
  assert n_cores <= 1
816
- results = run_detector_with_image_queue(image_file_names,
817
- model_file,
818
- confidence_threshold,
819
- quiet,
816
+ results = run_detector_with_image_queue(image_file_names,
817
+ model_file,
818
+ confidence_threshold,
819
+ quiet,
820
820
  image_size=image_size,
821
821
  include_image_size=include_image_size,
822
822
  include_image_timestamp=include_image_timestamp,
@@ -825,7 +825,7 @@ def load_and_run_detector_batch(model_file,
825
825
  detector_options=detector_options,
826
826
  loader_workers=loader_workers,
827
827
  preprocess_on_image_queue=preprocess_on_image_queue)
828
-
828
+
829
829
  elif n_cores <= 1:
830
830
 
831
831
  # Load the detector
@@ -848,11 +848,11 @@ def load_and_run_detector_batch(model_file,
848
848
 
849
849
  count += 1
850
850
 
851
- result = process_image(im_file,
852
- detector,
853
- confidence_threshold,
854
- quiet=quiet,
855
- image_size=image_size,
851
+ result = process_image(im_file,
852
+ detector,
853
+ confidence_threshold,
854
+ quiet=quiet,
855
+ image_size=image_size,
856
856
  include_image_size=include_image_size,
857
857
  include_image_timestamp=include_image_timestamp,
858
858
  include_exif_data=include_exif_data,
@@ -861,97 +861,100 @@ def load_and_run_detector_batch(model_file,
861
861
 
862
862
  # Write a checkpoint if necessary
863
863
  if (checkpoint_frequency != -1) and ((count % checkpoint_frequency) == 0):
864
-
864
+
865
865
  print('Writing a new checkpoint after having processed {} images since '
866
866
  'last restart'.format(count))
867
-
867
+
868
868
  _write_checkpoint(checkpoint_path, results)
869
-
869
+
870
870
  else:
871
-
871
+
872
872
  # Multiprocessing is enabled at this point
873
-
873
+
874
874
  # When using multiprocessing, tell the workers to load the model on each
875
875
  # process, by passing the model_file string as the "model" argument to
876
876
  # process_images.
877
877
  detector = model_file
878
878
 
879
- print('Creating pool with {} cores'.format(n_cores))
879
+ print('Creating worker pool with {} cores'.format(n_cores))
880
880
 
881
881
  if len(already_processed) > 0:
882
882
  n_images_all = len(image_file_names)
883
883
  image_file_names = [fn for fn in image_file_names if fn not in already_processed]
884
884
  print('Loaded {} of {} images from checkpoint'.format(
885
885
  len(already_processed),n_images_all))
886
-
887
- # 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
888
888
  image_batches = list(_chunks_by_number_of_chunks(image_file_names, n_cores))
889
-
890
- pool = workerpool(n_cores)
891
-
892
- if checkpoint_path is not None:
893
-
894
- # Multiprocessing and checkpointing are both enabled at this point
895
-
896
- checkpoint_queue = Manager().Queue()
897
-
898
- # Pass the "results" array (which may already contain images loaded from an existing
899
- # checkpoint) to the checkpoint queue handler function, which will append results to
900
- # the list as they become available.
901
- checkpoint_thread = Thread(target=_checkpoint_queue_handler,
902
- args=(checkpoint_path, checkpoint_frequency,
903
- checkpoint_queue, results), daemon=True)
904
- checkpoint_thread.start()
905
-
906
- pool.map(partial(process_images,
907
- detector=detector,
908
- confidence_threshold=confidence_threshold,
909
- use_image_queue=False,
910
- quiet=quiet,
911
- image_size=image_size,
912
- checkpoint_queue=checkpoint_queue,
913
- include_image_size=include_image_size,
914
- include_image_timestamp=include_image_timestamp,
915
- include_exif_data=include_exif_data,
916
- augment=augment,
917
- detector_options=detector_options),
918
- image_batches)
919
-
920
- checkpoint_queue.put(None)
921
889
 
922
- else:
923
-
924
- # Multprocessing is enabled, but checkpointing is not
925
-
926
- new_results = pool.map(partial(process_images,
927
- detector=detector,
928
- confidence_threshold=confidence_threshold,
929
- use_image_queue=False,
930
- quiet=quiet,
931
- checkpoint_queue=None,
932
- image_size=image_size,
933
- include_image_size=include_image_size,
934
- include_image_timestamp=include_image_timestamp,
935
- include_exif_data=include_exif_data,
936
- augment=augment,
937
- detector_options=detector_options),
938
- image_batches)
939
-
940
- new_results = list(itertools.chain.from_iterable(new_results))
941
-
942
- # Append the results we just computed to "results", which is *usually* empty, but will
943
- # be non-empty if we resumed from a checkpoint
944
- results += new_results
945
-
946
- # ...if checkpointing is/isn't enabled
947
-
890
+ pool = None
948
891
  try:
949
- pool.close()
950
- except Exception as e:
951
- print('Warning: error closing multiprocessing pool:\n{}'.format(str(e)))
952
-
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
+
953
956
  # ...if we're running (1) with image queue, (2) on one core, or (3) on multiple cores
954
-
957
+
955
958
  # 'results' may have been modified in place, but we also return it for
956
959
  # backwards-compatibility.
957
960
  return results
@@ -964,21 +967,21 @@ def _checkpoint_queue_handler(checkpoint_path, checkpoint_frequency, checkpoint_
964
967
  Thread function to accumulate results and write checkpoints when checkpointing and
965
968
  multiprocessing are both enabled.
966
969
  """
967
-
970
+
968
971
  result_count = 0
969
972
  while True:
970
- result = checkpoint_queue.get()
971
- if result is None:
972
- break
973
-
973
+ result = checkpoint_queue.get()
974
+ if result is None:
975
+ break
976
+
974
977
  result_count +=1
975
978
  results.append(result)
976
979
 
977
980
  if (checkpoint_frequency != -1) and (result_count % checkpoint_frequency == 0):
978
-
981
+
979
982
  print('Writing a new checkpoint after having processed {} images since '
980
983
  'last restart'.format(result_count))
981
-
984
+
982
985
  _write_checkpoint(checkpoint_path, results)
983
986
 
984
987
 
@@ -986,20 +989,19 @@ def _write_checkpoint(checkpoint_path, results):
986
989
  """
987
990
  Writes the 'images' field in the dict 'results' to a json checkpoint file.
988
991
  """
989
-
990
- assert checkpoint_path is not None
991
-
992
+
993
+ assert checkpoint_path is not None
994
+
992
995
  # Back up any previous checkpoints, to protect against crashes while we're writing
993
996
  # the checkpoint file.
994
997
  checkpoint_tmp_path = None
995
998
  if os.path.isfile(checkpoint_path):
996
999
  checkpoint_tmp_path = checkpoint_path + '_tmp'
997
1000
  shutil.copyfile(checkpoint_path,checkpoint_tmp_path)
998
-
1001
+
999
1002
  # Write the new checkpoint
1000
- with open(checkpoint_path, 'w') as f:
1001
- json.dump({'images': results}, f, indent=1, default=str)
1002
-
1003
+ ct_utils.write_json(checkpoint_path, {'images': results}, force_str=True)
1004
+
1003
1005
  # Remove the backup checkpoint if it exists
1004
1006
  if checkpoint_tmp_path is not None:
1005
1007
  os.remove(checkpoint_tmp_path)
@@ -1008,33 +1010,33 @@ def _write_checkpoint(checkpoint_path, results):
1008
1010
  def get_image_datetime(image):
1009
1011
  """
1010
1012
  Reads EXIF datetime from a PIL Image object.
1011
-
1013
+
1012
1014
  Args:
1013
1015
  image (Image): the PIL Image object from which we should read datetime information
1014
-
1016
+
1015
1017
  Returns:
1016
1018
  str: the EXIF datetime from [image] (a PIL Image object), if available, as a string;
1017
1019
  returns None if EXIF datetime is not available.
1018
1020
  """
1019
-
1021
+
1020
1022
  exif_tags = read_exif.read_pil_exif(image,exif_options)
1021
-
1023
+
1022
1024
  try:
1023
1025
  datetime_str = exif_tags['DateTimeOriginal']
1024
1026
  _ = time.strptime(datetime_str, '%Y:%m:%d %H:%M:%S')
1025
1027
  return datetime_str
1026
1028
 
1027
1029
  except Exception:
1028
- return None
1030
+ return None
1029
1031
 
1030
1032
 
1031
- def write_results_to_file(results,
1032
- output_file,
1033
- relative_path_base=None,
1034
- detector_file=None,
1035
- info=None,
1033
+ def write_results_to_file(results,
1034
+ output_file,
1035
+ relative_path_base=None,
1036
+ detector_file=None,
1037
+ info=None,
1036
1038
  include_max_conf=False,
1037
- custom_metadata=None,
1039
+ custom_metadata=None,
1038
1040
  force_forward_slashes=True):
1039
1041
  """
1040
1042
  Writes list of detection results to JSON output file. Format matches:
@@ -1056,11 +1058,11 @@ def write_results_to_file(results,
1056
1058
  a dictionary, but no type/format checks are performed
1057
1059
  force_forward_slashes (bool, optional): convert all slashes in filenames within [results] to
1058
1060
  forward slashes
1059
-
1061
+
1060
1062
  Returns:
1061
1063
  dict: the MD-formatted dictionary that was written to [output_file]
1062
1064
  """
1063
-
1065
+
1064
1066
  if relative_path_base is not None:
1065
1067
  results_relative = []
1066
1068
  for r in results:
@@ -1076,68 +1078,67 @@ def write_results_to_file(results,
1076
1078
  r_converted['file'] = r_converted['file'].replace('\\','/')
1077
1079
  results_converted.append(r_converted)
1078
1080
  results = results_converted
1079
-
1081
+
1080
1082
  # The typical case: we need to build the 'info' struct
1081
1083
  if info is None:
1082
-
1083
- info = {
1084
+
1085
+ info = {
1084
1086
  'detection_completion_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
1085
- 'format_version': '1.4'
1087
+ 'format_version': '1.4'
1086
1088
  }
1087
-
1089
+
1088
1090
  if detector_file is not None:
1089
1091
  detector_filename = os.path.basename(detector_file)
1090
1092
  detector_version = get_detector_version_from_filename(detector_filename)
1091
1093
  detector_metadata = get_detector_metadata_from_version_string(detector_version)
1092
- info['detector'] = detector_filename
1094
+ info['detector'] = detector_filename
1093
1095
  info['detector_metadata'] = detector_metadata
1094
1096
  else:
1095
1097
  info['detector'] = 'unknown'
1096
1098
  info['detector_metadata'] = get_detector_metadata_from_version_string('unknown')
1097
-
1099
+
1098
1100
  # If the caller supplied the entire "info" struct
1099
1101
  else:
1100
-
1101
- if detector_file is not None:
1102
+
1103
+ if detector_file is not None:
1102
1104
  print('Warning (write_results_to_file): info struct and detector file ' + \
1103
1105
  'supplied, ignoring detector file')
1104
1106
 
1105
1107
  if custom_metadata is not None:
1106
1108
  info['custom_metadata'] = custom_metadata
1107
-
1109
+
1108
1110
  # The 'max_detection_conf' field used to be included by default, and it caused all kinds
1109
1111
  # of headaches, so it's no longer included unless the user explicitly requests it.
1110
1112
  if not include_max_conf:
1111
1113
  for im in results:
1112
1114
  if 'max_detection_conf' in im:
1113
1115
  del im['max_detection_conf']
1114
-
1116
+
1115
1117
  # Sort results by filename; not required by the format, but convenient for consistency
1116
1118
  results = sort_list_of_dicts_by_key(results,'file')
1117
-
1119
+
1118
1120
  # Sort detections in descending order by confidence; not required by the format, but
1119
1121
  # convenient for consistency
1120
1122
  for r in results:
1121
1123
  if ('detections' in r) and (r['detections'] is not None):
1122
1124
  r['detections'] = sort_list_of_dicts_by_key(r['detections'], 'conf', reverse=True)
1123
-
1125
+
1124
1126
  final_output = {
1125
1127
  'images': results,
1126
1128
  'detection_categories': run_detector.DEFAULT_DETECTOR_LABEL_MAP,
1127
1129
  'info': info
1128
1130
  }
1129
-
1131
+
1130
1132
  # Create the folder where the output file belongs; this will fail if
1131
1133
  # this is a relative path with no folder component
1132
1134
  try:
1133
1135
  os.makedirs(os.path.dirname(output_file),exist_ok=True)
1134
1136
  except Exception:
1135
1137
  pass
1136
-
1137
- with open(output_file, 'w') as f:
1138
- json.dump(final_output, f, indent=1, default=str)
1138
+
1139
+ ct_utils.write_json(output_file, final_output, force_str=True)
1139
1140
  print('Output file saved at {}'.format(output_file))
1140
-
1141
+
1141
1142
  return final_output
1142
1143
 
1143
1144
  # ...def write_results_to_file(...)
@@ -1146,15 +1147,15 @@ def write_results_to_file(results,
1146
1147
  #%% Interactive driver
1147
1148
 
1148
1149
  if False:
1149
-
1150
+
1150
1151
  pass
1151
1152
 
1152
1153
  #%%
1153
-
1154
+
1154
1155
  model_file = 'MDV5A'
1155
1156
  image_dir = r'g:\camera_traps\camera_trap_images'
1156
1157
  output_file = r'g:\temp\md-test.json'
1157
-
1158
+
1158
1159
  recursive = True
1159
1160
  output_relative_filenames = True
1160
1161
  include_max_conf = False
@@ -1162,7 +1163,7 @@ if False:
1162
1163
  image_size = None
1163
1164
  use_image_queue = False
1164
1165
  confidence_threshold = 0.0001
1165
- checkpoint_frequency = 5
1166
+ checkpoint_frequency = 5
1166
1167
  checkpoint_path = None
1167
1168
  resume_from_checkpoint = 'auto'
1168
1169
  allow_checkpoint_overwrite = False
@@ -1172,11 +1173,11 @@ if False:
1172
1173
  include_image_timestamp = True
1173
1174
  include_exif_data = True
1174
1175
  overwrite_handling = None
1175
-
1176
+
1176
1177
  # Generate a command line
1177
1178
  cmd = 'python run_detector_batch.py "{}" "{}" "{}"'.format(
1178
1179
  model_file,image_dir,output_file)
1179
-
1180
+
1180
1181
  if recursive:
1181
1182
  cmd += ' --recursive'
1182
1183
  if output_relative_filenames:
@@ -1211,18 +1212,18 @@ if False:
1211
1212
  cmd += ' --include_exif_data'
1212
1213
  if overwrite_handling is not None:
1213
1214
  cmd += ' --overwrite_handling {}'.format(overwrite_handling)
1214
-
1215
+
1215
1216
  print(cmd)
1216
1217
  import clipboard; clipboard.copy(cmd)
1217
-
1218
-
1218
+
1219
+
1219
1220
  #%% Run inference interactively
1220
-
1221
- image_file_names = path_utils.find_images(image_dir, recursive=False)
1221
+
1222
+ image_file_names = path_utils.find_images(image_dir, recursive=False)
1222
1223
  results = None
1223
-
1224
+
1224
1225
  start_time = time.time()
1225
-
1226
+
1226
1227
  results = load_and_run_detector_batch(model_file=model_file,
1227
1228
  image_file_names=image_file_names,
1228
1229
  checkpoint_path=checkpoint_path,
@@ -1233,21 +1234,22 @@ if False:
1233
1234
  use_image_queue=use_image_queue,
1234
1235
  quiet=quiet,
1235
1236
  image_size=image_size)
1236
-
1237
+
1237
1238
  elapsed = time.time() - start_time
1238
-
1239
+
1239
1240
  print('Finished inference in {}'.format(humanfriendly.format_timespan(elapsed)))
1240
1241
 
1241
-
1242
+
1242
1243
  #%% Command-line driver
1243
1244
 
1244
- def main():
1245
-
1245
+ def main(): # noqa
1246
+
1246
1247
  parser = argparse.ArgumentParser(
1247
1248
  description='Module to run a TF/PT animal detection model on lots of images')
1248
1249
  parser.add_argument(
1249
1250
  'detector_file',
1250
- 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.')
1251
1253
  parser.add_argument(
1252
1254
  'image_file',
1253
1255
  help=\
@@ -1279,7 +1281,7 @@ def main():
1279
1281
  '--image_size',
1280
1282
  type=int,
1281
1283
  default=None,
1282
- 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)'))
1283
1285
  parser.add_argument(
1284
1286
  '--augment',
1285
1287
  action='store_true',
@@ -1316,7 +1318,7 @@ def main():
1316
1318
  type=str,
1317
1319
  default=None,
1318
1320
  help='File name to which checkpoints will be written if checkpoint_frequency is > 0, ' + \
1319
- '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')
1320
1322
  parser.add_argument(
1321
1323
  '--resume_from_checkpoint',
1322
1324
  type=str,
@@ -1367,7 +1369,7 @@ def main():
1367
1369
  type=str,
1368
1370
  default='overwrite',
1369
1371
  help='What should we do if the output file exists? overwrite/skip/error (default overwrite)'
1370
- )
1372
+ )
1371
1373
  parser.add_argument(
1372
1374
  '--force_model_download',
1373
1375
  action='store_true',
@@ -1387,28 +1389,28 @@ def main():
1387
1389
  metavar='KEY=VALUE',
1388
1390
  default='',
1389
1391
  help='Detector-specific options, as a space-separated list of key-value pairs')
1390
-
1392
+
1391
1393
  if len(sys.argv[1:]) == 0:
1392
1394
  parser.print_help()
1393
1395
  parser.exit()
1394
1396
 
1395
1397
  args = parser.parse_args()
1396
-
1398
+
1397
1399
  global verbose
1398
1400
  global use_threads_for_queue
1399
-
1401
+
1400
1402
  if args.verbose:
1401
1403
  verbose = True
1402
1404
  if args.use_threads_for_queue:
1403
1405
  use_threads_for_queue = True
1404
-
1406
+
1405
1407
  detector_options = parse_kvp_list(args.detector_options)
1406
-
1407
- # 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
1408
1410
  # (and possibly download) that model
1409
- args.detector_file = try_download_known_detector(args.detector_file,
1411
+ args.detector_file = try_download_known_detector(args.detector_file,
1410
1412
  force_download=args.force_model_download)
1411
-
1413
+
1412
1414
  assert os.path.exists(args.detector_file), \
1413
1415
  'detector file {} does not exist'.format(args.detector_file)
1414
1416
  assert 0.0 <= args.threshold <= 1.0, 'Confidence threshold needs to be between 0 and 1'
@@ -1439,12 +1441,12 @@ def main():
1439
1441
 
1440
1442
  if len(output_dir) > 0:
1441
1443
  os.makedirs(output_dir,exist_ok=True)
1442
-
1444
+
1443
1445
  assert not os.path.isdir(args.output_file), 'Specified output file is a directory'
1444
-
1446
+
1445
1447
  if args.class_mapping_filename is not None:
1446
1448
  _load_custom_class_mapping(args.class_mapping_filename)
1447
-
1449
+
1448
1450
  # Load the checkpoint if available
1449
1451
  #
1450
1452
  # File paths in the checkpoint are always absolute paths; conversion to relative paths
@@ -1463,7 +1465,7 @@ def main():
1463
1465
  len(checkpoint_files),output_dir))
1464
1466
  checkpoint_files = sorted(checkpoint_files)
1465
1467
  checkpoint_file_relative = checkpoint_files[-1]
1466
- checkpoint_file = os.path.join(output_dir,checkpoint_file_relative)
1468
+ checkpoint_file = os.path.join(output_dir,checkpoint_file_relative)
1467
1469
  else:
1468
1470
  checkpoint_file = args.resume_from_checkpoint
1469
1471
  assert os.path.exists(checkpoint_file), \
@@ -1483,7 +1485,7 @@ def main():
1483
1485
  if os.path.isdir(args.image_file):
1484
1486
  image_file_names = path_utils.find_images(args.image_file, args.recursive)
1485
1487
  if len(image_file_names) > 0:
1486
- 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)))
1487
1489
  else:
1488
1490
  if (args.recursive):
1489
1491
  print('No image files found in directory {}, exiting'.format(args.image_file))
@@ -1492,14 +1494,14 @@ def main():
1492
1494
  '--recursive?'.format(
1493
1495
  args.image_file))
1494
1496
  return
1495
-
1497
+
1496
1498
  # A json list of image paths
1497
- 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'):
1498
1500
  with open(args.image_file) as f:
1499
1501
  image_file_names = json.load(f)
1500
1502
  print('Loaded {} image filenames from .json list file {}'.format(
1501
1503
  len(image_file_names),args.image_file))
1502
-
1504
+
1503
1505
  # A text list of image paths
1504
1506
  elif os.path.isfile(args.image_file) and args.image_file.endswith('.txt'):
1505
1507
  with open(args.image_file) as f:
@@ -1507,51 +1509,51 @@ def main():
1507
1509
  image_file_names = [fn.strip() for fn in image_file_names if len(fn.strip()) > 0]
1508
1510
  print('Loaded {} image filenames from .txt list file {}'.format(
1509
1511
  len(image_file_names),args.image_file))
1510
-
1512
+
1511
1513
  # A single image file
1512
1514
  elif os.path.isfile(args.image_file) and path_utils.is_image_file(args.image_file):
1513
1515
  image_file_names = [args.image_file]
1514
1516
  print('Processing image {}'.format(args.image_file))
1515
-
1516
- else:
1517
+
1518
+ else:
1517
1519
  raise ValueError('image_file specified is not a directory, a json list, or an image file, '
1518
1520
  '(or does not have recognizable extensions).')
1519
1521
 
1520
- # 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
1521
1523
  # absolute image paths.
1522
1524
  assert len(image_file_names) > 0, 'Specified image_file does not point to valid image files'
1523
-
1525
+
1524
1526
  # Convert to forward slashes to facilitate comparison with previous results
1525
1527
  image_file_names = [fn.replace('\\','/') for fn in image_file_names]
1526
-
1528
+
1527
1529
  # We can head off many problems related to incorrect command line formulation if we confirm
1528
- # 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
1529
1531
  # arbitrary.
1530
1532
  assert os.path.exists(image_file_names[0]), \
1531
1533
  'The first image to be processed does not exist at {}'.format(image_file_names[0])
1532
1534
 
1533
1535
  # Possibly load results from a previous pass
1534
1536
  previous_results = None
1535
-
1537
+
1536
1538
  if args.previous_results_file is not None:
1537
-
1539
+
1538
1540
  assert os.path.isfile(args.previous_results_file), \
1539
1541
  'Could not find previous results file {}'.format(args.previous_results_file)
1540
1542
  with open(args.previous_results_file,'r') as f:
1541
1543
  previous_results = json.load(f)
1542
-
1544
+
1543
1545
  assert previous_results['detection_categories'] == run_detector.DEFAULT_DETECTOR_LABEL_MAP, \
1544
1546
  "Can't merge previous results when those results use a different set of detection categories"
1545
-
1547
+
1546
1548
  print('Loaded previous results for {} images from {}'.format(
1547
1549
  len(previous_results['images']), args.previous_results_file))
1548
-
1549
- # Convert previous result filenames to absolute paths if necessary
1550
+
1551
+ # Convert previous result filenames to absolute paths if necessary
1550
1552
  #
1551
- # 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
1552
1554
  # folder, but just to be super-clear...
1553
1555
  assert os.path.isdir(args.image_file)
1554
-
1556
+
1555
1557
  previous_image_files_set = set()
1556
1558
  for im in previous_results['images']:
1557
1559
  assert not os.path.isabs(im['file']), \
@@ -1559,54 +1561,53 @@ def main():
1559
1561
  fn_abs = os.path.join(args.image_file,im['file']).replace('\\','/')
1560
1562
  # Absolute paths are expected at the final output stage below
1561
1563
  im['file'] = fn_abs
1562
- previous_image_files_set.add(fn_abs)
1563
-
1564
+ previous_image_files_set.add(fn_abs)
1565
+
1564
1566
  image_file_names_to_keep = []
1565
1567
  for fn_abs in image_file_names:
1566
1568
  if fn_abs not in previous_image_files_set:
1567
1569
  image_file_names_to_keep.append(fn_abs)
1568
-
1570
+
1569
1571
  print('Based on previous results file, processing {} of {} images'.format(
1570
1572
  len(image_file_names_to_keep), len(image_file_names)))
1571
-
1573
+
1572
1574
  image_file_names = image_file_names_to_keep
1573
-
1575
+
1574
1576
  # ...if we're handling previous results
1575
-
1577
+
1576
1578
  # Test that we can write to the output_file's dir if checkpointing requested
1577
1579
  if args.checkpoint_frequency != -1:
1578
-
1580
+
1579
1581
  if args.checkpoint_path is not None:
1580
1582
  checkpoint_path = args.checkpoint_path
1581
1583
  else:
1582
1584
  checkpoint_path = os.path.join(output_dir,
1583
1585
  'md_checkpoint_{}.json'.format(
1584
1586
  datetime.now().strftime("%Y%m%d%H%M%S")))
1585
-
1587
+
1586
1588
  # Don't overwrite existing checkpoint files, this is a sure-fire way to eventually
1587
1589
  # erase someone's checkpoint.
1588
1590
  if (checkpoint_path is not None) and (not args.allow_checkpoint_overwrite) \
1589
1591
  and (args.resume_from_checkpoint is None):
1590
-
1592
+
1591
1593
  assert not os.path.isfile(checkpoint_path), \
1592
1594
  f'Checkpoint path {checkpoint_path} already exists, delete or move it before ' + \
1593
1595
  're-using the same checkpoint path, or specify --allow_checkpoint_overwrite'
1594
1596
 
1595
-
1597
+
1596
1598
  # Confirm that we can write to the checkpoint path; this avoids issues where
1597
1599
  # we crash after several thousand images.
1598
1600
  #
1599
- # 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
1600
1602
  # checkpoint, then immediately overwrite that checkpoint with empty data is higher-risk
1601
1603
  # than the annoyance of crashing a few minutes after starting a job.
1602
1604
  if False:
1603
- with open(checkpoint_path, 'w') as f:
1604
- json.dump({'images': []}, f)
1605
-
1605
+ ct_utils.write_json(checkpoint_path, {'images': []}, indent=None)
1606
+
1606
1607
  print('The checkpoint file will be written to {}'.format(checkpoint_path))
1607
-
1608
+
1608
1609
  else:
1609
-
1610
+
1610
1611
  if args.checkpoint_path is not None:
1611
1612
  print('Warning: checkpointing disabled because checkpoint_frequency is -1, ' + \
1612
1613
  'but a checkpoint path was specified')
@@ -1641,23 +1642,23 @@ def main():
1641
1642
  len(results),humanfriendly.format_timespan(elapsed),images_per_second))
1642
1643
 
1643
1644
  relative_path_base = None
1644
-
1645
- # 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,
1646
1647
  # args.image_file is a folder, but we'll double-check for clarity.
1647
1648
  if args.output_relative_filenames:
1648
1649
  assert os.path.isdir(args.image_file)
1649
1650
  relative_path_base = args.image_file
1650
-
1651
+
1651
1652
  # Merge results from a previous file if necessary
1652
1653
  if previous_results is not None:
1653
1654
  previous_filenames_set = set([im['file'] for im in previous_results['images']])
1654
1655
  new_filenames_set = set([im['file'] for im in results])
1655
1656
  assert len(previous_filenames_set.intersection(new_filenames_set)) == 0, \
1656
1657
  'Previous results handling error: redundant image filenames'
1657
- results.extend(previous_results['images'])
1658
-
1659
- write_results_to_file(results,
1660
- args.output_file,
1658
+ results.extend(previous_results['images'])
1659
+
1660
+ write_results_to_file(results,
1661
+ args.output_file,
1661
1662
  relative_path_base=relative_path_base,
1662
1663
  detector_file=args.detector_file,
1663
1664
  include_max_conf=args.include_max_conf)