├── .gitignore
├── Brisbane Event VPR.ipynb
├── README.md
├── code_helpers_public.py
├── code_helpers_public_pr_curve.py
├── convert_rosbags.py
├── correspondence_event_camera_frame_camera.py
├── dataset.png
├── export_frames_from_rosbag.py
├── read_gps.py
└── reconstruct_videos.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # vs-code
2 | .vscode
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | pip-wheel-metadata/
27 | share/python-wheels/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 | MANIFEST
32 |
33 | # PyInstaller
34 | # Usually these files are written by a python script from a template
35 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
36 | *.manifest
37 | *.spec
38 |
39 | # Installer logs
40 | pip-log.txt
41 | pip-delete-this-directory.txt
42 |
43 | # Unit test / coverage reports
44 | htmlcov/
45 | .tox/
46 | .nox/
47 | .coverage
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *.cover
53 | *.py,cover
54 | .hypothesis/
55 | .pytest_cache/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 | db.sqlite3
65 | db.sqlite3-journal
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Event-Based Visual Place Recognition With Ensembles of Temporal Windows
2 |
3 | [](https://creativecommons.org/licenses/by-nc-sa/4.0/)
4 | [](https://github.com/Tobias-Fischer/ensemble-event-vpr/stargazers)
5 | [](https://github.com/Tobias-Fischer/ensemble-event-vpr/issues)
6 | [](./README.md)
7 |
8 |
9 | ### License + Attribution
10 | This code is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Commercial usage is not permitted. If you use this dataset or the code in a scientific publication, please cite the following [paper](http://doi.org/10.1109/LRA.2020.3025505) ([preprint and additional material](https://arxiv.org/abs/2006.02826)):
11 |
12 | ```
13 | @article{fischer2020event,
14 | title={Event-Based Visual Place Recognition With Ensembles of Temporal Windows},
15 | author={Fischer, Tobias and Milford, Michael},
16 | journal={IEEE Robotics and Automation Letters},
17 | volume={5},
18 | number={4},
19 | pages={6924--6931},
20 | year={2020}
21 | }
22 | ```
23 |
24 | The [Brisbane-Event-VPR dataset](https://zenodo.org/record/4302805) accompanies this code repository: https://zenodo.org/record/4302805
25 |
26 | 
27 |
28 |
29 | ## Code overview
30 | The following code is available:
31 | - The [correspondence_event_camera_frame_camera.py](./correspondence_event_camera_frame_camera.py) file contains the mapping between the rosbag names and the consumer camera video names. The variable `video_beginning` indicates the ROS timestamp within the bag file that corresponds to the first frame of the consumer camera video file.
32 | - The [read_gps.py](./read_gps.py) file contains some helper functions to read in GPS data from the provided nmea files, and find matches between two traverses.
33 | - The [code_helpers_public.py](./code_helpers_public.py) file contains some helper codes.
34 | - The [code_helpers_public_pr_curve.py](./code_helpers_public_pr_curve.py) file contains some helper codes to get precision, recall and precision-recall curves given a distance matrix.
35 | - The main code is contained in the [Brisbane Event VPR.ipynb](./Brisbane%20Event%20VPR.ipynb) notebook.
36 |
37 | Please note that in our paper we used manually annotated and then interpolated correspondences; instead here we provide matches based on the GPS data. Therefore, the results between what is reported in the paper and what is obtained using the methods here will be slightly different.
38 |
39 | ## Reconstruct videos from events
40 | 1. Clone this repository: `git clone https://github.com/Tobias-Fischer/ensemble-event-vpr.git`
41 |
42 | 1. Clone https://github.com/cedric-scheerlinck/rpg_e2vid and follow the instructions to create a conda environment and download the pretrained models.
43 |
44 | 1. Download the [Brisbane-Event-VPR dataset](https://zenodo.org/record/4302805).
45 |
46 | 1. Now convert the bag files to txt/zip files that can be used by the event2video code: `python convert_rosbags.py`. Make sure to adjust the path to the `extract_events_from_rosbag.py` file from the rpg_e2vid repository.
47 |
48 | 1. Now do the event to video conversion: `python reconstruct_videos.py`. Make sure to adjust the path to the `run_reconstruction.py` file from the rpg_e2vid repository.
49 |
50 | ## Create suitable conda environment
51 | 1. Create a new conda environment with the dependencies: `conda create --name brisbaneeventvpr tensorflow-gpu pynmea2 scipy matplotlib numpy tqdm jupyterlab opencv pip ros-noetic-rosbag ros-noetic-cv-bridge python=3.8 -c conda-forge -c robostack`
52 |
53 | ## Export RGB frames from rosbags
54 | 1. `conda activate brisbaneeventvpr`
55 |
56 | 1. `python export_frames_from_rosbag.py`
57 |
58 | ## Event-based VPR with ensembles
59 | 1. Create a new conda environment with the dependencies: `conda create --name brisbaneeventvpr tensorflow-gpu pynmea2 scipy matplotlib numpy tqdm jupyterlab opencv pip`
60 |
61 | 1. `conda activate brisbaneeventvpr`
62 |
63 | 1. `git clone https://github.com/QVPR/netvlad_tf_open.git`
64 |
65 | 1. `cd netvlad_tf_open && pip install -e .`
66 |
67 | 1. Download the NetVLAD checkpoint [here](http://rpg.ifi.uzh.ch/datasets/netvlad/vd16_pitts30k_conv5_3_vlad_preL2_intra_white.zip) (1.1 GB). Extract the zip and move its contents to the checkpoints folder of the `netvlad_tf_open` repository.
68 |
69 | 1. Open the [Brisbane Event VPR.ipynb](./Brisbane%20Event%20VPR.ipynb) and adjust the path to the `dataset_folder`.
70 |
71 | 1. You can now run the code in [Brisbane Event VPR.ipynb](./Brisbane%20Event%20VPR.ipynb).
72 |
73 | # Related works
74 | Please check out [this collection](https://qcr.github.io/collection/vpr_overview/) of related works on place recognition.
75 |
--------------------------------------------------------------------------------
/code_helpers_public.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as np
3 | from os.path import join, isfile, basename, abspath
4 | import matplotlib.pyplot as plt
5 |
6 |
7 | def compare_images(image_paths1, image_paths2, image_num1, image_num2):
8 | if isinstance(image_paths1[image_num1], np.ndarray):
9 | img_A = image_paths1[image_num1]
10 | else:
11 | img_A = cv2.imread(image_paths1[image_num1])
12 |
13 | if isinstance(image_paths2[image_num2], np.ndarray):
14 | img_B = image_paths2[image_num2]
15 | else:
16 | img_B = cv2.imread(image_paths2[image_num2])
17 |
18 | fig, ax = plt.subplots(1, 2, figsize=(2 * 7.2, 5.4))
19 |
20 | if isinstance(image_paths1[image_num1], str):
21 | ax[0].set_title(basename(image_paths1[image_num1]) + ' (Place ' + str(image_num1) + ')')
22 | else:
23 | ax[0].set_title('Place ' + str(image_num1))
24 | if isinstance(image_paths2[image_num2], str):
25 | ax[1].set_title(basename(image_paths2[image_num2]) + ' (Place ' + str(image_num2) + ')')
26 | else:
27 | ax[1].set_title('Place ' + str(image_num2))
28 | if len(img_A.shape) == 3 and img_A.shape[2] == 3:
29 | ax[0].imshow(img_A[..., ::-1])
30 | else:
31 | ax[0].imshow(img_A, 'gray', vmin=0, vmax=255)
32 | if len(img_B.shape) == 3 and img_B.shape[2] == 3:
33 | ax[1].imshow(img_B[..., ::-1])
34 | else:
35 | ax[1].imshow(img_B, 'gray', vmin=0, vmax=255)
36 |
37 |
38 | def get_timestamps(comparison_folder1, comparison_folder2):
39 | def read_timestamps(comparison_folder):
40 | with open(join(comparison_folder, 'timestamps.txt'), "r") as file_in:
41 | timestamps_in = [float(line.rstrip()) for line in file_in]
42 | return timestamps_in
43 | return np.array(read_timestamps(comparison_folder1)), np.array(read_timestamps(comparison_folder2))
44 |
45 |
46 | def get_timestamp_matches(timestamps, timestamps_to_match):
47 | timestamps_matched = np.array([np.abs(timestamps - ts).argmin() for ts in timestamps_to_match])
48 | return timestamps_matched
49 |
50 |
51 | class ListOnDemand(object):
52 | def __init__(self):
53 | self.path_list = []
54 | self.image_list = []
55 |
56 | def append(self, item):
57 | self.path_list.append(item)
58 | self.image_list.append(None)
59 |
60 | def get_path_list(self):
61 | return self.path_list
62 |
63 | def __len__(self):
64 | return len(self.path_list)
65 |
66 | def __getitem__(self, idx):
67 | if isinstance(idx, slice):
68 | #Get the start, stop, and step from the slice
69 | return [self[ii] for ii in range(*idx.indices(len(self)))]
70 | elif isinstance(idx, list):
71 | return [self[ii] for ii in idx]
72 | elif isinstance(idx, np.ndarray):
73 | return self[idx.tolist()]
74 | elif isinstance(idx, str):
75 | return self[int(idx)]
76 | elif isinstance(idx, int) or isinstance(idx, np.generic):
77 | if idx < 0 : #Handle negative indices
78 | idx += len( self )
79 | if idx < 0 or idx >= len( self ) :
80 | raise IndexError("Index %d is out of range."%idx)
81 |
82 | if self.image_list[idx] is None:
83 | self.image_list[idx] = cv2.imread(self.path_list[idx])
84 |
85 | return self.image_list[idx]
86 | else:
87 | raise TypeError("Invalid argument type:", type(idx))
88 |
89 | def get_image_sets_on_demand(image_paths1, image_paths2, matches1, matches2):
90 | images_set1 = ListOnDemand()
91 | images_set2 = ListOnDemand()
92 | for idx1 in range(len(matches1)):
93 | image_num1 = matches1[idx1]
94 | images_set1.append(image_paths1[image_num1])
95 | for idx2 in range(len(matches2)):
96 | image_num2 = matches2[idx2]
97 | images_set2.append(image_paths2[image_num2])
98 | return images_set1, images_set2
99 |
100 |
101 | def get_vlad_features(sess, net_out, image_batch, images_set, save_name=None, batch_size=4, tqdm_position=1):
102 | if save_name and isfile(save_name):
103 | pre_extracted_features = np.load(save_name)
104 | if pre_extracted_features.shape[0] == len(images_set):
105 | return pre_extracted_features
106 | else:
107 | print('Warning: shapes of pre_extracted_features and images_set do not match, re-compute!')
108 |
109 | descs = []
110 | for batch_offset in tqdm(range(0, len(images_set), batch_size), position=tqdm_position, leave=False):
111 | # for batch_offset in range(0, len(images_set), batch_size):
112 | images = []
113 | for i in range(batch_offset, batch_offset + batch_size):
114 | if i == len(images_set):
115 | break
116 |
117 | image = images_set[i]
118 | if not isinstance(image, (np.ndarray, np.generic) ):
119 | image = cv2.imread(image)
120 |
121 | if image_batch.shape[3] == 1: # grayscale
122 | images.append(np.expand_dims(np.expand_dims(image, axis=0), axis=-1))
123 | else: # color
124 | image_inference = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
125 | images.append(np.expand_dims(image_inference, axis=0))
126 |
127 | batch = np.concatenate(images, 0)
128 | descs = descs + list(sess.run(net_out, feed_dict={image_batch: batch}))
129 |
130 | netvlad_feature_list = np.array(descs)
131 |
132 | if save_name:
133 | np.save(save_name, netvlad_feature_list)
134 |
135 | return netvlad_feature_list
136 |
--------------------------------------------------------------------------------
/code_helpers_public_pr_curve.py:
--------------------------------------------------------------------------------
1 | # PR code stuff
2 | # code taken from https://github.com/oravus/DeltaDescriptors/blob/master/src/outFuncs.py
3 | # MIT License; copyright Sourav Garg
4 |
5 | import numpy as np
6 |
7 |
8 | def getPR(mInds,gt,locRad):
9 | positives = np.argwhere(mInds!=-1)[:,0]
10 | tp = np.sum(gt[positives] <= locRad)
11 | fp = len(positives) - tp
12 |
13 | negatives = np.argwhere(mInds==-1)[:,0]
14 | tn = np.sum(gt[negatives]>locRad)
15 | fn = len(negatives) - tn
16 |
17 | assert(tp+tn+fp+fn==len(gt))
18 |
19 | if tp == 0:
20 | return 0,0,0 # what else?
21 |
22 | prec = tp/float(tp+fp)
23 | recall = tp/float(tp+fn)
24 | fscore = 2*prec*recall/(prec+recall)
25 |
26 | return prec, recall, fscore
27 |
28 |
29 | def getPRCurve(mInds,mDists,gt,locRad):
30 | prfData = []
31 | lb, ub = mDists.min(),mDists.max()
32 | step = (ub-lb)/100.0
33 | for thresh in np.arange(lb,ub+step,step):
34 | matchFlags = mDists<=thresh
35 | outVals = mInds.copy()
36 | outVals[~matchFlags] = -1
37 |
38 | p,r,f = getPR(outVals,gt,locRad)
39 | prfData.append([p,r,f])
40 | return np.array(prfData)
41 |
42 |
43 | def getPAt100R(dists,maxLocRad):
44 | pAt100R = []
45 | for i1 in range(maxLocRad):
46 | pAt100R.append([np.sum(dists<=i1)])
47 | pAt100R = np.array(pAt100R) / float(len(dists))
48 | return pAt100R
49 |
50 |
51 | def getRAt99P(prfData):
52 | # select all rows where precision == 1 and then get max of recall
53 | if len(prfData[prfData[:, 0]>0.99]) == 0:
54 | return 0
55 | else:
56 | return np.max(prfData[prfData[:, 0]>0.99][:, 1])
57 |
58 |
59 | def getPRCurveWrapper(dist_matrix, plot_threshold_fps):
60 | dist_matrix_plot_pr = dist_matrix.copy()
61 |
62 | gt = np.abs(np.arange(len(dist_matrix_plot_pr))-np.argmin(dist_matrix_plot_pr, axis=0))
63 | prvals = getPRCurve(np.argmin(dist_matrix_plot_pr, axis=0),np.min(dist_matrix_plot_pr, axis=0), gt, plot_threshold_fps)
64 | return prvals
65 |
66 |
67 | def get_recall_helper(dist_matrix, max_dist=None, transpose=True, progress_bar_position=None):
68 | if max_dist is None:
69 | max_dist = len(dist_matrix)
70 |
71 | tps = []
72 | best_matches = []
73 |
74 | if progress_bar_position is None:
75 | list_to_iterate = range(dist_matrix.shape[0])
76 | else:
77 | list_to_iterate = tqdm(range(dist_matrix.shape[0]), position=progress_bar_position, leave=False)
78 |
79 | for idx in list_to_iterate:
80 | if len(dist_matrix.shape) == 2:
81 | if transpose:
82 | best_match = np.argmin(dist_matrix.T[idx])
83 | else:
84 | best_match = np.argmin(dist_matrix[idx])
85 | elif len(dist_matrix.shape) == 1: # for SeqSLAM
86 | best_match = dist_matrix[idx]
87 | else:
88 | raise Exception('Not supported')
89 |
90 | best_matches.append(best_match)
91 | # tp = np.array([np.count_nonzero(abs(sorted_matches[:i+1]-idx)==0) for i in range(0, max_dist+1)]) # Consider top-k matches
92 | tp = np.array([np.count_nonzero(abs(best_match-idx) 0.0001:
23 | timestamp_diff = (msg.timestamp.hour - first_timestamp.hour) * 3600 + (msg.timestamp.minute - first_timestamp.minute) * 60 + (msg.timestamp.second - first_timestamp.second)
24 | latitudes.append(msg.latitude); longitudes.append(msg.longitude); timestamps.append(timestamp_diff)
25 | previous_lat, previous_lon = msg.latitude, msg.longitude
26 |
27 | except pynmea2.ParseError as e:
28 | # print('Parse error: {} {}'.format(msg.sentence_type, e))
29 | continue
30 |
31 | return np.array(np.vstack((latitudes, longitudes, timestamps))).T
32 |
--------------------------------------------------------------------------------
/reconstruct_videos.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 |
5 | filenames = ["dvs_vpr_2020-04-21-17-03-03.zip",
6 | "dvs_vpr_2020-04-22-17-24-21.zip",
7 | "dvs_vpr_2020-04-24-15-12-03.zip",
8 | "dvs_vpr_2020-04-27-18-13-29.zip",
9 | "dvs_vpr_2020-04-28-09-14-11.zip",
10 | "dvs_vpr_2020-04-29-06-20-23.zip"]
11 |
12 | model = 'firenet_1000.pth.tar'
13 | # model = 'E2VID_lightweight.pth.tar'
14 |
15 | for filename in filenames:
16 | for num_events_per_pixel in [0.1, 0.3, 0.6, 0.8]:
17 | os.system("python run_reconstruction.py -c firenet_1000.pth.tar -i " + str(filename) + " --auto_hdr --color --num_events_per_pixel " + str(num_events_per_pixel) + " --hot_pixels_file " + str(filename.replace('.zip', '_hot_pixels.txt') + " --output_folder N_" + str(num_events_per_pixel) + "/" + str(filename.replace('.zip', '')) + " --dataset_name " + str(filename.replace('.zip', ''))))
18 | for window_duration in [44, 66, 88, 120, 140]:
19 | os.system("python run_reconstruction.py -c firenet_1000.pth.tar -i " + str(filename) + " --auto_hdr --color --window_duration " + str(window_duration) + " --hot_pixels_file " + str(filename.replace('.zip', '_hot_pixels.txt') + " --output_folder t_" + str(window_duration) + "/" + str(filename.replace('.zip', '')) + " --dataset_name " + str(filename.replace('.zip', ''))))
20 |
--------------------------------------------------------------------------------