├── .gitignore ├── LICENSE ├── README.md ├── download_amstertime.py ├── download_eynsham.py ├── download_nordland.py ├── download_san_francisco.py ├── download_sped.py ├── download_st_lucia.py ├── download_svox.py ├── format_mapillary.py ├── format_pitts250k.py ├── format_pitts30k.py ├── format_tokyo247.py ├── map_builder.py ├── requirements.txt └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | # custom 2 | msls 3 | testing.py 4 | 5 | 6 | z_others 7 | datasets 8 | .spyproject 9 | __pycache__ 10 | map_*.png 11 | tmp 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2019 VRG, CTU Prague 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VPR Datasets Downloader 2 | 3 | This repo is made to automatically download Visual Place Recognition (VPR) datasets in a simple and standardized way. 4 | This is useful because all VPR datasets use different ways of storing the labels (some in csv files, some in matlab files, some in the filename), making it very inconvenient to train and test on different datasets. 5 | Furthermore, some datasets (e.g. Pitts30k and Nordland) require some pre-processing, and small changes in the pre-processing can lead to changes in results. 6 | The goal of this codebase is therefore to ensure that researchers and practitioners in VPR can use the same standardized datasets for their experiments. 7 | 8 | ## Overview 9 | 10 | The datasets are downloaded and formatted using a standard format: for some of the datasets also the maps are automatically created. 11 | The adopted convention is that the names of the files with the images are: 12 | 13 | `@ UTM_east @ UTM_north @ UTM_zone_number @ UTM_zone_letter @ latitude @ longitude @ pano_id @ tile_num @ heading @ pitch @ roll @ height @ timestamp @ note @ extension` 14 | 15 | Note that for many datasets some of these values are empty, and however the only required values for VPR are UTM coordinates (obtained from latitude and longitude). 16 | 17 | The reason for using the character `@` as a separator, is that commonly used characters such as dash `-` or underscore `_` might be used in the fields, for example in the `pano_id` field. 18 | 19 | The directory tree that is generated is as follows: 20 | ``` 21 | . 22 | └── datasets 23 | └── dataset_name 24 | └── images 25 | ├── train 26 | │ ├── database 27 | │ └── queries 28 | ├── val 29 | │ ├── database 30 | │ └── queries 31 | └── test 32 | ├── database 33 | └── queries 34 | ``` 35 | 36 | Most datasets are used only for testing, and therefore do not have a train and validation set. 37 | 38 | ## Available datasets 39 | 40 | The list of datasets that you can download with this codebase is the following: 41 | - Pitts30k* 42 | - Pitts250k* 43 | - Mapillary SLS** 44 | - Eysham - as test set only 45 | - San Francisco - as test set only 46 | - Tokyo 24/7* - as test set only 47 | - St Lucia - as test set only 48 | - SVOX - as test set only 49 | - Nordland - as test set only 50 | - AmsterTime - as test set only 51 | - SPED - as test set only 52 | 53 | To download each dataset, simply run the corresponding python script, that will download, 54 | unpack and format the file according to the structure above. 55 | 56 | *: for Pitts30k, Pitts250k and Tokyo 24/7 the images should be downloaded by asking permission to the respective authors. Then they can be formatted with this codebase 57 | 58 | *\*: for Mapillary SLS, you need to first log in into their website, download it [here](https://www.mapillary.com/dataset/places), 59 | then extract the zip files and run 60 | `$ python format_mapillary.py` 61 | 62 | #### Pitts30k 63 | 64 | For Pitts30k, first download the data under datasets/pitts30k/raw_data, then simply run `$ python format_pitts30k.py` 65 | 66 |
67 | Cite / BibTex 68 |
 69 | @article{Torii_2015_pitts,
 70 |     author = {A. {Torii} and J. {Sivic} and M. {Okutomi} and T. {Pajdla}},
 71 |     journal = {IEEE Transactions on Pattern Analysis and Machine Intelligence}, 
 72 |     title = {Visual Place Recognition with Repetitive Structures}, 
 73 |     year = {2015}
 74 | }
 75 | 
76 |
77 | 78 | #### Pitts250k 79 | 80 | For Pitts250k, first download the data under datasets/pitts250k/raw_data, then simply run `$ python format_pitts250k.py` 81 | 82 |
83 | Cite / BibTex 84 |
85 |
 86 | @article{Torii_2015_pitts,
 87 |     author = {A. {Torii} and J. {Sivic} and M. {Okutomi} and T. {Pajdla}},
 88 |     journal = {IEEE Transactions on Pattern Analysis and Machine Intelligence}, 
 89 |     title = {Visual Place Recognition with Repetitive Structures}, 
 90 |     year = {2015}
 91 | }
 92 | 
