megadetector 5.0.11__py3-none-any.whl → 5.0.12__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 +98 -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 +152 -0
- megadetector/api/batch_processing/api_core/server_orchestration.py +360 -0
- megadetector/api/batch_processing/api_core/server_utils.py +92 -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 +126 -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 +266 -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 +610 -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 +239 -0
- megadetector/data_management/cct_json_utils.py +395 -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 +272 -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 +477 -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 +796 -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 +874 -0
- megadetector/data_management/read_exif.py +681 -0
- megadetector/data_management/remap_coco_categories.py +84 -0
- megadetector/data_management/remove_exif.py +66 -0
- megadetector/data_management/resize_coco_dataset.py +189 -0
- megadetector/data_management/wi_download_csv_to_coco.py +246 -0
- megadetector/data_management/yolo_output_to_md_output.py +441 -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 +702 -0
- megadetector/detection/pytorch_detector.py +341 -0
- megadetector/detection/run_detector.py +779 -0
- megadetector/detection/run_detector_batch.py +1219 -0
- megadetector/detection/run_inference_with_yolov5_val.py +917 -0
- megadetector/detection/run_tiled_inference.py +934 -0
- megadetector/detection/tf_detector.py +189 -0
- megadetector/detection/video_utils.py +606 -0
- megadetector/postprocessing/__init__.py +0 -0
- megadetector/postprocessing/add_max_conf.py +64 -0
- megadetector/postprocessing/categorize_detections_by_size.py +163 -0
- megadetector/postprocessing/combine_api_outputs.py +249 -0
- megadetector/postprocessing/compare_batch_results.py +958 -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 +401 -0
- megadetector/postprocessing/postprocess_batch_results.py +1902 -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 +1631 -0
- megadetector/postprocessing/separate_detections_into_folders.py +730 -0
- megadetector/postprocessing/subset_json_detector_output.py +696 -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 +590 -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 +612 -0
- megadetector/utils/directory_listing.py +246 -0
- megadetector/utils/md_tests.py +968 -0
- megadetector/utils/path_utils.py +1044 -0
- megadetector/utils/process_utils.py +157 -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 +550 -0
- megadetector/visualization/visualize_detector_output.py +405 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/METADATA +1 -1
- megadetector-5.0.12.dist-info/RECORD +199 -0
- megadetector-5.0.12.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.12.dist-info}/LICENSE +0 -0
- {megadetector-5.0.11.dist-info → megadetector-5.0.12.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
visualize_db.py
|
|
4
|
+
|
|
5
|
+
Outputs an HTML page visualizing annotations (class labels and/or bounding boxes)
|
|
6
|
+
on a sample of images in a database in the COCO Camera Traps format.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
#%% Imports
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import inspect
|
|
14
|
+
import random
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
import pandas as pd
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
import humanfriendly
|
|
25
|
+
|
|
26
|
+
from itertools import compress
|
|
27
|
+
from multiprocessing.pool import ThreadPool
|
|
28
|
+
from multiprocessing.pool import Pool
|
|
29
|
+
from tqdm import tqdm
|
|
30
|
+
|
|
31
|
+
from megadetector.utils.write_html_image_list import write_html_image_list
|
|
32
|
+
from megadetector.data_management.cct_json_utils import IndexedJsonDb
|
|
33
|
+
from megadetector.visualization import visualization_utils as vis_utils
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#%% Settings
|
|
37
|
+
|
|
38
|
+
class DbVizOptions:
|
|
39
|
+
"""
|
|
40
|
+
Parameters controlling the behavior of visualize_db().
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
#: Number of images to sample from the database, or None to visualize all images
|
|
44
|
+
num_to_visualize = None
|
|
45
|
+
|
|
46
|
+
#: Target size for rendering; set either dimension to -1 to preserve aspect ratio.
|
|
47
|
+
#:
|
|
48
|
+
#: If viz_size is None or (-1,-1), the original image size is used.
|
|
49
|
+
viz_size = (800, -1)
|
|
50
|
+
|
|
51
|
+
#: HTML rendering options; see write_html_image_list for details
|
|
52
|
+
#:
|
|
53
|
+
#:The most relevant option one might want to set here is:
|
|
54
|
+
#:
|
|
55
|
+
#: htmlOptions['maxFiguresPerHtmlFile']
|
|
56
|
+
#:
|
|
57
|
+
#: ...which can be used to paginate previews to a number of images that will load well
|
|
58
|
+
#: in a browser (5000 is a reasonable limit).
|
|
59
|
+
htmlOptions = write_html_image_list()
|
|
60
|
+
|
|
61
|
+
#: Whether to sort images by filename (True) or randomly (False)
|
|
62
|
+
sort_by_filename = True
|
|
63
|
+
|
|
64
|
+
#: Only show images that contain bounding boxes
|
|
65
|
+
trim_to_images_with_bboxes = False
|
|
66
|
+
|
|
67
|
+
#: Random seed to use for sampling images
|
|
68
|
+
random_seed = 0
|
|
69
|
+
|
|
70
|
+
#: Should we include Web search links for each category name?
|
|
71
|
+
add_search_links = False
|
|
72
|
+
|
|
73
|
+
#: Should each thumbnail image link back to the original image?
|
|
74
|
+
include_image_links = False
|
|
75
|
+
|
|
76
|
+
#: Should there be a text link back to each original image?
|
|
77
|
+
include_filename_links = False
|
|
78
|
+
|
|
79
|
+
#: Line width in pixels
|
|
80
|
+
box_thickness = 4
|
|
81
|
+
|
|
82
|
+
#: Number of pixels to expand each bounding box
|
|
83
|
+
box_expansion = 0
|
|
84
|
+
|
|
85
|
+
#: Only include images that contain annotations with these class names (not IDs)
|
|
86
|
+
#:
|
|
87
|
+
#: Mutually exclusive with classes_to_exclude
|
|
88
|
+
classes_to_include = None
|
|
89
|
+
|
|
90
|
+
#: Exclude images that contain annotations with these class names (not IDs)
|
|
91
|
+
#:
|
|
92
|
+
#: Mutually exclusive with classes_to_include
|
|
93
|
+
classes_to_exclude = None
|
|
94
|
+
|
|
95
|
+
#: Special tag used to say "show me all images with multiple categories"
|
|
96
|
+
#:
|
|
97
|
+
#: :meta private:
|
|
98
|
+
multiple_categories_tag = '*multiple*'
|
|
99
|
+
|
|
100
|
+
#: We sometimes flatten image directories by replacing a path separator with
|
|
101
|
+
#: another character. Leave blank for the typical case where this isn't necessary.
|
|
102
|
+
pathsep_replacement = '' # '~'
|
|
103
|
+
|
|
104
|
+
#: Parallelize rendering across multiple workers
|
|
105
|
+
parallelize_rendering = False
|
|
106
|
+
|
|
107
|
+
#: In theory, whether to parallelize with threads (True) or processes (False), but
|
|
108
|
+
#: process-based parallelization in this function is currently unsupported
|
|
109
|
+
parallelize_rendering_with_threads = True
|
|
110
|
+
|
|
111
|
+
#: Number of workers to use for parallelization; ignored if parallelize_rendering
|
|
112
|
+
#: is False
|
|
113
|
+
parallelize_rendering_n_cores = 25
|
|
114
|
+
|
|
115
|
+
#: Should we show absolute (True) or relative (False) paths for each image?
|
|
116
|
+
show_full_paths = False
|
|
117
|
+
|
|
118
|
+
#: Set to False to skip existing images
|
|
119
|
+
force_rendering = True
|
|
120
|
+
|
|
121
|
+
#: Enable additionald debug console output
|
|
122
|
+
verbose = False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
#%% Helper functions
|
|
126
|
+
|
|
127
|
+
def _image_filename_to_path(image_file_name, image_base_dir, pathsep_replacement=''):
|
|
128
|
+
"""
|
|
129
|
+
Translates the file name in an image entry in the json database to a path, possibly doing
|
|
130
|
+
some manipulation of path separators.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
if len(pathsep_replacement) > 0:
|
|
134
|
+
image_file_name = os.path.normpath(image_file_name).replace(os.pathsep,pathsep_replacement)
|
|
135
|
+
return os.path.join(image_base_dir, image_file_name)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
#%% Core functions
|
|
139
|
+
|
|
140
|
+
def visualize_db(db_path, output_dir, image_base_dir, options=None):
|
|
141
|
+
"""
|
|
142
|
+
Writes images and html to output_dir to visualize the annotations in a .json file.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
db_path (str or dict): the .json filename to load, or a previously-loaded database
|
|
146
|
+
image_base_dir (str): the folder where the images live; filenames in [db_path] should
|
|
147
|
+
be relative to this folder.
|
|
148
|
+
options (DbVizOptions, optional): See DbVizOptions for details
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
tuple: A length-two tuple containing (the html filename) and (the loaded database).
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
if options is None:
|
|
155
|
+
options = DbVizOptions()
|
|
156
|
+
|
|
157
|
+
if not options.parallelize_rendering_with_threads:
|
|
158
|
+
print('Warning: process-based parallelization is not yet supported by visualize_db')
|
|
159
|
+
options.parallelize_rendering_with_threads = True
|
|
160
|
+
|
|
161
|
+
if image_base_dir.startswith('http'):
|
|
162
|
+
if not image_base_dir.endswith('/'):
|
|
163
|
+
image_base_dir += '/'
|
|
164
|
+
else:
|
|
165
|
+
assert(os.path.isdir(image_base_dir))
|
|
166
|
+
|
|
167
|
+
os.makedirs(os.path.join(output_dir, 'rendered_images'), exist_ok=True)
|
|
168
|
+
|
|
169
|
+
if isinstance(db_path,str):
|
|
170
|
+
assert(os.path.isfile(db_path))
|
|
171
|
+
print('Loading database from {}...'.format(db_path))
|
|
172
|
+
image_db = json.load(open(db_path))
|
|
173
|
+
print('...done')
|
|
174
|
+
elif isinstance(db_path,dict):
|
|
175
|
+
print('Using previously-loaded DB')
|
|
176
|
+
image_db = db_path
|
|
177
|
+
else:
|
|
178
|
+
raise ValueError('Illegal dictionary or filename')
|
|
179
|
+
|
|
180
|
+
annotations = image_db['annotations']
|
|
181
|
+
images = image_db['images']
|
|
182
|
+
categories = image_db['categories']
|
|
183
|
+
|
|
184
|
+
# Optionally remove all images without bounding boxes, *before* sampling
|
|
185
|
+
if options.trim_to_images_with_bboxes:
|
|
186
|
+
|
|
187
|
+
bHasBbox = [False] * len(annotations)
|
|
188
|
+
for iAnn,ann in enumerate(annotations):
|
|
189
|
+
if 'bbox' in ann:
|
|
190
|
+
assert isinstance(ann['bbox'],list)
|
|
191
|
+
bHasBbox[iAnn] = True
|
|
192
|
+
annotationsWithBboxes = list(compress(annotations, bHasBbox))
|
|
193
|
+
|
|
194
|
+
imageIDsWithBboxes = [x['image_id'] for x in annotationsWithBboxes]
|
|
195
|
+
imageIDsWithBboxes = set(imageIDsWithBboxes)
|
|
196
|
+
|
|
197
|
+
bImageHasBbox = [False] * len(images)
|
|
198
|
+
for iImage,image in enumerate(images):
|
|
199
|
+
imageID = image['id']
|
|
200
|
+
if imageID in imageIDsWithBboxes:
|
|
201
|
+
bImageHasBbox[iImage] = True
|
|
202
|
+
imagesWithBboxes = list(compress(images, bImageHasBbox))
|
|
203
|
+
images = imagesWithBboxes
|
|
204
|
+
|
|
205
|
+
# Optionally include/remove images with specific labels, *before* sampling
|
|
206
|
+
|
|
207
|
+
assert (not ((options.classes_to_exclude is not None) and \
|
|
208
|
+
(options.classes_to_include is not None))), \
|
|
209
|
+
'Cannot specify an inclusion and exclusion list'
|
|
210
|
+
|
|
211
|
+
if (options.classes_to_exclude is not None) or (options.classes_to_include is not None):
|
|
212
|
+
|
|
213
|
+
print('Indexing database')
|
|
214
|
+
indexed_db = IndexedJsonDb(image_db)
|
|
215
|
+
bValidClass = [True] * len(images)
|
|
216
|
+
for iImage,image in enumerate(images):
|
|
217
|
+
classes = indexed_db.get_classes_for_image(image)
|
|
218
|
+
if options.classes_to_exclude is not None:
|
|
219
|
+
for excludedClass in options.classes_to_exclude:
|
|
220
|
+
if excludedClass in classes:
|
|
221
|
+
bValidClass[iImage] = False
|
|
222
|
+
break
|
|
223
|
+
elif options.classes_to_include is not None:
|
|
224
|
+
bValidClass[iImage] = False
|
|
225
|
+
if options.multiple_categories_tag in options.classes_to_include:
|
|
226
|
+
if len(classes) > 1:
|
|
227
|
+
bValidClass[iImage] = True
|
|
228
|
+
if not bValidClass[iImage]:
|
|
229
|
+
for c in classes:
|
|
230
|
+
if c in options.classes_to_include:
|
|
231
|
+
bValidClass[iImage] = True
|
|
232
|
+
break
|
|
233
|
+
else:
|
|
234
|
+
raise ValueError('Illegal include/exclude combination')
|
|
235
|
+
|
|
236
|
+
imagesWithValidClasses = list(compress(images, bValidClass))
|
|
237
|
+
images = imagesWithValidClasses
|
|
238
|
+
|
|
239
|
+
# ...if we need to include/exclude categories
|
|
240
|
+
|
|
241
|
+
# Put the annotations in a dataframe so we can select all annotations for a given image
|
|
242
|
+
print('Creating data frames')
|
|
243
|
+
df_anno = pd.DataFrame(annotations)
|
|
244
|
+
df_img = pd.DataFrame(images)
|
|
245
|
+
|
|
246
|
+
# Construct label map
|
|
247
|
+
label_map = {}
|
|
248
|
+
for cat in categories:
|
|
249
|
+
label_map[int(cat['id'])] = cat['name']
|
|
250
|
+
|
|
251
|
+
# Take a sample of images
|
|
252
|
+
if options.num_to_visualize is not None:
|
|
253
|
+
if options.num_to_visualize > len(df_img):
|
|
254
|
+
print('Warning: asked to visualize {} images, but only {} are available, keeping them all'.\
|
|
255
|
+
format(options.num_to_visualize,len(df_img)))
|
|
256
|
+
else:
|
|
257
|
+
df_img = df_img.sample(n=options.num_to_visualize,random_state=options.random_seed)
|
|
258
|
+
|
|
259
|
+
images_html = []
|
|
260
|
+
|
|
261
|
+
# Set of dicts representing inputs to render_db_bounding_boxes:
|
|
262
|
+
#
|
|
263
|
+
# bboxes, boxClasses, image_path
|
|
264
|
+
rendering_info = []
|
|
265
|
+
|
|
266
|
+
print('Preparing rendering list')
|
|
267
|
+
|
|
268
|
+
for iImage,img in tqdm(df_img.iterrows(),total=len(df_img)):
|
|
269
|
+
|
|
270
|
+
img_id = img['id']
|
|
271
|
+
assert img_id is not None
|
|
272
|
+
|
|
273
|
+
img_relative_path = img['file_name']
|
|
274
|
+
|
|
275
|
+
if image_base_dir.startswith('http'):
|
|
276
|
+
img_path = image_base_dir + img_relative_path
|
|
277
|
+
else:
|
|
278
|
+
img_path = os.path.join(image_base_dir,
|
|
279
|
+
_image_filename_to_path(img_relative_path, image_base_dir))
|
|
280
|
+
|
|
281
|
+
annos_i = df_anno.loc[df_anno['image_id'] == img_id, :] # all annotations on this image
|
|
282
|
+
|
|
283
|
+
bboxes = []
|
|
284
|
+
boxClasses = []
|
|
285
|
+
|
|
286
|
+
# All the class labels we've seen for this image (with out without bboxes)
|
|
287
|
+
imageCategories = set()
|
|
288
|
+
|
|
289
|
+
annotationLevelForImage = ''
|
|
290
|
+
|
|
291
|
+
# Iterate over annotations for this image
|
|
292
|
+
# iAnn = 0; anno = annos_i.iloc[iAnn]
|
|
293
|
+
for iAnn,anno in annos_i.iterrows():
|
|
294
|
+
|
|
295
|
+
if 'sequence_level_annotation' in anno:
|
|
296
|
+
bSequenceLevelAnnotation = anno['sequence_level_annotation']
|
|
297
|
+
if bSequenceLevelAnnotation:
|
|
298
|
+
annLevel = 'sequence'
|
|
299
|
+
else:
|
|
300
|
+
annLevel = 'image'
|
|
301
|
+
if annotationLevelForImage == '':
|
|
302
|
+
annotationLevelForImage = annLevel
|
|
303
|
+
elif annotationLevelForImage != annLevel:
|
|
304
|
+
annotationLevelForImage = 'mixed'
|
|
305
|
+
|
|
306
|
+
categoryID = anno['category_id']
|
|
307
|
+
categoryName = label_map[categoryID]
|
|
308
|
+
if options.add_search_links:
|
|
309
|
+
categoryName = categoryName.replace('"','')
|
|
310
|
+
categoryName = '<a href="https://www.google.com/search?tbm=isch&q={}">{}</a>'.format(
|
|
311
|
+
categoryName,categoryName)
|
|
312
|
+
imageCategories.add(categoryName)
|
|
313
|
+
|
|
314
|
+
if 'bbox' in anno:
|
|
315
|
+
bbox = anno['bbox']
|
|
316
|
+
if isinstance(bbox,float):
|
|
317
|
+
assert math.isnan(bbox), "I shouldn't see a bbox that's neither a box nor NaN"
|
|
318
|
+
continue
|
|
319
|
+
bboxes.append(bbox)
|
|
320
|
+
boxClasses.append(anno['category_id'])
|
|
321
|
+
|
|
322
|
+
# ...for each of this image's annotations
|
|
323
|
+
|
|
324
|
+
imageClasses = ', '.join(imageCategories)
|
|
325
|
+
|
|
326
|
+
img_id_string = str(img_id).lower()
|
|
327
|
+
file_name = '{}_gt.jpg'.format(os.path.splitext(img_id_string)[0])
|
|
328
|
+
|
|
329
|
+
# Replace characters that muck up image links
|
|
330
|
+
illegal_characters = ['/','\\',':','\t','#',' ','%']
|
|
331
|
+
for c in illegal_characters:
|
|
332
|
+
file_name = file_name.replace(c,'~')
|
|
333
|
+
|
|
334
|
+
rendering_info.append({'bboxes':bboxes, 'boxClasses':boxClasses, 'img_path':img_path,
|
|
335
|
+
'output_file_name':file_name})
|
|
336
|
+
|
|
337
|
+
labelLevelString = ' '
|
|
338
|
+
if len(annotationLevelForImage) > 0:
|
|
339
|
+
labelLevelString = ' (annotation level: {})'.format(annotationLevelForImage)
|
|
340
|
+
|
|
341
|
+
if 'frame_num' in img and 'seq_num_frames' in img:
|
|
342
|
+
frameString = ' frame: {} of {}, '.format(img['frame_num'],img['seq_num_frames'])
|
|
343
|
+
elif 'frame_num' in img:
|
|
344
|
+
frameString = ' frame: {}, '.format(img['frame_num'])
|
|
345
|
+
else:
|
|
346
|
+
frameString = ' '
|
|
347
|
+
|
|
348
|
+
if options.show_full_paths:
|
|
349
|
+
filename_text = img_path
|
|
350
|
+
else:
|
|
351
|
+
filename_text = img_relative_path
|
|
352
|
+
if options.include_filename_links:
|
|
353
|
+
filename_text = '<a href="{}">{}</a>'.format(img_path,filename_text)
|
|
354
|
+
|
|
355
|
+
flagString = ''
|
|
356
|
+
|
|
357
|
+
def isnan(x):
|
|
358
|
+
return (isinstance(x,float) and np.isnan(x))
|
|
359
|
+
|
|
360
|
+
if ('flags' in img) and (not isnan(img['flags'])):
|
|
361
|
+
flagString = ', flags: {}'.format(str(img['flags']))
|
|
362
|
+
|
|
363
|
+
# We're adding html for an image before we render it, so it's possible this image will
|
|
364
|
+
# fail to render. For applications where this script is being used to debua a database
|
|
365
|
+
# (the common case?), this is useful behavior, for other applications, this is annoying.
|
|
366
|
+
image_dict = \
|
|
367
|
+
{
|
|
368
|
+
'filename': '{}/{}'.format('rendered_images', file_name),
|
|
369
|
+
'title': '{}<br/>{}, num boxes: {}, {}class labels: {}{}{}'.format(
|
|
370
|
+
filename_text, img_id, len(bboxes), frameString, imageClasses, labelLevelString, flagString),
|
|
371
|
+
'textStyle': 'font-family:verdana,arial,calibri;font-size:80%;' + \
|
|
372
|
+
'text-align:left;margin-top:20;margin-bottom:5'
|
|
373
|
+
}
|
|
374
|
+
if options.include_image_links:
|
|
375
|
+
image_dict['linkTarget'] = img_path
|
|
376
|
+
|
|
377
|
+
images_html.append(image_dict)
|
|
378
|
+
|
|
379
|
+
# ...for each image
|
|
380
|
+
|
|
381
|
+
def render_image_info(rendering_info):
|
|
382
|
+
|
|
383
|
+
img_path = rendering_info['img_path']
|
|
384
|
+
bboxes = rendering_info['bboxes']
|
|
385
|
+
bboxClasses = rendering_info['boxClasses']
|
|
386
|
+
output_file_name = rendering_info['output_file_name']
|
|
387
|
+
output_full_path = os.path.join(output_dir, 'rendered_images', output_file_name)
|
|
388
|
+
|
|
389
|
+
if (os.path.isfile(output_full_path)) and (not options.force_rendering):
|
|
390
|
+
if options.verbose:
|
|
391
|
+
print('Skipping existing image {}'.format(output_full_path))
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
if not img_path.startswith('http'):
|
|
395
|
+
if not os.path.exists(img_path):
|
|
396
|
+
print('Image {} cannot be found'.format(img_path))
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
original_image = vis_utils.open_image(img_path)
|
|
401
|
+
original_size = original_image.size
|
|
402
|
+
if (options.viz_size is None) or (options.viz_size[0] == -1 and options.viz_size[1] == -1):
|
|
403
|
+
image = original_image
|
|
404
|
+
else:
|
|
405
|
+
image = vis_utils.resize_image(original_image, options.viz_size[0],
|
|
406
|
+
options.viz_size[1])
|
|
407
|
+
except Exception as e:
|
|
408
|
+
print('Image {} failed to open, error: {}'.format(img_path, e))
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
vis_utils.render_db_bounding_boxes(boxes=bboxes, classes=bboxClasses,
|
|
412
|
+
image=image, original_size=original_size,
|
|
413
|
+
label_map=label_map,
|
|
414
|
+
thickness=options.box_thickness,
|
|
415
|
+
expansion=options.box_expansion)
|
|
416
|
+
|
|
417
|
+
image.save(output_full_path)
|
|
418
|
+
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
# ...def render_image_info
|
|
422
|
+
|
|
423
|
+
print('Rendering images')
|
|
424
|
+
start_time = time.time()
|
|
425
|
+
|
|
426
|
+
if options.parallelize_rendering:
|
|
427
|
+
|
|
428
|
+
if options.parallelize_rendering_with_threads:
|
|
429
|
+
worker_string = 'threads'
|
|
430
|
+
else:
|
|
431
|
+
worker_string = 'processes'
|
|
432
|
+
|
|
433
|
+
if options.parallelize_rendering_n_cores is None:
|
|
434
|
+
if options.parallelize_rendering_with_threads:
|
|
435
|
+
pool = ThreadPool()
|
|
436
|
+
else:
|
|
437
|
+
pool = Pool()
|
|
438
|
+
else:
|
|
439
|
+
if options.parallelize_rendering_with_threads:
|
|
440
|
+
pool = ThreadPool(options.parallelize_rendering_n_cores)
|
|
441
|
+
else:
|
|
442
|
+
pool = Pool(options.parallelize_rendering_n_cores)
|
|
443
|
+
print('Rendering images with {} {}'.format(options.parallelize_rendering_n_cores,
|
|
444
|
+
worker_string))
|
|
445
|
+
rendering_success = list(tqdm(pool.imap(render_image_info, rendering_info),
|
|
446
|
+
total=len(rendering_info)))
|
|
447
|
+
|
|
448
|
+
else:
|
|
449
|
+
|
|
450
|
+
rendering_success = []
|
|
451
|
+
for file_info in tqdm(rendering_info):
|
|
452
|
+
rendering_success.append(render_image_info(file_info))
|
|
453
|
+
|
|
454
|
+
elapsed = time.time() - start_time
|
|
455
|
+
|
|
456
|
+
print('Rendered {} images in {} ({} successful)'.format(
|
|
457
|
+
len(rendering_info),humanfriendly.format_timespan(elapsed),sum(rendering_success)))
|
|
458
|
+
|
|
459
|
+
if options.sort_by_filename:
|
|
460
|
+
images_html = sorted(images_html, key=lambda x: x['filename'])
|
|
461
|
+
else:
|
|
462
|
+
random.shuffle(images_html)
|
|
463
|
+
|
|
464
|
+
htmlOutputFile = os.path.join(output_dir, 'index.html')
|
|
465
|
+
|
|
466
|
+
htmlOptions = options.htmlOptions
|
|
467
|
+
if isinstance(db_path,str):
|
|
468
|
+
htmlOptions['headerHtml'] = '<h1>Sample annotations from {}</h1>'.format(db_path)
|
|
469
|
+
else:
|
|
470
|
+
htmlOptions['headerHtml'] = '<h1>Sample annotations</h1>'
|
|
471
|
+
|
|
472
|
+
write_html_image_list(
|
|
473
|
+
filename=htmlOutputFile,
|
|
474
|
+
images=images_html,
|
|
475
|
+
options=htmlOptions)
|
|
476
|
+
|
|
477
|
+
print('Visualized {} images, wrote results to {}'.format(len(images_html),htmlOutputFile))
|
|
478
|
+
|
|
479
|
+
return htmlOutputFile,image_db
|
|
480
|
+
|
|
481
|
+
# def visualize_db(...)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
#%% Command-line driver
|
|
485
|
+
|
|
486
|
+
# Copy all fields from a Namespace (i.e., the output from parse_args) to an object.
|
|
487
|
+
#
|
|
488
|
+
# Skips fields starting with _. Does not check existence in the target object.
|
|
489
|
+
def args_to_object(args, obj):
|
|
490
|
+
|
|
491
|
+
for n, v in inspect.getmembers(args):
|
|
492
|
+
if not n.startswith('_'):
|
|
493
|
+
setattr(obj, n, v)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def main():
|
|
497
|
+
|
|
498
|
+
parser = argparse.ArgumentParser()
|
|
499
|
+
parser.add_argument('db_path', action='store', type=str,
|
|
500
|
+
help='.json file to visualize')
|
|
501
|
+
parser.add_argument('output_dir', action='store', type=str,
|
|
502
|
+
help='Output directory for html and rendered images')
|
|
503
|
+
parser.add_argument('image_base_dir', action='store', type=str,
|
|
504
|
+
help='Base directory (or URL) for input images')
|
|
505
|
+
|
|
506
|
+
parser.add_argument('--num_to_visualize', action='store', type=int, default=None,
|
|
507
|
+
help='Number of images to visualize (randomly drawn) (defaults to all)')
|
|
508
|
+
parser.add_argument('--random_sort', action='store_true',
|
|
509
|
+
help='Sort randomly (rather than by filename) in output html')
|
|
510
|
+
parser.add_argument('--trim_to_images_with_bboxes', action='store_true',
|
|
511
|
+
help='Only include images with bounding boxes (defaults to false)')
|
|
512
|
+
parser.add_argument('--random_seed', action='store', type=int, default=None,
|
|
513
|
+
help='Random seed for image selection')
|
|
514
|
+
parser.add_argument('--pathsep_replacement', action='store', type=str, default='',
|
|
515
|
+
help='Replace path separators in relative filenames with another ' + \
|
|
516
|
+
'character (frequently ~)')
|
|
517
|
+
|
|
518
|
+
if len(sys.argv[1:]) == 0:
|
|
519
|
+
parser.print_help()
|
|
520
|
+
parser.exit()
|
|
521
|
+
|
|
522
|
+
args = parser.parse_args()
|
|
523
|
+
|
|
524
|
+
# Convert to an options object
|
|
525
|
+
options = DbVizOptions()
|
|
526
|
+
args_to_object(args, options)
|
|
527
|
+
if options.random_sort:
|
|
528
|
+
options.sort_by_filename = False
|
|
529
|
+
|
|
530
|
+
visualize_db(options.db_path,options.output_dir,options.image_base_dir,options)
|
|
531
|
+
|
|
532
|
+
if __name__ == '__main__':
|
|
533
|
+
main()
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
#%% Interactive driver
|
|
537
|
+
|
|
538
|
+
if False:
|
|
539
|
+
|
|
540
|
+
#%%
|
|
541
|
+
|
|
542
|
+
db_path = r'e:\wildlife_data\missouri_camera_traps\missouri_camera_traps_set1.json'
|
|
543
|
+
output_dir = r'e:\wildlife_data\missouri_camera_traps\preview'
|
|
544
|
+
image_base_dir = r'e:\wildlife_data\missouri_camera_traps'
|
|
545
|
+
|
|
546
|
+
options = DbVizOptions()
|
|
547
|
+
options.num_to_visualize = 100
|
|
548
|
+
|
|
549
|
+
htmlOutputFile,db = visualize_db(db_path,output_dir,image_base_dir,options)
|
|
550
|
+
# os.startfile(htmlOutputFile)
|