megadetector 5.0.11__py3-none-any.whl → 5.0.13__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.
- megadetector/api/__init__.py +0 -0
- megadetector/api/batch_processing/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
- megadetector/api/batch_processing/api_core/batch_service/score.py +439 -0
- megadetector/api/batch_processing/api_core/server.py +294 -0
- megadetector/api/batch_processing/api_core/server_api_config.py +97 -0
- megadetector/api/batch_processing/api_core/server_app_config.py +55 -0
- megadetector/api/batch_processing/api_core/server_batch_job_manager.py +220 -0
- megadetector/api/batch_processing/api_core/server_job_status_table.py +149 -0
- megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
- megadetector/api/batch_processing/api_core/server_utils.py +88 -0
- megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +46 -0
- megadetector/api/batch_processing/api_support/__init__.py +0 -0
- megadetector/api/batch_processing/api_support/summarize_daily_activity.py +152 -0
- megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
- megadetector/api/batch_processing/integration/digiKam/setup.py +6 -0
- megadetector/api/batch_processing/integration/digiKam/xmp_integration.py +465 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/config_template.py +5 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/push_annotations_to_emammal.py +125 -0
- megadetector/api/batch_processing/integration/eMammal/test_scripts/select_images_for_testing.py +55 -0
- megadetector/api/synchronous/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +152 -0
- megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +263 -0
- megadetector/api/synchronous/api_core/animal_detection_api/config.py +35 -0
- megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
- megadetector/api/synchronous/api_core/tests/load_test.py +110 -0
- megadetector/classification/__init__.py +0 -0
- megadetector/classification/aggregate_classifier_probs.py +108 -0
- megadetector/classification/analyze_failed_images.py +227 -0
- megadetector/classification/cache_batchapi_outputs.py +198 -0
- megadetector/classification/create_classification_dataset.py +627 -0
- megadetector/classification/crop_detections.py +516 -0
- megadetector/classification/csv_to_json.py +226 -0
- megadetector/classification/detect_and_crop.py +855 -0
- megadetector/classification/efficientnet/__init__.py +9 -0
- megadetector/classification/efficientnet/model.py +415 -0
- megadetector/classification/efficientnet/utils.py +607 -0
- megadetector/classification/evaluate_model.py +520 -0
- megadetector/classification/identify_mislabeled_candidates.py +152 -0
- megadetector/classification/json_to_azcopy_list.py +63 -0
- megadetector/classification/json_validator.py +699 -0
- megadetector/classification/map_classification_categories.py +276 -0
- megadetector/classification/merge_classification_detection_output.py +506 -0
- megadetector/classification/prepare_classification_script.py +194 -0
- megadetector/classification/prepare_classification_script_mc.py +228 -0
- megadetector/classification/run_classifier.py +287 -0
- megadetector/classification/save_mislabeled.py +110 -0
- megadetector/classification/train_classifier.py +827 -0
- megadetector/classification/train_classifier_tf.py +725 -0
- megadetector/classification/train_utils.py +323 -0
- megadetector/data_management/__init__.py +0 -0
- megadetector/data_management/annotations/__init__.py +0 -0
- megadetector/data_management/annotations/annotation_constants.py +34 -0
- megadetector/data_management/camtrap_dp_to_coco.py +237 -0
- megadetector/data_management/cct_json_utils.py +404 -0
- megadetector/data_management/cct_to_md.py +176 -0
- megadetector/data_management/cct_to_wi.py +289 -0
- megadetector/data_management/coco_to_labelme.py +283 -0
- megadetector/data_management/coco_to_yolo.py +662 -0
- megadetector/data_management/databases/__init__.py +0 -0
- megadetector/data_management/databases/add_width_and_height_to_db.py +33 -0
- megadetector/data_management/databases/combine_coco_camera_traps_files.py +206 -0
- megadetector/data_management/databases/integrity_check_json_db.py +493 -0
- megadetector/data_management/databases/subset_json_db.py +115 -0
- megadetector/data_management/generate_crops_from_cct.py +149 -0
- megadetector/data_management/get_image_sizes.py +189 -0
- megadetector/data_management/importers/add_nacti_sizes.py +52 -0
- megadetector/data_management/importers/add_timestamps_to_icct.py +79 -0
- megadetector/data_management/importers/animl_results_to_md_results.py +158 -0
- megadetector/data_management/importers/auckland_doc_test_to_json.py +373 -0
- megadetector/data_management/importers/auckland_doc_to_json.py +201 -0
- megadetector/data_management/importers/awc_to_json.py +191 -0
- megadetector/data_management/importers/bellevue_to_json.py +273 -0
- megadetector/data_management/importers/cacophony-thermal-importer.py +793 -0
- megadetector/data_management/importers/carrizo_shrubfree_2018.py +269 -0
- megadetector/data_management/importers/carrizo_trail_cam_2017.py +289 -0
- megadetector/data_management/importers/cct_field_adjustments.py +58 -0
- megadetector/data_management/importers/channel_islands_to_cct.py +913 -0
- megadetector/data_management/importers/eMammal/copy_and_unzip_emammal.py +180 -0
- megadetector/data_management/importers/eMammal/eMammal_helpers.py +249 -0
- megadetector/data_management/importers/eMammal/make_eMammal_json.py +223 -0
- megadetector/data_management/importers/ena24_to_json.py +276 -0
- megadetector/data_management/importers/filenames_to_json.py +386 -0
- megadetector/data_management/importers/helena_to_cct.py +283 -0
- megadetector/data_management/importers/idaho-camera-traps.py +1407 -0
- megadetector/data_management/importers/idfg_iwildcam_lila_prep.py +294 -0
- megadetector/data_management/importers/jb_csv_to_json.py +150 -0
- megadetector/data_management/importers/mcgill_to_json.py +250 -0
- megadetector/data_management/importers/missouri_to_json.py +490 -0
- megadetector/data_management/importers/nacti_fieldname_adjustments.py +79 -0
- megadetector/data_management/importers/noaa_seals_2019.py +181 -0
- megadetector/data_management/importers/pc_to_json.py +365 -0
- megadetector/data_management/importers/plot_wni_giraffes.py +123 -0
- megadetector/data_management/importers/prepare-noaa-fish-data-for-lila.py +359 -0
- megadetector/data_management/importers/prepare_zsl_imerit.py +131 -0
- megadetector/data_management/importers/rspb_to_json.py +356 -0
- megadetector/data_management/importers/save_the_elephants_survey_A.py +320 -0
- megadetector/data_management/importers/save_the_elephants_survey_B.py +329 -0
- megadetector/data_management/importers/snapshot_safari_importer.py +758 -0
- megadetector/data_management/importers/snapshot_safari_importer_reprise.py +665 -0
- megadetector/data_management/importers/snapshot_serengeti_lila.py +1067 -0
- megadetector/data_management/importers/snapshotserengeti/make_full_SS_json.py +150 -0
- megadetector/data_management/importers/snapshotserengeti/make_per_season_SS_json.py +153 -0
- megadetector/data_management/importers/sulross_get_exif.py +65 -0
- megadetector/data_management/importers/timelapse_csv_set_to_json.py +490 -0
- megadetector/data_management/importers/ubc_to_json.py +399 -0
- megadetector/data_management/importers/umn_to_json.py +507 -0
- megadetector/data_management/importers/wellington_to_json.py +263 -0
- megadetector/data_management/importers/wi_to_json.py +442 -0
- megadetector/data_management/importers/zamba_results_to_md_results.py +181 -0
- megadetector/data_management/labelme_to_coco.py +547 -0
- megadetector/data_management/labelme_to_yolo.py +272 -0
- megadetector/data_management/lila/__init__.py +0 -0
- megadetector/data_management/lila/add_locations_to_island_camera_traps.py +97 -0
- megadetector/data_management/lila/add_locations_to_nacti.py +147 -0
- megadetector/data_management/lila/create_lila_blank_set.py +558 -0
- megadetector/data_management/lila/create_lila_test_set.py +152 -0
- megadetector/data_management/lila/create_links_to_md_results_files.py +106 -0
- megadetector/data_management/lila/download_lila_subset.py +178 -0
- megadetector/data_management/lila/generate_lila_per_image_labels.py +516 -0
- megadetector/data_management/lila/get_lila_annotation_counts.py +170 -0
- megadetector/data_management/lila/get_lila_image_counts.py +112 -0
- megadetector/data_management/lila/lila_common.py +300 -0
- megadetector/data_management/lila/test_lila_metadata_urls.py +132 -0
- megadetector/data_management/ocr_tools.py +870 -0
- megadetector/data_management/read_exif.py +809 -0
- megadetector/data_management/remap_coco_categories.py +84 -0
- megadetector/data_management/remove_exif.py +66 -0
- megadetector/data_management/rename_images.py +187 -0
- megadetector/data_management/resize_coco_dataset.py +189 -0
- megadetector/data_management/wi_download_csv_to_coco.py +247 -0
- megadetector/data_management/yolo_output_to_md_output.py +446 -0
- megadetector/data_management/yolo_to_coco.py +676 -0
- megadetector/detection/__init__.py +0 -0
- megadetector/detection/detector_training/__init__.py +0 -0
- megadetector/detection/detector_training/model_main_tf2.py +114 -0
- megadetector/detection/process_video.py +846 -0
- megadetector/detection/pytorch_detector.py +355 -0
- megadetector/detection/run_detector.py +779 -0
- megadetector/detection/run_detector_batch.py +1219 -0
- megadetector/detection/run_inference_with_yolov5_val.py +1087 -0
- megadetector/detection/run_tiled_inference.py +934 -0
- megadetector/detection/tf_detector.py +192 -0
- megadetector/detection/video_utils.py +698 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +64 -0
- megadetector/postprocessing/categorize_detections_by_size.py +165 -0
- megadetector/postprocessing/classification_postprocessing.py +716 -0
- megadetector/postprocessing/combine_api_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +966 -0
- megadetector/postprocessing/convert_output_format.py +396 -0
- megadetector/postprocessing/load_api_results.py +195 -0
- megadetector/postprocessing/md_to_coco.py +310 -0
- megadetector/postprocessing/md_to_labelme.py +330 -0
- megadetector/postprocessing/merge_detections.py +412 -0
- megadetector/postprocessing/postprocess_batch_results.py +1908 -0
- megadetector/postprocessing/remap_detection_categories.py +170 -0
- megadetector/postprocessing/render_detection_confusion_matrix.py +660 -0
- megadetector/postprocessing/repeat_detection_elimination/find_repeat_detections.py +211 -0
- megadetector/postprocessing/repeat_detection_elimination/remove_repeat_detections.py +83 -0
- megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +1635 -0
- megadetector/postprocessing/separate_detections_into_folders.py +730 -0
- megadetector/postprocessing/subset_json_detector_output.py +700 -0
- megadetector/postprocessing/top_folders_to_bottom.py +223 -0
- megadetector/taxonomy_mapping/__init__.py +0 -0
- megadetector/taxonomy_mapping/map_lila_taxonomy_to_wi_taxonomy.py +491 -0
- megadetector/taxonomy_mapping/map_new_lila_datasets.py +150 -0
- megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +142 -0
- megadetector/taxonomy_mapping/preview_lila_taxonomy.py +588 -0
- megadetector/taxonomy_mapping/retrieve_sample_image.py +71 -0
- megadetector/taxonomy_mapping/simple_image_download.py +219 -0
- megadetector/taxonomy_mapping/species_lookup.py +834 -0
- megadetector/taxonomy_mapping/taxonomy_csv_checker.py +159 -0
- megadetector/taxonomy_mapping/taxonomy_graph.py +346 -0
- megadetector/taxonomy_mapping/validate_lila_category_mappings.py +83 -0
- megadetector/utils/__init__.py +0 -0
- megadetector/utils/azure_utils.py +178 -0
- megadetector/utils/ct_utils.py +613 -0
- megadetector/utils/directory_listing.py +246 -0
- megadetector/utils/md_tests.py +1164 -0
- megadetector/utils/path_utils.py +1045 -0
- megadetector/utils/process_utils.py +160 -0
- megadetector/utils/sas_blob_utils.py +509 -0
- megadetector/utils/split_locations_into_train_val.py +228 -0
- megadetector/utils/string_utils.py +92 -0
- megadetector/utils/url_utils.py +323 -0
- megadetector/utils/write_html_image_list.py +225 -0
- megadetector/visualization/__init__.py +0 -0
- megadetector/visualization/plot_utils.py +293 -0
- megadetector/visualization/render_images_with_thumbnails.py +275 -0
- megadetector/visualization/visualization_utils.py +1536 -0
- megadetector/visualization/visualize_db.py +552 -0
- megadetector/visualization/visualize_detector_output.py +405 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/LICENSE +0 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/METADATA +2 -2
- megadetector-5.0.13.dist-info/RECORD +201 -0
- megadetector-5.0.13.dist-info/top_level.txt +1 -0
- megadetector-5.0.11.dist-info/RECORD +0 -5
- megadetector-5.0.11.dist-info/top_level.txt +0 -1
- {megadetector-5.0.11.dist-info → megadetector-5.0.13.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
cct_json_utils.py
|
|
4
|
+
|
|
5
|
+
Utilities for working with COCO Camera Traps .json databases:
|
|
6
|
+
|
|
7
|
+
https://github.com/agentmorris/MegaDetector/blob/main/megadetector/data_management/README.md#coco-cameratraps-format
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
#%% Constants and imports
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
from tqdm import tqdm
|
|
17
|
+
from collections import defaultdict, OrderedDict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
#%% Classes
|
|
21
|
+
|
|
22
|
+
class CameraTrapJsonUtils:
|
|
23
|
+
"""
|
|
24
|
+
Miscellaneous utility functions for working with COCO Camera Traps databases
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def annotations_to_string(annotations, cat_id_to_name):
|
|
29
|
+
"""
|
|
30
|
+
Given a list of annotations and a mapping from class IDs to names, produces
|
|
31
|
+
a comma-delimited string containing a list of class names, sorted alphabetically.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
annotations (list): a list of annotation dicts
|
|
35
|
+
cat_id_to_name (dict): a dict mapping category IDs to category names
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
str: a comma-delimited list of class names
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
class_names = CameraTrapJsonUtils.annotations_to_class_names(annotations, cat_id_to_name)
|
|
42
|
+
return ','.join(class_names)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def annotations_to_class_names(annotations, cat_id_to_name):
|
|
47
|
+
"""
|
|
48
|
+
Given a list of annotations and a mapping from class IDs to names, produces
|
|
49
|
+
a list of class names, sorted alphabetically.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
annotations (list): a list of annotation dicts
|
|
53
|
+
cat_id_to_name (dict): a dict mapping category IDs to category names
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
list: a list of class names present in [annotations]
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Collect all names
|
|
60
|
+
class_names = [cat_id_to_name[ann['category_id']] for ann in annotations]
|
|
61
|
+
# Make names unique and sort
|
|
62
|
+
class_names = sorted(set(class_names))
|
|
63
|
+
return class_names
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def order_db_keys(db):
|
|
68
|
+
"""
|
|
69
|
+
Given a dict representing a JSON database in the COCO Camera Trap
|
|
70
|
+
format, returns an OrderedDict with keys in the order of 'info',
|
|
71
|
+
'categories', 'annotations' and 'images'. When this OrderedDict is
|
|
72
|
+
serialized with json.dump(), the order of the keys are preserved.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
db (dict): a JSON database in the COCO Camera Trap format
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
dict: the same content as [db] but as an OrderedDict with keys ordered for
|
|
79
|
+
readability
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
ordered = OrderedDict([
|
|
83
|
+
('info', db['info']),
|
|
84
|
+
('categories', db['categories']),
|
|
85
|
+
('annotations', db['annotations']),
|
|
86
|
+
('images', db['images'])])
|
|
87
|
+
return ordered
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def group_annotations_by_image_field(db_indexed, image_field='seq_id'):
|
|
92
|
+
"""
|
|
93
|
+
Given an instance of IndexedJsonDb, group annotation entries by a field in the
|
|
94
|
+
image entry. Typically used to find all the annotations associated with a sequence.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
db_indexed (IndexedJsonDb): an initialized IndexedJsonDb, typically loaded from a
|
|
98
|
+
COCO Camera Traps .json file
|
|
99
|
+
image_field (str, optional): a field by which to group annotations (defaults
|
|
100
|
+
to 'seq_id')
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
dict: a dict mapping objects (typically strings, in fact typically sequence IDs) to
|
|
104
|
+
lists of annotations
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
image_id_to_image_field = {}
|
|
108
|
+
for image_id, image_entry in db_indexed.image_id_to_image.items():
|
|
109
|
+
image_id_to_image_field[image_id] = image_entry[image_field]
|
|
110
|
+
|
|
111
|
+
res = defaultdict(list)
|
|
112
|
+
for annotations in db_indexed.image_id_to_annotations.values():
|
|
113
|
+
for annotation_entry in annotations:
|
|
114
|
+
field_value = image_id_to_image_field[annotation_entry['image_id']]
|
|
115
|
+
res[field_value].append(annotation_entry)
|
|
116
|
+
return res
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def get_entries_for_locations(db, locations):
|
|
121
|
+
"""
|
|
122
|
+
Given a dict representing a JSON database in the COCO Camera Trap format, returns a dict
|
|
123
|
+
with the 'images' and 'annotations' fields in the CCT format, each is an array that only
|
|
124
|
+
includes entries in the original [db] that are in the [locations] set.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
db (dict): a dict representing a JSON database in the COCO Camera Trap format
|
|
128
|
+
locations (set): a set or list of locations to include; each item is a string
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
dict: a dict with the 'images' and 'annotations' fields in the CCT format
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
locations = set(locations)
|
|
135
|
+
print('Original DB has {} image and {} annotation entries.'.format(
|
|
136
|
+
len(db['images']), len(db['annotations'])))
|
|
137
|
+
new_db = { 'images': [], 'annotations': [] }
|
|
138
|
+
new_images = set()
|
|
139
|
+
for i in db['images']:
|
|
140
|
+
# cast location to string as the entries in locations are strings
|
|
141
|
+
if str(i['location']) in locations:
|
|
142
|
+
new_db['images'].append(i)
|
|
143
|
+
new_images.add(i['id'])
|
|
144
|
+
for a in db['annotations']:
|
|
145
|
+
if a['image_id'] in new_images:
|
|
146
|
+
new_db['annotations'].append(a)
|
|
147
|
+
print(
|
|
148
|
+
'New DB has {} image and {} annotation entries.'.format(
|
|
149
|
+
len(new_db['images']), len(new_db['annotations'])))
|
|
150
|
+
return new_db
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class IndexedJsonDb:
|
|
154
|
+
"""
|
|
155
|
+
Wrapper for a COCO Camera Traps database.
|
|
156
|
+
|
|
157
|
+
Handles boilerplate dictionary creation that we do almost every time we load
|
|
158
|
+
a .json database.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self,
|
|
162
|
+
json_filename,
|
|
163
|
+
b_normalize_paths=False,
|
|
164
|
+
filename_replacements=None,
|
|
165
|
+
b_convert_classes_to_lower=True,
|
|
166
|
+
b_force_forward_slashes=True):
|
|
167
|
+
"""
|
|
168
|
+
Constructor for IndexedJsonDb that loads from a .json file or CCT-formatted dict.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
json_filename (str): filename to load, or an already-loaded dict
|
|
172
|
+
b_normalize_paths (bool, optional): whether to invoke os.path.normpath on
|
|
173
|
+
all filenames. Not relevant if b_force_forward_slashes is True.
|
|
174
|
+
filename_replacements (dict, optional): a set of string --> string mappings
|
|
175
|
+
that will trigger replacements in all filenames, typically used to remove
|
|
176
|
+
leading folders
|
|
177
|
+
b_convert_classes_to_lower (bool, optional): whether to convert all class
|
|
178
|
+
names to lowercase
|
|
179
|
+
b_force_forward_slashes (bool, optional): whether to convert backslashes to
|
|
180
|
+
forward slashes in all path names
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
if isinstance(json_filename, str):
|
|
184
|
+
with open(json_filename) as f:
|
|
185
|
+
self.db = json.load(f)
|
|
186
|
+
else:
|
|
187
|
+
self.db = json_filename
|
|
188
|
+
|
|
189
|
+
assert 'images' in self.db, (
|
|
190
|
+
f'Could not find image list in file {json_filename}, are you sure '
|
|
191
|
+
'this is a COCO camera traps file?')
|
|
192
|
+
|
|
193
|
+
if b_convert_classes_to_lower:
|
|
194
|
+
# Convert classnames to lowercase to simplify comparisons later
|
|
195
|
+
for c in self.db['categories']:
|
|
196
|
+
c['name'] = c['name'].lower()
|
|
197
|
+
|
|
198
|
+
# Normalize paths to simplify comparisons later
|
|
199
|
+
if b_normalize_paths:
|
|
200
|
+
for im in self.db['images']:
|
|
201
|
+
im['file_name'] = os.path.normpath(im['file_name'])
|
|
202
|
+
|
|
203
|
+
if b_force_forward_slashes:
|
|
204
|
+
for im in self.db['images']:
|
|
205
|
+
im['file_name'] = im['file_name'].replace('\\','/')
|
|
206
|
+
|
|
207
|
+
if filename_replacements is not None:
|
|
208
|
+
for s in filename_replacements:
|
|
209
|
+
# Make custom replacements in filenames, typically used to
|
|
210
|
+
# accommodate changes in root paths after DB construction
|
|
211
|
+
r = filename_replacements[s]
|
|
212
|
+
for im in self.db['images']:
|
|
213
|
+
im['file_name'] = im['file_name'].replace(s, r)
|
|
214
|
+
|
|
215
|
+
### Build useful mappings to facilitate working with the DB
|
|
216
|
+
|
|
217
|
+
# Category ID <--> name
|
|
218
|
+
self.cat_id_to_name = {
|
|
219
|
+
cat['id']: cat['name'] for cat in self.db['categories']}
|
|
220
|
+
self.cat_name_to_id = {
|
|
221
|
+
cat['name']: cat['id'] for cat in self.db['categories']}
|
|
222
|
+
|
|
223
|
+
# Image filename --> ID
|
|
224
|
+
self.filename_to_id = {
|
|
225
|
+
im['file_name']: im['id'] for im in self.db['images']}
|
|
226
|
+
|
|
227
|
+
# Image ID --> image object
|
|
228
|
+
self.image_id_to_image = {im['id']: im for im in self.db['images']}
|
|
229
|
+
|
|
230
|
+
# Image ID --> annotations
|
|
231
|
+
# Each image can potentially multiple annotations, hence using lists
|
|
232
|
+
self.image_id_to_annotations = {}
|
|
233
|
+
self.image_id_to_annotations = defaultdict(list)
|
|
234
|
+
for ann in self.db['annotations']:
|
|
235
|
+
self.image_id_to_annotations[ann['image_id']].append(ann)
|
|
236
|
+
|
|
237
|
+
# ...__init__
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_annotations_for_image(self, image):
|
|
241
|
+
"""
|
|
242
|
+
Finds all the annnotations associated with the image dict [image].
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
image (dict): an image dict loaded from a CCT .json file. Only the 'id' field
|
|
246
|
+
is used.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
list: list of annotations associated with this image. Returns None if the db
|
|
250
|
+
has not been loaded, or [] if no annotations are available for this image.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
if self.db is None:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
if image['id'] not in self.image_id_to_annotations:
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
image_annotations = self.image_id_to_annotations[image['id']]
|
|
260
|
+
return image_annotations
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def get_classes_for_image(self, image):
|
|
264
|
+
"""
|
|
265
|
+
Returns a list of class names associated with [image].
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
image (dict): an image dict loaded from a CCT .json file. Only the 'id' field
|
|
269
|
+
is used.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
list: list of class names associated with this image. Returns None if the db
|
|
273
|
+
has not been loaded, or [] if no annotations are available for this image.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
if self.db is None:
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
if image['id'] not in self.image_id_to_annotations:
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
class_ids = []
|
|
283
|
+
image_annotations = self.image_id_to_annotations[image['id']]
|
|
284
|
+
for ann in image_annotations:
|
|
285
|
+
class_ids.append(ann['category_id'])
|
|
286
|
+
class_ids = sorted(set(class_ids))
|
|
287
|
+
class_names = [self.cat_id_to_name[x] for x in class_ids]
|
|
288
|
+
|
|
289
|
+
return class_names
|
|
290
|
+
|
|
291
|
+
# ...class IndexedJsonDb
|
|
292
|
+
|
|
293
|
+
class SequenceOptions:
|
|
294
|
+
"""
|
|
295
|
+
Options parameterizing the grouping of images into sequences by time.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(self):
|
|
299
|
+
#: Images separated by <= this duration will be grouped into the same sequence.
|
|
300
|
+
self.episode_interval_seconds = 60.0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
#%% Functions
|
|
304
|
+
|
|
305
|
+
def create_sequences(image_info,options=None):
|
|
306
|
+
"""
|
|
307
|
+
Synthesizes episodes/sequences/bursts for the images in [image_info].
|
|
308
|
+
|
|
309
|
+
Modifies [image_info] in place, populating the 'seq_id', 'seq_num_frames', and 'frame_num'
|
|
310
|
+
fields for each image.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
image_info (str, dict, or list): a dict in CCT format, a CCT .json file, or just the 'images' component
|
|
314
|
+
of a CCT dataset (a list of dicts with fields 'file_name' (str), 'datetime' (datetime), and
|
|
315
|
+
'location' (str)).
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
if options is None:
|
|
319
|
+
options = SequenceOptions()
|
|
320
|
+
|
|
321
|
+
if isinstance(image_info,str):
|
|
322
|
+
with open(image_info,'r') as f:
|
|
323
|
+
image_info = json.load(f)
|
|
324
|
+
|
|
325
|
+
if isinstance(image_info,dict):
|
|
326
|
+
image_info = image_info['images']
|
|
327
|
+
|
|
328
|
+
# Find all unique locations
|
|
329
|
+
locations = set()
|
|
330
|
+
for im in image_info:
|
|
331
|
+
locations.add(im['location'])
|
|
332
|
+
|
|
333
|
+
print('Found {} locations'.format(len(locations)))
|
|
334
|
+
locations = list(locations)
|
|
335
|
+
locations.sort()
|
|
336
|
+
|
|
337
|
+
all_sequences = set()
|
|
338
|
+
|
|
339
|
+
# i_location = 0; location = locations[i_location]
|
|
340
|
+
for i_location,location in tqdm(enumerate(locations),total=len(locations)):
|
|
341
|
+
|
|
342
|
+
images_this_location = [im for im in image_info if im['location'] == location]
|
|
343
|
+
|
|
344
|
+
# Sorting datetimes fails when there are None's in the list. So instead of sorting datetimes
|
|
345
|
+
# directly, sort tuples with a boolean for none-ness, then the datetime itself.
|
|
346
|
+
#
|
|
347
|
+
# https://stackoverflow.com/questions/18411560/sort-list-while-pushing-none-values-to-the-end
|
|
348
|
+
sorted_images_this_location = sorted(images_this_location,
|
|
349
|
+
key = lambda im: (im['datetime'] is None,im['datetime']))
|
|
350
|
+
|
|
351
|
+
sequence_id_to_images_this_location = defaultdict(list)
|
|
352
|
+
|
|
353
|
+
current_sequence_id = None
|
|
354
|
+
next_frame_number = 0
|
|
355
|
+
next_sequence_number = 0
|
|
356
|
+
previous_datetime = None
|
|
357
|
+
|
|
358
|
+
# previous_datetime = sorted_images_this_location[0]['datetime']
|
|
359
|
+
# im = sorted_images_this_location[1]
|
|
360
|
+
for im in sorted_images_this_location:
|
|
361
|
+
|
|
362
|
+
invalid_datetime = False
|
|
363
|
+
|
|
364
|
+
if previous_datetime is None:
|
|
365
|
+
delta = None
|
|
366
|
+
elif im['datetime'] is None:
|
|
367
|
+
invalid_datetime = True
|
|
368
|
+
else:
|
|
369
|
+
delta = (im['datetime'] - previous_datetime).total_seconds()
|
|
370
|
+
|
|
371
|
+
# Start a new sequence if necessary, including the case where this datetime is invalid
|
|
372
|
+
if delta is None or delta > options.episode_interval_seconds or invalid_datetime:
|
|
373
|
+
next_frame_number = 0
|
|
374
|
+
current_sequence_id = 'location_{}_sequence_index_{}'.format(
|
|
375
|
+
location,str(next_sequence_number).zfill(5))
|
|
376
|
+
next_sequence_number = next_sequence_number + 1
|
|
377
|
+
assert current_sequence_id not in all_sequences
|
|
378
|
+
all_sequences.add(current_sequence_id)
|
|
379
|
+
|
|
380
|
+
im['seq_id'] = current_sequence_id
|
|
381
|
+
im['seq_num_frames'] = None
|
|
382
|
+
im['frame_num'] = next_frame_number
|
|
383
|
+
sequence_id_to_images_this_location[current_sequence_id].append(im)
|
|
384
|
+
next_frame_number = next_frame_number + 1
|
|
385
|
+
|
|
386
|
+
# If this was an invalid datetime, this will record the previous datetime
|
|
387
|
+
# as None, which will force the next image to start a new sequence.
|
|
388
|
+
previous_datetime = im['datetime']
|
|
389
|
+
|
|
390
|
+
# ...for each image in this location
|
|
391
|
+
|
|
392
|
+
# Fill in seq_num_frames
|
|
393
|
+
for seq_id in sequence_id_to_images_this_location.keys():
|
|
394
|
+
assert seq_id in sequence_id_to_images_this_location
|
|
395
|
+
images_this_sequence = sequence_id_to_images_this_location[seq_id]
|
|
396
|
+
assert len(images_this_sequence) > 0
|
|
397
|
+
for im in images_this_sequence:
|
|
398
|
+
im['seq_num_frames'] = len(images_this_sequence)
|
|
399
|
+
|
|
400
|
+
# ...for each location
|
|
401
|
+
|
|
402
|
+
print('Created {} sequences from {} images'.format(len(all_sequences),len(image_info)))
|
|
403
|
+
|
|
404
|
+
# ...create_sequences()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
cct_to_md.py
|
|
4
|
+
|
|
5
|
+
"Converts" a COCO Camera Traps file to a MD results file. Currently ignores
|
|
6
|
+
non-bounding-box annotations, and gives all annotations a confidence of 1.0.
|
|
7
|
+
|
|
8
|
+
The only reason to do this is if you are going to add information to an existing
|
|
9
|
+
CCT-formatted dataset, and you want to do that in Timelapse.
|
|
10
|
+
|
|
11
|
+
Currently assumes that width and height are present in the input data, does not
|
|
12
|
+
read them from images.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
#%% Constants and imports
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from tqdm import tqdm
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#%% Functions
|
|
26
|
+
|
|
27
|
+
def cct_to_md(input_filename,output_filename=None):
|
|
28
|
+
"""
|
|
29
|
+
"Converts" a COCO Camera Traps file to a MD results file. Currently ignores
|
|
30
|
+
non-bounding-box annotations, and gives all annotations a confidence of 1.0.
|
|
31
|
+
|
|
32
|
+
The only reason to do this is if you are going to add information to an existing
|
|
33
|
+
CCT-formatted dataset, and you want to do that in Timelapse.
|
|
34
|
+
|
|
35
|
+
Currently assumes that width and height are present in the input data, does not
|
|
36
|
+
read them from images.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
input_filename (str): the COCO Camera Traps .json file to read
|
|
40
|
+
output_filename (str, optional): the .json file to write in MD results format
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
dict: MD-formatted results, identical to the content of [output_filename] if
|
|
44
|
+
[output_filename] is not None
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
## Validate input
|
|
48
|
+
|
|
49
|
+
assert os.path.isfile(input_filename)
|
|
50
|
+
|
|
51
|
+
if (output_filename is None):
|
|
52
|
+
|
|
53
|
+
tokens = os.path.splitext(input_filename)
|
|
54
|
+
assert len(tokens) == 2
|
|
55
|
+
output_filename = tokens[0] + '_md-format' + tokens[1]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Read input
|
|
59
|
+
|
|
60
|
+
with open(input_filename,'r') as f:
|
|
61
|
+
d = json.load(f)
|
|
62
|
+
|
|
63
|
+
for s in ['annotations','images','categories']:
|
|
64
|
+
assert s in d.keys(), 'Cannot find category {} in input file, is this a CCT file?'.format(s)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Prepare metadata
|
|
68
|
+
|
|
69
|
+
image_id_to_annotations = defaultdict(list)
|
|
70
|
+
|
|
71
|
+
# ann = d['annotations'][0]
|
|
72
|
+
for ann in tqdm(d['annotations']):
|
|
73
|
+
image_id_to_annotations[ann['image_id']].append(ann)
|
|
74
|
+
|
|
75
|
+
category_id_to_name = {}
|
|
76
|
+
for cat in d['categories']:
|
|
77
|
+
category_id_to_name[str(cat['id'])] = cat['name']
|
|
78
|
+
|
|
79
|
+
results = {}
|
|
80
|
+
|
|
81
|
+
info = {}
|
|
82
|
+
info['format_version'] = "1.3"
|
|
83
|
+
info['detector'] = 'cct_to_md'
|
|
84
|
+
results['info'] = info
|
|
85
|
+
results['detection_categories'] = category_id_to_name
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Process images
|
|
89
|
+
|
|
90
|
+
images_out = []
|
|
91
|
+
|
|
92
|
+
# im = d['images'][0]
|
|
93
|
+
for im in tqdm(d['images']):
|
|
94
|
+
|
|
95
|
+
im_out = {}
|
|
96
|
+
im_out['file'] = im['file_name']
|
|
97
|
+
im_out['location'] = im['location']
|
|
98
|
+
im_out['id'] = im['id']
|
|
99
|
+
|
|
100
|
+
image_h = im['height']
|
|
101
|
+
image_w = im['width']
|
|
102
|
+
|
|
103
|
+
detections = []
|
|
104
|
+
|
|
105
|
+
annotations_this_image = image_id_to_annotations[im['id']]
|
|
106
|
+
|
|
107
|
+
# This field is no longer included in MD output files by default
|
|
108
|
+
# max_detection_conf = 0
|
|
109
|
+
|
|
110
|
+
for ann in annotations_this_image:
|
|
111
|
+
|
|
112
|
+
if 'bbox' in ann:
|
|
113
|
+
|
|
114
|
+
det = {}
|
|
115
|
+
det['category'] = str(ann['category_id'])
|
|
116
|
+
det['conf'] = 1.0
|
|
117
|
+
# max_detection_conf = 1.0
|
|
118
|
+
|
|
119
|
+
# MegaDetector: [x,y,width,height] (normalized, origin upper-left)
|
|
120
|
+
# CCT: [x,y,width,height] (absolute, origin upper-left)
|
|
121
|
+
bbox_in = ann['bbox']
|
|
122
|
+
bbox_out = [bbox_in[0]/image_w,bbox_in[1]/image_h,
|
|
123
|
+
bbox_in[2]/image_w,bbox_in[3]/image_h]
|
|
124
|
+
det['bbox'] = bbox_out
|
|
125
|
+
detections.append(det)
|
|
126
|
+
|
|
127
|
+
# ...if there's a bounding box
|
|
128
|
+
|
|
129
|
+
# ...for each annotation
|
|
130
|
+
|
|
131
|
+
im_out['detections'] = detections
|
|
132
|
+
|
|
133
|
+
# This field is no longer included in MD output files by default
|
|
134
|
+
# im_out['max_detection_conf'] = max_detection_conf
|
|
135
|
+
|
|
136
|
+
images_out.append(im_out)
|
|
137
|
+
|
|
138
|
+
# ...for each image
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## Write output
|
|
142
|
+
|
|
143
|
+
results['images'] = images_out
|
|
144
|
+
|
|
145
|
+
with open(output_filename,'w') as f:
|
|
146
|
+
json.dump(results, f, indent=1)
|
|
147
|
+
|
|
148
|
+
return output_filename
|
|
149
|
+
|
|
150
|
+
# ...cct_to_md()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
#%% Interactive driver
|
|
154
|
+
|
|
155
|
+
if False:
|
|
156
|
+
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
#%%
|
|
160
|
+
|
|
161
|
+
input_filename = r"G:\temp\noaa_estuary_fish.json"
|
|
162
|
+
output_filename = None
|
|
163
|
+
output_filename = cct_to_md(input_filename,output_filename)
|
|
164
|
+
|
|
165
|
+
#%%
|
|
166
|
+
|
|
167
|
+
from megadetector.visualization import visualize_detector_output
|
|
168
|
+
|
|
169
|
+
visualize_detector_output.visualize_detector_output(
|
|
170
|
+
detector_output_path=output_filename,
|
|
171
|
+
out_dir=r'g:\temp\fish_output',
|
|
172
|
+
images_dir=r'g:\temp\noaa_estuary_fish-images\JPEGImages',
|
|
173
|
+
output_image_width=-1,
|
|
174
|
+
sample=100,
|
|
175
|
+
render_detections_only=True)
|
|
176
|
+
|