93 |
94 | 95 | #### Mapillary SLS 96 | 97 | For Mapillary SLS, you need to first log in into their website, download it [here](https://www.mapillary.com/dataset/places), 98 | then extract the zip files, and place it in a folder `datasets` inside the repository root and name it 99 | `mapillary_sls`. 100 | Then you can run: 101 | 102 | `$ python format_mapillary.py` 103 | 104 |
105 | Cite / BibTex 106 |
107 |
108 | @inproceedings{Warburg_2020_msls,
109 |     author={Warburg, Frederik and Hauberg, Soren and Lopez-Antequera, Manuel and Gargallo, Pau and Kuang, Yubin and Civera, Javier},
110 |     title={Mapillary Street-Level Sequences: A Dataset for Lifelong Place Recognition},
111 |     booktitle={IEEE Conference on Computer Vision and Pattern Recognition},
112 |     month={June},
113 |     year={2020}
114 | }
115 | 
116 |
117 | 118 | #### Eynsham 119 | 120 | To download Eynsham, simply run `$ python download_eynsham.py` 121 | 122 |
123 | Cite / BibTex 124 |
125 |
126 | @inproceedings{Cummins_2009_eynsham,
127 |     title={Highly scalable appearance-only SLAM - {FAB-MAP} 2.0},
128 |     author={M. Cummins and P. Newman},
129 |     booktitle={Robotics: Science and Systems},
130 |     year={2009}
131 | }
132 | 
133 |
134 | 135 | #### San Francisco 136 | 137 | To download San Francisco, simply run `$ python download_san_francisco.py` 138 | 139 |
140 | Cite / BibTex 141 |
142 |
143 | @inproceedings{Chen_2011_san_francisco,
144 |     author={D. M. {Chen} and G. {Baatz} and K. {Köser} and S. S. {Tsai} and R. {Vedantham} and T. {Pylvänäinen} and K. {Roimela} and X. {Chen} and J. {Bach} and M. {Pollefeys} and B. {Girod} and R. {Grzeszczuk}},
145 |     booktitle={IEEE Conference on Computer Vision and Pattern Recognition},
146 |     title={City-scale landmark identification on mobile devices}, 
147 |     year={2011},
148 |     pages={737-744},
149 |     doi={10.1109/CVPR.2011.5995610}
150 | }
151 | 
152 |
153 | 154 | #### St Lucia 155 | 156 | To download St Lucia, simply run `$ python download_st_lucia.py` 157 | 158 |
159 | Cite / BibTex 160 |
161 |
162 | @article{Milford_2008_st_lucia,
163 |     title={Mapping a Suburb With a Single Camera Using a Biologically Inspired SLAM System},
164 |     author={Michael Milford and G. Wyeth},
165 |     journal={IEEE Transactions on Robotics},
166 |     year={2008},
167 |     volume={24},
168 |     pages={1038-1053}
169 | }
170 | 
171 |
172 | 173 | #### SVOX 174 | 175 | To download SVOX, simply run `$ python download_svox.py` 176 | 177 |
178 | Cite / BibTex 179 |
180 |
181 | @inproceedings{Berton_2021_svox, 
182 |     author = {Berton, Gabriele and Paolicelli, Valerio and Masone, Carlo and Caputo, Barbara},
183 |     title = {Adaptive-Attentive Geolocalization From Few Queries: A Hybrid Approach},
184 |     booktitle = {IEEE Winter Conference on Applications of Computer Vision},
185 |     month = {January},
186 |     year = {2021},
187 |     pages = {2918-2927}
188 | }
189 | 
190 |
191 | 192 | #### Nordland 193 | 194 | To download Nordland, simply run `$ python download_nordland.py` 195 | 196 | The images will be arranged to have GPS/UTM labels compatible with the benchmarking code. More info on it are in the comment on top of the `download_nordland.py` script. We used the splits used by the [Patch-NetVLAD paper](https://arxiv.org/abs/2103.01486). 197 | 198 |
199 | Cite / BibTex 200 |
201 |
202 | @inproceedings{Sunderhauf_2013_nordland,
203 |     title = {Are we there yet? Challenging {SeqSLAM} on a 3000 km journey across all four seasons},
204 |     author = {N. S{\"u}nderhauf and P. Neubert and P. Protzel},
205 |     booktitle = {Proc. of Workshop on Long-Term Autonomy, }#icra,
206 |     pages = {2013},
207 |     year = {2013}
208 | }
209 | 
210 |
211 | 212 | #### Tokyo 24/7 213 | 214 | For Tokyo 24/7, first download the data under datasets/tokyo247/raw_data, then simply run `$ python format_tokyo247.py`. Queries are automatically downloaded. 215 | 216 |
217 | Cite / BibTex 218 |
219 |
220 | @article{Torii_2018_tokyo247,
221 |     author = {A. {Torii} and R. {Arandjelović} and J. {Sivic} and M. {Okutomi} and T. {Pajdla}},
222 |     journal = {IEEE Transactions on Pattern Analysis and Machine Intelligence}, 
223 |     title = {24/7 Place Recognition by View Synthesis}, 
224 |     year = {2018},
225 |     volume = {40},
226 |     number = {2},
227 |     pages = {257-271}
228 | }
229 | 
230 |
231 | 232 | ### Other datasets 233 | 234 | Other datasets can be found in the [directory of Anyloc's datasets](https://iiitaphyd-my.sharepoint.com/personal/robotics_iiit_ac_in/_layouts/15/onedrive.aspx?ga=1&id=%2Fpersonal%2Frobotics%5Fiiit%5Fac%5Fin%2FDocuments%2FStudent%20Research%2FAnyLoc%2D2023%2FPublic%2FDatasets%2DAll) and [the directory of SALAD's datasets](https://surfdrive.surf.nl/files/index.php/s/sbZRXzYe3l0v67W). 235 | 236 | 237 | ## Cite / BibTex 238 | If you use this codebase, please cite [our benchmark](https://github.com/gmberton/deep-visual-geo-localization-benchmark) for which this code was built, and the respective paper for the datasets. 239 | ``` 240 | @inProceedings{Berton_CVPR_2022_benchmark, 241 | author = {Berton, Gabriele and Mereu, Riccardo and Trivigno, Gabriele and Masone, Carlo and 242 | Csurka, Gabriela and Sattler, Torsten and Caputo, Barbara}, 243 | title = {Deep Visual Geo-localization Benchmark}, 244 | booktitle = {CVPR}, 245 | month = {June}, 246 | year = {2022}, 247 | } 248 | ``` 249 | -------------------------------------------------------------------------------- /download_amstertime.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script download the AmsterTime dataset, from the paper by Yildiz et al 3 | "AmsterTime: A Visual Place Recognition Benchmark Dataset for Severe Domain Shift" 4 | - https://arxiv.org/abs/2203.16291. 5 | The AmsterTime dataset is organize in pair of images, each pair being one query 6 | and one database image. The query is a historical photo, while the database 7 | image is a modern photo. Because their positions (e.g. GPS) are not available, 8 | we create mock positions for each pair, with images within a pair having the 9 | same position, and consecutive pairs being 100 meters away from each other. 10 | """ 11 | 12 | import os 13 | import shutil 14 | import py3_wget 15 | from glob import glob 16 | from tqdm import tqdm 17 | from PIL import Image 18 | from os.path import join 19 | 20 | 21 | datasets_folder = join(os.curdir, "datasets") 22 | dataset_name = "amstertime" 23 | dataset_folder = join(datasets_folder, dataset_name) 24 | raw_data_folder = join(datasets_folder, dataset_name, "raw_data") 25 | database_folder = join(dataset_folder, "images", "test", "database") 26 | queries_folder = join(dataset_folder, "images", "test", "queries") 27 | os.makedirs(dataset_folder, exist_ok=True) 28 | os.makedirs(database_folder, exist_ok=True) 29 | os.makedirs(queries_folder, exist_ok=True) 30 | os.makedirs(raw_data_folder, exist_ok=True) 31 | 32 | py3_wget.download_file( 33 | url="https://data.4tu.nl/ndownloader/items/d2ee2551-986a-46bc-8540-b43f5b01ec4d/versions/4", 34 | output_path=join(raw_data_folder, "data.zip") 35 | ) 36 | 37 | shutil.unpack_archive(join(raw_data_folder, "data.zip"), raw_data_folder) 38 | shutil.unpack_archive(join(raw_data_folder, "AmsterTime-v1.0.zip"), raw_data_folder) 39 | 40 | database_paths = sorted(glob(join(raw_data_folder, "new", "*.png"))) 41 | queries_paths = sorted(glob(join(raw_data_folder, "old", "*.jpg"))) 42 | 43 | for db_filepath, q_filepath in zip(tqdm(database_paths), queries_paths): 44 | db_filename = os.path.basename(db_filepath) 45 | q_filename = os.path.basename(q_filepath) 46 | # Query and DB images from the same pair have the same name 47 | assert os.path.splitext(db_filename)[0] == os.path.splitext(q_filename)[0] 48 | pair_name = os.path.splitext(db_filename)[0] 49 | # Simulate a distance of at least 100 meters between any two pairs of non-matching images 50 | mock_utm_east = int(pair_name) * 1000 51 | new_image_name = f"@0@{mock_utm_east}@@@@@{pair_name}@@@@@@@@.jpg" 52 | new_q_path = os.path.join(queries_folder, new_image_name) # queries are in JPEG 53 | _ = shutil.move(q_filepath, new_q_path) 54 | new_db_path = os.path.join(database_folder, new_image_name) 55 | Image.open(db_filepath).convert("RGB").save(new_db_path) # db images are in PNG, so convert to JPEG 56 | os.remove(db_filepath) 57 | 58 | -------------------------------------------------------------------------------- /download_eynsham.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import shutil 4 | import py3_wget 5 | import numpy as np 6 | from tqdm import tqdm 7 | from glob import glob 8 | from PIL import Image 9 | from os.path import join 10 | from datetime import datetime 11 | 12 | import util 13 | import map_builder 14 | 15 | datasets_folder = join(os.curdir, "datasets") 16 | dataset_name = "eynsham" 17 | dataset_folder = join(datasets_folder, dataset_name) 18 | raw_data_folder = join(datasets_folder, dataset_name, "raw_data") 19 | database_folder = join(dataset_folder, "images", "test", "database") 20 | queries_folder = join(dataset_folder, "images", "test", "queries") 21 | os.makedirs(dataset_folder, exist_ok=True) 22 | os.makedirs(database_folder, exist_ok=True) 23 | os.makedirs(queries_folder, exist_ok=True) 24 | os.makedirs(raw_data_folder, exist_ok=True) 25 | 26 | py3_wget.download_file( 27 | url="https://zenodo.org/record/1243106/files/Eynsham.zip?download=1", 28 | output_path=join(raw_data_folder, "Eynsham.zip") 29 | ) 30 | 31 | shutil.unpack_archive(join(raw_data_folder, "Eynsham.zip"), raw_data_folder) 32 | print("Unzipping, this will take a while") 33 | 34 | with open(join(raw_data_folder, "Eynsham", "Route_map", "Eynsham.kml"), "r") as file: 35 | lines = file.readlines() 36 | 37 | lines = [l.replace("\n", "") for l in lines] 38 | text = lines[11] 39 | splits = text.split("")[1].split("")[0].split(" ")[:-2] 40 | coords = np.array([s.split(",")[:2] for s in splits]).astype(np.float64) 41 | 42 | src_images_paths = sorted(glob(join(raw_data_folder, "Eynsham", "Images", "*.ppm")))[5:] 43 | 44 | for pano_num, (lon, lat) in enumerate(tqdm(coords)): 45 | for tile_num in range(5): 46 | src_image_path = src_images_paths[pano_num*5 + tile_num] 47 | timestamp = src_image_path.split("grab_")[1].split(".")[0] 48 | timestamp = datetime.utcfromtimestamp(int(timestamp)).strftime('%Y%m%d_%H%M%S') 49 | dst_image_name = util.get_dst_image_name(lat, lon, pano_id=f"{pano_num:04d}", 50 | tile_num=tile_num, timestamp=timestamp) 51 | # The first 4787 images correspond to the first sequence, and it is the database 52 | if pano_num < 4787: 53 | Image.open(src_image_path).save(join(database_folder, dst_image_name)) 54 | else: 55 | Image.open(src_image_path).save(join(queries_folder, dst_image_name)) 56 | 57 | map_builder.build_map_from_dataset(dataset_folder) 58 | shutil.rmtree(raw_data_folder) 59 | 60 | -------------------------------------------------------------------------------- /download_nordland.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script downloads the Nordland dataset with the split used in Patch-NetVLAD. 3 | The images are arranged to be compatible with our benchmarking framework, and 4 | also with the CosPlace repository. 5 | In Nordland usually a prediction for a given query is considered correct if it 6 | is within 10 frames from the query (there's a 1-to-1 match between queries and 7 | database images). Given that our codebases rely on UTM coordinates to find 8 | positives and negatives, we create dummy UTM coordinates for all images, 9 | ensuring that images that are within 10 frames are also within 25 meters. 10 | In practice, we organize all images with UTM_east = 0 (i.e. in a straight line) 11 | and set UTM_north to be 2.4 meters apart between consecutive frames. 12 | 13 | NOTE: in some works [1-3] the split between database and queries is flipped, 14 | in the sense that winter images are used for the database and summer images for queries. 15 | [1] Zaffar et al, 2020, VPR-Bench: An Open-Source Visual Place Recognition Evaluation Framework with Quantifiable Viewpoint and Appearance Change 16 | [2] Ali-bey et al, 2024, BoQ: A Place is Worth a Bag of Learnable Queries 17 | [3] Izquierdo et al, 2024, Optimal transport aggregation for visual place recognition 18 | """ 19 | 20 | import os 21 | import shutil 22 | import py3_wget 23 | from tqdm import tqdm 24 | from glob import glob 25 | from PIL import Image 26 | from os.path import join 27 | 28 | import util 29 | 30 | THRESHOLD_METERS = 25 31 | THRESHOLD_FRAMES = 10 32 | DISTANCE_BETWEEN_FRAMES = THRESHOLD_METERS / (THRESHOLD_FRAMES + 0.5) 33 | 34 | datasets_folder = join(os.curdir, "datasets") 35 | dataset_name = "nordland" 36 | dataset_folder = join(datasets_folder, dataset_name) 37 | raw_data_folder = join(datasets_folder, dataset_name, "raw_data") 38 | database_folder = join(dataset_folder, "images", "test", "database") 39 | queries_folder = join(dataset_folder, "images", "test", "queries") 40 | os.makedirs(dataset_folder, exist_ok=True) 41 | os.makedirs(database_folder, exist_ok=True) 42 | os.makedirs(queries_folder, exist_ok=True) 43 | os.makedirs(raw_data_folder, exist_ok=True) 44 | 45 | print("Downloading tars with the images") 46 | py3_wget.download_file( 47 | url='https://universityofadelaide.app.box.com/index.php?rm=box_download_shared_file&shared_name=zkfk1akpbo5318fzqmtvlpp7030ex4up&file_id=f_1424421870101', 48 | output_path=join(raw_data_folder, "summer.tar.gz") 49 | ) 50 | py3_wget.download_file( 51 | url='https://universityofadelaide.app.box.com/index.php?rm=box_download_shared_file&shared_name=zkfk1akpbo5318fzqmtvlpp7030ex4up&file_id=f_1521702837314', 52 | output_path=join(raw_data_folder, "winter.tar.gz") 53 | ) 54 | py3_wget.download_file( 55 | url="https://universityofadelaide.app.box.com/index.php?rm=box_download_shared_file&shared_name=zkfk1akpbo5318fzqmtvlpp7030ex4up&file_id=f_1424408901067", 56 | output_path=join(raw_data_folder, "cleanImageNames.txt") 57 | ) 58 | 59 | print("Unpacking tars with the images, this will take a few minutes") 60 | shutil.unpack_archive(join(raw_data_folder, "summer.tar.gz"), raw_data_folder) 61 | shutil.unpack_archive(join(raw_data_folder, "winter.tar.gz"), join(raw_data_folder, "winter")) 62 | 63 | with open(join(raw_data_folder, "cleanImageNames.txt")) as file: 64 | selected_images = file.readlines() 65 | selected_images = [i.replace("\n", "") for i in selected_images] 66 | selected_images = set(selected_images) 67 | 68 | database_paths = sorted(glob(join(raw_data_folder, "summer", "*.png"))) 69 | queries_paths = sorted(glob(join(raw_data_folder, "winter", "*.png"))) 70 | 71 | num_image = 0 72 | for path in tqdm(database_paths, desc="Copying DB images to dst"): 73 | if os.path.basename(path) not in selected_images: 74 | continue 75 | utm_north = util.format_coord(num_image*DISTANCE_BETWEEN_FRAMES, 5, 1) 76 | filename = f"@0@{utm_north}@@@@@{num_image}@@@@@@@@.jpg" 77 | new_path = join(database_folder, filename) 78 | Image.open(path).save(new_path) 79 | num_image += 1 80 | 81 | num_image = 0 82 | for path in tqdm(queries_paths, desc="Copying queries to dst"): 83 | if os.path.basename(path) not in selected_images: 84 | continue 85 | utm_north = util.format_coord(num_image*DISTANCE_BETWEEN_FRAMES, 5, 1) 86 | filename = f"@0@{utm_north}@@@@@{num_image}@@@@@@@@.jpg" 87 | new_path = join(queries_folder, filename) 88 | Image.open(path).save(new_path) 89 | num_image += 1 90 | 91 | -------------------------------------------------------------------------------- /download_san_francisco.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import utm 4 | import math 5 | import shutil 6 | import py3_wget 7 | from glob import glob 8 | from tqdm import tqdm 9 | from os.path import join 10 | 11 | import util 12 | import map_builder 13 | 14 | datasets_folder = join(os.curdir, "datasets") 15 | dataset_name = "san_francisco" 16 | dataset_folder = join(datasets_folder, dataset_name) 17 | raw_data_folder = join(datasets_folder, dataset_name, "raw_data") 18 | os.makedirs(dataset_folder, exist_ok=True) 19 | os.makedirs(raw_data_folder, exist_ok=True) 20 | os.makedirs(join(dataset_folder, "images", "test"), exist_ok=True) 21 | 22 | #### Database 23 | print("Downloading database archives") 24 | filenames = [f"PCIs_{i*1000:08d}_{(i+1)*1000:08d}_3.tar" for i in range(11, 150)] 25 | urls = [f"https://stacks.stanford.edu/file/druid:vn158kj2087/{f}" for f in filenames] 26 | tars_paths = [join(raw_data_folder, f) for f in filenames] 27 | for i, (url, tar_path) in enumerate(zip(urls, tars_paths)): 28 | if os.path.exists(tar_path.replace("PCIs_", "").replace(".tar", "")): 29 | continue 30 | print(f"{i:>3} / {len(filenames)} ) downloading {tar_path}") 31 | py3_wget.download_file(url, tar_path) 32 | try: # Unpacking database archives 33 | shutil.unpack_archive(tar_path, raw_data_folder) 34 | except shutil.ReadError: 35 | pass # Some tars are empty files 36 | 37 | print("Formatting database files") 38 | dst_database_folder = join(dataset_folder, "images", "test", "database") 39 | os.makedirs(dst_database_folder, exist_ok=True) 40 | src_images_paths = sorted(glob(join(raw_data_folder, "**", "*.jpg"), recursive=True)) 41 | for src_image_path in tqdm(src_images_paths): 42 | _, _, pano_id, latitude, longitude, building_id, tile_num, carto_id, heading, pitch = os.path.basename(src_image_path).split("_") 43 | pitch = pitch.replace(".jpg", "") 44 | dst_image_name = util.get_dst_image_name(latitude, longitude, pano_id, tile_num, 45 | heading, pitch, extension=".jpg") 46 | dst_image_path = join(dst_database_folder, dst_image_name) 47 | _ = shutil.move(src_image_path, dst_image_path) 48 | 49 | #### Queries 50 | print("Downloading query archive") 51 | queries_zip_filename = "BuildingQueryImagesCartoIDCorrected-Upright.zip" 52 | url = f"https://stacks.stanford.edu/file/druid:vn158kj2087/{queries_zip_filename}" 53 | queries_zip_path = join(raw_data_folder, queries_zip_filename) 54 | py3_wget.download_file(url, queries_zip_path) 55 | 56 | print("Unpacking query archive") 57 | shutil.unpack_archive(queries_zip_path, raw_data_folder) 58 | 59 | print("Formatting query files") 60 | dst_queries_folder = join(dataset_folder, "images", "test", "queries") 61 | os.makedirs(dst_queries_folder, exist_ok=True) 62 | poses_file_path = join(raw_data_folder, "reference_poses_598.zip") 63 | py3_wget.download_file( 64 | url="http://www.ok.sc.e.titech.ac.jp/~torii/project/vlocalization/icons/reference_poses_598.zip", 65 | output_path=poses_file_path 66 | ) 67 | shutil.unpack_archive(poses_file_path, raw_data_folder) 68 | 69 | with open(join(raw_data_folder, "reference_poses_598", "reference_poses_addTM_all_598.txt"), "r") as file: 70 | lines = file.readlines()[1:] 71 | 72 | for line in lines: 73 | _, image_id, x, y, w, z, utm_east, utm_north, _ = line.split(" ") 74 | latitude, longitude = utm.to_latlon(float(utm_east), float(utm_north), 10, "S") 75 | x, y, w, z = float(x), float(y), float(w), float(z) 76 | yaw = math.atan2(2.0 * (z * x + y * w) , - 1.0 + 2.0 * (x * x + y * y)) 77 | heading = ((((yaw / math.pi) + 1) * 180) + 180) % 360 78 | dst_image_name = util.get_dst_image_name(latitude, longitude, pano_id=image_id, heading=heading) 79 | dst_image_path = join(dst_queries_folder, dst_image_name) 80 | src_image_path = join(raw_data_folder, "BuildingQueryImagesCartoIDCorrected-Upright", f"{image_id}.jpg") 81 | _ = shutil.move(src_image_path, dst_image_path) 82 | 83 | map_builder.build_map_from_dataset(dataset_folder) 84 | shutil.rmtree(raw_data_folder) 85 | 86 | -------------------------------------------------------------------------------- /download_sped.py: -------------------------------------------------------------------------------- 1 | 2 | import shutil 3 | import py3_wget 4 | from pathlib import Path 5 | from zipfile import ZipFile 6 | 7 | 8 | zip_filepath = Path("datasets", "sped", "raw_data", "sped.zip") 9 | zip_filepath.parent.mkdir(exist_ok=True, parents=True) 10 | 11 | py3_wget.download_file( 12 | url="https://surfdrive.surf.nl/files/index.php/s/sbZRXzYe3l0v67W/download?path=%2F&files=SPEDTEST.zip", 13 | output_path=zip_filepath 14 | ) 15 | 16 | zf = ZipFile(zip_filepath, 'r') 17 | zf.extractall(zip_filepath.parent) 18 | zf.close() 19 | 20 | database_paths = sorted((zip_filepath.parent / "SPEDTEST" / "ref").glob("*.jpg")) 21 | queries_paths = sorted((zip_filepath.parent / "SPEDTEST" / "query").glob("*.jpg")) 22 | 23 | database_folder = Path("datasets", "sped", "images", "test", "database") 24 | queries_folder = Path("datasets", "sped", "images", "test", "queries") 25 | database_folder.mkdir(exist_ok=True, parents=True) 26 | queries_folder.mkdir(exist_ok=True, parents=True) 27 | 28 | assert len(database_paths) == len(queries_paths) 29 | for db_path, q_path in zip(database_paths, queries_paths): 30 | db_path = Path(db_path) 31 | q_path = Path(q_path) 32 | assert db_path.name == q_path.name 33 | mock_utm_east = int(db_path.stem) * 1000 34 | new_image_name = f"@0@{mock_utm_east}@@@@@{db_path.stem}@@@@@@@@.jpg" 35 | new_db_path = Path(database_folder, new_image_name) 36 | new_q_path = Path(queries_folder, new_image_name) 37 | _ = shutil.move(db_path, new_db_path) 38 | _ = shutil.move(q_path, new_q_path) 39 | 40 | shutil.rmtree(zip_filepath.parent) 41 | 42 | 43 | -------------------------------------------------------------------------------- /download_st_lucia.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import shutil 4 | from tqdm import tqdm 5 | from mega import Mega 6 | from skimage import io 7 | from os.path import join 8 | 9 | import util 10 | import map_builder 11 | 12 | THRESHOLD_IN_METERS = 5 13 | 14 | datasets_folder = join(os.curdir, "datasets") 15 | dataset_name = "st_lucia" 16 | dataset_folder = join(datasets_folder, dataset_name) 17 | raw_data_folder = join(datasets_folder, dataset_name, "raw_data") 18 | dst_database_folder = join(dataset_folder, "images", "test", "database") 19 | dst_queries_folder = join(dataset_folder, "images", "test", "queries") 20 | os.makedirs(dataset_folder, exist_ok=True) 21 | os.makedirs(dst_database_folder, exist_ok=True) 22 | os.makedirs(dst_queries_folder, exist_ok=True) 23 | os.makedirs(raw_data_folder, exist_ok=True) 24 | 25 | # Use the first pass for the database, and the last one for the queries 26 | urls = ['https://mega.nz/file/nE4g0LzZ#c8eL_H3ZfXElqEukw38i32p5cjwusTuNJYYeEP1d5Pg', 27 | # 'https://mega.nz/file/TVRXlDZR#WUJad1yQunPLpA38Z0rPqBeXXh4g_jnt4n-ZjDF8hKw', 28 | # 'https://mega.nz/file/LNRkiZAB#LemKJHU7kDunl_9CMQM6xCUdLzVhMYqE3DIZ6QVDOUo', 29 | # 'https://mega.nz/file/PRJy2BpI#TWSaNioftbPxw7T4LFV3CTtVNN08XnI94WH6_SZEJHE', 30 | # 'https://mega.nz/file/OFZngZwJ#L3rF3wU69v0Fdfh-iUcKLzkdrUhD6DI6SSYoRZuJkHE', 31 | # 'https://mega.nz/file/SRJ2wLYQ#EEBH4wCy5je0lbsBNoX9EmGuonYJ2hMothK5w0ehh9U', 32 | # 'https://mega.nz/file/3U4XnQqT#WifPXhWVmkYw5xAUFyIbaHzjEcghcUfjCK7rqBVl-YY', 33 | # 'https://mega.nz/file/WN4jzA7Q#Qlg1DxSuYvylIWuZRGFqwjnL1lYV698rsf_eGE1QWnI', 34 | # 'https://mega.nz/file/fQBmELwa#HHKNU28SreKnQjxIIZ_qUQorWqxZjCraiPT1oiYR4wo', 35 | 'https://mega.nz/file/PAgWSIhD#UeeA6knWL3pDh_IczbYkcA1R1MwSZ2vhEg2DTr1_oNw'] 36 | login = Mega().login() 37 | 38 | for sequence_num, url in enumerate(urls): 39 | print(f"{sequence_num:>2} / {len(urls)} ) downloading {url}") 40 | zip_path = login.download_url(url, raw_data_folder) 41 | zip_path = str(zip_path) 42 | subset_name = os.path.basename(zip_path).replace(".zip", "") 43 | shutil.unpack_archive(zip_path, raw_data_folder) 44 | 45 | vr = util.VideoReader(join(raw_data_folder, subset_name, "webcam_video.avi")) 46 | 47 | with open(join(raw_data_folder, subset_name, "fGPS.txt"), "r") as file: 48 | lines = file.readlines() 49 | 50 | last_coordinates = None 51 | for frame_num, line in zip(tqdm(range(vr.frames_num)), lines): 52 | latitude, longitude = line.split(",") 53 | latitude = "-" + latitude # Given latitude is positive, real latitude is negative (in Australia) 54 | easting, northing = util.format_location_info(latitude, longitude)[:2] 55 | if last_coordinates is None: 56 | last_coordinates = (easting, northing) 57 | else: 58 | distance_in_meters = util.get_distance(last_coordinates, (easting, northing)) 59 | if distance_in_meters < THRESHOLD_IN_METERS: 60 | continue # If this frame is too close to the previous one, skip it 61 | else: 62 | last_coordinates = (easting, northing) 63 | 64 | frame = vr.get_frame_at_frame_num(frame_num) 65 | image_name = util.get_dst_image_name(latitude, longitude, pano_id=f"{subset_name}_{frame_num:05d}") 66 | if sequence_num == 0: # The first sequence is the database 67 | io.imsave(join(dst_database_folder, image_name), frame) 68 | else: 69 | io.imsave(join(dst_queries_folder, image_name), frame) 70 | 71 | map_builder.build_map_from_dataset(dataset_folder) 72 | shutil.rmtree(raw_data_folder) 73 | 74 | -------------------------------------------------------------------------------- /download_svox.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import shutil 4 | 5 | import gdown 6 | 7 | datasets_folder = os.path.join(os.curdir, "datasets") 8 | os.makedirs(datasets_folder, exist_ok=True) 9 | zip_filepath = os.path.join(datasets_folder, 'svox.zip') 10 | gdown.download(id="16iuk8voW65GaywNUQlWAbDt6HZzAJ_t9", 11 | output=zip_filepath, 12 | quiet=False) 13 | 14 | shutil.unpack_archive(zip_filepath, datasets_folder) 15 | os.remove(zip_filepath) 16 | -------------------------------------------------------------------------------- /format_mapillary.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import shutil 4 | from glob import glob 5 | from tqdm import tqdm 6 | from os.path import join 7 | 8 | import util 9 | 10 | # This dictionary is copied from the original code 11 | # https://github.com/mapillary/mapillary_sls/blob/master/mapillary_sls/datasets/msls.py#L16 12 | default_cities = { 13 | 'train': ["trondheim", "london", "boston", "melbourne", "amsterdam","helsinki", 14 | "tokyo","toronto","saopaulo","moscow","zurich","paris","bangkok", 15 | "budapest","austin","berlin","ottawa","phoenix","goa","amman","nairobi","manila"], 16 | 'val': ["cph", "sf"], 17 | 'test': ["miami","athens","buenosaires","stockholm","bengaluru","kampala"] 18 | } 19 | 20 | csv_files_paths = sorted(glob(join("datasets", "mapillary_sls", "*", "*", "*", "postprocessed.csv"), 21 | recursive=True)) 22 | 23 | for csv_file_path in csv_files_paths: 24 | with open(csv_file_path, "r") as file: 25 | postprocessed_lines = file.readlines()[1:] 26 | with open(csv_file_path.replace("postprocessed", "raw"), "r") as file: 27 | raw_lines = file.readlines()[1:] 28 | assert len(raw_lines) == len(postprocessed_lines) 29 | 30 | csv_dir = os.path.dirname(csv_file_path) 31 | city_path, folder = os.path.split(csv_dir) 32 | city = os.path.split(city_path)[1] 33 | 34 | folder = "database" if folder == "database" else "queries" 35 | train_val = "train" if city in default_cities["train"] else "val" 36 | dst_folder = os.path.join('msls', train_val, folder) 37 | 38 | os.makedirs(dst_folder, exist_ok=True) 39 | for postprocessed_line, raw_line in zip(tqdm(postprocessed_lines, desc=city), raw_lines): 40 | _, pano_id, lon, lat, _, timestamp, is_panorama = raw_line.split(",") 41 | if is_panorama == "True\n": 42 | continue 43 | timestamp = timestamp.replace("-", "") 44 | view_direction = postprocessed_line.split(",")[-1].replace("\n", "").lower() 45 | day_night = "day" if postprocessed_line.split(",")[-2] == "False" else "night" 46 | note = f"{day_night}_{view_direction}_{city}" 47 | dst_image_name = util.get_dst_image_name(lat, lon, pano_id, timestamp=timestamp, note=note) 48 | src_image_path = os.path.join(os.path.dirname(csv_file_path), 'images', f'{pano_id}.jpg') 49 | dst_image_path = os.path.join(dst_folder, dst_image_name) 50 | _ = shutil.move(src_image_path, dst_image_path) 51 | 52 | val_path = os.path.join('msls', 'val') 53 | test_path = os.path.join('msls', 'test') 54 | os.symlink(os.path.abspath(val_path), test_path) 55 | -------------------------------------------------------------------------------- /format_pitts250k.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | In datasets/pitts250k/raw_data there should be the following files/folders: 4 | 000, 001, 002, 003, 004, 005, 006, 007, 008, 009, 010, queries_real, datasets 5 | (datasets is a folder that contains files such as pitts250k_train.mat) 6 | """ 7 | 8 | import os 9 | import re 10 | import utm 11 | import shutil 12 | from tqdm import tqdm 13 | from os.path import join 14 | from scipy.io import loadmat 15 | 16 | import util 17 | import map_builder 18 | 19 | datasets_folder = join(os.curdir, "datasets") 20 | dataset_name = "pitts250k" 21 | dataset_folder = join(datasets_folder, dataset_name) 22 | raw_data_folder = join(datasets_folder, dataset_name, "raw_data") 23 | os.makedirs(dataset_folder, exist_ok=True) 24 | os.makedirs(raw_data_folder, exist_ok=True) 25 | 26 | def move_images(dst_folder, src_images_paths, utms): 27 | os.makedirs(dst_folder, exist_ok=True) 28 | for src_image_path, (utm_east, utm_north) in zip(tqdm(src_images_paths, desc=f"Move images to {dst_folder}"), 29 | utms): 30 | src_image_name = os.path.basename(src_image_path) 31 | latitude, longitude = utm.to_latlon(utm_east, utm_north, 17, "T") 32 | pitch = int(re.findall('pitch(\d+)_', src_image_name)[0])-1 33 | yaw = int(re.findall('yaw(\d+)\.', src_image_name)[0])-1 34 | note = re.findall('_(.+)\.jpg', src_image_name)[0] 35 | tile_num = pitch*24 + yaw 36 | dst_image_name = util.get_dst_image_name(latitude, longitude, pano_id=src_image_name.split("_")[0], 37 | tile_num=tile_num, note=note) 38 | 39 | src_path = os.path.join(dataset_folder, 'raw_data', src_image_path) 40 | dst_path = os.path.join(dst_folder, dst_image_name) 41 | shutil.move(src_path, dst_path) 42 | 43 | 44 | for dataset in ["train", "val", "test"]: 45 | matlab_struct_file_path = os.path.join(dataset_folder, "raw_data", "datasets", f"pitts250k_{dataset}.mat") 46 | mat_struct = loadmat(matlab_struct_file_path)["dbStruct"].item() 47 | # Database 48 | g_images = [f[0].item() for f in mat_struct[1]] 49 | g_utms = mat_struct[2].T 50 | move_images(os.path.join(dataset_folder, 'images', dataset, 'database'), g_images, g_utms) 51 | # Queries 52 | q_images = [os.path.join("queries_real", f"{f[0].item()}") for f in mat_struct[3]] 53 | q_utms = mat_struct[4].T 54 | move_images(os.path.join(dataset_folder, 'images', dataset, 'queries'), q_images, q_utms) 55 | 56 | map_builder.build_map_from_dataset(dataset_folder) 57 | shutil.rmtree(raw_data_folder) 58 | -------------------------------------------------------------------------------- /format_pitts30k.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | In datasets/pitts30k/raw_data there should be the following files/folders: 4 | 000, 001, 002, 003, 004, 005, 006, queries_real, datasets 5 | (datasets is a folder that contains files such as pitts250k_train.mat) 6 | """ 7 | 8 | import os 9 | import re 10 | import utm 11 | import shutil 12 | from tqdm import tqdm 13 | from os.path import join 14 | from scipy.io import loadmat 15 | 16 | import util 17 | import map_builder 18 | 19 | datasets_folder = join(os.curdir, "datasets") 20 | dataset_name = "pitts30k" 21 | dataset_folder = join(datasets_folder, dataset_name) 22 | raw_data_folder = join(datasets_folder, dataset_name, "raw_data") 23 | os.makedirs(dataset_folder, exist_ok=True) 24 | os.makedirs(raw_data_folder, exist_ok=True) 25 | 26 | def copy_images(dst_folder, src_images_paths, utms): 27 | os.makedirs(dst_folder, exist_ok=True) 28 | for src_image_path, (utm_east, utm_north) in zip(tqdm(src_images_paths, desc=f"Copy to {dst_folder}"), 29 | utms): 30 | src_image_name = os.path.basename(src_image_path) 31 | latitude, longitude = utm.to_latlon(utm_east, utm_north, 17, "T") 32 | pitch = int(re.findall('pitch(\d+)_', src_image_name)[0])-1 33 | yaw = int(re.findall('yaw(\d+)\.', src_image_name)[0])-1 34 | note = re.findall('_(.+)\.jpg', src_image_name)[0] 35 | tile_num = pitch*24 + yaw 36 | dst_image_name = util.get_dst_image_name(latitude, longitude, pano_id=src_image_name.split("_")[0], 37 | tile_num=tile_num, note=note) 38 | src_path = os.path.join(dataset_folder, 'raw_data', src_image_path) 39 | dst_path = os.path.join(dst_folder, dst_image_name) 40 | shutil.move(src_path, dst_path) 41 | 42 | 43 | for dataset in ["train", "val", "test"]: 44 | matlab_struct_file_path = os.path.join(dataset_folder, "raw_data", "datasets", f"pitts30k_{dataset}.mat") 45 | mat_struct = loadmat(matlab_struct_file_path)["dbStruct"].item() 46 | # Database 47 | g_images = [f[0].item() for f in mat_struct[1]] 48 | g_utms = mat_struct[2].T 49 | copy_images(os.path.join(dataset_folder, 'images', dataset, 'database'), g_images, g_utms) 50 | # Queries 51 | q_images = [os.path.join("queries_real", f"{f[0].item()}") for f in mat_struct[3]] 52 | q_utms = mat_struct[4].T 53 | copy_images(os.path.join(dataset_folder, 'images', dataset, 'queries'), q_images, q_utms) 54 | 55 | map_builder.build_map_from_dataset(dataset_folder) 56 | shutil.rmtree(raw_data_folder) 57 | -------------------------------------------------------------------------------- /format_tokyo247.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script allows to format the dataset of Tokyo 24/7 to a common format. 3 | The dataset should be downloaded before running this script (contact its 4 | authors for the link), and the files should be arranged as in the tree below. 5 | Folders like 03814 contain the images, and tokyo247.mat contains the metadata. 6 | Folders like 038XX should be extracted from archive 038XX.tar 7 | The queries are automatically downloaded by this script. 8 | Note that Tokyo 24/7 is a test-only dataset, i.e. there is no train and 9 | validation set. In some works (e.g. NetVLAD) Tokyo TM (Time Machine) has been 10 | used as a training set, but we do not provide the code to download it and 11 | format it. 12 | 13 | . 14 | |-- format_tokyo247.py 15 | |-- map_builder.py 16 | |-- util.py 17 | `-- datasets 18 | `-- tokyo247 19 | `-- raw_data 20 | |-- datasets 21 | | `-- tokyo247.mat 22 | `-- tokyo247 23 | |-- 03814 24 | |-- 03815 25 | |-- 03816 26 | |-- 03817 27 | |-- 03818 28 | |-- 03819 29 | |-- 03820 30 | |-- 03821 31 | |-- 03822 32 | |-- 03823 33 | |-- 03824 34 | |-- 03825 35 | |-- 03826 36 | |-- 03827 37 | |-- 03828 38 | `-- 03829 39 | 40 | """ 41 | 42 | import os 43 | import re 44 | import utm 45 | import shutil 46 | import py3_wget 47 | import torchvision 48 | from glob import glob 49 | from tqdm import tqdm 50 | from PIL import Image 51 | from os.path import join 52 | from scipy.io import loadmat 53 | 54 | import util 55 | import map_builder 56 | 57 | datasets_folder = join(os.curdir, "datasets") 58 | dataset_name = "tokyo247" 59 | dataset_folder = join(datasets_folder, dataset_name) 60 | raw_data_folder = join(datasets_folder, dataset_name, "raw_data") 61 | os.makedirs(dataset_folder, exist_ok=True) 62 | os.makedirs(raw_data_folder, exist_ok=True) 63 | os.makedirs(join(raw_data_folder, "tokyo247"), exist_ok=True) 64 | 65 | #### Database 66 | matlab_struct_file_path = join(dataset_folder, 'raw_data', 'datasets', 'tokyo247.mat') 67 | 68 | mat_struct = loadmat(matlab_struct_file_path)["dbStruct"].item() 69 | db_images = [join('tokyo247', f[0].item().replace('.jpg', '.png')) for f in mat_struct[1]] 70 | 71 | db_utms = mat_struct[2].T 72 | dst_folder = join(dataset_folder, 'images', 'test', 'database') 73 | 74 | os.makedirs(dst_folder, exist_ok=True) 75 | for src_image_path, (utm_east, utm_north) in zip(tqdm(db_images, desc=f"Copy to {dst_folder}"), 76 | db_utms): 77 | src_image_name = os.path.basename(src_image_path) 78 | latitude, longitude = utm.to_latlon(utm_east, utm_north, 54, 'S') 79 | pano_id = src_image_name[:22] 80 | tile_num = int(re.findall('_012_(\d+)\.png', src_image_name)[0])//30 81 | assert 0 <= tile_num < 12 82 | dst_image_name = util.get_dst_image_name(latitude, longitude, pano_id=pano_id, 83 | tile_num=tile_num) 84 | src_image_path = f"{dataset_folder}/raw_data/{src_image_path}" 85 | try: 86 | Image.open(src_image_path).save(f"{dst_folder}/{dst_image_name}") 87 | except OSError as e: 88 | print(f"Exception {e} with file {src_image_path}") 89 | raise e 90 | 91 | #### Queries 92 | filename = "247query_subset_v2.zip" 93 | url = f"https://data.ciirc.cvut.cz/public/projects/2015netVLAD/Tokyo247/queries/{filename}" 94 | file_zip_path = join(raw_data_folder, "tokyo247", filename) 95 | py3_wget.download_file(url, file_zip_path) 96 | shutil.unpack_archive(file_zip_path, join(raw_data_folder, "tokyo247")) 97 | src_queries_folder = file_zip_path.replace(".zip", "") 98 | src_queries_paths = sorted(glob(join(src_queries_folder, "*.jpg"))) 99 | os.makedirs(join(dataset_folder, "images", "test", "queries"), exist_ok=True) 100 | for src_query_path in tqdm(src_queries_paths, desc=f"Copy to {dataset_folder}/images/test/queries"): 101 | csv_path = src_query_path.replace(".jpg", ".csv") 102 | with open(csv_path, "r") as file: 103 | info = file.readline() 104 | pano_id, latitude, longitude = info.split(",")[:3] 105 | pano_id = pano_id.replace(",jpg", "") 106 | dst_image_name = util.get_dst_image_name(latitude, longitude, pano_id=pano_id) 107 | dst_image_path = join(dataset_folder, "images", "test", "queries", dst_image_name) 108 | try: 109 | pil_img = Image.open(src_query_path) 110 | except OSError as e: 111 | print(f"Exception {e} with file {src_query_path}") 112 | raise e 113 | resized_pil_img = torchvision.transforms.Resize(480)(pil_img) 114 | resized_pil_img.save(dst_image_path) 115 | 116 | map_builder.build_map_from_dataset(dataset_folder) 117 | shutil.rmtree(raw_data_folder) 118 | 119 | -------------------------------------------------------------------------------- /map_builder.py: -------------------------------------------------------------------------------- 1 | 2 | import matplotlib 3 | matplotlib.use('Agg') 4 | import os 5 | import cv2 6 | import copy 7 | import math 8 | import numpy as np 9 | from glob import glob 10 | from skimage import io 11 | from os.path import join 12 | import matplotlib.cm as cm 13 | import matplotlib.pyplot as plt 14 | from collections import defaultdict 15 | from staticmap import StaticMap, Polygon 16 | from matplotlib.colors import ListedColormap 17 | 18 | 19 | def _lon_to_x(lon, zoom): 20 | if not (-180 <= lon <= 180): lon = (lon + 180) % 360 - 180 21 | return ((lon + 180.) / 360) * pow(2, zoom) 22 | 23 | 24 | def _lat_to_y(lat, zoom): 25 | if not (-90 <= lat <= 90): lat = (lat + 90) % 180 - 90 26 | return (1 - math.log(math.tan(lat * math.pi / 180) + 1 / math.cos(lat * math.pi / 180)) / math.pi) / 2 * pow(2, zoom) 27 | 28 | 29 | def _download_map_image(min_lat=45.0, min_lon=7.6, max_lat=45.1, max_lon=7.7, size=2000): 30 | """"Download a map of the chosen area as a numpy image""" 31 | mean_lat = (min_lat + max_lat) / 2 32 | mean_lon = (min_lon + max_lon) / 2 33 | static_map = StaticMap(size, size) 34 | static_map.add_polygon( 35 | Polygon(((min_lon, min_lat), (min_lon, max_lat), (max_lon, max_lat), (max_lon, min_lat)), None, '#FFFFFF')) 36 | zoom = static_map._calculate_zoom() 37 | static_map = StaticMap(size, size) 38 | image = static_map.render(zoom, [mean_lon, mean_lat]) 39 | print( 40 | f"You can see the map on Google Maps at this link www.google.com/maps/place/@{mean_lat},{mean_lon},{zoom - 1}z") 41 | min_lat_px, min_lon_px, max_lat_px, max_lon_px = \ 42 | static_map._y_to_px(_lat_to_y(min_lat, zoom)), \ 43 | static_map._x_to_px(_lon_to_x(min_lon, zoom)), \ 44 | static_map._y_to_px(_lat_to_y(max_lat, zoom)), \ 45 | static_map._x_to_px(_lon_to_x(max_lon, zoom)) 46 | assert 0 <= max_lat_px < min_lat_px < size and 0 <= min_lon_px < max_lon_px < size 47 | return np.array(image)[max_lat_px:min_lat_px, min_lon_px:max_lon_px], static_map, zoom 48 | 49 | 50 | def get_edges(coordinates, enlarge=0): 51 | """ 52 | Send the edges of the coordinates, i.e. the most south, west, north and 53 | east coordinates. 54 | :param coordinates: A list of numpy.arrays of shape (Nx2) 55 | :param float enlarge: How much to increase the coordinates, to enlarge 56 | the area included between the points 57 | :return: a tuple with the four float 58 | """ 59 | min_lat, min_lon, max_lat, max_lon = (*np.concatenate(coordinates).min(0), *np.concatenate(coordinates).max(0)) 60 | diff_lat = (max_lat - min_lat) * enlarge 61 | diff_lon = (max_lon - min_lon) * enlarge 62 | inc_min_lat, inc_min_lon, inc_max_lat, inc_max_lon = \ 63 | min_lat - diff_lat, min_lon - diff_lon, max_lat + diff_lat, max_lon + diff_lon 64 | return inc_min_lat, inc_min_lon, inc_max_lat, inc_max_lon 65 | 66 | 67 | def _create_map(coordinates, colors=None, dot_sizes=None, legend_names=None, map_intensity=0.6): 68 | coordinates = copy.deepcopy(coordinates) 69 | dot_sizes = dot_sizes if dot_sizes is not None else [10] * len(coordinates) 70 | colors = colors if colors is not None else ["r"] * len(coordinates) 71 | assert len(coordinates) == len(dot_sizes) == len(colors), \ 72 | f"The number of coordinates must be equals to the number of colors and dot_sizes, but they're " \ 73 | f"{len(coordinates)}, {len(colors)}, {len(dot_sizes)}" 74 | 75 | # Add two dummy points to slightly enlarge the map 76 | min_lat, min_lon, max_lat, max_lon = get_edges(coordinates, enlarge=0.1) 77 | coordinates.append(np.array([[min_lat, min_lon], [max_lat, max_lon]])) 78 | # Download the map of the chosen area 79 | map_img, static_map, zoom = _download_map_image(min_lat, min_lon, max_lat, max_lon) 80 | 81 | scatters = [] 82 | fig = plt.figure(figsize=(map_img.shape[1] / 100, map_img.shape[0] / 100), dpi=1000) 83 | for i, coord in enumerate(coordinates): 84 | for i in range(len(coord)): # Scale latitudes because of earth's curvature 85 | coord[i, 0] = -static_map._y_to_px(_lat_to_y(coord[i, 0], zoom)) 86 | for coord, size, color in zip(coordinates, dot_sizes, colors): 87 | scatters.append(plt.scatter(coord[:, 1], coord[:, 0], s=size, color=color)) 88 | 89 | if legend_names != None: 90 | plt.legend(scatters, legend_names, scatterpoints=10000, loc='lower left', 91 | ncol=1, framealpha=0, prop={"weight": "bold", "size": 30}) 92 | 93 | min_lat, min_lon, max_lat, max_lon = get_edges(coordinates) 94 | plt.ylim(min_lat, max_lat) 95 | plt.xlim(min_lon, max_lon) 96 | fig.subplots_adjust(bottom=0, top=1, left=0, right=1) 97 | fig.canvas.draw() 98 | plot_img = np.array(fig.canvas.renderer._renderer) 99 | plt.close() 100 | 101 | plot_img = cv2.resize(plot_img[:, :, :3], map_img.shape[:2][::-1], interpolation=cv2.INTER_LANCZOS4) 102 | map_img[(map_img.sum(2) < 444)] = 188 # brighten dark pixels 103 | map_img = (((map_img / 255) ** map_intensity) * 255).astype(np.uint8) # fade map 104 | mask = (plot_img.sum(2) == 255 * 3)[:, :, None] # mask of plot, to find white pixels 105 | final_map = map_img * mask + plot_img * (~mask) 106 | return final_map 107 | 108 | 109 | def _get_coordinates_from_dataset(dataset_folder, extension="jpg"): 110 | """ 111 | Takes as input the path of a dataset, such as "datasets/st_lucia/images" 112 | and returns 113 | [("train/database", [[45, 8.1], [45.2, 8.2]]), ("train/queries", [[45, 8.1], [45.2, 8.2]])] 114 | """ 115 | images_paths = glob(join(dataset_folder, "**", f"*.{extension}"), recursive=True) 116 | if len(images_paths) != 0: 117 | print(f"I found {len(images_paths)} images in {dataset_folder}") 118 | else: 119 | raise ValueError(f"I found no images in {dataset_folder} !") 120 | 121 | grouped_gps_coords = defaultdict(list) 122 | 123 | for image_path in images_paths: 124 | full_path = os.path.dirname(image_path) 125 | full_parent_path, parent_dir = os.path.split(full_path) 126 | parent_parent_dir = os.path.split(full_parent_path)[1] 127 | 128 | # folder_name is for example "train - database" 129 | folder_name = " - ".join([parent_parent_dir, parent_dir]) 130 | 131 | gps_coords = image_path.split("@")[5], image_path.split("@")[6] 132 | grouped_gps_coords[folder_name].append(gps_coords) 133 | 134 | grouped_gps_coords = sorted([(k, np.array(v).astype(np.float64)) 135 | for k, v in grouped_gps_coords.items()]) 136 | return grouped_gps_coords 137 | 138 | 139 | def build_map_from_dataset(dataset_folder, dot_sizes=None): 140 | """dataset_folder is the path that contains the 'images' folder.""" 141 | grouped_gps_coords = _get_coordinates_from_dataset(join(dataset_folder, "images")) 142 | SORTED_FOLDERS = ["train - database", "train - queries", "val - database", "val - queries", 143 | "test - database", "test - queries"] 144 | try: 145 | grouped_gps_coords = sorted(grouped_gps_coords, key=lambda x: SORTED_FOLDERS.index(x[0])) 146 | except ValueError: 147 | pass # This dataset has different folder names than the standard train-val-test database-queries. 148 | coordinates = [] 149 | legend_names = [] 150 | for folder_name, coords in grouped_gps_coords: 151 | legend_names.append(f"{folder_name} - {len(coords)}") 152 | coordinates.append(coords) 153 | 154 | colors = cm.rainbow(np.linspace(0, 1, len(legend_names))) 155 | colors = ListedColormap(colors) 156 | colors = colors.colors 157 | if len(legend_names) == 1: 158 | legend_names = None # If there's only one folder, don't show the legend 159 | colors = np.array([[1, 0, 0]]) 160 | 161 | map_img = _create_map(coordinates, colors, dot_sizes, legend_names) 162 | 163 | print(f"Map image resolution: {map_img.shape}") 164 | dataset_name = os.path.basename(os.path.abspath(dataset_folder)) 165 | io.imsave(join(dataset_folder, f"map_{dataset_name}.png"), map_img) 166 | 167 | 168 | if __name__ == "__main__": 169 | coordinates = [ 170 | np.array([[41.8931, 12.4828], [45.4669, 9.1900], [40.8333, 14.2500]]), 171 | np.array([[52.5200, 13.4050], [48.7775, 9.1800], [48.1375, 11.5750]]), 172 | np.array([[48.8567, 2.3522], [43.2964, 5.3700], [45.7600, 4.8400]]) 173 | ] 174 | map_img = _create_map( 175 | coordinates, 176 | colors=["green", "black", "blue"], 177 | dot_sizes=[1000, 1000, 1000], 178 | legend_names=[ 179 | "Main Italian Cities", 180 | "Main German Cities", 181 | "Main French Cities", 182 | ]) 183 | 184 | io.imsave("cities.png", map_img) 185 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | staticmap 2 | tqdm 3 | mega.py 4 | requests 5 | opencv_contrib_python 6 | scikit_image 7 | scipy 8 | matplotlib 9 | numpy 10 | utm 11 | Pillow 12 | protobuf 13 | torchvision 14 | gdown 15 | py3_wget -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import re 4 | import utm 5 | import cv2 6 | import math 7 | 8 | 9 | def get_distance(coords_A, coords_B): 10 | return math.sqrt((float(coords_B[0])-float(coords_A[0]))**2 + (float(coords_B[1])-float(coords_A[1]))**2) 11 | 12 | 13 | def is_valid_timestamp(timestamp): 14 | """Return True if it's a valid timestamp, in format YYYYMMDD_hhmmss, 15 | with all fields from left to right optional. 16 | >>> is_valid_timestamp('') 17 | True 18 | >>> is_valid_timestamp('201901') 19 | True 20 | >>> is_valid_timestamp('20190101_123000') 21 | True 22 | """ 23 | return bool(re.match("^(\d{4}(\d{2}(\d{2}(_(\d{2})(\d{2})?(\d{2})?)?)?)?)?$", timestamp)) 24 | 25 | 26 | def format_coord(num, left=2, right=5): 27 | """Return the formatted number as a string with (left) int digits 28 | (including sign '-' for negatives) and (right) float digits. 29 | >>> format_coord2(1.1, 3, 3) 30 | '001.100' 31 | >>> format_coord2(-0.12345, 3, 3) 32 | '-00.123' 33 | >>> format_coord2(-0.123, 5, 5) 34 | '-0000.12300' 35 | """ 36 | return f'{float(num):0={left+right+1}.{right}f}' 37 | 38 | import doctest 39 | doctest.testmod() # Automatically execute unit-test of format_coord() 40 | 41 | 42 | def format_location_info(latitude, longitude): 43 | easting, northing, zone_number, zone_letter = utm.from_latlon(float(latitude), float(longitude)) 44 | easting = format_coord(easting, 7, 2) 45 | northing = format_coord(northing, 7, 2) 46 | latitude = format_coord(latitude, 3, 5) 47 | longitude = format_coord(longitude, 4, 5) 48 | return easting, northing, zone_number, zone_letter, latitude, longitude 49 | 50 | 51 | def get_dst_image_name(latitude, longitude, pano_id=None, tile_num=None, heading=None, 52 | pitch=None, roll=None, height=None, timestamp=None, note=None, extension=".jpg"): 53 | easting, northing, zone_number, zone_letter, latitude, longitude = format_location_info(latitude, longitude) 54 | tile_num = f"{int(float(tile_num)):02d}" if tile_num is not None else "" 55 | heading = f"{int(float(heading)):03d}" if heading is not None else "" 56 | pitch = f"{int(float(pitch)):03d}" if pitch is not None else "" 57 | timestamp = f"{timestamp}" if timestamp is not None else "" 58 | note = f"{note}" if note is not None else "" 59 | assert is_valid_timestamp(timestamp), f"{timestamp} is not in YYYYMMDD_hhmmss format" 60 | if roll is None: roll = "" 61 | else: raise NotImplementedError() 62 | if height is None: height = "" 63 | else: raise NotImplementedError() 64 | 65 | return f"@{easting}@{northing}@{zone_number:02d}@{zone_letter}@{latitude}@{longitude}" + \ 66 | f"@{pano_id}@{tile_num}@{heading}@{pitch}@{roll}@{height}@{timestamp}@{note}@{extension}" 67 | 68 | 69 | class VideoReader: 70 | def __init__(self, video_name, size=None): 71 | if not os.path.exists(video_name): 72 | raise FileNotFoundError(f"{video_name} does not exist") 73 | self.video_name = video_name 74 | self.size = size 75 | self.vc = cv2.VideoCapture(f"{video_name}") 76 | self.frames_per_second = self.vc.get(cv2.CAP_PROP_FPS) 77 | self.frame_duration_millis = 1000 / self.frames_per_second 78 | self.frames_num = int(self.vc.get(cv2.CAP_PROP_FRAME_COUNT)) 79 | self.video_length_in_millis = int(self.frames_num * 1000 / self.frames_per_second) 80 | 81 | def get_time_at_frame(self, frame_num): 82 | return int(self.frame_duration_millis * frame_num) 83 | 84 | def get_frame_num_at_time(self, time): 85 | # time can be str ('21:59') or int in milliseconds 86 | millis = time if type(time) == int else self.str_to_millis(time) 87 | return min(int(millis / self.frame_duration_millis), self.frames_num) 88 | 89 | def get_frame_at_frame_num(self, frame_num): 90 | self.vc.set(cv2.CAP_PROP_POS_FRAMES, frame_num) 91 | frame = self.vc.read()[1] 92 | if frame is None: return None # In case of corrupt videos 93 | if self.size is not None: 94 | frame = cv2.resize(frame, self.size[::-1], cv2.INTER_CUBIC) 95 | frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 96 | return frame 97 | 98 | @staticmethod 99 | def str_to_millis(time_str): 100 | return (int(time_str.split(":")[0]) * 60 + int(time_str.split(":")[1])) * 1000 101 | 102 | @staticmethod 103 | def millis_to_str(millis): 104 | if millis < 60*60*1000: 105 | return f"{math.floor((millis//1000//60)%60):02d}:{millis//1000%60:02d}" 106 | else: 107 | return f"{math.floor((millis//1000//60//60)%60):02d}:{math.floor((millis//1000//60)%60):02d}:{millis//1000%60:02d}" 108 | 109 | def __repr__(self): 110 | H, W = int(self.vc.get(cv2.CAP_PROP_FRAME_HEIGHT)), int(self.vc.get(cv2.CAP_PROP_FRAME_WIDTH)) 111 | return (f"Video '{self.video_name}' has {self.frames_num} frames, " + 112 | f"with resolution {H}x{W}, " + 113 | f"and lasts {self.video_length_in_millis // 1000} seconds " 114 | f"({self.millis_to_str(self.video_length_in_millis)}), therefore " 115 | f"there's a frame every {int(self.frame_duration_millis)} millis") 116 | 117 | def __del__(self): 118 | self.vc.release() 119 | 120 | --------------------------------------------------------------------------------