Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +219 -0
- FoL/reranking.py +96 -0
- LICENSE +29 -0
- README.md +67 -14
- __init__.py +0 -0
- assets/.gitkeep +1 -0
- calib.txt +4 -0
- camera_calib.txt +5 -0
- configs/dry/cambogan_20250812_122101.yaml +6 -0
- configs/dry/cambogan_20250812_122339.yaml +6 -0
- configs/dry/cambogan_20250812_122621.yaml +6 -0
- configs/dry/dairycreek_20250812_122954.yaml +6 -0
- configs/dry/dairycreek_20250812_123312.yaml +6 -0
- configs/dry/holmview_20250812_120100.yaml +6 -0
- configs/dry/holmview_20250812_120856.yaml +6 -0
- configs/dry/pullenvale_20250812_134316.yaml +6 -0
- configs/dry/pullenvale_20250812_134524.yaml +6 -0
- configs/flooded/cambogan.yaml +6 -0
- configs/flooded/dairycreek.yaml +6 -0
- configs/flooded/holmview.yaml +6 -0
- configs/flooded/mountcotton.yaml +6 -0
- configs/flooded/pullenvale.yaml +6 -0
- lidar-camera-calibration.py +526 -0
- lidar_postprocessing/create_pointcloud_labels.py +143 -0
- lidar_postprocessing/fill_pointcloud_gaps.py +98 -0
- lidar_postprocessing/project_water_label.py +122 -0
- localisation/.gitkeep +0 -0
- localisation/VPR_eval-all-v2.py +215 -0
- localisation/VPR_eval-all.py +247 -0
- localisation/VPR_eval.ipynb +383 -0
- localisation/VPR_eval.py +199 -0
- localisation/__init__.py +0 -0
- localisation/create_VPR_eval_dataset.ipynb +132 -0
- localisation/groundtruth_utm-single.ipynb +0 -0
- localisation/groundtruth_utm_checker-all.py +112 -0
- localisation/plot_utm_traj.ipynb +122 -0
- segmentation/.gitkeep +0 -0
- segmentation/__init__.py +0 -0
- segmentation/evaluate-predictions.py +191 -0
- segmentation/show_labels-all.py +133 -0
- segmentation/show_labels-single.ipynb +0 -0
- utils/.gitkeep +0 -0
- utils/__init__.py +0 -0
- utils/camera.py +143 -0
- utils/lidar.py +163 -0
- utils/utils.py +494 -0
- visualisation/.gitkeep +0 -0
- visualisation/__init__.py +0 -0
- visualisation/convert_imgs2video.py +106 -0
- visualisation/points2image-all.py +88 -0
.gitignore
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# All others
|
| 2 |
+
*.png
|
| 3 |
+
*.svg
|
| 4 |
+
*.pdf
|
| 5 |
+
*.profile_default
|
| 6 |
+
*.json
|
| 7 |
+
|
| 8 |
+
# Byte-compiled / optimized / DLL files
|
| 9 |
+
__pycache__/
|
| 10 |
+
*.py[codz]
|
| 11 |
+
*$py.class
|
| 12 |
+
|
| 13 |
+
# C extensions
|
| 14 |
+
*.so
|
| 15 |
+
|
| 16 |
+
# Distribution / packaging
|
| 17 |
+
ss/
|
| 18 |
+
.Python
|
| 19 |
+
build/
|
| 20 |
+
develop-eggs/
|
| 21 |
+
dist/
|
| 22 |
+
downloads/
|
| 23 |
+
eggs/
|
| 24 |
+
.eggs/
|
| 25 |
+
lib/
|
| 26 |
+
lib64/
|
| 27 |
+
parts/
|
| 28 |
+
sdist/
|
| 29 |
+
var/
|
| 30 |
+
wheels/
|
| 31 |
+
share/python-wheels/
|
| 32 |
+
*.egg-info/
|
| 33 |
+
.installed.cfg
|
| 34 |
+
*.egg
|
| 35 |
+
MANIFEST
|
| 36 |
+
|
| 37 |
+
# PyInstaller
|
| 38 |
+
# Usually these files are written by a python script from a template
|
| 39 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 40 |
+
*.manifest
|
| 41 |
+
*.spec
|
| 42 |
+
|
| 43 |
+
# Installer logs
|
| 44 |
+
pip-log.txt
|
| 45 |
+
pip-delete-this-directory.txt
|
| 46 |
+
|
| 47 |
+
# Unit test / coverage reports
|
| 48 |
+
htmlcov/
|
| 49 |
+
.tox/
|
| 50 |
+
.nox/
|
| 51 |
+
.coverage
|
| 52 |
+
.coverage.*
|
| 53 |
+
.cache
|
| 54 |
+
nosetests.xml
|
| 55 |
+
coverage.xml
|
| 56 |
+
*.cover
|
| 57 |
+
*.py.cover
|
| 58 |
+
.hypothesis/
|
| 59 |
+
.pytest_cache/
|
| 60 |
+
cover/
|
| 61 |
+
|
| 62 |
+
# Translations
|
| 63 |
+
*.mo
|
| 64 |
+
*.pot
|
| 65 |
+
|
| 66 |
+
# Django stuff:
|
| 67 |
+
*.log
|
| 68 |
+
local_settings.py
|
| 69 |
+
db.sqlite3
|
| 70 |
+
db.sqlite3-journal
|
| 71 |
+
|
| 72 |
+
# Flask stuff:
|
| 73 |
+
instance/
|
| 74 |
+
.webassets-cache
|
| 75 |
+
|
| 76 |
+
# Scrapy stuff:
|
| 77 |
+
.scrapy
|
| 78 |
+
|
| 79 |
+
# Sphinx documentation
|
| 80 |
+
docs/_build/
|
| 81 |
+
|
| 82 |
+
# PyBuilder
|
| 83 |
+
.pybuilder/
|
| 84 |
+
target/
|
| 85 |
+
|
| 86 |
+
# Jupyter Notebook
|
| 87 |
+
.ipynb_checkpoints
|
| 88 |
+
|
| 89 |
+
# IPython
|
| 90 |
+
profile_default/
|
| 91 |
+
ipython_config.py
|
| 92 |
+
|
| 93 |
+
# pyenv
|
| 94 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 95 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 96 |
+
# .python-version
|
| 97 |
+
|
| 98 |
+
# pipenv
|
| 99 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 100 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 101 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 102 |
+
# install all needed dependencies.
|
| 103 |
+
#Pipfile.lock
|
| 104 |
+
|
| 105 |
+
# UV
|
| 106 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 107 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 108 |
+
# commonly ignored for libraries.
|
| 109 |
+
#uv.lock
|
| 110 |
+
|
| 111 |
+
# poetry
|
| 112 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 113 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 114 |
+
# commonly ignored for libraries.
|
| 115 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 116 |
+
#poetry.lock
|
| 117 |
+
#poetry.toml
|
| 118 |
+
|
| 119 |
+
# pdm
|
| 120 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 121 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 122 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 123 |
+
#pdm.lock
|
| 124 |
+
#pdm.toml
|
| 125 |
+
.pdm-python
|
| 126 |
+
.pdm-build/
|
| 127 |
+
|
| 128 |
+
# pixi
|
| 129 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 130 |
+
#pixi.lock
|
| 131 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 132 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 133 |
+
.pixi
|
| 134 |
+
|
| 135 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 136 |
+
__pypackages__/
|
| 137 |
+
|
| 138 |
+
# Celery stuff
|
| 139 |
+
celerybeat-schedule
|
| 140 |
+
celerybeat.pid
|
| 141 |
+
|
| 142 |
+
# SageMath parsed files
|
| 143 |
+
*.sage.py
|
| 144 |
+
|
| 145 |
+
# Environments
|
| 146 |
+
.env
|
| 147 |
+
.envrc
|
| 148 |
+
.venv
|
| 149 |
+
env/
|
| 150 |
+
venv/
|
| 151 |
+
ENV/
|
| 152 |
+
env.bak/
|
| 153 |
+
venv.bak/
|
| 154 |
+
|
| 155 |
+
# Spyder project settings
|
| 156 |
+
.spyderproject
|
| 157 |
+
.spyproject
|
| 158 |
+
|
| 159 |
+
# Rope project settings
|
| 160 |
+
.ropeproject
|
| 161 |
+
|
| 162 |
+
# mkdocs documentation
|
| 163 |
+
/site
|
| 164 |
+
|
| 165 |
+
# mypy
|
| 166 |
+
.mypy_cache/
|
| 167 |
+
.dmypy.json
|
| 168 |
+
dmypy.json
|
| 169 |
+
|
| 170 |
+
# Pyre type checker
|
| 171 |
+
.pyre/
|
| 172 |
+
|
| 173 |
+
# pytype static type analyzer
|
| 174 |
+
.pytype/
|
| 175 |
+
|
| 176 |
+
# Cython debug symbols
|
| 177 |
+
cython_debug/
|
| 178 |
+
|
| 179 |
+
# PyCharm
|
| 180 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 181 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 182 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 183 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 184 |
+
#.idea/
|
| 185 |
+
|
| 186 |
+
# Abstra
|
| 187 |
+
# Abstra is an AI-powered process automation framework.
|
| 188 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 189 |
+
# Learn more at https://abstra.io/docs
|
| 190 |
+
.abstra/
|
| 191 |
+
|
| 192 |
+
# Visual Studio Code
|
| 193 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 194 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 195 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 196 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 197 |
+
# .vscode/
|
| 198 |
+
|
| 199 |
+
# Ruff stuff:
|
| 200 |
+
.ruff_cache/
|
| 201 |
+
|
| 202 |
+
# PyPI configuration file
|
| 203 |
+
.pypirc
|
| 204 |
+
|
| 205 |
+
# Cursor
|
| 206 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 207 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 208 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 209 |
+
.cursorignore
|
| 210 |
+
.cursorindexingignore
|
| 211 |
+
|
| 212 |
+
# Marimo
|
| 213 |
+
marimo/_static/
|
| 214 |
+
marimo/_lsp/
|
| 215 |
+
__marimo__/
|
| 216 |
+
|
| 217 |
+
# Other
|
| 218 |
+
hazard_detection*
|
| 219 |
+
results*
|
FoL/reranking.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: UTF-8 -*-
|
| 2 |
+
import faiss
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
import logging
|
| 6 |
+
import numpy as np
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
from torch.utils.data import DataLoader
|
| 9 |
+
from torch.utils.data.dataset import Subset
|
| 10 |
+
# from prettytable import PrettyTable
|
| 11 |
+
import warnings
|
| 12 |
+
import cv2
|
| 13 |
+
from os.path import join
|
| 14 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 15 |
+
# import matplotlib.pyplot as plt
|
| 16 |
+
|
| 17 |
+
def match_batch_tensor(fm1, fm2, trainflag, grid_size, T2=0.7):
|
| 18 |
+
'''
|
| 19 |
+
fm1: (l,D) 529,768
|
| 20 |
+
fm2: (N,l,D) 100,529,768
|
| 21 |
+
mask1: (l)
|
| 22 |
+
mask2: (N,l)
|
| 23 |
+
'''
|
| 24 |
+
M = torch.matmul(fm2, fm1.T)
|
| 25 |
+
|
| 26 |
+
max1 = torch.argmax(M, dim=1)
|
| 27 |
+
max2 = torch.argmax(M, dim=2)
|
| 28 |
+
m = max2[torch.arange(M.shape[0]).reshape((-1, 1)), max1]
|
| 29 |
+
valid = torch.arange(M.shape[-1]).repeat((M.shape[0], 1)).cuda() == m
|
| 30 |
+
scores = torch.zeros(fm2.shape[0]).cuda()
|
| 31 |
+
|
| 32 |
+
for i in range(fm2.shape[0]):
|
| 33 |
+
idx1 = torch.nonzero(valid[i, :]).squeeze()
|
| 34 |
+
idx2 = max1[i, :][idx1]
|
| 35 |
+
assert idx1.shape == idx2.shape
|
| 36 |
+
|
| 37 |
+
if len(idx1.shape) > 0:
|
| 38 |
+
# Calculate cosine similarity and apply threshold
|
| 39 |
+
cos_similarity = torch.sum(fm1[idx1] * fm2[i][idx2], dim=1)
|
| 40 |
+
valid_pairs = cos_similarity > T2
|
| 41 |
+
idx1 = idx1[valid_pairs]
|
| 42 |
+
idx2 = idx2[valid_pairs]
|
| 43 |
+
|
| 44 |
+
if trainflag:
|
| 45 |
+
if len(idx1.shape) > 0:
|
| 46 |
+
similarity = torch.mean(torch.sum(fm1[idx1] * fm2[i][idx2], dim=1), dim=0)
|
| 47 |
+
else:
|
| 48 |
+
print("No mutual nearest neighbors!")
|
| 49 |
+
similarity = torch.mean(torch.sum(fm1 * fm2[i], dim=1), dim=0)
|
| 50 |
+
return similarity
|
| 51 |
+
|
| 52 |
+
else:
|
| 53 |
+
if len(idx1.shape) < 1:
|
| 54 |
+
scores[i] = 0
|
| 55 |
+
else:
|
| 56 |
+
scores[i] = len(idx1)
|
| 57 |
+
return scores
|
| 58 |
+
|
| 59 |
+
def local_sim(features_1, features_2, trainflag=False):
|
| 60 |
+
B, Num, C = features_2.shape
|
| 61 |
+
if trainflag:
|
| 62 |
+
queries = features_1
|
| 63 |
+
preds = features_2
|
| 64 |
+
similarity = torch.zeros(B).cuda()
|
| 65 |
+
for i in range(B):
|
| 66 |
+
query,pred = queries[i],preds[i].unsqueeze(0)
|
| 67 |
+
similarity[i] = match_batch_tensor(query, pred, trainflag, grid_size=(61,61))
|
| 68 |
+
return similarity
|
| 69 |
+
else:
|
| 70 |
+
query = features_1
|
| 71 |
+
preds = features_2
|
| 72 |
+
scores = match_batch_tensor(query, preds,trainflag, grid_size=(61,61))
|
| 73 |
+
return scores
|
| 74 |
+
|
| 75 |
+
def rerank(predictions, queries_local_features, database_local_features):
|
| 76 |
+
pred2 = []
|
| 77 |
+
print("reranking...")
|
| 78 |
+
for query_index, pred in enumerate(tqdm(predictions)):
|
| 79 |
+
query_local_features = torch.tensor(queries_local_features[query_index]).cuda()
|
| 80 |
+
positives_local_features = torch.tensor(database_local_features[pred]).cuda()
|
| 81 |
+
rerank_index = local_sim(query_local_features, positives_local_features, trainflag=False)
|
| 82 |
+
rerank_index_sorted = rerank_index.cpu().numpy().argsort()[::-1]
|
| 83 |
+
pred2.append(predictions[query_index][rerank_index_sorted])
|
| 84 |
+
return np.array(pred2)
|
| 85 |
+
|
| 86 |
+
def run_rerank(queries_features, database_features, q_local_list, r_local_list, recall_values = [1,]):
|
| 87 |
+
|
| 88 |
+
faiss_index = faiss.IndexFlatL2(8448)
|
| 89 |
+
faiss_index.add(database_features)
|
| 90 |
+
|
| 91 |
+
distances, predictions = faiss_index.search(queries_features, max(recall_values))
|
| 92 |
+
|
| 93 |
+
# rerank
|
| 94 |
+
predictions2 = rerank(predictions, q_local_list, r_local_list)
|
| 95 |
+
|
| 96 |
+
return predictions2
|
LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BSD 3-Clause License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025, Queensland University of Technology (QUT)
|
| 4 |
+
All rights reserved.
|
| 5 |
+
|
| 6 |
+
Redistribution and use in source and binary forms, with or without
|
| 7 |
+
modification, are permitted provided that the following conditions are met:
|
| 8 |
+
|
| 9 |
+
1. Redistributions of source code must retain the above copyright notice, this
|
| 10 |
+
list of conditions and the following disclaimer.
|
| 11 |
+
|
| 12 |
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
| 13 |
+
this list of conditions and the following disclaimer in the documentation
|
| 14 |
+
and/or other materials provided with the distribution.
|
| 15 |
+
|
| 16 |
+
3. Neither the name of the copyright holder nor the names of its
|
| 17 |
+
contributors may be used to endorse or promote products derived from
|
| 18 |
+
this software without specific prior written permission.
|
| 19 |
+
|
| 20 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 21 |
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 22 |
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 23 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 24 |
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 25 |
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 26 |
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 27 |
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 28 |
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 29 |
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
README.md
CHANGED
|
@@ -1,14 +1,67 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
--
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# python-FRED
|
| 2 |
+
<!--  -->
|
| 3 |
+
<p align="center">
|
| 4 |
+
<img src="assets/Zoe2-FRED.svg" alt="Zoe 2 img">
|
| 5 |
+
</p>
|
| 6 |
+
This repository provides the devkit tools for working with the Flooded Road Environments Dataset (FRED). This autonomous vehicle dataset has been developed to enable research into the detection of flooded roads during on-road deployment. The dataset was collected using a Renault Zoe with custom modifications to enable autonomy, including front and rear Blackfly cameras, an Ouster OS1 LiDAR, and a GNSS-corrected IMU. Data has been collected using the vehicle's sensor stack from 5 separate locations around Brisbane, Australia, both during and after flooding events. Semantic labels are provided for images to enable the development of detection methods, and corresponding position information from the GNSS-corrected IMU has been provided across sequences to additionally enable localization research for these scenarios.
|
| 7 |
+
|
| 8 |
+
## Dataset Structure
|
| 9 |
+
We adopt the following structure for FRED to include a KITTI-style format for the dataset and the native recording format using RTmaps.
|
| 10 |
+
```
|
| 11 |
+
├── flooded # Location sequences captured during flooding events
|
| 12 |
+
│ ├── KITTI-style # Sequences in a KITTI-style format
|
| 13 |
+
│ | ├── Cambogan_20250811_113017 # Sequence by location
|
| 14 |
+
│ | | ├── back-imgs
|
| 15 |
+
│ | | | └── <timestamp>.png # Images in 'png' format
|
| 16 |
+
│ | | ├── back-labels
|
| 17 |
+
│ | | | └── <timestamp>.png # Semantic labels in 'png' format
|
| 18 |
+
│ | | ├── front-imgs
|
| 19 |
+
│ | | | └── <timestamp>.png # Images in 'png' format
|
| 20 |
+
│ | | ├── front-labels
|
| 21 |
+
│ | | | └── <timestamp>.png # Semantic labels in 'png' format
|
| 22 |
+
│ | | ├── imu
|
| 23 |
+
│ | | | └── <timestamp>.txt # IMU data formatted as a 'txt' file
|
| 24 |
+
│ | | ├── ouster
|
| 25 |
+
│ | | | └── <timestamp>.bin # Point clouds formatted as a binary file
|
| 26 |
+
│ | | └── utm
|
| 27 |
+
│ | | └── <timestamp>.txt # UTM locations formatted as a 'txt' file
|
| 28 |
+
│ | ├── ...
|
| 29 |
+
│ | └── ...
|
| 30 |
+
│ └── native-RTmaps # Sequences in native recording format
|
| 31 |
+
│ ├── Cambogan_20250811_113017 # Sequence by location
|
| 32 |
+
│ | ├── Camera_Rec # Recording files for image playback
|
| 33 |
+
│ | ├── IMU_Info_Rec # Recording files for IMU playback
|
| 34 |
+
│ | └── Ouster_Rec # Recording files for LiDAR playback
|
| 35 |
+
│ ├── ...
|
| 36 |
+
│ └── ...
|
| 37 |
+
│
|
| 38 |
+
└── dry # Location sequences captured while 'dry'
|
| 39 |
+
├── KITTI-style
|
| 40 |
+
└── native-RTmaps
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Data Formats
|
| 44 |
+
### Image Format
|
| 45 |
+
Images are stored in PNG format.
|
| 46 |
+
|
| 47 |
+
### Point Cloud Format
|
| 48 |
+
Point clouds are stored in binary format (.bin), with each point containing x, y, z positions, as well as reflectivity values. Reflectivity values are surface normalized signal intensity measurements that range from 0 to 255. 3D coordinates are captured in the right-hand coordinate frame with the positive x-axis in the vehicle's direction of travel.
|
| 49 |
+
|
| 50 |
+
### UTM Format
|
| 51 |
+
UTM data is stored in text file format (.txt), with UTM x and y values being stored as space separated values in the file.
|
| 52 |
+
|
| 53 |
+
### IMU Format
|
| 54 |
+
Additional IMU information is also stored in text file format (.txt). A space delimiter is again used to separate values. The additional IMU data is stored in the following order:
|
| 55 |
+
```
|
| 56 |
+
[ Latitude, Longitude, Altitude,
|
| 57 |
+
Roll, Pitch, Yaw,
|
| 58 |
+
North Velocity, East Velocity,
|
| 59 |
+
x Velocity, y Velocity, z Velocity,
|
| 60 |
+
x Angular Velocity, y Angular Velocity, z Angular Velocity,
|
| 61 |
+
x Angular Velocity, y Angular Velocity, z Angular Velocity,
|
| 62 |
+
x Angular Accel, y Angular Accel, z Angular Accel,
|
| 63 |
+
x Angular Accel, y Angular Accel, z Angular Accel,
|
| 64 |
+
Position Accuracy, Velocity Accuracy,
|
| 65 |
+
Navstate Value, Numstat Value,
|
| 66 |
+
Position Mode, Velocity Mode, Orientation Mode ]
|
| 67 |
+
```
|
__init__.py
ADDED
|
File without changes
|
assets/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
calib.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
P2: 2.047776e+03 0.000000e+00 9.600000e+02 0.000000e+00 0.000000e+00 2.047776e+03 6.000000e+02 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00
|
| 2 |
+
R0_rect: 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00
|
| 3 |
+
Tr_ouster_to_cam: 1.13360350e-02 -9.99935650e-01 4.35486538e-04 1.00000000e-01 3.83878091e-02 0.00000000e+00 -9.99262916e-01 -5.00000000e-01 9.99198614e-01 1.13443968e-02 3.83853388e-02 -4.74000000e-01
|
| 4 |
+
Tr_ouster_to_cam_old: 9.59208808e-03 -9.99953927e-01 3.68490854e-04 1.00000000e-01 3.83878091e-02 0.00000000e+00 -9.99262916e-01 -5.00000000e-01 9.99216877e-01 9.59916346e-03 3.83860404e-02 -1.25600000e+00
|
camera_calib.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
focal_len: 12.00
|
| 2 |
+
principal_x: 960.00
|
| 3 |
+
principal_y: 600.00
|
| 4 |
+
pp_mm_x: 170.648
|
| 5 |
+
pp_mm_y: 170.648
|
configs/dry/cambogan_20250812_122101.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Cambogan"
|
| 2 |
+
sequence: "20250812_122101"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/dry/cambogan_20250812_122339.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Cambogan"
|
| 2 |
+
sequence: "20250812_122339"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/dry/cambogan_20250812_122621.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Cambogan"
|
| 2 |
+
sequence: "20250812_122621"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/dry/dairycreek_20250812_122954.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Dairy-Creek"
|
| 2 |
+
sequence: "20250812_122954"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/dry/dairycreek_20250812_123312.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Dairy-Creek"
|
| 2 |
+
sequence: "20250812_120856"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/dry/holmview_20250812_120100.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Holmview"
|
| 2 |
+
sequence: "20250812_120100"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/dry/holmview_20250812_120856.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Holmview"
|
| 2 |
+
sequence: "20250812_120100"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/dry/pullenvale_20250812_134316.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Pullenvale"
|
| 2 |
+
sequence: "20250812_134316"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/dry/pullenvale_20250812_134524.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Pullenvale"
|
| 2 |
+
sequence: "20250812_134524"
|
| 3 |
+
condition: "dry"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/flooded/cambogan.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Cambogan"
|
| 2 |
+
sequence: "20250811_113017"
|
| 3 |
+
condition: "flooded"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/flooded/dairycreek.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "DairyCreek"
|
| 2 |
+
sequence: "20250811_103318"
|
| 3 |
+
condition: "flooded"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/flooded/holmview.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Holmview"
|
| 2 |
+
sequence: "20250820_130327"
|
| 3 |
+
condition: "flooded"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/flooded/mountcotton.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Mount-Cotton"
|
| 2 |
+
sequence: "20241217_113410"
|
| 3 |
+
condition: "flooded"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
configs/flooded/pullenvale.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
location: "Pullenvale"
|
| 2 |
+
sequence: "20250916_124105"
|
| 3 |
+
condition: "flooded"
|
| 4 |
+
camera_pos: "front"
|
| 5 |
+
root: "../Datasets/FRED/"
|
| 6 |
+
img_calib_file: "./camera_calib.txt"
|
lidar-camera-calibration.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import cv2
|
| 3 |
+
import open3d as o3d
|
| 4 |
+
from scipy.spatial.transform import Rotation
|
| 5 |
+
import argparse
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
class LiDARCameraCalibrator:
|
| 9 |
+
def __init__(self, point_cloud_path, image_path, camera_matrix, dist_coeffs):
|
| 10 |
+
"""
|
| 11 |
+
Initialize the calibrator with point cloud and image data
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
point_cloud_path (str): Path to LiDAR point cloud file (.pcd, .ply, .bin, etc.)
|
| 15 |
+
image_path (str): Path to camera image
|
| 16 |
+
camera_matrix (np.array): 3x3 camera intrinsic matrix
|
| 17 |
+
dist_coeffs (np.array): Camera distortion coefficients
|
| 18 |
+
"""
|
| 19 |
+
# Load point cloud
|
| 20 |
+
self.points_3d = self.load_point_cloud(point_cloud_path)
|
| 21 |
+
|
| 22 |
+
# Load image
|
| 23 |
+
self.image = cv2.imread(image_path)
|
| 24 |
+
self.original_image = self.image.copy()
|
| 25 |
+
self.height, self.width = self.image.shape[:2]
|
| 26 |
+
|
| 27 |
+
# Camera parameters
|
| 28 |
+
self.camera_matrix = camera_matrix
|
| 29 |
+
self.dist_coeffs = dist_coeffs
|
| 30 |
+
|
| 31 |
+
# self.fixed_translation = np.array([0.676, 0.0, 1.486])
|
| 32 |
+
# self.fixed_transform = np.eye(4)
|
| 33 |
+
# self.fixed_transform[:3, 3] = self.fixed_translation
|
| 34 |
+
|
| 35 |
+
# Initial transformation parameters (adjustable via trackbars)
|
| 36 |
+
# tx=1.932+0.676, ty=0.25+0, tz=1.4+1.486, rx=0, ry=-2.0, rz=1.5
|
| 37 |
+
self.tx = -1.16 # Translation X
|
| 38 |
+
self.ty = -0.23 # Translation Y
|
| 39 |
+
self.tz = 0.42 # Translation Z
|
| 40 |
+
self.rx = 0.0 # Rotation X (degrees)
|
| 41 |
+
self.ry = 2.0 # Rotation Y (degrees)
|
| 42 |
+
self.rz = -0.55 # Rotation Z (degrees)
|
| 43 |
+
|
| 44 |
+
# Trackbar ranges (scaled by 1000 for precision)
|
| 45 |
+
self.t_range = 50000 # ±50m in mm
|
| 46 |
+
self.r_range = 18000 # ±180 degrees in 0.01 degree units
|
| 47 |
+
|
| 48 |
+
# Setup OpenCV window and trackbars
|
| 49 |
+
self.setup_gui()
|
| 50 |
+
|
| 51 |
+
def load_point_cloud(self, file_path):
|
| 52 |
+
"""
|
| 53 |
+
Load point cloud from various formats including .bin files
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
file_path (str): Path to point cloud file
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
np.array: Nx3 array of 3D points
|
| 60 |
+
"""
|
| 61 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
| 62 |
+
|
| 63 |
+
if file_ext == '.bin':
|
| 64 |
+
# Load binary point cloud (common format for KITTI, nuScenes, etc.)
|
| 65 |
+
# Assumes format: [x, y, z, intensity] or [x, y, z, intensity, ring, time, etc.]
|
| 66 |
+
points = np.fromfile(file_path, dtype=np.float32)
|
| 67 |
+
|
| 68 |
+
# Determine the number of fields per point
|
| 69 |
+
# Common formats: 4 (x,y,z,intensity), 5 (x,y,z,intensity,ring), 6+ (additional fields)
|
| 70 |
+
if len(points) % 4 == 0:
|
| 71 |
+
points = points.reshape(-1, 4)
|
| 72 |
+
points_3d = points[:, :3] # Take only x, y, z
|
| 73 |
+
self.intensities = points[:, 3] # Store intensity for potential use
|
| 74 |
+
# print(self.intensities.max())
|
| 75 |
+
# print(self.intensities.min())
|
| 76 |
+
print(f"Loaded {len(points_3d)} points from .bin file (4 fields per point)")
|
| 77 |
+
elif len(points) % 6 == 0:
|
| 78 |
+
points = points.reshape(-1, 6)
|
| 79 |
+
points_3d = points[:, :3] # Take only x, y, z
|
| 80 |
+
self.intensities = points[:, 3] # Store intensity
|
| 81 |
+
print(f"Loaded {len(points_3d)} points from .bin file (6 fields per point)")
|
| 82 |
+
else:
|
| 83 |
+
# Try to infer the structure or default to 3D points only
|
| 84 |
+
# Assume the most common case where points are stored as [x,y,z,...]
|
| 85 |
+
n_fields = 4 # Default assumption
|
| 86 |
+
if len(points) % 3 == 0 and len(points) % 4 != 0:
|
| 87 |
+
n_fields = 3
|
| 88 |
+
|
| 89 |
+
points = points.reshape(-1, n_fields)
|
| 90 |
+
points_3d = points[:, :3]
|
| 91 |
+
if n_fields > 3:
|
| 92 |
+
self.intensities = points[:, 3]
|
| 93 |
+
else:
|
| 94 |
+
self.intensities = None
|
| 95 |
+
print(f"Loaded {len(points_3d)} points from .bin file ({n_fields} fields per point)")
|
| 96 |
+
|
| 97 |
+
return points_3d
|
| 98 |
+
|
| 99 |
+
else:
|
| 100 |
+
# Use Open3D for standard formats (.pcd, .ply, etc.)
|
| 101 |
+
try:
|
| 102 |
+
pcd = o3d.io.read_point_cloud(file_path)
|
| 103 |
+
points_3d = np.asarray(pcd.points)
|
| 104 |
+
self.intensities = None # Open3D formats don't always have intensity
|
| 105 |
+
print(f"Loaded {len(points_3d)} points using Open3D")
|
| 106 |
+
return points_3d
|
| 107 |
+
except Exception as e:
|
| 108 |
+
raise ValueError(f"Could not load point cloud {file_path}: {str(e)}")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def setup_gui(self):
|
| 112 |
+
"""Setup OpenCV window and trackbars for real-time adjustment"""
|
| 113 |
+
cv2.namedWindow('LiDAR-Camera Calibration', cv2.WINDOW_NORMAL)
|
| 114 |
+
cv2.resizeWindow('LiDAR-Camera Calibration', 1200, 800)
|
| 115 |
+
|
| 116 |
+
# Create trackbars for translation (in mm for precision)
|
| 117 |
+
cv2.createTrackbar('TX (mm)', 'LiDAR-Camera Calibration',
|
| 118 |
+
int(self.tx * 1000) + self.t_range, 2 * self.t_range, self.update_transform)
|
| 119 |
+
cv2.createTrackbar('TY (mm)', 'LiDAR-Camera Calibration',
|
| 120 |
+
int(self.ty * 1000) + self.t_range, 2 * self.t_range, self.update_transform)
|
| 121 |
+
cv2.createTrackbar('TZ (mm)', 'LiDAR-Camera Calibration',
|
| 122 |
+
int(self.tz * 1000) + self.t_range, 2 * self.t_range, self.update_transform)
|
| 123 |
+
|
| 124 |
+
# Create trackbars for rotation (in 0.01 degree units)
|
| 125 |
+
cv2.createTrackbar('RX (0.01°)', 'LiDAR-Camera Calibration',
|
| 126 |
+
int(self.rx * 100) + self.r_range, 2 * self.r_range, self.update_transform)
|
| 127 |
+
cv2.createTrackbar('RY (0.01°)', 'LiDAR-Camera Calibration',
|
| 128 |
+
int(self.ry * 100) + self.r_range, 2 * self.r_range, self.update_transform)
|
| 129 |
+
cv2.createTrackbar('RZ (0.01°)', 'LiDAR-Camera Calibration',
|
| 130 |
+
int(self.rz * 100) + self.r_range, 2 * self.r_range, self.update_transform)
|
| 131 |
+
|
| 132 |
+
# Additional controls
|
| 133 |
+
cv2.createTrackbar('Point Size', 'LiDAR-Camera Calibration', 2, 10, self.update_transform)
|
| 134 |
+
cv2.createTrackbar('Max Distance (m)', 'LiDAR-Camera Calibration', 100, 200, self.update_transform)
|
| 135 |
+
cv2.createTrackbar('Min Distance (m)', 'LiDAR-Camera Calibration', 0, 50, self.update_transform)
|
| 136 |
+
cv2.createTrackbar('Intensity Filter', 'LiDAR-Camera Calibration', 0, 100, self.update_transform)
|
| 137 |
+
|
| 138 |
+
def set_initial_values(self, tx=0, ty=0, tz=0, rx=0, ry=0, rz=0):
|
| 139 |
+
"""Set initial transformation values"""
|
| 140 |
+
self.tx, self.ty, self.tz = tx, ty, tz
|
| 141 |
+
self.rx, self.ry, self.rz = rx, ry, rz
|
| 142 |
+
|
| 143 |
+
# Update trackbars
|
| 144 |
+
cv2.setTrackbarPos('TX (mm)', 'LiDAR-Camera Calibration', int(tx * 1000) + self.t_range)
|
| 145 |
+
cv2.setTrackbarPos('TY (mm)', 'LiDAR-Camera Calibration', int(ty * 1000) + self.t_range)
|
| 146 |
+
cv2.setTrackbarPos('TZ (mm)', 'LiDAR-Camera Calibration', int(tz * 1000) + self.t_range)
|
| 147 |
+
cv2.setTrackbarPos('RX (0.01°)', 'LiDAR-Camera Calibration', int(rx * 100) + self.r_range)
|
| 148 |
+
cv2.setTrackbarPos('RY (0.01°)', 'LiDAR-Camera Calibration', int(ry * 100) + self.r_range)
|
| 149 |
+
cv2.setTrackbarPos('RZ (0.01°)', 'LiDAR-Camera Calibration', int(rz * 100) + self.r_range)
|
| 150 |
+
|
| 151 |
+
def update_transform(self, val):
|
| 152 |
+
"""Callback function for trackbar updates"""
|
| 153 |
+
# Get current trackbar values
|
| 154 |
+
self.tx = (cv2.getTrackbarPos('TX (mm)', 'LiDAR-Camera Calibration') - self.t_range) / 1000.0
|
| 155 |
+
self.ty = (cv2.getTrackbarPos('TY (mm)', 'LiDAR-Camera Calibration') - self.t_range) / 1000.0
|
| 156 |
+
self.tz = (cv2.getTrackbarPos('TZ (mm)', 'LiDAR-Camera Calibration') - self.t_range) / 1000.0
|
| 157 |
+
self.rx = (cv2.getTrackbarPos('RX (0.01°)', 'LiDAR-Camera Calibration') - self.r_range) / 100.0
|
| 158 |
+
self.ry = (cv2.getTrackbarPos('RY (0.01°)', 'LiDAR-Camera Calibration') - self.r_range) / 100.0
|
| 159 |
+
self.rz = (cv2.getTrackbarPos('RZ (0.01°)', 'LiDAR-Camera Calibration') - self.r_range) / 100.0
|
| 160 |
+
|
| 161 |
+
# Update visualization
|
| 162 |
+
self.visualize_projection()
|
| 163 |
+
|
| 164 |
+
def get_transformation_matrix(self):
|
| 165 |
+
"""Calculate 4x4 transformation matrix from current parameters"""
|
| 166 |
+
# Create rotation matrix from Euler angles (XYZ order)
|
| 167 |
+
rotation = Rotation.from_euler('xyz', [self.rx, self.ry, self.rz], degrees=True)
|
| 168 |
+
# R = rotation.as_matrix()
|
| 169 |
+
|
| 170 |
+
coord_transform = np.array([
|
| 171 |
+
[0, -1, 0],
|
| 172 |
+
[0, 0, -1],
|
| 173 |
+
[1, 0, 0]
|
| 174 |
+
])
|
| 175 |
+
|
| 176 |
+
R = rotation.as_matrix() #@ coord_transform
|
| 177 |
+
|
| 178 |
+
# Create translation vector
|
| 179 |
+
t = np.array([self.tx, self.ty, self.tz])
|
| 180 |
+
|
| 181 |
+
# Build 4x4 transformation matrix
|
| 182 |
+
variable_T = np.eye(4)
|
| 183 |
+
variable_T[:3, :3] = R
|
| 184 |
+
variable_T[:3, 3] = t
|
| 185 |
+
|
| 186 |
+
# T = variable_T @ self.fixed_transform
|
| 187 |
+
T = variable_T
|
| 188 |
+
|
| 189 |
+
# Convert combined transform to camera coordinate frame
|
| 190 |
+
C4 = np.eye(4)
|
| 191 |
+
C4[:3, :3] = coord_transform # maps robot -> camera
|
| 192 |
+
T_camera = C4 @ T
|
| 193 |
+
|
| 194 |
+
return T_camera
|
| 195 |
+
|
| 196 |
+
def project_points(self):
|
| 197 |
+
"""Project 3D LiDAR points to image coordinates"""
|
| 198 |
+
if len(self.points_3d) == 0:
|
| 199 |
+
return np.array([]), np.array([])
|
| 200 |
+
|
| 201 |
+
# Apply transformation
|
| 202 |
+
T = self.get_transformation_matrix()
|
| 203 |
+
points_homo = np.column_stack([self.points_3d, np.ones(len(self.points_3d))])
|
| 204 |
+
transformed_points = (T @ points_homo.T).T[:, :3]
|
| 205 |
+
|
| 206 |
+
# Filter points that are in front of camera
|
| 207 |
+
valid_mask = transformed_points[:, 2] > 0
|
| 208 |
+
valid_points = transformed_points[valid_mask]
|
| 209 |
+
valid_inten = self.intensities[valid_mask]
|
| 210 |
+
|
| 211 |
+
if len(valid_points) == 0:
|
| 212 |
+
return np.array([]), np.array([])
|
| 213 |
+
|
| 214 |
+
# Filter by distance
|
| 215 |
+
max_dist = cv2.getTrackbarPos('Max Distance (m)', 'LiDAR-Camera Calibration')
|
| 216 |
+
min_dist = cv2.getTrackbarPos('Min Distance (m)', 'LiDAR-Camera Calibration')
|
| 217 |
+
distances = np.linalg.norm(valid_points, axis=1)
|
| 218 |
+
dist_mask = (distances >= min_dist) & (distances <= max_dist)
|
| 219 |
+
valid_points = valid_points[dist_mask]
|
| 220 |
+
valid_inten = valid_inten[dist_mask]
|
| 221 |
+
valid_distances = distances[dist_mask]
|
| 222 |
+
|
| 223 |
+
# Filter by intensity if available
|
| 224 |
+
if hasattr(self, 'intensities') and self.intensities is not None:
|
| 225 |
+
intensity_threshold = cv2.getTrackbarPos('Intensity Filter', 'LiDAR-Camera Calibration') / 100.0
|
| 226 |
+
# Apply original valid_mask and dist_mask to intensities
|
| 227 |
+
original_valid_mask = np.arange(len(self.points_3d))[valid_mask[:len(valid_mask)]][:len(valid_points)]
|
| 228 |
+
if len(original_valid_mask) > 0 and len(original_valid_mask) <= len(self.intensities):
|
| 229 |
+
valid_intensities = self.intensities[original_valid_mask]
|
| 230 |
+
# Normalize intensities to 0-1 range
|
| 231 |
+
if len(valid_intensities) > 0:
|
| 232 |
+
intensity_norm = (valid_intensities - np.min(valid_intensities)) / (np.max(valid_intensities) - np.min(valid_intensities) + 1e-8)
|
| 233 |
+
intensity_mask = intensity_norm >= intensity_threshold
|
| 234 |
+
valid_points = valid_points[intensity_mask]
|
| 235 |
+
valid_inten = valid_inten[intensity_mask]
|
| 236 |
+
valid_distances = valid_distances[intensity_mask]
|
| 237 |
+
|
| 238 |
+
if len(valid_points) == 0:
|
| 239 |
+
return np.array([]), np.array([])
|
| 240 |
+
|
| 241 |
+
# Project to image coordinates
|
| 242 |
+
rvec = np.zeros(3) # No additional rotation
|
| 243 |
+
tvec = np.zeros(3) # No additional translation
|
| 244 |
+
|
| 245 |
+
image_points, _ = cv2.projectPoints(
|
| 246 |
+
valid_points, rvec, tvec, self.camera_matrix, self.dist_coeffs
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
image_points = image_points.reshape(-1, 2)
|
| 250 |
+
|
| 251 |
+
# Filter points within image bounds
|
| 252 |
+
h, w = self.height, self.width
|
| 253 |
+
valid_img_mask = ((image_points[:, 0] >= 0) & (image_points[:, 0] < w) &
|
| 254 |
+
(image_points[:, 1] >= 0) & (image_points[:, 1] < h))
|
| 255 |
+
|
| 256 |
+
return image_points[valid_img_mask], valid_distances[valid_img_mask], valid_inten[valid_img_mask]
|
| 257 |
+
|
| 258 |
+
def visualize_projection(self):
|
| 259 |
+
"""Update the visualization with current transformation"""
|
| 260 |
+
# Reset image
|
| 261 |
+
self.image = self.original_image.copy()
|
| 262 |
+
|
| 263 |
+
# Project points
|
| 264 |
+
projected_points, distances, intensities = self.project_points()
|
| 265 |
+
|
| 266 |
+
if len(projected_points) == 0:
|
| 267 |
+
cv2.putText(self.image, "No valid points to display", (50, 50),
|
| 268 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
|
| 269 |
+
else:
|
| 270 |
+
# Color points by distance (closer = red, farther = blue)
|
| 271 |
+
max_dist = cv2.getTrackbarPos('Max Distance (m)', 'LiDAR-Camera Calibration')
|
| 272 |
+
normalized_dist = distances.clip(0, 100) / min(max_dist,distances.max())
|
| 273 |
+
normalized_intensity = intensities/255
|
| 274 |
+
|
| 275 |
+
point_size = cv2.getTrackbarPos('Point Size', 'LiDAR-Camera Calibration')
|
| 276 |
+
if point_size == 0:
|
| 277 |
+
point_size = 1
|
| 278 |
+
|
| 279 |
+
# for i, (point, dist) in enumerate(zip(projected_points, normalized_dist)):
|
| 280 |
+
for i, (point, dist) in enumerate(zip(projected_points, normalized_intensity)):
|
| 281 |
+
# Color from red (close) to blue (far)
|
| 282 |
+
color = (255 * (1 - dist), 0, 255 * dist)
|
| 283 |
+
cv2.circle(self.image, (int(point[0]), int(point[1])),
|
| 284 |
+
point_size, color, -1)
|
| 285 |
+
|
| 286 |
+
# Add parameter information
|
| 287 |
+
info_text = [
|
| 288 |
+
f"TX: {self.tx:.3f}m TY: {self.ty:.3f}m TZ: {self.tz:.3f}m",
|
| 289 |
+
f"RX: {self.rx:.2f}° RY: {self.ry:.2f}° RZ: {self.rz:.2f}°",
|
| 290 |
+
f"Projected points: {len(projected_points)}",
|
| 291 |
+
"Press 'q' to quit, 's' to save parameters, 'r' to reset"
|
| 292 |
+
]
|
| 293 |
+
|
| 294 |
+
for i, text in enumerate(info_text):
|
| 295 |
+
cv2.putText(self.image, text, (10, 30 + i * 25),
|
| 296 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA)
|
| 297 |
+
cv2.putText(self.image, text, (10, 30 + i * 25),
|
| 298 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA)
|
| 299 |
+
|
| 300 |
+
cv2.imshow('LiDAR-Camera Calibration', self.image)
|
| 301 |
+
|
| 302 |
+
def save_parameters(self, filename="calibration_params.txt"):
|
| 303 |
+
"""Save current transformation parameters to file"""
|
| 304 |
+
T = self.get_transformation_matrix()
|
| 305 |
+
|
| 306 |
+
with open(filename, 'w') as f:
|
| 307 |
+
f.write("LiDAR-Camera Calibration Parameters\n")
|
| 308 |
+
f.write("=" * 40 + "\n\n")
|
| 309 |
+
f.write(f"Translation (meters):\n")
|
| 310 |
+
f.write(f" TX: {self.tx:.6f}\n")
|
| 311 |
+
f.write(f" TY: {self.ty:.6f}\n")
|
| 312 |
+
f.write(f" TZ: {self.tz:.6f}\n\n")
|
| 313 |
+
f.write(f"Rotation (degrees):\n")
|
| 314 |
+
f.write(f" RX: {self.rx:.6f}\n")
|
| 315 |
+
f.write(f" RY: {self.ry:.6f}\n")
|
| 316 |
+
f.write(f" RZ: {self.rz:.6f}\n\n")
|
| 317 |
+
f.write("4x4 Transformation Matrix:\n")
|
| 318 |
+
f.write(str(T) + "\n")
|
| 319 |
+
|
| 320 |
+
print(f"Parameters saved to {filename}")
|
| 321 |
+
|
| 322 |
+
def run(self):
|
| 323 |
+
"""Start the interactive calibration tool"""
|
| 324 |
+
print("LiDAR-Camera Calibration Tool")
|
| 325 |
+
print("Use trackbars to adjust transformation parameters")
|
| 326 |
+
print("Controls:")
|
| 327 |
+
print(" 'q' - Quit")
|
| 328 |
+
print(" 's' - Save current parameters")
|
| 329 |
+
print(" 'r' - Reset to initial values")
|
| 330 |
+
|
| 331 |
+
# Initial visualization
|
| 332 |
+
self.visualize_projection()
|
| 333 |
+
|
| 334 |
+
while True:
|
| 335 |
+
key = cv2.waitKey(30) & 0xFF
|
| 336 |
+
|
| 337 |
+
if key == ord('q'):
|
| 338 |
+
break
|
| 339 |
+
elif key == ord('s'):
|
| 340 |
+
self.save_parameters()
|
| 341 |
+
elif key == ord('r'):
|
| 342 |
+
self.set_initial_values()
|
| 343 |
+
self.visualize_projection()
|
| 344 |
+
|
| 345 |
+
cv2.destroyAllWindows()
|
| 346 |
+
|
| 347 |
+
def create_camera_matrix(focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
|
| 348 |
+
pixels_per_mm_x, pixels_per_mm_y):
|
| 349 |
+
"""
|
| 350 |
+
Create camera matrix from physical camera parameters.
|
| 351 |
+
|
| 352 |
+
Args:
|
| 353 |
+
focal_length_mm: Focal length in millimeters
|
| 354 |
+
principal_point_x_pixels: Principal point X coordinate in pixels
|
| 355 |
+
principal_point_y_pixels: Principal point Y coordinate in pixels
|
| 356 |
+
pixels_per_mm_x: Pixels per millimeter in X direction
|
| 357 |
+
pixels_per_mm_y: Pixels per millimeter in Y direction
|
| 358 |
+
|
| 359 |
+
Returns:
|
| 360 |
+
camera_matrix: 3x3 camera intrinsic matrix
|
| 361 |
+
"""
|
| 362 |
+
# Debug: Check inputs
|
| 363 |
+
# print(f"Input values:")
|
| 364 |
+
# print(f" focal_length_mm: {focal_length_mm}")
|
| 365 |
+
# print(f" principal_point_x_pixels: {principal_point_x_pixels}")
|
| 366 |
+
# print(f" principal_point_y_pixels: {principal_point_y_pixels}")
|
| 367 |
+
# print(f" pixels_per_mm_x: {pixels_per_mm_x}")
|
| 368 |
+
# print(f" pixels_per_mm_y: {pixels_per_mm_y}")
|
| 369 |
+
|
| 370 |
+
# Check for None or invalid values
|
| 371 |
+
if any(x is None for x in [focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
|
| 372 |
+
pixels_per_mm_x, pixels_per_mm_y]):
|
| 373 |
+
print("ERROR: One or more input parameters is None!")
|
| 374 |
+
return None
|
| 375 |
+
|
| 376 |
+
if pixels_per_mm_x == 0 or pixels_per_mm_y == 0:
|
| 377 |
+
print("ERROR: pixels_per_mm cannot be zero!")
|
| 378 |
+
return None
|
| 379 |
+
|
| 380 |
+
try:
|
| 381 |
+
# Convert focal length from mm to pixels
|
| 382 |
+
fx = focal_length_mm * pixels_per_mm_x
|
| 383 |
+
fy = focal_length_mm * pixels_per_mm_y
|
| 384 |
+
|
| 385 |
+
# Principal point is already in pixels
|
| 386 |
+
cx = principal_point_x_pixels
|
| 387 |
+
cy = principal_point_y_pixels
|
| 388 |
+
|
| 389 |
+
# print(f"DEBUG: Camera matrix calculation:")
|
| 390 |
+
# print(f" fx = {focal_length_mm} * {pixels_per_mm_x} = {fx}")
|
| 391 |
+
# print(f" fy = {focal_length_mm} * {pixels_per_mm_y} = {fy}")
|
| 392 |
+
# print(f" cx = {cx}")
|
| 393 |
+
# print(f" cy = {cy}")
|
| 394 |
+
|
| 395 |
+
camera_matrix = np.array([
|
| 396 |
+
[fx, 0, cx],
|
| 397 |
+
[0, fy, cy],
|
| 398 |
+
[0, 0, 1]
|
| 399 |
+
], dtype=np.float64)
|
| 400 |
+
|
| 401 |
+
# print(f"DEBUG: Final camera matrix:\n{camera_matrix}")
|
| 402 |
+
|
| 403 |
+
return camera_matrix
|
| 404 |
+
|
| 405 |
+
except Exception as e:
|
| 406 |
+
print(f"ERROR in create_camera_matrix: {e}")
|
| 407 |
+
return None
|
| 408 |
+
|
| 409 |
+
def create_sample_data():
|
| 410 |
+
"""Create sample point cloud and camera parameters for testing"""
|
| 411 |
+
# Generate sample point cloud (random points in a cube)
|
| 412 |
+
np.random.seed(42)
|
| 413 |
+
n_points = 1000
|
| 414 |
+
points = np.random.uniform(-10, 10, (n_points, 3))
|
| 415 |
+
points[:, 2] += 20 # Move points in front of camera
|
| 416 |
+
|
| 417 |
+
# Add intensity values
|
| 418 |
+
intensities = np.random.uniform(0, 255, n_points)
|
| 419 |
+
|
| 420 |
+
# Create .bin file (KITTI format: x, y, z, intensity)
|
| 421 |
+
bin_data = np.column_stack([points, intensities]).astype(np.float32)
|
| 422 |
+
bin_data.tofile("sample_pointcloud.bin")
|
| 423 |
+
|
| 424 |
+
# Also create .pcd for compatibility
|
| 425 |
+
pcd = o3d.geometry.PointCloud()
|
| 426 |
+
pcd.points = o3d.utility.Vector3dVector(points)
|
| 427 |
+
o3d.io.write_point_cloud("sample_pointcloud.pcd", pcd)
|
| 428 |
+
|
| 429 |
+
# Create sample camera matrix (typical values)
|
| 430 |
+
|
| 431 |
+
focal_length_mm = 12.0 # Your focal length in mm
|
| 432 |
+
principal_point_x_pixels = 960 # Principal point X in pixels
|
| 433 |
+
principal_point_y_pixels = 600 # Principal point Y in pixels
|
| 434 |
+
pixels_per_mm_x = 170.648 # Your pixels per mm in X
|
| 435 |
+
pixels_per_mm_y = 170.648 # Your pixels per mm in Y
|
| 436 |
+
|
| 437 |
+
# Create camera matrix
|
| 438 |
+
camera_matrix = create_camera_matrix(
|
| 439 |
+
focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
|
| 440 |
+
pixels_per_mm_x, pixels_per_mm_y
|
| 441 |
+
)
|
| 442 |
+
# camera_matrix = np.array([
|
| 443 |
+
# [800, 0, 320],
|
| 444 |
+
# [0, 800, 240],
|
| 445 |
+
# [0, 0, 1]
|
| 446 |
+
# ], dtype=np.float32)
|
| 447 |
+
|
| 448 |
+
# Sample distortion coefficients
|
| 449 |
+
# dist_coeffs = np.array([0.1, -0.2, 0, 0, 0.1], dtype=np.float32)
|
| 450 |
+
dist_coeffs = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
|
| 451 |
+
|
| 452 |
+
# Create a simple test image
|
| 453 |
+
test_image = np.zeros((1200, 1920, 3), dtype=np.uint8)
|
| 454 |
+
cv2.putText(test_image, "Sample Camera Image", (150, 240),
|
| 455 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
| 456 |
+
cv2.imwrite("sample_image.jpg", test_image)
|
| 457 |
+
|
| 458 |
+
return camera_matrix, dist_coeffs
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def main():
|
| 462 |
+
parser = argparse.ArgumentParser(description='LiDAR-Camera Calibration Tool')
|
| 463 |
+
parser.add_argument('--pcd', type=str, help='Path to point cloud file (.pcd, .ply, .bin, etc.)')
|
| 464 |
+
parser.add_argument('--image', type=str, help='Path to camera image')
|
| 465 |
+
parser.add_argument('--demo', action='store_true', help='Run with sample data')
|
| 466 |
+
|
| 467 |
+
args = parser.parse_args()
|
| 468 |
+
|
| 469 |
+
if args.demo or (not args.pcd or not args.image):
|
| 470 |
+
print("Creating sample data for demonstration...")
|
| 471 |
+
camera_matrix, dist_coeffs = create_sample_data()
|
| 472 |
+
pcd_path = "sample_pointcloud.bin" # Use .bin file for demo
|
| 473 |
+
img_path = "sample_image.jpg"
|
| 474 |
+
else:
|
| 475 |
+
pcd_path = args.pcd
|
| 476 |
+
img_path = args.image
|
| 477 |
+
|
| 478 |
+
# You'll need to provide your camera calibration parameters here
|
| 479 |
+
focal_length_mm = 12.0 # Your focal length in mm
|
| 480 |
+
principal_point_x_pixels = 960 # Principal point X in pixels
|
| 481 |
+
principal_point_y_pixels = 600 # Principal point Y in pixels
|
| 482 |
+
pixels_per_mm_x = 170.648 # Your pixels per mm in X
|
| 483 |
+
pixels_per_mm_y = 170.648 # Your pixels per mm in Y
|
| 484 |
+
|
| 485 |
+
# Create camera matrix
|
| 486 |
+
camera_matrix = create_camera_matrix(
|
| 487 |
+
focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
|
| 488 |
+
pixels_per_mm_x, pixels_per_mm_y
|
| 489 |
+
)
|
| 490 |
+
# camera_matrix = np.array([
|
| 491 |
+
# [800, 0, 320],
|
| 492 |
+
# [0, 800, 240],
|
| 493 |
+
# [0, 0, 1]
|
| 494 |
+
# ], dtype=np.float32)
|
| 495 |
+
|
| 496 |
+
# Sample distortion coefficients
|
| 497 |
+
# dist_coeffs = np.array([0.1, -0.2, 0, 0, 0.1], dtype=np.float32)
|
| 498 |
+
dist_coeffs = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
|
| 499 |
+
|
| 500 |
+
# Check if files exist
|
| 501 |
+
if not os.path.exists(pcd_path):
|
| 502 |
+
print(f"Error: Point cloud file {pcd_path} not found")
|
| 503 |
+
return
|
| 504 |
+
|
| 505 |
+
if not os.path.exists(img_path):
|
| 506 |
+
print(f"Error: Image file {img_path} not found")
|
| 507 |
+
return
|
| 508 |
+
|
| 509 |
+
# Initialize calibrator
|
| 510 |
+
calibrator = LiDARCameraCalibrator(pcd_path, img_path, camera_matrix, dist_coeffs)
|
| 511 |
+
|
| 512 |
+
# Set initial transformation values (modify these as needed)
|
| 513 |
+
# calibrator.set_initial_values(tx=1.932, ty=0, tz=1.4, rx=0, ry=-2.0, rz=1.5)
|
| 514 |
+
# calibrator.set_initial_values(tx=1.932, ty=0.25, tz=1.4, rx=0.0, ry=-2.0, rz=1.5)
|
| 515 |
+
calibrator.set_initial_values(tx=-1.256, ty=-0.1, tz=0.5, rx=0.0, ry=2.2, rz=-0.55)
|
| 516 |
+
# calibrator.set_initial_values(tx=-1.16, ty=-0.23, tz=0.42, rx=0.0, ry=2.0, rz=-0.55)
|
| 517 |
+
# calibrator.set_initial_values(tx=1.932, ty=0.25, tz=1.486, rx=0, ry=-2.0, rz=1.5)
|
| 518 |
+
# calibrator.set_initial_values(tx=0.23, ty=-0.42, tz=-1.16, rx=-2.5, ry=0.46, rz=-0.7)
|
| 519 |
+
# calibrator.set_initial_values(tx=0.192, ty=-0.641, tz=-1.474, rx=-2.51, ry=0.46, rz=-0.69)
|
| 520 |
+
|
| 521 |
+
# Run the calibration tool
|
| 522 |
+
calibrator.run()
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
if __name__ == "__main__":
|
| 526 |
+
main(
|
lidar_postprocessing/create_pointcloud_labels.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.image as mpimg
|
| 4 |
+
import sys
|
| 5 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
import open3d as o3d
|
| 9 |
+
from scipy.spatial.transform import Rotation
|
| 10 |
+
from utils.lidar import PointCloud
|
| 11 |
+
from utils.camera import ImageData
|
| 12 |
+
import utils.utils as utils
|
| 13 |
+
from natsort import natsorted
|
| 14 |
+
|
| 15 |
+
cmap = plt.get_cmap("jet")
|
| 16 |
+
LABEL_UNKNOWN = -1
|
| 17 |
+
|
| 18 |
+
# User parameters
|
| 19 |
+
location = 'Cambogan'
|
| 20 |
+
sequence = '20250811_113017'
|
| 21 |
+
# location = 'Holmview'
|
| 22 |
+
# sequence = '20250820_130327'
|
| 23 |
+
# location = 'Mount-Cotton'
|
| 24 |
+
# sequence = '20241217_113410'
|
| 25 |
+
condition = 'flooded'
|
| 26 |
+
camera_pos = 'front'
|
| 27 |
+
root_directory = f"../Datasets/FRED/{condition}/KITTI-style"
|
| 28 |
+
# 01000000
|
| 29 |
+
|
| 30 |
+
############ Define filenames and directories ####################################
|
| 31 |
+
|
| 32 |
+
image_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-imgs/"
|
| 33 |
+
label_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-labels/"
|
| 34 |
+
lidar_dir = f"{root_directory}/{location}_{sequence}/ouster/"
|
| 35 |
+
utm_dir = f"{root_directory}/{location}_{sequence}/utm/"
|
| 36 |
+
|
| 37 |
+
img_calib_file = f"./camera_calib.txt"
|
| 38 |
+
lidar_calib_file = f"./calib.txt"
|
| 39 |
+
|
| 40 |
+
timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(image_dir)) if os.path.isfile(image_dir+filename)]
|
| 41 |
+
groundplane_eqn = tuple(np.loadtxt(f"{root_directory}/{location}_{sequence}/ground_plane_eqn.txt"))
|
| 42 |
+
a, b, c, d = groundplane_eqn
|
| 43 |
+
|
| 44 |
+
# timestamps.sort()
|
| 45 |
+
|
| 46 |
+
fig, ax = plt.subplots(figsize=(12.8, 8))
|
| 47 |
+
# idx = [0] # mutable index
|
| 48 |
+
idx = [183]
|
| 49 |
+
|
| 50 |
+
def show_image(i):
|
| 51 |
+
ax.clear()
|
| 52 |
+
if i >= len(timestamps):
|
| 53 |
+
plt.close(fig)
|
| 54 |
+
return
|
| 55 |
+
image_timestamp = timestamps[i]
|
| 56 |
+
try:
|
| 57 |
+
image_filename = f"{image_dir}/{image_timestamp}.png"
|
| 58 |
+
label_filename = f"{label_dir}/{image_timestamp}.png"
|
| 59 |
+
lidar_filename, utm_filename = utils.get_corr_files(image_timestamp, [lidar_dir, utm_dir])
|
| 60 |
+
|
| 61 |
+
image = ImageData(image_filename, img_calib_file, label_filename)
|
| 62 |
+
pointcloud = PointCloud(lidar_filename, lidar_calib_file)
|
| 63 |
+
|
| 64 |
+
point_cam, distances_cam, intensities_cam, all_points_cam, valid_cam = pointcloud.points_ouster_to_cam() #, beam_id, azimuth
|
| 65 |
+
img_vis, uv, valid_img = image.project_points(all_points_cam, intensities_cam, cmap, valid_cam) #, beam_id, azimuth
|
| 66 |
+
|
| 67 |
+
valid_semantic = valid_cam & valid_img
|
| 68 |
+
|
| 69 |
+
# Assign semantics
|
| 70 |
+
semantic_labels = utils.assign_semantic_labels(
|
| 71 |
+
pointcloud.points[:, :3],
|
| 72 |
+
uv,
|
| 73 |
+
valid_semantic,
|
| 74 |
+
image.label_img,
|
| 75 |
+
interp_flags=None,
|
| 76 |
+
unknown_label=3
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
print(f"Max x distance: {pointcloud.points[semantic_labels==0,0].max()}")
|
| 80 |
+
|
| 81 |
+
ground_filter = pointcloud.ground_semantic == 0
|
| 82 |
+
inlier_filter = pointcloud.ground_inlier == 1
|
| 83 |
+
|
| 84 |
+
img_vis, uv, valid_img = image.project_points(all_points_cam, semantic_labels, cmap, valid_cam) #, beam_id, azimuth
|
| 85 |
+
|
| 86 |
+
# filtered_points = pointcloud.points[(semantic_labels==0) & (abs(pointcloud.points[:,1]) < 1),:]
|
| 87 |
+
# max_lookahead = filtered_points[:,0].max()
|
| 88 |
+
# far_points = filtered_points[filtered_points[:,0]==max_lookahead,:]
|
| 89 |
+
|
| 90 |
+
# if far_points.shape[0] > 1:
|
| 91 |
+
# far_point = far_points[abs(far_points[:,1]) == abs(far_points[:,1]).min(),:]
|
| 92 |
+
# else:
|
| 93 |
+
# far_point = far_points
|
| 94 |
+
# far_point_cam, far_point_distnace, far_point_intensity = pointcloud.select_points_ouster_to_cam(far_point)
|
| 95 |
+
# far_pixel = image.get_image_coords(far_point_cam)
|
| 96 |
+
|
| 97 |
+
# if far_pixel is not None and len(far_pixel) > 0:
|
| 98 |
+
# u, v = far_pixel[0] # pixel coordinates
|
| 99 |
+
|
| 100 |
+
# h, w = img_vis.shape[:2]
|
| 101 |
+
# bottom_center = (w // 2, h)
|
| 102 |
+
|
| 103 |
+
# ax.plot(
|
| 104 |
+
# [bottom_center[0], u],
|
| 105 |
+
# [bottom_center[1], v],
|
| 106 |
+
# color="lime",
|
| 107 |
+
# linewidth=2
|
| 108 |
+
# )
|
| 109 |
+
# ax.text(
|
| 110 |
+
# u,
|
| 111 |
+
# v - 10,
|
| 112 |
+
# f"{far_point[0,0]:.2f}",
|
| 113 |
+
# color="lime",
|
| 114 |
+
# fontsize=12,
|
| 115 |
+
# ha="center",
|
| 116 |
+
# bbox=dict(facecolor="black", alpha=0.6, edgecolor="none")
|
| 117 |
+
# )
|
| 118 |
+
|
| 119 |
+
ax.imshow(img_vis[:, :, ::-1])
|
| 120 |
+
ax.set_title(f"{image_timestamp}.png")
|
| 121 |
+
ax.axis("off")
|
| 122 |
+
# plt.savefig('paper_figures/labelled_pointcloud.pdf', format="pdf", bbox_inches='tight')
|
| 123 |
+
fig.canvas.draw()
|
| 124 |
+
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f"Could not project pointcloud onto {image_timestamp}.png: {e}")
|
| 127 |
+
idx[0] += 1
|
| 128 |
+
show_image(idx[0]) # skip bad one
|
| 129 |
+
|
| 130 |
+
def on_key(event):
|
| 131 |
+
if event.key in [' ', 'right']: # space or right arrow
|
| 132 |
+
idx[0] += 1
|
| 133 |
+
show_image(idx[0])
|
| 134 |
+
elif event.key in [' ', 'left']: # space or right arrow
|
| 135 |
+
if idx[0] > 0:
|
| 136 |
+
idx[0] -= 1
|
| 137 |
+
show_image(idx[0])
|
| 138 |
+
elif event.key in ['q', 'escape']: # q or Esc → quit
|
| 139 |
+
plt.close(fig)
|
| 140 |
+
|
| 141 |
+
fig.canvas.mpl_connect('key_press_event', on_key)
|
| 142 |
+
show_image(idx[0])
|
| 143 |
+
plt.show()
|
lidar_postprocessing/fill_pointcloud_gaps.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.image as mpimg
|
| 4 |
+
import sys
|
| 5 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
import open3d as o3d
|
| 9 |
+
from scipy.spatial.transform import Rotation
|
| 10 |
+
from utils.lidar import PointCloud
|
| 11 |
+
from utils.camera import ImageData
|
| 12 |
+
import utils.utils as utils
|
| 13 |
+
from natsort import natsorted
|
| 14 |
+
|
| 15 |
+
cmap = plt.get_cmap("jet")
|
| 16 |
+
LABEL_UNKNOWN = -1
|
| 17 |
+
|
| 18 |
+
# User parameters
|
| 19 |
+
location = 'Cambogan'
|
| 20 |
+
sequence = '20250811_113017'
|
| 21 |
+
# location = 'Holmview'
|
| 22 |
+
# sequence = '20250820_130327'
|
| 23 |
+
# location = 'Mount-Cotton'
|
| 24 |
+
# sequence = '20241217_113410'
|
| 25 |
+
condition = 'flooded'
|
| 26 |
+
camera_pos = 'front'
|
| 27 |
+
root_directory = f"../Datasets/FRED/{condition}/KITTI-style"
|
| 28 |
+
# 01000000
|
| 29 |
+
|
| 30 |
+
############ Define filenames and directories ####################################
|
| 31 |
+
|
| 32 |
+
image_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-imgs/"
|
| 33 |
+
label_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-labels/"
|
| 34 |
+
lidar_dir = f"{root_directory}/{location}_{sequence}/ouster/"
|
| 35 |
+
utm_dir = f"{root_directory}/{location}_{sequence}/utm/"
|
| 36 |
+
|
| 37 |
+
img_calib_file = f"./camera_calib.txt"
|
| 38 |
+
lidar_calib_file = f"./calib.txt"
|
| 39 |
+
|
| 40 |
+
timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(image_dir)) if os.path.isfile(image_dir+filename)]
|
| 41 |
+
groundplane_eqn = tuple(np.loadtxt(f"{root_directory}/{location}_{sequence}/ground_plane_eqn.txt"))
|
| 42 |
+
a, b, c, d = groundplane_eqn
|
| 43 |
+
|
| 44 |
+
# timestamps.sort()
|
| 45 |
+
|
| 46 |
+
# idx = [0] # mutable index
|
| 47 |
+
# idx = [0]
|
| 48 |
+
idx = [183]
|
| 49 |
+
|
| 50 |
+
def show_image(i):
|
| 51 |
+
# ax.clear()
|
| 52 |
+
if i >= len(timestamps):
|
| 53 |
+
return
|
| 54 |
+
image_timestamp = timestamps[i]
|
| 55 |
+
try:
|
| 56 |
+
image_filename = f"{image_dir}/{image_timestamp}.png"
|
| 57 |
+
label_filename = f"{label_dir}/{image_timestamp}.png"
|
| 58 |
+
lidar_filename, utm_filename = utils.get_corr_files(image_timestamp, [lidar_dir, utm_dir])
|
| 59 |
+
|
| 60 |
+
image = ImageData(image_filename, img_calib_file, label_filename)
|
| 61 |
+
pointcloud = PointCloud(lidar_filename, lidar_calib_file)
|
| 62 |
+
# print(f"Number of points in pointcloud: {pointcloud.points.shape}")
|
| 63 |
+
# # print(f"Nan's in pointcloud? {np.any( == np.nan)}")
|
| 64 |
+
# print(f"Number of zeroed points: {np.sum(np.all(pointcloud.points == 0, axis=1))}")
|
| 65 |
+
# print(f"invalid points in ground plane: {np.sum(pointcloud.ground_semantic[np.all(pointcloud.points == 0, axis=1)]==0)}")
|
| 66 |
+
pointcloud.points, pointcloud.ground_semantic, pointcloud.ground_inlier = pointcloud.destagger(pointcloud.points, pointcloud.ground_semantic, pointcloud.ground_inlier)
|
| 67 |
+
groundplane_eqn = utils.fit_height_field_linear(pointcloud.points[pointcloud.ground_semantic==0,:3])
|
| 68 |
+
pointcloud.points, interp_flags = utils.complete_cloud(pointcloud.points, groundplane_eqn)
|
| 69 |
+
|
| 70 |
+
point_cam, distances_cam, intensities_cam, all_points_cam, valid_cam = pointcloud.points_ouster_to_cam() #, beam_id, azimuth
|
| 71 |
+
img_vis, uv, valid_img = image.project_points(all_points_cam, intensities_cam, cmap, valid_cam, colour_norm=255) #, beam_id, azimuth
|
| 72 |
+
semantic_labels = interp_flags.astype(int) + 1
|
| 73 |
+
|
| 74 |
+
labels_norm = semantic_labels.astype(np.float64) / semantic_labels.max()
|
| 75 |
+
|
| 76 |
+
colors = np.stack(
|
| 77 |
+
(labels_norm, np.zeros(labels_norm.shape[0]), np.zeros(labels_norm.shape[0])),
|
| 78 |
+
axis=1
|
| 79 |
+
) # shape (N, 3)
|
| 80 |
+
|
| 81 |
+
pcd = o3d.geometry.PointCloud()
|
| 82 |
+
pcd.points = o3d.utility.Vector3dVector(pointcloud.points[:,:3])
|
| 83 |
+
|
| 84 |
+
pcd.colors = o3d.utility.Vector3dVector(colors)
|
| 85 |
+
o3d.visualization.draw_geometries([pcd,])
|
| 86 |
+
|
| 87 |
+
idx[0] += 1
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
except Exception as e:
|
| 91 |
+
print(f"Could not project pointcloud onto {image_timestamp}.png: {e}")
|
| 92 |
+
idx[0] += 1
|
| 93 |
+
show_image(idx[0]) # skip bad one
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
while idx[0] < len(timestamps):
|
| 97 |
+
show_image(idx[0])
|
| 98 |
+
print(f"Finished all pointclouds")
|
lidar_postprocessing/project_water_label.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.image as mpimg
|
| 4 |
+
import sys
|
| 5 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
import open3d as o3d
|
| 9 |
+
from scipy.spatial.transform import Rotation
|
| 10 |
+
from utils.lidar import PointCloud
|
| 11 |
+
from utils.camera import ImageData
|
| 12 |
+
import utils.utils as utils
|
| 13 |
+
from natsort import natsorted
|
| 14 |
+
|
| 15 |
+
cmap = plt.get_cmap("jet")
|
| 16 |
+
|
| 17 |
+
# User parameters
|
| 18 |
+
location = 'Cambogan'
|
| 19 |
+
sequence = '20250811_113017'
|
| 20 |
+
# location = 'Holmview'
|
| 21 |
+
# sequence = '20250820_130327'
|
| 22 |
+
# location = 'Mount-Cotton'
|
| 23 |
+
# sequence = '20241217_113410'
|
| 24 |
+
condition = 'flooded'
|
| 25 |
+
camera_pos = 'front'
|
| 26 |
+
root_directory = f"D:/Datasets/FRED/{condition}/KITTI-style"
|
| 27 |
+
# 01000000
|
| 28 |
+
|
| 29 |
+
############ Define filenames and directories ####################################
|
| 30 |
+
|
| 31 |
+
image_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-imgs/"
|
| 32 |
+
lidar_dir = f"{root_directory}/{location}_{sequence}/ouster/"
|
| 33 |
+
utm_dir = f"{root_directory}/{location}_{sequence}/utm/"
|
| 34 |
+
|
| 35 |
+
img_calib_file = f"./camera_calib.txt"
|
| 36 |
+
lidar_calib_file = f"./calib.txt"
|
| 37 |
+
|
| 38 |
+
timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(image_dir)) if os.path.isfile(image_dir+filename)]
|
| 39 |
+
|
| 40 |
+
# timestamps.sort()
|
| 41 |
+
|
| 42 |
+
fig, ax = plt.subplots(figsize=(12.8, 8))
|
| 43 |
+
idx = [0] # mutable index
|
| 44 |
+
|
| 45 |
+
def show_image(i):
|
| 46 |
+
ax.clear()
|
| 47 |
+
if i >= len(timestamps):
|
| 48 |
+
plt.close(fig)
|
| 49 |
+
return
|
| 50 |
+
image_timestamp = timestamps[i]
|
| 51 |
+
try:
|
| 52 |
+
image_filename = f"{image_dir}/{image_timestamp}.png"
|
| 53 |
+
lidar_filename, utm_filename = utils.get_corr_files(image_timestamp, [lidar_dir, utm_dir])
|
| 54 |
+
|
| 55 |
+
image = ImageData(image_filename, img_calib_file)
|
| 56 |
+
pointcloud = PointCloud(lidar_filename, lidar_calib_file)
|
| 57 |
+
|
| 58 |
+
lateral_filter = np.logical_and(-4 < pointcloud.points[:,1], pointcloud.points[:,1] < 1.5)
|
| 59 |
+
ground_filter = pointcloud.ground_semantic == 0
|
| 60 |
+
inlier_filter = pointcloud.ground_inlier == 1
|
| 61 |
+
points_filter = np.logical_and(np.logical_and(lateral_filter, ground_filter), inlier_filter)
|
| 62 |
+
filtered_points = pointcloud.points[points_filter]
|
| 63 |
+
pointcloud.points = filtered_points
|
| 64 |
+
max_lookahead = pointcloud.points[:,0].max()
|
| 65 |
+
far_points = pointcloud.points[pointcloud.points[:,0]==max_lookahead,:]
|
| 66 |
+
|
| 67 |
+
if far_points.shape[0] > 1:
|
| 68 |
+
# pointcloud.points = far_points[abs(far_points[:,1]) == abs(far_points[:,1]).min(),:]
|
| 69 |
+
far_point = far_points[abs(far_points[:,1]) == abs(far_points[:,1]).min(),:]
|
| 70 |
+
else:
|
| 71 |
+
# pointcloud.points = far_points
|
| 72 |
+
far_point = far_points
|
| 73 |
+
|
| 74 |
+
points_cam, distances_cam, intensities_cam, beam_id, azimuth = pointcloud.points_ouster_to_cam()
|
| 75 |
+
far_point_cam, far_point_distnace, far_point_intensity = pointcloud.select_points_ouster_to_cam(far_point)
|
| 76 |
+
img_vis = image.project_points(np.vstack((points_cam, far_point_cam)), np.append(intensities_cam, 128), beam_id, azimuth, cmap)
|
| 77 |
+
far_pixel = image.get_image_coords(far_point_cam)
|
| 78 |
+
|
| 79 |
+
if far_pixel is not None and len(far_pixel) > 0:
|
| 80 |
+
u, v = far_pixel[0] # pixel coordinates
|
| 81 |
+
|
| 82 |
+
h, w = img_vis.shape[:2]
|
| 83 |
+
bottom_center = (w // 2, h)
|
| 84 |
+
|
| 85 |
+
ax.imshow(img_vis[:, :, ::-1])
|
| 86 |
+
ax.plot(
|
| 87 |
+
[bottom_center[0], u],
|
| 88 |
+
[bottom_center[1], v],
|
| 89 |
+
color="lime",
|
| 90 |
+
linewidth=2
|
| 91 |
+
)
|
| 92 |
+
ax.text(
|
| 93 |
+
u,
|
| 94 |
+
v - 10,
|
| 95 |
+
f"{far_point[0,0]:.2f}",
|
| 96 |
+
color="lime",
|
| 97 |
+
fontsize=12,
|
| 98 |
+
ha="center",
|
| 99 |
+
bbox=dict(facecolor="black", alpha=0.6, edgecolor="none")
|
| 100 |
+
)
|
| 101 |
+
ax.set_title(f"{image_timestamp}.png")
|
| 102 |
+
ax.axis("off")
|
| 103 |
+
fig.canvas.draw()
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"Could not project pointcloud onto {image_timestamp}.png: {e}")
|
| 106 |
+
idx[0] += 1
|
| 107 |
+
show_image(idx[0]) # skip bad one
|
| 108 |
+
|
| 109 |
+
def on_key(event):
|
| 110 |
+
if event.key in [' ', 'right']: # space or right arrow
|
| 111 |
+
idx[0] += 1
|
| 112 |
+
show_image(idx[0])
|
| 113 |
+
elif event.key in [' ', 'left']: # space or right arrow
|
| 114 |
+
if idx[0] > 0:
|
| 115 |
+
idx[0] -= 1
|
| 116 |
+
show_image(idx[0])
|
| 117 |
+
elif event.key in ['q', 'escape']: # q or Esc → quit
|
| 118 |
+
plt.close(fig)
|
| 119 |
+
|
| 120 |
+
fig.canvas.mpl_connect('key_press_event', on_key)
|
| 121 |
+
show_image(idx[0])
|
| 122 |
+
plt.show()
|
localisation/.gitkeep
ADDED
|
File without changes
|
localisation/VPR_eval-all-v2.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.image as mpimg
|
| 4 |
+
import sys
|
| 5 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
import open3d as o3d
|
| 9 |
+
from scipy.spatial.transform import Rotation
|
| 10 |
+
from utils.lidar import PointCloud
|
| 11 |
+
from utils.camera import ImageData
|
| 12 |
+
import utils.utils as utils
|
| 13 |
+
from natsort import natsorted, index_natsorted
|
| 14 |
+
import torch
|
| 15 |
+
from tqdm import tqdm
|
| 16 |
+
|
| 17 |
+
################## set device based on cuda availability #################
|
| 18 |
+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
| 19 |
+
|
| 20 |
+
print('CUDA availability: ' + str(torch.cuda.is_available()))
|
| 21 |
+
|
| 22 |
+
####################### Functions for matching using numpy on CPU or Pytorch on GPU ###################
|
| 23 |
+
def getMatchIndsCPU(ft_ref,ft_qry,topK=20,metric='cosine'):
|
| 24 |
+
"""
|
| 25 |
+
metric: 'euclidean' or 'cosine'
|
| 26 |
+
"""
|
| 27 |
+
# dMat = cdist(ft_ref,ft_qry,metric)
|
| 28 |
+
|
| 29 |
+
ft_qry_norm = ft_qry / np.linalg.norm(ft_qry, axis=1, keepdims=True) # Shape (M, N)
|
| 30 |
+
ft_ref_norm = ft_ref / np.linalg.norm(ft_ref, axis=1, keepdims=True) # Shape (C, N)
|
| 31 |
+
|
| 32 |
+
# Step 2: Compute cosine similarity
|
| 33 |
+
dMat = 1 - (ft_ref_norm @ ft_qry_norm.T)
|
| 34 |
+
mInds = np.argsort(dMat,axis=0)[:topK].squeeze() # shape: K x ft_qry.shape[0]
|
| 35 |
+
return mInds, dMat
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def getMatchIndsGPU(ft_ref, ft_qry,topK=20, metric='cosine'):
|
| 39 |
+
# metric: 'euclidean' or 'cosine'
|
| 40 |
+
ft_qry_tensor = torch.Tensor(ft_qry).to(device)
|
| 41 |
+
ft_ref_tensor = torch.Tensor(ft_ref).to(device)
|
| 42 |
+
|
| 43 |
+
if metric == 'euclidean':
|
| 44 |
+
# Use torch's cdist for Euclidean distance
|
| 45 |
+
dMat = torch.cdist(ft_ref, ft_qry)
|
| 46 |
+
|
| 47 |
+
elif metric == 'cosine':
|
| 48 |
+
# # Normalize both the query and reference tensors
|
| 49 |
+
ft_qry_norm = ft_qry_tensor / ft_qry_tensor.norm(dim=1, keepdim=True)
|
| 50 |
+
ft_ref_norm = ft_ref_tensor / ft_ref_tensor.norm(dim=1, keepdim=True)
|
| 51 |
+
# Compute cosine similarity (1 - cosine similarity for distance)
|
| 52 |
+
dMat = 1 - ft_ref_norm @ ft_qry_norm.t()
|
| 53 |
+
|
| 54 |
+
# Get the indices of the top 5 closest matches
|
| 55 |
+
mInds = torch.argsort(dMat, dim=0)[:topK].squeeze()
|
| 56 |
+
|
| 57 |
+
return mInds, dMat
|
| 58 |
+
|
| 59 |
+
qry_sets = [
|
| 60 |
+
'20210909_124816_v2',
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
ref_sets = [
|
| 64 |
+
'20230509_115540_v2',
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
vpr_descs = [
|
| 68 |
+
'cosplace',
|
| 69 |
+
'boq',
|
| 70 |
+
'clique-mining',
|
| 71 |
+
'cricavpr',
|
| 72 |
+
'eigenplaces',
|
| 73 |
+
'mixvpr',
|
| 74 |
+
'megaloc',
|
| 75 |
+
'salad',
|
| 76 |
+
'supervlad',
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
img_calib_file = f"./camera_calib.txt"
|
| 81 |
+
|
| 82 |
+
dist_tolerance = 10 # metres
|
| 83 |
+
# qry_idx = 4
|
| 84 |
+
|
| 85 |
+
# User parameters
|
| 86 |
+
location = 'dalby-to-brigalow'
|
| 87 |
+
|
| 88 |
+
################ Reference filenames and directories #################################
|
| 89 |
+
ref_condition = ''
|
| 90 |
+
ref_camera_pos = 'front'
|
| 91 |
+
|
| 92 |
+
ref_timestamps = []
|
| 93 |
+
ref_utms = []
|
| 94 |
+
ref_img_filenames = []
|
| 95 |
+
ref_utm_filenames = []
|
| 96 |
+
|
| 97 |
+
for ref_set in ref_sets:
|
| 98 |
+
print(f"Loading {ref_set}")
|
| 99 |
+
|
| 100 |
+
ref_root_directory = f"../Datasets/dalby/KITTI-style/{location}"
|
| 101 |
+
ref_vpr_root = f"../Datasets/dalby/KITTI-style/{location}/vpr_ftrs/"
|
| 102 |
+
ref_image_dir = f"{ref_root_directory}/{ref_set}/{ref_camera_pos}-imgs/"
|
| 103 |
+
ref_utm_dir = f"{ref_root_directory}/{ref_set}/utm/"
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
this_ref_timestamp = [filename.split('.png')[0] for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]
|
| 107 |
+
ref_utms = ref_utms+[np.loadtxt(ref_utm_dir+filename) for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)][55::]
|
| 108 |
+
ref_img_filenames = [filename for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]
|
| 109 |
+
ref_utm_filenames = np.array([filename for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])[:len(os.listdir(ref_utm_dir))-55]
|
| 110 |
+
ref_timestamps = ref_timestamps+this_ref_timestamp
|
| 111 |
+
|
| 112 |
+
ref_utms = np.array(ref_utms)
|
| 113 |
+
|
| 114 |
+
for vpr_desc in vpr_descs:
|
| 115 |
+
|
| 116 |
+
all_results = []
|
| 117 |
+
|
| 118 |
+
first = True
|
| 119 |
+
|
| 120 |
+
print(f"Loading references")
|
| 121 |
+
|
| 122 |
+
for ref_set in ref_sets:
|
| 123 |
+
print(f"Loading {ref_set} {vpr_desc} descriptors")
|
| 124 |
+
ref_root_directory = f"../Datasets/dalby/KITTI-style/{location}"
|
| 125 |
+
ref_vpr_root = f"../Datasets/dalby/KITTI-style/{location}/vpr_ftrs/"
|
| 126 |
+
|
| 127 |
+
ref_image_dir = f"{ref_root_directory}/{ref_set}/{ref_camera_pos}-imgs/"
|
| 128 |
+
|
| 129 |
+
ref_name_sort_idx = index_natsorted(os.listdir(ref_image_dir))
|
| 130 |
+
ref_ftr = np.load(f"{ref_vpr_root}/{ref_set}/{vpr_desc}/queries_descriptors.npy")
|
| 131 |
+
if first:
|
| 132 |
+
ref_ftrs = ref_ftr[ref_name_sort_idx]
|
| 133 |
+
first = False
|
| 134 |
+
else:
|
| 135 |
+
ref_ftrs = np.vstack((ref_ftrs, ref_ftr[ref_name_sort_idx]))
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
for qry_set in qry_sets:
|
| 139 |
+
|
| 140 |
+
################ Query filenames and directories #################################
|
| 141 |
+
qry_condition = ''
|
| 142 |
+
qry_camera_pos = 'front'
|
| 143 |
+
|
| 144 |
+
qry_root_directory = f"../Datasets/dalby/KITTI-style/{location}"
|
| 145 |
+
qry_vpr_root = f"../Datasets/dalby/KITTI-style/{location}/vpr_ftrs/"
|
| 146 |
+
qry_image_dir = f"{qry_root_directory}/{qry_set}/{qry_camera_pos}-imgs/"
|
| 147 |
+
qry_utm_dir = f"{qry_root_directory}/{qry_set}/utm/"
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
qry_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(qry_image_dir)) if os.path.isfile(qry_image_dir+filename)]
|
| 151 |
+
qry_utms = np.array([np.loadtxt(qry_utm_dir+filename) for filename in natsorted(os.listdir(qry_utm_dir)) if os.path.isfile(qry_utm_dir+filename)])
|
| 152 |
+
qry_name_sort_idx = index_natsorted(os.listdir(qry_image_dir))
|
| 153 |
+
qry_ftrs = np.load(f"{qry_vpr_root}/{qry_set}/{vpr_desc}/queries_descriptors.npy")
|
| 154 |
+
qry_ftrs = qry_ftrs[qry_name_sort_idx]
|
| 155 |
+
|
| 156 |
+
mInds, dMat = getMatchIndsGPU(ref_ftrs,qry_ftrs,topK=1)
|
| 157 |
+
mInds = mInds.cpu().numpy()
|
| 158 |
+
in_tol = []
|
| 159 |
+
dists = []
|
| 160 |
+
valid_qry = 0
|
| 161 |
+
|
| 162 |
+
qry_utm_timestamps, qry_utm_idxs = utils.get_all_corr_files(qry_timestamps, [qry_utm_dir,])
|
| 163 |
+
ref_utm_timestamp, ref_utm_idxs = utils.get_all_corr_files(ref_timestamps, [ref_utm_dir,])
|
| 164 |
+
|
| 165 |
+
for qry_idx in tqdm(range(len(qry_timestamps))):
|
| 166 |
+
|
| 167 |
+
qry_image_timestamp = qry_timestamps[qry_idx]
|
| 168 |
+
qry_image_filename = f"{qry_image_dir}/{qry_image_timestamp}.png"
|
| 169 |
+
qry_utm = qry_utms[qry_utm_idxs[qry_idx]]
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
diffs = ref_utms - qry_utm # shape (N, 2)
|
| 173 |
+
qry_dists = np.linalg.norm(diffs, axis=1) # shape (N,)
|
| 174 |
+
if qry_dists.min() > dist_tolerance:
|
| 175 |
+
continue
|
| 176 |
+
else:
|
| 177 |
+
valid_qry += 1
|
| 178 |
+
|
| 179 |
+
ref_utm = ref_utms[ref_utm_idxs[int(mInds[qry_idx])]]
|
| 180 |
+
|
| 181 |
+
diff = ref_utm - qry_utm # shape (N, 2)
|
| 182 |
+
dist = np.linalg.norm(diff) # shape (N,)
|
| 183 |
+
dists.append(dist)
|
| 184 |
+
if dist < dist_tolerance:
|
| 185 |
+
in_tol.append(1)
|
| 186 |
+
else:
|
| 187 |
+
in_tol.append(0)
|
| 188 |
+
|
| 189 |
+
# qry_image = ImageData(qry_image_filename, img_calib_file)
|
| 190 |
+
|
| 191 |
+
# fig, ax = plt.subplots(1, 2, figsize=(19.4, 6))
|
| 192 |
+
# ax[0].clear()
|
| 193 |
+
# ax[1].clear()
|
| 194 |
+
|
| 195 |
+
# ax[0].imshow(qry_image.image[:, :, ::-1])
|
| 196 |
+
# ax[0].set_title(f"{qry_image_timestamp}.png")
|
| 197 |
+
# ax[0].axis("off")
|
| 198 |
+
|
| 199 |
+
# # Show matching reference image
|
| 200 |
+
# # ref_img_timestamp = utils.get_corr_files(ref_timestamps[int(mInds[qry_idx])], [ref_image_dir,])
|
| 201 |
+
# ref_image = ImageData(f"{ref_image_dir}/{ref_timestamps[int(mInds[qry_idx])]}.png", img_calib_file)
|
| 202 |
+
# ax[1].imshow(ref_image.image[:, :, ::-1])
|
| 203 |
+
# ax[1].set_title(f"{ref_timestamps[int(mInds[qry_idx])]}\nDist={dist:.2f}m")
|
| 204 |
+
|
| 205 |
+
# ax[1].axis("off")
|
| 206 |
+
# fig.canvas.draw()
|
| 207 |
+
|
| 208 |
+
print(f"Recall for {qry_set} using {vpr_desc}: {np.sum(np.array(in_tol))/valid_qry:.02%}")
|
| 209 |
+
all_results.append(np.sum(np.array(in_tol))/valid_qry)
|
| 210 |
+
# plt.figure()
|
| 211 |
+
# plt.plot(np.clip(dists, 0, 30))
|
| 212 |
+
# plt.ylim((0,35))
|
| 213 |
+
|
| 214 |
+
print(f"All {vpr_desc} results:")
|
| 215 |
+
print(all_results)
|
localisation/VPR_eval-all.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.image as mpimg
|
| 4 |
+
import sys
|
| 5 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
import open3d as o3d
|
| 9 |
+
from scipy.spatial.transform import Rotation
|
| 10 |
+
from utils.lidar import PointCloud
|
| 11 |
+
from utils.camera import ImageData
|
| 12 |
+
import utils.utils as utils
|
| 13 |
+
from natsort import natsorted, index_natsorted
|
| 14 |
+
import torch
|
| 15 |
+
from tqdm import tqdm
|
| 16 |
+
|
| 17 |
+
################## set device based on cuda availability #################
|
| 18 |
+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
| 19 |
+
|
| 20 |
+
print('CUDA availability: ' + str(torch.cuda.is_available()))
|
| 21 |
+
|
| 22 |
+
####################### Functions for matching using numpy on CPU or Pytorch on GPU ###################
|
| 23 |
+
def getMatchIndsCPU(ft_ref,ft_qry,topK=20,metric='cosine'):
|
| 24 |
+
"""
|
| 25 |
+
metric: 'euclidean' or 'cosine'
|
| 26 |
+
"""
|
| 27 |
+
# dMat = cdist(ft_ref,ft_qry,metric)
|
| 28 |
+
|
| 29 |
+
ft_qry_norm = ft_qry / np.linalg.norm(ft_qry, axis=1, keepdims=True) # Shape (M, N)
|
| 30 |
+
ft_ref_norm = ft_ref / np.linalg.norm(ft_ref, axis=1, keepdims=True) # Shape (C, N)
|
| 31 |
+
|
| 32 |
+
# Step 2: Compute cosine similarity
|
| 33 |
+
dMat = 1 - (ft_ref_norm @ ft_qry_norm.T)
|
| 34 |
+
mInds = np.argsort(dMat,axis=0)[:topK].squeeze() # shape: K x ft_qry.shape[0]
|
| 35 |
+
return mInds, dMat
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def getMatchIndsGPU(ft_ref, ft_qry,topK=20, metric='cosine'):
|
| 39 |
+
# metric: 'euclidean' or 'cosine'
|
| 40 |
+
ft_qry_tensor = torch.Tensor(ft_qry).to(device)
|
| 41 |
+
ft_ref_tensor = torch.Tensor(ft_ref).to(device)
|
| 42 |
+
|
| 43 |
+
if metric == 'euclidean':
|
| 44 |
+
# Use torch's cdist for Euclidean distance
|
| 45 |
+
dMat = torch.cdist(ft_ref, ft_qry)
|
| 46 |
+
|
| 47 |
+
elif metric == 'cosine':
|
| 48 |
+
# # Normalize both the query and reference tensors
|
| 49 |
+
ft_qry_norm = ft_qry_tensor / ft_qry_tensor.norm(dim=1, keepdim=True)
|
| 50 |
+
ft_ref_norm = ft_ref_tensor / ft_ref_tensor.norm(dim=1, keepdim=True)
|
| 51 |
+
# Compute cosine similarity (1 - cosine similarity for distance)
|
| 52 |
+
dMat = 1 - ft_ref_norm @ ft_qry_norm.t()
|
| 53 |
+
|
| 54 |
+
# Get the indices of the top 5 closest matches
|
| 55 |
+
mInds = torch.argsort(dMat, dim=0)[:topK].squeeze()
|
| 56 |
+
|
| 57 |
+
return mInds, dMat
|
| 58 |
+
|
| 59 |
+
qry_sets = [
|
| 60 |
+
'Cambogan_20250811_113017',
|
| 61 |
+
'DairyCreek_20250811_103318',
|
| 62 |
+
'Holmview_20250820_130327',
|
| 63 |
+
'Pullenvale_20250916_124105',
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
# qry_sets = [
|
| 67 |
+
# 'Cambogan_20250812_122101',
|
| 68 |
+
# 'Dairy-Creek_20250812_123312',
|
| 69 |
+
# 'Holmview_20250812_120856',
|
| 70 |
+
# 'Pullenvale_20250812_134524',
|
| 71 |
+
# ]
|
| 72 |
+
|
| 73 |
+
ref_sets = [
|
| 74 |
+
'Cambogan_20250812_122339',
|
| 75 |
+
'Dairy-Creek_20250812_122954',
|
| 76 |
+
'Holmview_20250812_120100',
|
| 77 |
+
'Pullenvale_20250812_134316',
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
vpr_descs = [
|
| 81 |
+
'cosplace',
|
| 82 |
+
'boq',
|
| 83 |
+
'clique-mining',
|
| 84 |
+
'cricavpr',
|
| 85 |
+
'eigenplaces',
|
| 86 |
+
'mixvpr',
|
| 87 |
+
'salad',
|
| 88 |
+
'supervlad',
|
| 89 |
+
# 'anyloc-urban',
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
# vpr_descs = [
|
| 93 |
+
# 'cosplace',
|
| 94 |
+
# ]
|
| 95 |
+
|
| 96 |
+
img_calib_file = f"./camera_calib.txt"
|
| 97 |
+
|
| 98 |
+
dist_tolerance = 10 # metres
|
| 99 |
+
# qry_idx = 4
|
| 100 |
+
|
| 101 |
+
# User parameters
|
| 102 |
+
# vpr_desc = 'cosplace'
|
| 103 |
+
# location = 'Holmview'
|
| 104 |
+
# location = 'Cambogan'
|
| 105 |
+
|
| 106 |
+
################ Reference filenames and directories #################################
|
| 107 |
+
# ref_sequence = '20250812_120100'
|
| 108 |
+
# 20250812_120856
|
| 109 |
+
# ref_sequence = '20250812_122339'
|
| 110 |
+
# 20250812_122621
|
| 111 |
+
ref_condition = 'dry'
|
| 112 |
+
ref_camera_pos = 'front'
|
| 113 |
+
|
| 114 |
+
ref_timestamps = []
|
| 115 |
+
ref_utms = []
|
| 116 |
+
ref_img_filenames = []
|
| 117 |
+
ref_utm_filenames = []
|
| 118 |
+
|
| 119 |
+
for ref_set in ref_sets:
|
| 120 |
+
print(f"Loading {ref_set}")
|
| 121 |
+
ref_root_directory = f"../Datasets/FRED/{ref_condition}/KITTI-style"
|
| 122 |
+
ref_vpr_root = f"../Datasets/FRED/vpr_ftrs/{ref_condition}/KITTI-style"
|
| 123 |
+
|
| 124 |
+
ref_image_dir = f"{ref_root_directory}/{ref_set}/{ref_camera_pos}-imgs/"
|
| 125 |
+
ref_utm_dir = f"{ref_root_directory}/{ref_set}/utm/"
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
this_ref_timestamp = [filename.split('.png')[0] for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]
|
| 129 |
+
ref_timestamps = ref_timestamps+this_ref_timestamp
|
| 130 |
+
ref_utms = ref_utms+[np.loadtxt(ref_utm_dir+filename) for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)]
|
| 131 |
+
ref_utm_filenames = ref_utm_filenames+[utils.get_corr_files(ref_timestamp, [ref_utm_dir,]) for ref_timestamp in this_ref_timestamp]
|
| 132 |
+
ref_img_filenames = ref_img_filenames+[filename for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]
|
| 133 |
+
|
| 134 |
+
ref_utms = np.array(ref_utms)
|
| 135 |
+
|
| 136 |
+
for vpr_desc in vpr_descs:
|
| 137 |
+
|
| 138 |
+
all_results = []
|
| 139 |
+
|
| 140 |
+
first = True
|
| 141 |
+
|
| 142 |
+
print(f"Loading references")
|
| 143 |
+
|
| 144 |
+
for ref_set in ref_sets:
|
| 145 |
+
print(f"Loading {ref_set} {vpr_desc} descriptors")
|
| 146 |
+
ref_root_directory = f"../Datasets/FRED/{ref_condition}/KITTI-style"
|
| 147 |
+
ref_vpr_root = f"../Datasets/FRED/vpr_ftrs/{ref_condition}/KITTI-style"
|
| 148 |
+
|
| 149 |
+
ref_image_dir = f"{ref_root_directory}/{ref_set}/{ref_camera_pos}-imgs/"
|
| 150 |
+
|
| 151 |
+
ref_name_sort_idx = index_natsorted(os.listdir(ref_image_dir))
|
| 152 |
+
ref_ftr = np.load(f"{ref_vpr_root}/{ref_set}/{vpr_desc}/queries_descriptors.npy")
|
| 153 |
+
if first:
|
| 154 |
+
ref_ftrs = ref_ftr[ref_name_sort_idx]
|
| 155 |
+
first = False
|
| 156 |
+
else:
|
| 157 |
+
ref_ftrs = np.vstack((ref_ftrs, ref_ftr[ref_name_sort_idx]))
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
for qry_set in qry_sets:
|
| 161 |
+
|
| 162 |
+
################ Query filenames and directories #################################
|
| 163 |
+
# qry_sequence = '20250820_130327'
|
| 164 |
+
# qry_sequence = '20250812_120856'
|
| 165 |
+
###
|
| 166 |
+
# qry_sequence = '20250811_113017'
|
| 167 |
+
# qry_sequence = '20250812_122621'
|
| 168 |
+
qry_condition = 'flooded'
|
| 169 |
+
# qry_sequence = '20250812_122339'
|
| 170 |
+
# qry_condition = 'dry'
|
| 171 |
+
qry_camera_pos = 'front'
|
| 172 |
+
qry_root_directory = f"../Datasets/FRED/{qry_condition}/KITTI-style"
|
| 173 |
+
qry_vpr_root = f"../Datasets/FRED/vpr_ftrs/{qry_condition}/KITTI-style"
|
| 174 |
+
|
| 175 |
+
qry_image_dir = f"{qry_root_directory}/{qry_set}/{qry_camera_pos}-imgs/"
|
| 176 |
+
qry_utm_dir = f"{qry_root_directory}/{qry_set}/utm/"
|
| 177 |
+
qry_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(qry_image_dir)) if os.path.isfile(qry_image_dir+filename)]
|
| 178 |
+
qry_name_sort_idx = index_natsorted(os.listdir(qry_image_dir))
|
| 179 |
+
qry_ftrs = np.load(f"{qry_vpr_root}/{qry_set}/{vpr_desc}/queries_descriptors.npy")
|
| 180 |
+
qry_ftrs = qry_ftrs[qry_name_sort_idx]
|
| 181 |
+
|
| 182 |
+
mInds, dMat = getMatchIndsGPU(ref_ftrs,qry_ftrs,topK=1)
|
| 183 |
+
mInds = mInds.cpu().numpy()
|
| 184 |
+
in_tol = []
|
| 185 |
+
dists = []
|
| 186 |
+
valid_qry = 0
|
| 187 |
+
|
| 188 |
+
for qry_idx in tqdm(range(len(qry_timestamps))):
|
| 189 |
+
|
| 190 |
+
qry_image_timestamp = qry_timestamps[qry_idx]
|
| 191 |
+
qry_image_filename = f"{qry_image_dir}/{qry_image_timestamp}.png"
|
| 192 |
+
qry_utm_timestamp = utils.get_corr_files(qry_image_timestamp, [qry_utm_dir,])
|
| 193 |
+
qry_utm = np.loadtxt(qry_utm_timestamp)
|
| 194 |
+
|
| 195 |
+
# print(f"Number of queries: {len(qry_timestamps)}")
|
| 196 |
+
# print(f"Number of references{ {len(ref_timestamps)}}")
|
| 197 |
+
# print(f"Matche {mInds[qry_idx]}")
|
| 198 |
+
# print(natsorted(os.listdir(qry_image_dir)))
|
| 199 |
+
# print(os.listdir(qry_image_dir))
|
| 200 |
+
|
| 201 |
+
diffs = ref_utms - qry_utm # shape (N, 2)
|
| 202 |
+
qry_dists = np.linalg.norm(diffs, axis=1) # shape (N,)
|
| 203 |
+
if qry_dists.min() > dist_tolerance:
|
| 204 |
+
continue
|
| 205 |
+
else:
|
| 206 |
+
valid_qry += 1
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# ref_utm_timestamp = utils.get_corr_files(ref_timestamps[int(mInds[qry_idx])], [ref_utm_dir,])
|
| 210 |
+
# ref_utm = np.loadtxt(ref_utm_timestamp)
|
| 211 |
+
ref_utm = np.loadtxt(ref_utm_filenames[int(mInds[qry_idx])])
|
| 212 |
+
|
| 213 |
+
diff = ref_utm - qry_utm # shape (N, 2)
|
| 214 |
+
dist = np.linalg.norm(diff) # shape (N,)
|
| 215 |
+
dists.append(dist)
|
| 216 |
+
if dist < dist_tolerance:
|
| 217 |
+
in_tol.append(1)
|
| 218 |
+
else:
|
| 219 |
+
in_tol.append(0)
|
| 220 |
+
|
| 221 |
+
# qry_image = ImageData(qry_image_filename, img_calib_file)
|
| 222 |
+
|
| 223 |
+
# fig, ax = plt.subplots(1, 2, figsize=(19.4, 6))
|
| 224 |
+
# ax[0].clear()
|
| 225 |
+
# ax[1].clear()
|
| 226 |
+
|
| 227 |
+
# ax[0].imshow(qry_image.image[:, :, ::-1])
|
| 228 |
+
# ax[0].set_title(f"{qry_image_timestamp}.png")
|
| 229 |
+
# ax[0].axis("off")
|
| 230 |
+
|
| 231 |
+
# # Show matching reference image
|
| 232 |
+
# # ref_img_timestamp = utils.get_corr_files(ref_timestamps[int(mInds[qry_idx])], [ref_image_dir,])
|
| 233 |
+
# ref_image = ImageData(f"{ref_image_dir}/{ref_timestamps[int(mInds[qry_idx])]}.png", img_calib_file)
|
| 234 |
+
# ax[1].imshow(ref_image.image[:, :, ::-1])
|
| 235 |
+
# ax[1].set_title(f"{ref_timestamps[int(mInds[qry_idx])]}\nDist={dist:.2f}m")
|
| 236 |
+
|
| 237 |
+
# ax[1].axis("off")
|
| 238 |
+
# fig.canvas.draw()
|
| 239 |
+
|
| 240 |
+
print(f"Recall for {qry_set} using {vpr_desc}: {np.sum(np.array(in_tol))/valid_qry:.02%}")
|
| 241 |
+
all_results.append(np.sum(np.array(in_tol))/valid_qry)
|
| 242 |
+
# plt.figure()
|
| 243 |
+
# plt.plot(np.clip(dists, 0, 30))
|
| 244 |
+
# plt.ylim((0,35))
|
| 245 |
+
|
| 246 |
+
print(f"All {vpr_desc} results:")
|
| 247 |
+
print(all_results)
|
localisation/VPR_eval.ipynb
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 26,
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"outputs": [
|
| 8 |
+
{
|
| 9 |
+
"name": "stdout",
|
| 10 |
+
"output_type": "stream",
|
| 11 |
+
"text": [
|
| 12 |
+
"CUDA availability: True\n"
|
| 13 |
+
]
|
| 14 |
+
}
|
| 15 |
+
],
|
| 16 |
+
"source": [
|
| 17 |
+
"import os\n",
|
| 18 |
+
"import matplotlib.pyplot as plt\n",
|
| 19 |
+
"import matplotlib.image as mpimg\n",
|
| 20 |
+
"import sys\n",
|
| 21 |
+
"sys.path.append(os.path.abspath(\"..\")) # one level up\n",
|
| 22 |
+
"import numpy as np\n",
|
| 23 |
+
"import cv2\n",
|
| 24 |
+
"import open3d as o3d\n",
|
| 25 |
+
"from scipy.spatial.transform import Rotation\n",
|
| 26 |
+
"from utils.lidar import PointCloud\n",
|
| 27 |
+
"from utils.camera import ImageData\n",
|
| 28 |
+
"import utils.utils as utils\n",
|
| 29 |
+
"from natsort import natsorted, index_natsorted\n",
|
| 30 |
+
"import torch\n",
|
| 31 |
+
"from tqdm.notebook import tqdm\n",
|
| 32 |
+
"\n",
|
| 33 |
+
"################## set device based on cuda availability #################\n",
|
| 34 |
+
"device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 35 |
+
"\n",
|
| 36 |
+
"print('CUDA availability: ' + str(torch.cuda.is_available()))"
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"cell_type": "code",
|
| 41 |
+
"execution_count": 27,
|
| 42 |
+
"metadata": {},
|
| 43 |
+
"outputs": [],
|
| 44 |
+
"source": [
|
| 45 |
+
"####################### Functions for matching using numpy on CPU or Pytorch on GPU ###################\n",
|
| 46 |
+
"def getMatchIndsCPU(ft_ref,ft_qry,topK=20,metric='cosine'):\n",
|
| 47 |
+
" \"\"\"\n",
|
| 48 |
+
" metric: 'euclidean' or 'cosine'\n",
|
| 49 |
+
" \"\"\"\n",
|
| 50 |
+
" # dMat = cdist(ft_ref,ft_qry,metric)\n",
|
| 51 |
+
"\n",
|
| 52 |
+
" ft_qry_norm = ft_qry / np.linalg.norm(ft_qry, axis=1, keepdims=True) # Shape (M, N)\n",
|
| 53 |
+
" ft_ref_norm = ft_ref / np.linalg.norm(ft_ref, axis=1, keepdims=True) # Shape (C, N)\n",
|
| 54 |
+
"\n",
|
| 55 |
+
" # Step 2: Compute cosine similarity\n",
|
| 56 |
+
" dMat = 1 - (ft_ref_norm @ ft_qry_norm.T)\n",
|
| 57 |
+
" mInds = np.argsort(dMat,axis=0)[:topK].squeeze() # shape: K x ft_qry.shape[0]\n",
|
| 58 |
+
" return mInds, dMat\n",
|
| 59 |
+
"\n",
|
| 60 |
+
"\n",
|
| 61 |
+
"def getMatchIndsGPU(ft_ref, ft_qry,topK=20, metric='cosine'):\n",
|
| 62 |
+
" # metric: 'euclidean' or 'cosine'\n",
|
| 63 |
+
" ft_qry_tensor = torch.Tensor(ft_qry).to(device)\n",
|
| 64 |
+
" ft_ref_tensor = torch.Tensor(ft_ref).to(device)\n",
|
| 65 |
+
"\n",
|
| 66 |
+
" if metric == 'euclidean':\n",
|
| 67 |
+
" # Use torch's cdist for Euclidean distance\n",
|
| 68 |
+
" dMat = torch.cdist(ft_ref, ft_qry)\n",
|
| 69 |
+
" \n",
|
| 70 |
+
" elif metric == 'cosine':\n",
|
| 71 |
+
" # # Normalize both the query and reference tensors\n",
|
| 72 |
+
" ft_qry_norm = ft_qry_tensor / ft_qry_tensor.norm(dim=1, keepdim=True)\n",
|
| 73 |
+
" ft_ref_norm = ft_ref_tensor / ft_ref_tensor.norm(dim=1, keepdim=True)\n",
|
| 74 |
+
" # Compute cosine similarity (1 - cosine similarity for distance)\n",
|
| 75 |
+
" dMat = 1 - ft_ref_norm @ ft_qry_norm.t()\n",
|
| 76 |
+
"\n",
|
| 77 |
+
" # Get the indices of the top 5 closest matches\n",
|
| 78 |
+
" mInds = torch.argsort(dMat, dim=0)[:topK].squeeze()\n",
|
| 79 |
+
" \n",
|
| 80 |
+
" return mInds, dMat"
|
| 81 |
+
]
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"cell_type": "code",
|
| 85 |
+
"execution_count": 28,
|
| 86 |
+
"metadata": {},
|
| 87 |
+
"outputs": [
|
| 88 |
+
{
|
| 89 |
+
"name": "stdout",
|
| 90 |
+
"output_type": "stream",
|
| 91 |
+
"text": [
|
| 92 |
+
"(18009, 2)\n"
|
| 93 |
+
]
|
| 94 |
+
}
|
| 95 |
+
],
|
| 96 |
+
"source": [
|
| 97 |
+
"# User parameters\n",
|
| 98 |
+
"# vpr_desc = 'boq'\n",
|
| 99 |
+
"# location = 'Holmview'\n",
|
| 100 |
+
"# # location = 'Cambogan'\n",
|
| 101 |
+
"\n",
|
| 102 |
+
"# ################ Query filenames and directories #################################\n",
|
| 103 |
+
"# # qry_sequence = '20250820_130327'\n",
|
| 104 |
+
"# qry_sequence = '20250812_120856'\n",
|
| 105 |
+
"# # qry_sequence = '20250811_113017'\n",
|
| 106 |
+
"# # qry_sequence = '20250812_122621'\n",
|
| 107 |
+
"# # qry_condition = 'flooded'\n",
|
| 108 |
+
"# # qry_sequence = '20250812_122339'\n",
|
| 109 |
+
"# qry_condition = 'dry'\n",
|
| 110 |
+
"# qry_camera_pos = 'front'\n",
|
| 111 |
+
"# qry_root_directory = f\"../../Datasets/FRED/{qry_condition}/KITTI-style\"\n",
|
| 112 |
+
"# qry_vpr_root = f\"../../Datasets/FRED/vpr_ftrs/{qry_condition}/KITTI-style\"\n",
|
| 113 |
+
"\n",
|
| 114 |
+
"vpr_desc = 'salad'\n",
|
| 115 |
+
"location = 'dalby-to-brigalow'\n",
|
| 116 |
+
"# location = 'Cambogan'\n",
|
| 117 |
+
"\n",
|
| 118 |
+
"################ Query filenames and directories #################################\n",
|
| 119 |
+
"qry_sequence = '20210909_124816_v2'\n",
|
| 120 |
+
"qry_condition = ''\n",
|
| 121 |
+
"qry_camera_pos = 'front'\n",
|
| 122 |
+
"qry_root_directory = f\"../../Datasets/dalby/KITTI-style/{location}\"\n",
|
| 123 |
+
"qry_vpr_root = f\"../../Datasets/dalby/KITTI-style/{location}/vpr_ftrs/\"\n",
|
| 124 |
+
"\n",
|
| 125 |
+
"qry_image_dir = f\"{qry_root_directory}/{qry_sequence}/{qry_camera_pos}-imgs/\"\n",
|
| 126 |
+
"qry_utm_dir = f\"{qry_root_directory}/{qry_sequence}/utm/\"\n",
|
| 127 |
+
"qry_utms = np.array([np.loadtxt(qry_utm_dir+filename) for filename in natsorted(os.listdir(qry_utm_dir)) if os.path.isfile(qry_utm_dir+filename)])\n",
|
| 128 |
+
"qry_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(qry_image_dir)) if os.path.isfile(qry_image_dir+filename)]\n",
|
| 129 |
+
"qry_name_sort_idx = index_natsorted(os.listdir(qry_image_dir))\n",
|
| 130 |
+
"qry_ftrs = np.load(f\"{qry_vpr_root}/{qry_sequence}/{vpr_desc}/queries_descriptors.npy\")\n",
|
| 131 |
+
"qry_ftrs = qry_ftrs[qry_name_sort_idx]\n",
|
| 132 |
+
"\n",
|
| 133 |
+
"# qry_ftrs = qry_ftrs[::2]\n",
|
| 134 |
+
"# qry_timestamps = qry_timestamps[::2]\n",
|
| 135 |
+
"\n",
|
| 136 |
+
"\n",
|
| 137 |
+
"################ Reference filenames and directories #################################\n",
|
| 138 |
+
"# ref_sequence = '20250812_120100'\n",
|
| 139 |
+
"# # 20250812_120856\n",
|
| 140 |
+
"# # ref_sequence = '20250812_122339'\n",
|
| 141 |
+
"# # 20250812_122621\n",
|
| 142 |
+
"# ref_condition = 'dry'\n",
|
| 143 |
+
"# ref_camera_pos = 'front'\n",
|
| 144 |
+
"# ref_root_directory = f\"../../Datasets/FRED/{ref_condition}/KITTI-style\"\n",
|
| 145 |
+
"# ref_vpr_root = f\"../../Datasets/FRED/vpr_ftrs/{ref_condition}/KITTI-style\"\n",
|
| 146 |
+
"\n",
|
| 147 |
+
"ref_sequence = '20230509_115540_v2'\n",
|
| 148 |
+
"ref_condition = ''\n",
|
| 149 |
+
"ref_camera_pos = 'front'\n",
|
| 150 |
+
"ref_root_directory = f\"../../Datasets/dalby/KITTI-style/{location}\"\n",
|
| 151 |
+
"ref_vpr_root = f\"../../Datasets/dalby/KITTI-style/{location}/vpr_ftrs/\"\n",
|
| 152 |
+
"\n",
|
| 153 |
+
"ref_image_dir = f\"{ref_root_directory}/{ref_sequence}/{ref_camera_pos}-imgs/\"\n",
|
| 154 |
+
"ref_utm_dir = f\"{ref_root_directory}/{ref_sequence}/utm/\"\n",
|
| 155 |
+
"ref_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]\n",
|
| 156 |
+
"ref_name_sort_idx = index_natsorted(os.listdir(ref_image_dir))\n",
|
| 157 |
+
"ref_utms = np.array([np.loadtxt(ref_utm_dir+filename) for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])[55::]\n",
|
| 158 |
+
"print(ref_utms.shape)\n",
|
| 159 |
+
"ref_img_filenames = [filename for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]\n",
|
| 160 |
+
"ref_utm_filenames = np.array([filename for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])[:len(os.listdir(ref_utm_dir))-55]\n",
|
| 161 |
+
"ref_ftrs = np.load(f\"{ref_vpr_root}/{ref_sequence}/{vpr_desc}/queries_descriptors.npy\")\n",
|
| 162 |
+
"ref_ftrs = ref_ftrs[ref_name_sort_idx]\n",
|
| 163 |
+
"\n",
|
| 164 |
+
"# ref_timestamps = ref_timestamps[::2]\n",
|
| 165 |
+
"# ref_img_filenames = ref_img_filenames[::2]\n",
|
| 166 |
+
"# ref_ftrs = ref_ftrs[::2]\n",
|
| 167 |
+
"\n",
|
| 168 |
+
"img_calib_file = f\"../camera_calib.txt\"\n",
|
| 169 |
+
"\n",
|
| 170 |
+
"dist_tolerance = 10 # metres\n",
|
| 171 |
+
"# qry_idx = 4\n",
|
| 172 |
+
"\n",
|
| 173 |
+
"qry_utm_timestamps, qry_utm_idxs = utils.get_all_corr_files(qry_timestamps, [qry_utm_dir,])\n",
|
| 174 |
+
"ref_utm_timestamp, ref_utm_idxs = utils.get_all_corr_files(ref_timestamps, [ref_utm_dir,])"
|
| 175 |
+
]
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"cell_type": "code",
|
| 179 |
+
"execution_count": 29,
|
| 180 |
+
"metadata": {},
|
| 181 |
+
"outputs": [
|
| 182 |
+
{
|
| 183 |
+
"data": {
|
| 184 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 185 |
+
"model_id": "fe0f5b498d8f437b9e761d3747cfddc3",
|
| 186 |
+
"version_major": 2,
|
| 187 |
+
"version_minor": 0
|
| 188 |
+
},
|
| 189 |
+
"text/plain": [
|
| 190 |
+
" 0%| | 0/24299 [00:00<?, ?it/s]"
|
| 191 |
+
]
|
| 192 |
+
},
|
| 193 |
+
"metadata": {},
|
| 194 |
+
"output_type": "display_data"
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"name": "stdout",
|
| 198 |
+
"output_type": "stream",
|
| 199 |
+
"text": [
|
| 200 |
+
"Recall: 52.49%\n",
|
| 201 |
+
"Valid queries: 23438\n"
|
| 202 |
+
]
|
| 203 |
+
}
|
| 204 |
+
],
|
| 205 |
+
"source": [
|
| 206 |
+
"mInds, dMat = getMatchIndsGPU(ref_ftrs,qry_ftrs,topK=1)\n",
|
| 207 |
+
"mInds = mInds.cpu().numpy()\n",
|
| 208 |
+
"in_tol = []\n",
|
| 209 |
+
"dists = []\n",
|
| 210 |
+
"\n",
|
| 211 |
+
"valid_qry = []\n",
|
| 212 |
+
"colors = []\n",
|
| 213 |
+
"plot_utms = []\n",
|
| 214 |
+
"\n",
|
| 215 |
+
"for qry_idx in tqdm(range(len(qry_timestamps))):\n",
|
| 216 |
+
"\n",
|
| 217 |
+
" qry_image_timestamp = qry_timestamps[qry_idx]\n",
|
| 218 |
+
" qry_image_filename = f\"{qry_image_dir}/{qry_image_timestamp}.png\"\n",
|
| 219 |
+
" # qry_utm_timestamp = utils.get_corr_files(qry_image_timestamp, [qry_utm_dir,])\n",
|
| 220 |
+
" qry_utm = qry_utms[qry_utm_idxs[qry_idx]]\n",
|
| 221 |
+
" # qry_utm = np.loadtxt(qry_utm_timestamp)\n",
|
| 222 |
+
"\n",
|
| 223 |
+
" # print(f\"Number of queries: {len(qry_timestamps)}\")\n",
|
| 224 |
+
" # print(f\"Number of references{ {len(ref_timestamps)}}\")\n",
|
| 225 |
+
" # print(f\"Matche {mInds[qry_idx]}\")\n",
|
| 226 |
+
" # print(natsorted(os.listdir(qry_image_dir)))\n",
|
| 227 |
+
" # print(os.listdir(qry_image_dir))\n",
|
| 228 |
+
"\n",
|
| 229 |
+
" # ref_utm_timestamp = utils.get_corr_files(ref_timestamps[int(mInds[qry_idx])], [ref_utm_dir,])\n",
|
| 230 |
+
" # ref_utm = np.loadtxt(ref_utm_timestamp)\n",
|
| 231 |
+
" ref_utm = ref_utms[ref_utm_idxs[int(mInds[qry_idx])]]\n",
|
| 232 |
+
"\n",
|
| 233 |
+
" diff = ref_utm - qry_utm # shape (N, 2)\n",
|
| 234 |
+
" dist = np.linalg.norm(diff) # shape (N,)\n",
|
| 235 |
+
" dists.append(dist)\n",
|
| 236 |
+
" if dist < dist_tolerance:\n",
|
| 237 |
+
" in_tol.append(1)\n",
|
| 238 |
+
" else:\n",
|
| 239 |
+
" in_tol.append(0)\n",
|
| 240 |
+
"\n",
|
| 241 |
+
" diffs = ref_utms - qry_utm # shape (N, 2)\n",
|
| 242 |
+
" qry_dists = np.linalg.norm(diffs, axis=1) # shape (N,)\n",
|
| 243 |
+
" if qry_dists.min() > dist_tolerance:\n",
|
| 244 |
+
" # continue\n",
|
| 245 |
+
" valid_qry.append(0)\n",
|
| 246 |
+
" else:\n",
|
| 247 |
+
" valid_qry.append(1)\n",
|
| 248 |
+
" plot_utms.append(qry_utm)\n",
|
| 249 |
+
" if in_tol[-1]:\n",
|
| 250 |
+
" colors.append('g')\n",
|
| 251 |
+
" else:\n",
|
| 252 |
+
" colors.append('r')\n",
|
| 253 |
+
"\n",
|
| 254 |
+
"\n",
|
| 255 |
+
" # if qry_idx%1000 == 0:\n",
|
| 256 |
+
"\n",
|
| 257 |
+
" # fig, ax = plt.subplots(1, 2, figsize=(9.5, 3))\n",
|
| 258 |
+
" # ax[0].clear()\n",
|
| 259 |
+
" # ax[1].clear()\n",
|
| 260 |
+
" # qry_image = ImageData(qry_image_filename, img_calib_file)\n",
|
| 261 |
+
"\n",
|
| 262 |
+
" # ax[0].imshow(qry_image.image[:, :, ::-1])\n",
|
| 263 |
+
" # ax[0].set_title(f\"{qry_image_timestamp}.png\")\n",
|
| 264 |
+
" # ax[0].axis(\"off\")\n",
|
| 265 |
+
"\n",
|
| 266 |
+
" # ref_img_timestamp = utils.get_corr_files(ref_timestamps[int(mInds[qry_idx])], [ref_image_dir,])\n",
|
| 267 |
+
"\n",
|
| 268 |
+
" # ref_image = ImageData(ref_img_timestamp, img_calib_file)\n",
|
| 269 |
+
" # ax[1].imshow(ref_image.image[:, :, ::-1]) #cv2.flip(,1)\n",
|
| 270 |
+
" # ax[1].set_title(f\"{ref_img_timestamp.split('/')[-1]}\\nDist={dist:.2f}m\")\n",
|
| 271 |
+
"\n",
|
| 272 |
+
" # qry_image = ImageData(qry_image_filename, img_calib_file)\n",
|
| 273 |
+
"\n",
|
| 274 |
+
" # fig, ax = plt.subplots(1, 2, figsize=(19.4, 6))\n",
|
| 275 |
+
" # ax[0].clear()\n",
|
| 276 |
+
" # ax[1].clear()\n",
|
| 277 |
+
"\n",
|
| 278 |
+
" # ax[0].imshow(qry_image.image[:, :, ::-1])\n",
|
| 279 |
+
" # ax[0].set_title(f\"{qry_image_timestamp}.png\")\n",
|
| 280 |
+
" # ax[0].axis(\"off\")\n",
|
| 281 |
+
"\n",
|
| 282 |
+
" # # Show matching reference image\n",
|
| 283 |
+
" # # ref_img_timestamp = utils.get_corr_files(ref_timestamps[int(mInds[qry_idx])], [ref_image_dir,])\n",
|
| 284 |
+
" # ref_image = ImageData(f\"{ref_image_dir}/{ref_timestamps[int(mInds[qry_idx])]}.png\", img_calib_file)\n",
|
| 285 |
+
" # ax[1].imshow(ref_image.image[:, :, ::-1])\n",
|
| 286 |
+
" # ax[1].set_title(f\"{ref_timestamps[int(mInds[qry_idx])]}\\nDist={dist:.2f}m\")\n",
|
| 287 |
+
"\n",
|
| 288 |
+
" # ax[1].axis(\"off\")\n",
|
| 289 |
+
" # fig.canvas.draw()\n",
|
| 290 |
+
"\n",
|
| 291 |
+
"# print(f\"Recall: {np.sum(np.array(in_tol))/len(qry_timestamps):.02%}\")\n",
|
| 292 |
+
"print(f\"Recall: {np.sum(np.array(in_tol))/np.sum(valid_qry):.02%}\")\n",
|
| 293 |
+
"print(f\"Valid queries: {np.sum(valid_qry)}\")\n",
|
| 294 |
+
"# plt.figure()\n",
|
| 295 |
+
"# plt.plot(np.clip(dists, 0, 30))\n",
|
| 296 |
+
"# plt.ylim((0,35))"
|
| 297 |
+
]
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"cell_type": "code",
|
| 301 |
+
"execution_count": 33,
|
| 302 |
+
"metadata": {},
|
| 303 |
+
"outputs": [
|
| 304 |
+
{
|
| 305 |
+
"data": {
|
| 306 |
+
"text/plain": [
|
| 307 |
+
"<matplotlib.collections.PathCollection at 0x120f23ef5e0>"
|
| 308 |
+
]
|
| 309 |
+
},
|
| 310 |
+
"execution_count": 33,
|
| 311 |
+
"metadata": {},
|
| 312 |
+
"output_type": "execute_result"
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"data": {
|
| 316 |
+
"image/png": "iVBORw0KGgoAAAANSUhEUgAAB9cAAAM8CAYAAADtLSDqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAADK5klEQVR4nOzdeZjd89k/8PeZmeybJAhRkcQaeyR5rKm1idjV3taW2kqrqit9+qtuD1VVVUUpIbSoWopqiSWqxBJi36KWRBYEySAkmTnn98epGcckSGRmsrxe13Wumc/nc3+/c89zuZ5L3XPfn0KpVCoFAAAAAAAAAFigqtZOAAAAAAAAAACWdIrrAAAAAAAAAPAJFNcBAAAAAAAA4BMorgMAAAAAAADAJ1BcBwAAAAAAAIBPoLgOAAAAAAAAAJ9AcR0AAAAAAAAAPoHiOgAAAAAAAAB8AsV1AAAAAAAAAPgEiusAAAAAAAAA8AmWy+L6v/71r+y+++7p3bt3CoVCrr/++oV+R6lUyhlnnJF11lkn7dq1y+qrr57/+7//W/zJAgAAAAAAANDqalo7gdbw7rvvZpNNNsnhhx+effbZZ5He8c1vfjO33nprzjjjjGy00UaZNWtWZsyYsZgzBQAAAAAAAGBJUCiVSqXWTqI1FQqFXHfdddlrr70a9ubOnZv//d//zZ/+9KfMnDkzG264YX75y19mu+22S5I8/fTT2XjjjfPEE09k3XXXbZ3EAQAAAAAAAGgxy+VY+E9y+OGH55577smVV16Zxx57LPvtt1923nnnTJw4MUly4403pn///rnpppvSr1+/9O3bN0cccUTefPPNVs4cAAAAAAAAgOaguP4R//nPf3LFFVfk6quvztChQ7PmmmvmO9/5TrbZZpuMGjUqSfLCCy/k5ZdfztVXX53Ro0fnkksuyUMPPZR99923lbMHAAAAAAAAoDksl3euf5yHH344pVIp66yzTsX+nDlz0rNnzyRJsVjMnDlzMnr06Ia4iy66KIMGDcqzzz5rVDwAAAAAAADAMkZx/SOKxWKqq6vz0EMPpbq6uuKsc+fOSZJVV101NTU1FQX4AQMGJEkmTZqkuA4AAAAAAACwjFFc/4iBAwemvr4+r732WoYOHTrfmK233jp1dXX5z3/+kzXXXDNJ8txzzyVJ1lhjjRbLFQAAAAAAAICWUSiVSqXWTqKlvfPOO3n++eeTlIvpZ555Zrbffvv06NEjffr0yVe+8pXcc889+fWvf52BAwdmxowZueOOO7LRRhtll112SbFYzJAhQ9K5c+ecddZZKRaLOe6449K1a9fceuutrfzbAQAAAAAAALC4LZfF9bFjx2b77bdvsn/ooYfmkksuybx58/Lzn/88o0ePzpQpU9KzZ89sueWW+clPfpKNNtooSTJ16tR84xvfyK233ppOnTplxIgR+fWvf50ePXq09K8DAAAAAAAAQDNbLovrAAAAAAAAALAwqlo7AQAAAAAAAABY0tW0dgItqVgsZurUqenSpUsKhUJrpwMAAAAAAABAKyqVSnn77bfTu3fvVFV9fG/6clVcnzp1alZfffXWTgMAAAAAAACAJcjkyZPzuc997mNjlqviepcuXZKU/w/TtWvXVs4GAAAAAAAAgNZUW1ub1VdfvaGW/HGWq+L6B6Pgu3btqrgOAAAAAAAAQJJ8qmvFP35oPAAAAAAAAACguA4AAAAAAAAAn0RxHQAAAAAAAAA+geI6AAAAAAAAAHwCxXUAAAAAAAAA+ASK6wAAAAAAAADwCRTXAQAAAAAAAOATKK4DAAAAAAAAwCdQXAcAAAAAAACAT7BQxfW+ffumUCg0+Rx33HELfOauu+7KoEGD0r59+/Tv3z/nn39+xfm1116bwYMHZ4UVVkinTp2y6aab5rLLLmvynnPPPTf9+vVL+/btM2jQoNx9990LkzoAAAAAAAAALLKFKq4/+OCDmTZtWsNnzJgxSZL99ttvvvEvvvhidtlllwwdOjQTJkzIySefnOOPPz7XXHNNQ0yPHj3ywx/+MOPGjctjjz2Www8/PIcffnhuueWWhpirrroqJ5xwQn74wx9mwoQJGTp0aEaMGJFJkyYtyu8MAAAAAAAAAAulUCqVSov68AknnJCbbropEydOTKFQaHL+/e9/PzfccEOefvrphr1jjjkmjz76aMaNG7fA92622WbZdddd87Of/SxJsvnmm2ezzTbLeeed1xAzYMCA7LXXXjn11FM/db61tbXp1q1bZs2ala5du37q5wAAAAAAAABY9ixMDXmR71yfO3duLr/88owcOXK+hfUkGTduXIYNG1axN3z48IwfPz7z5s1rEl8qlXL77bfn2Wefzec///mGn/PQQw81ec+wYcNy7733fmyOc+bMSW1tbcUHAAAA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dGgmTJiQk08+Occff3yuueaahpixY8fmoIMOyp133plx48alT58+GTZsWKZMabxj9KqrrsoJJ5yQH/7wh5kwYUKGDh2aESNGZNKkSYvyOwMALHG27rN1nj/++cz70bw8fPTDWXfFdZMkD097uGIMfH2pPuOnjm+tNAEAAAAAlluFUqlUWtSHTzjhhNx0002ZOHFiCoVCk/Pvf//7ueGGG/L000837B1zzDF59NFHM27cuPm+s76+Pt27d88555yTQw45JEmy+eabZ7PNNst5553XEDdgwIDstddeOfXUUz91vrW1tenWrVtmzZqVrl27furnAABay8//9fP8eOyPUywVk5RHxg9ZbUjGfXVcrn362vz0rp/m7TlvZ/8N9s9Pt/9p2lS3aeWMAQAAAACWHgtTQ17kO9fnzp2byy+/PCNHjpxvYT1Jxo0bl2HDhlXsDR8+POPHj8+8efPm+8zs2bMzb9689OjRo+HnPPTQQ03eM2zYsNx7770fm+OcOXNSW1tb8QEAWJp8a4tvZdCqgxrWXdt1zfm7np+xL43Nvn/ZN4+9+lhemPlCfnnPL/PDO37YipkCAAAAACzbFrm4fv3112fmzJk57LDDFhgzffr09OrVq2KvV69eqaury4wZM+b7zA9+8IOsttpq2WmnnZIkM2bMSH19/XzfM3369I/N8dRTT023bt0aPquvvvqn+M0AAJYcndp2yr9H/jv/+PI/cs3+1+TZrz+bTVbZJFc/eXWqq6pTSnkIUSmlXP7Y5UmSybMm58onrswtz9+SumJda6YPAAAAALDMqFnUBy+66KKMGDEivXv3/ti4j3a1fzCFfn7d7qeffnquuOKKjB07Nu3bt//E9yyoY/4DJ510Uk488cSGdW1trQI7ALDUaVvdNjuvtXPFXvua9k3i2te0z79e/ld2vnznvFf3XpJk+77b559f+WfaVrdtkVwBAAAAAJZVi9S5/vLLL+e2227LEUcc8bFxq6yySpPu8tdeey01NTXp2bNnxf4ZZ5yR//u//8utt96ajTfeuGF/xRVXTHV19Xzf89Fu9o9q165dunbtWvEBAFgWHD346LSrbpeaqppUF6qTJCcPPTkj/zYyc+rnNMSNfWlsLnr4otQV6zJ+6viMnzr+E7vZn3su+c53ku22SwYOLH/9znfK+wAAAAAAy6tF6lwfNWpUVl555ey6664fG7flllvmxhtvrNi79dZbM3jw4LRp06Zh71e/+lV+/vOf55ZbbsngwYMr4tu2bZtBgwZlzJgx2XvvvRv2x4wZkz333HNR0gcAWOqt03OdjD9qfH53/+/yzrx3svd6e2ev9fbKsX8/NsVSsSGupqomz7zxTDa/cPM8PP3hJMmgVQdlzMFj0r1D94p3PvpocuKJyR13JNXVSX1949m//538+tfJjjuWv26ySYv8mgAAAAAAS4xC6YM57Z9SsVhMv379ctBBB+W0006rODvppJMyZcqUjB49Okny4osvZsMNN8zRRx+dI488MuPGjcsxxxyTK664Ivvss0+S8ij4H/3oR/nzn/+crbfeuuFdnTt3TufOnZMkV111VQ4++OCcf/752XLLLXPBBRfkwgsvzJNPPpk11ljjU+deW1ubbt26ZdasWbrYAYBl0hZ/3CIPTX0odaXG7vSd+u+UO1+8M/WlcrW8ulCdYwYfk98M/00enlYuuM98alD23qsmc+dWFtU/qro6ads2ufHGcqEdAAAAAGBptjA15IXuXL/tttsyadKkjBw5ssnZtGnTMmnSpIZ1v379cvPNN+db3/pWfv/736d37945++yzGwrrSXLuuedm7ty52XfffSve9eMf/zinnHJKkuSAAw7IG2+8kZ/+9KeZNm1aNtxww9x8880LVVgHAFge/HmfP2f45cPz/JvPJ0m+s+V3cs+kexoK60lSX6rPw9MezpALh+TRVx9Npm+cwkX3JXXVKZUKH/v++vpkzpxk992TceN0sAMAAAAAy4+F7lxfmulcBwCWB3XFurw88+V0a98tK3ZcMUfdeFQunnBxRef6uj3XzbNvPFveu3RM8tJ2SenT/91ldXX5Lvbbbmue3wEAAAAAoCUsTA25qoVyAgCghdRU1WTNHmtmxY4rJkn+b8f/y3orrtdwvv5K66dLuy7lwvqMtZMXd1qownpS7mC//fZk4sTFmjoAAAAAwBJLcR0AYBm3YscV8/DRD+ffh/87/z783xl/1PgMXGVgqgvVyUNHJYW6T37JfFRXJ3/4w2JOFgAAAABgCbXQd64DALD0aVvdNlv32bph/Ysdf5F7X7k3j00dvNBd6x+or08eemhxZQgAAAAAsGTTuQ4AsBzq0aFHxh85Put0HvKZ3jNzZqlxceONyde+lpx0UjJlymfMEAAAAABgyaJz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mzbJpZeW71qfOTO57rqkQ4ckSalUyv+78/8lSeqKdakv1Wfq21Nz+WOXt2LCAAAAfByd6wAAAADNqUuXJlullPJ+3fsVe4UU8vbct1sqKwAAABbSQnWu9+3bN4VCocnnuOOOW+Azd911VwYNGpT27dunf//+Of/88yvOn3zyyeyzzz4N7z7rrLPm+55zzz03/fr1S/v27TNo0KDcfffdC5M6AAAAwBKjqlCV3dfZPdWF6iTlwnqS7Lr2ro1BU6YkX/pSMmhQcsQRyZtvtkaqAAAA/NdCFdcffPDBTJs2reEzZsyYJMl+++033/gXX3wxu+yyS4YOHZoJEybk5JNPzvHHH59rrrmmIWb27Nnp379/TjvttKyyyirzfc9VV12VE044IT/84Q8zYcKEDB06NCNGjMikSZMWJn0AAACAJcale1+aAzc8MD079Mw6PdfJ9Qden4GrDiwfvvtuMnRo8pe/JA8/nFxySbLzzkl9favmDAAAsDwrlEql0qI+fMIJJ+Smm27KxIkTUygUmpx///vfzw033JCnn366Ye+YY47Jo48+mnHjxjWJ79u3b0444YSccMIJFfubb755Nttss5x33nkNewMGDMhee+2VU0899VPnW1tbm27dumXWrFnp2rXrp34OAAAAoEXdemsyfHjT/SeeSDbYoOXzAQAAWEYtTA15oTrXP2zu3Lm5/PLLM3LkyPkW1pNk3LhxGTZsWMXe8OHDM378+MybN+9T/5yHHnqoyXuGDRuWe++992OfnTNnTmprays+AAAAAEu86ur571d95D/l1NcndXXNnw8AAACLXly//vrrM3PmzBx22GELjJk+fXp69epVsderV6/U1dVlxowZn+rnzJgxI/X19fN9z/Tp0z/22VNPPfX/t3fnYVpX5f/A388sDIvMiCgggogLKm6Z5oJbqWnmrpWpoWVftzRT81tRWlaWWmq2qbmR5i+xb+6ZC5Zr4q6I+4oL4oYwgwswy+f3x+QzjqCAwswAr9d1PZdzzrmfz9wPXR1x7rnPSV1dXfk1ePDgefqeAAAAAJ1qs82S4cPbiuyVlclWWyWrr946bmpKDj886d699fXNbyazZnVevgAAAEuAj11cP++887LDDjtk4MCBHxn3wa72906h/7Bu9/l5ztyeMWrUqNTX15dfL7744nx9TwAAAIBO0b17cuutySGHJNttl3z3u8k117R1rp9ySnLGGa1F9ubmZPTo5IQTOjdnAACAxVzVx3nT888/nxtvvDGXXXbZR8YNGDBgtu7y1157LVVVVenbt+88fa9ll102lZWVc3zOB7vZP6impiY1NTXz9H0AAAAAupS+fZM//GHOa//6V/LfBoYkrV/fcEPys591TG4AAABLoI/VuT569Oj069cvO+6440fGbbrpphk7dmy7uRtuuCEbbrhhqqur5+l7devWLRtssMFszxk7dmxGjBgxf4kDAAAALA76929/L3tlZTJgQPuYmTOTCRMSJ/kBAAAsEPNdXG9pacno0aOz//77p6qqfeP7qFGjst9++5XHhxxySJ5//vkcffTReeyxx3L++efnvPPOyzHHHFOOmTVrVh588ME8+OCDmTVrViZNmpQHH3wwTz/9dDnm6KOPzrnnnpvzzz8/jz32WI466qi88MILOeSQQz7OZwYAAABYtP34x0nv3q3HxFdUJD16JD//edv6448nq66arLtusuKKybe/3b7THQAAgPlWKor5+y+rG264Idtvv32eeOKJDBs2rN3a17/+9UycODE333xzee6WW27JUUcdlUceeSQDBw7M97///XZF8YkTJ2bo0KGzfZ+tttqq3XPOOOOM/OpXv8rkyZOz9tpr5ze/+U223HLL+Uk9DQ0NqaurS319fWpra+frvQAAAABdyqRJyf/9X9LSkuy5ZzJkSNva+uu3dq03N7fNXXJJ8pWvdHyeAAAAXdj81JDnu7i+KFNcBwAAAJYIVVXtC+vV1cn//m/yi190Xk4AAABd0PzUkD/WnesAAAAAdGErrdR6XPx7mpqSlVduHzNhQjJmTHL//R2aGgAAwKJKcR0AAABgcXPBBUnPnm3jHXdM9t+/bfyb37Tex7733skGGyQ/+1nH5wgAALCIcSw8AAAAwOLo1VeTu+5K+vRJNtusrZN90qRkxRVb72p/v8ceS9ZYo+PzBAAA6ETzU0Ou6qCcAAAAAOhI/fsnu+wy+/wLL8xeWE+S559XXAcAAPgIiusAAAAAS5Jhw5IePZIZM5L3DjSsrk6GD2+Lefvt1qPl33gj+dznki226JxcAQAAuhB3rgMAAAAsSfr2Tf72t9YCe5LU1CQXXZQMHtw6fvvtZNNNk8MPT37+82TLLZPzzuu8fAEAALoId64DAAAALInefrv1iPhBg5Le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zfW/mT3W3KMt4C9/SdZfPznooGSnnZI991RgBwCgQ32iO9ePPPLI/OMf/8hTTz2VUqk02/r3v//9XHXVVXnssbbjmw455JCMHz8+48aNS5LstddeaWhoyLXXXluO+cIXvpA+ffrk4osvTtLauX7FFVfkwQcfnK/8Zs6cmZkzZ5bHDQ0NGTx4sDvXAQAAAKALmvru1Ix/dXyW7bls1lpurbafOc6aldTVJTNmtH/DFVcku+7a4XkCALD4mJ871+erc/39Zs2alYsuuigHHHDAHAvrSWtX+nbbbddubvvtt8+9996bxsbGj4y544472s099dRTGThwYIYOHZqvfvWrefbZZ+ea44knnpi6urrya/DgwfPzEQEAAACADtSnR598dqXPZu1+a7f/mWN9/eyF9VIpmTRptmfMaJqRlkJHOwAAC97HLq5fccUVmTZtWr7+9a9/aMwrr7yS/v37t5vr379/mpqa8sYbb3xkzCuvvFIeb7zxxrnwwgtz/fXX55xzzskrr7ySESNGZMqUKR+Z46hRo1JfX19+vfjii/P5KQEAAACATte3bzJkSFJZ2X5+o43KX06ePjkjzhuRHr/okd4n9s6f7v1TBycJAMDi7mMX188777zssMMOGThw4EfGfbCr/b1T6N8/P6eY98/tsMMO2XPPPbPOOutk2223zTXXXJMkueCCCz7ye9fU1KS2trbdCwAAAABYxFRUJP/4R7LCCq3jbt2Ss85KNtywHPLVS7+ae16+J0nyTuM7OeSaQ3LLxFs6I1sAABZTVR/nTc8//3xuvPHGXHbZZR8ZN2DAgHYd6Eny2muvpaqqKn379v3ImA92s79fr169ss466+Spp576OOkDAAAAAIuatddOnn02efXVpE+fpEeP8lJL0ZLbX7i93XHwVRVVuXnizdlqpa06I1sAABZDH6tzffTo0enXr1923HHHj4zbdNNNM3bs2HZzN9xwQzbccMNUV1d/ZMyIESM+9LkzZ87MY489luWXX/7jpA8AAAAALIoqK5OBA9sV1pOkolSRpbsv3W6uuaU5y/VarjxuamnKMTcck2V/tWyWP3X5/Gbcb8qnbAIAwLyY7+J6S0tLRo8enf333z9VVe0b30eNGpX99tuvPD7kkEPy/PPP5+ijj85jjz2W888/P+edd16OOeaYcsx3vvOd3HDDDTn55JPz+OOP5+ST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kSMzfvz4jBw5Ml/5yldy1113lWMuueSSHHnkkfnRj36UBx54IFtssUV22GGHvPDCCx/nMwMAAAAAAAtbnz7JT3+anH12st9+SanUbvmdxndSpO3o+KIo8k7jO7M/p74+ufPOZOLEhZwwAHy0UlF88NKTeXfkkUfmH//4R5566qmUPvAvxST5/ve/n6uuuiqPPfZYee6QQw7J+PHjM27cuCTJXnvtlYaGhlx77bXlmC984Qvp06dPLr744iTJxhtvnE9/+tM588wzyzFrrrlmdtttt5x44onznG9DQ0Pq6upSX1+f2tra+f68AAAAAADAgnHcv4/LL277RbsC+9V7X52dhu3UFnT77clOO7UW2JPWTvgTTujgTAFYnM1PDflj37k+a9asXHTRRTnggAPmWFhPWrvSt9tuu3Zz22+/fe699940NjZ+ZMwdd9xR/j733XffbDHbbbddOebDzJw5Mw0NDe1eAAAAAABA5/vJZ3+SH27xwwypG5LV+66eC3a7oH1hvaWl9e726dPb5n7xi+Tmmzs8VwBIPkFx/Yorrsi0adPy9a9//UNjXnnllfTv37/dXP/+/dPU1JQ33njjI2NeeeWVJMkbb7yR5ubmj4z5MCeeeGLq6urKr8GDB8/rxwMAAAAAABaiqoqqnLD1CZl45MQ8fvjj2W+9/doHvPlm8vrrrUX295RKyYQJHZsoAPzXxy6un3feedlhhx0ycODAj4z7YFf7e6fQv39+TjEfnJuXmA8aNWpU6uvry68XX3zxI+MBAAAAAIAuok+fZOml29/VXhTJsGHt4yZMSEaMSPr3T3bYIXnppQ5NE4AlR9XHedPzzz+fG2+8MZdddtlHxg0YMGC27vLXXnstVVVV6du370fGvNepvuyyy6aysvIjYz5MTU1Nampq5ukzAQAAAAAAXUhlZTJmTLL77sm777bOHX548v5rZKdMST73uWTatKS5ObnxxuTzn28tuFd9rBIIAHyoj9W5Pnr06PTr1y877rjjR8ZtuummGTt2bLu5G264IRtuuGGqq6s/MmbEiBFJkm7dumWDDTaYLWbs2LHlGAAAAAAAYDG0/fbJs88m112XjB+f/P737TvZ77ijtcDe3Nw6bmpKHn88efLJzskXgMXafP/aVktLS0aPHp39998/VR/4ra9Ro0Zl0qRJufDCC5MkhxxySP7whz/k6KOPzoEHHphx48blvPPOy8UXX1x+z3e+851sueWWOfnkk7PrrrvmyiuvzI033pjbb7+9HHP00Udn5MiR2XDDDbPpppvm7LPPzgsvvJBDDjnk435uAAAAAABgUTBgQOtrTnr2nKf5N955I2ffd3amzZiW7VfZPtusvM0CThKAJcF8F9dvvPHGvPDCCznggANmW5s8eXJeeOGF8njo0KH55z//maOOOip//OMfM3DgwPzud7/LnnvuWY4ZMWJExowZk2OPPTbHHXdcVllllVxyySXZeOONyzF77bVXpkyZkp/97GeZPHly1l577fzzn//MkCFD5jd9AAAAAABgcbHllsmmmyZ33ZVUVLR2sH/1q8lKK5VDprwzJZ/+06fz8vSXUyqV8us7fp1zdz433/z0NzsvbwAWSaWiKIrOTqKjNDQ0pK6uLvX19amtre3sdAAAAAAAgE/qnXeS009vPT7+U59KDj209b72//rVf36VUf8alZaipTy3bM9l8/r/vt7xuQLQ5cxPDXm+O9cBAAAAAAC6jJ49kx/+8EOX62fUp6JU0a64/tast2YPbGpKJkxo/XrdddsV6AEgSSo6OwEAAAAAAICFZYfVdkhzS3N5XFmqzI6r7dg+aOrUZJNNkk9/uvU1YkRSX9/BmQLQ1SmuAwAAAAAAi63NV9w8F+1xUVbovUJ6VffKHmvukfN2Oa990A9/mDz4YNv4vvuSY4/t0DwB6PocCw8AAAAAACzW9llnn+yzzj4fHjB+fNLc1t2e5ubkoYcWfmIALFJ0rgMAAAAAAEu24cPb37FeWdk69z4zm2bmqOuOypDfDMm6Z66bKx+/soOTBKCzlYqiKDo7iY7S0NCQurq61NfXp7a2trPTAQAAAAAAuoLXX0+22ip57LHW8VprJbfckvTtWw751jXfyp/u+1NaipaUUkqS3PaN27LZipt1RsYALCDzU0N2LDwAAAAAALBkW2655IEHkrvuah1vvHFSU9MuZMzDY9JStCRJihSpqqjKZY9dprgOsARRXAcAAAAAAKipSbbc8kOXe1T1yNRMLY+LokiP6h7tg1pakj//OZkwIVltteTAA5Pq6oWUMAAdTXEdAAAAAABgLo7d8th865/fSlWpKiklvap75cBPH9gWUBTJN76RXHhha0G9qSm55prk6quTiorOSxyABcad6wAAAAAAAPPg8scuz9VP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M0lSpMhfJ/w1jx/+eIb1HdbJ2QFdiV+zAQAAAAAAYIn13NTnMqNpRru5IkUeff3RTsoI6KoU1wEAAAAAAFhirVi3YqorqmebX22Z1TohG6ArU1wHAAAAAABgidW3uVvO7bN/Kt9XNjtxmxOzVr+1OjEroCua7+L6pEmT8rWvfS19+/ZNz54986lPfSr33XffR77nj3/8Y9Zcc8306NEjq6++ei688MJ2642NjfnZz36WVVZZJd27d896662X6667rl3M8ccfn1Kp1O41YMCA+U0fAAAAAAAAWk2Zkqy/fvb79rl55vSWXHtJdR7f+KL8YPMfdHZmQBdUNT/BU6dOzWabbZbPfe5zufbaa9OvX78888wzWXrppT/0PWeeeWZGjRqVc845J5/5zGdy991358ADD0yfPn2y8847J0mOPfbYXHTRRTnnnHOyxhpr5Prrr8/uu++eO+64I+uvv375WWuttVZuvPHG8riysnI+Py4AAAAAAAD8129+k0ycmCQZMi0Z0tCc/PDU5Av7dmpaQNc0X8X1k08+OYMHD87o0aPLcyuttNJHvucvf/lLDj744Oy1115JkpVXXjl33nlnTj755HJx/S9/+Ut+9KMf5Ytf/GKS5NBDD83111+fU089NRdddFFbslVV89WtPnPmzMycObM8bmhomOf3AgAAAAAAsJh75ZWkVGobt7Qkkyd3Xj5AlzZfx8JfddVV2XDDDfPlL385/fr1y/rrr59zzjnnI98zc+bMdO/evd1cjx49cvfdd6exsfEjY26//fZ2c0899VQGDhyYoUOH5qtf/WqeffbZj/zeJ554Yurq6sqvwYMHz+tHBQAAAAAAYHG3xRZJU1PbuLIy2WqrzssH6NLmq7j+7LPP5swzz8xqq62W66+/PoccckiOOOKI2e5Qf7/tt98+5557bu67774URZF77703559/fhobG/PGG2+UY0477bQ89dRTaWlpydixY3PllVdm8vt+M2jjjTfOhRdemOuvvz7nnHNOXnnllYwYMSJTpkz50O89atSo1NfXl18vvvji/HxcAAAAAAAAFmf77Zf84AdJxX9LZquumuy2W2sHO8AHlIqiKOY1uFu3btlwww1zxx13lOeOOOKI3HPPPRk3btwc3/Puu+/msMMOy1/+8pcURZH+/fvna1/7Wn71q1/l1VdfTb9+/fL666/nwAMPzNVXX51SqZRVVlkl2267bUaPHp133nlnjs99++23s8oqq+R73/tejj766HnKv6GhIXV1damvr09tbe28fmwAAAAAAAAWZ2edlRx6aNt4jz2S//u/tqI7sNianxryfO0Iyy+/fIYPH95ubs0118wLL7zwoe/p0aNHzj///LzzzjuZOHFiXnjhhay00krp3bt3ll122STJcsstlyuuuCJvv/12nn/++Tz++ONZaqmlMnTo0A99bq9evbLOOuvkqaeemp+PAAAAAAAAAG1mzEi+8532c5ddllx7befkA3RZ81Vc32yzzfLEE0+0m3vyySczZMiQub63uro6gwYNSmVlZcaMGZOddtopFR/4bZ/u3btnhRVWSFNTUy699NLsuuuuH/q8mTNn5rHHHsvyyy8/Px8BAAAAAAAA2kydmsyaNfv8pEkdnwvQpVXNT/BRRx2VESNG5Je//GW+8pWv5O67787ZZ5+ds88+uxwzatSoTJo0qXwP+5NPPpm77747G2+8caZOnZrTTjstDz/8cC644ILye+66665MmjQpn/rUpzJp0qQcf/zxaWlpyfe+971yzDHHHJOdd945K664Yl577bWccMIJaWhoyP777/9J/wwAAAAAAABYUvXvnwwenLz8ctLc3DpXKiUbbdS5eQFdznx1rn/mM5/J5Zdfnosvvjhrr712fv7zn+f000/PvvvuW46ZPHlyu2Pim5ubc+qpp2a99dbL5z//+cyYMSN33HFHVlpppXLMjBkzcuyxx2b48OHZfffds8IKK+T222/P0ksvXY556aWXsvfee2f11VfPHnvskW7duuXOO++cp655AAAAAAAAmKOKiuSaa5IVVmgdd+uWnHde8sYbyRZbJOuskxx/fNLU1KlpAp2vVBRF0dlJdJT5uYweAAAAAACAJUhLS/Laa0mfPsmECckmmyRF0TpfKiXf/W7y6193dpbAAjY/NeT56lwHAAAAAACAxVJFRTJgQFJTk1xySWtBvaWlda0oktGjOzc/oNPN153rAAAAAAAAsNirmkMJ7X1zb896O3dNuivdKrtl4xU2TnVldQcmB3QWnesAAAAAAADwfgcc0Hr3emVlawd7khxzTJJk4rSJGX7G8Gxz4TbZYvQW2WL0Fnlr1ludmCzQURTXAQAAAAAA4P1WWy25++5kv/2SPfZILrig9c71JEdce0Renv5y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3Pb6yy67LNtvv32WXXbZlEqlPPjgg7M9w14P0HV91D7f2NiY73//+1lnnXXSq1evDBw4MPvtt19efvnlds+wzwMsmhTXFzG33HJLDjvssNx5550ZO3Zsmpqast122+Xtt98uxxx11FG57rrrctFFF+Wxxx7LUUcdlW9/+9u58soryzFHHnlkLr/88owZMya333573nrrrey0005pbm4ux+yzzz558MEHc9111+W6667Lgw8+mJEjR5bXm5ubs+OOO+btt9/O7bffnjFjxuTSSy/Nd7/73XJMQ0NDPv/5z2fgwIG555578vvf/z6nnHJKTjvttIX8JwWwaJrbPl8URXbbbbc8++yzufLKK/PAAw9kyJAh2Xbbbdv9u8A+D9B1DRo0KCeddFLuvffe3Hvvvdl6662z6667lgvov/rVr3LaaaflD3/4Q+65554MGDAgn//85zN9+vTyM+zzAF3b3Pb6t99+O5tttllOOumkD32GvR6g6/qoff6dd97J/fffn+OOOy73339/Lrvssjz55JPZZZdd2j3DPg+wiCpYpL322mtFkuKWW24pz6211lrFz372s3Zxn/70p4tjjz22KIqimDZtWlFdXV2MGTOmvD5p0qSioqKiuO6664qiKIpHH320SFLceeed5Zhx48YVSYrHH3+8KIqi+Oc//1lUVFQUkyZNKsdcfPHFRU1NTVFfX18URVGcccYZRV1dXTFjxoxyzIknnlgMHDiwaGlpWVB/DACLrQ/u80888USRpHj44YfLMU1NTcUyyyxTnHPOOUVR2OcBFkV9+vQpzj333KKlpaUYMGBAcdJJJ5XXZsyYUdTV1RVnnXVWURT2eYBF1Xt7/fs999xzRZLigQceaDdvrwdY9Mxpn3/P3XffXSQpnn/++aIo7PMAizKd64u4+vr6JMkyyyxTntt8881z1VVXZdKkSSmKIjfddFOefPLJbL/99kmS++67L42Njdluu+3K7xk4cGDWXnvt3HHHHUmScePGpa6uLhtvvHE5ZpNNNkldXV27mLXXXjsDBw4sx2y//faZOXNm7rvvvnLMVlttlZqamnYxL7/8ciZOnLiA/zQAFj8f3OdnzpyZJOnevXs5prKyMt26dcvtt9+exD4PsChpbm7OmDFj8vbbb2fTTTfNc889l1deeaXdHl5TU5OtttqqvD/b5wEWLR/c6+eFvR5g0TEv+3x9fX1KpVKWXnrpJPZ5gEWZ4voirCiKHH300dl8882z9tprl+d/97vfZfjw4Rk0aFC6deuWL3zhCznjjDOy+eabJ0leeeWVdOvWLX369Gn3vP79++eVV14px/Tr12+279mvX792Mf3792+33qdPn3Tr1u0jY94bvxcDwJzNaZ9fY401MmTIkIwaNSpTp07NrFmzctJJJ+WVV17J5MmTk9jnARYFEyZMyFJLLZWampoccsghufzyyzN8+PDy3jmnvfX9e699HqDr+7C9fl7Y6wG6vnnd52fMmJEf/OAH2WeffVJbW5vEPg+wKKvq7AT4+A4//PA89NBD5U7F9/zud7/LnXfemauuuipDhgzJrbfemm9961tZfvnls+22237o84qiSKlUKo/f//WCjCmK4kPfC0CbOe3z1dXVufTSS/PNb34zyyyzTCorK7Pttttmhx12mOvz7PMAXcfqq6+eBx98MNOmTcull16a/fffP7fcckt5fU5769z2Vfs8QNfyYXv9vBbY58ReD9B1zMs+39jYmK9+9atpaWnJGWecMddn2ucBuj6d64uob3/727nqqqty0003ZdCgQeX5d999Nz/84Q9z2mmnZeedd866666bww8/PHvttVdOOeWUJMmAAQMya9asTJ06td0zX3vttfJvrA0YMCCvvvrqbN/39ddfbxfzwd9smzp1ahobGz8y5rXXXksyezcOAG0+bJ9Pkg022KD8H2+TJ0/OddddlylTpmTo0KFJ7PMAi4Ju3bpl1VVXzYYbbpgTTzwx6623Xn77299mwIABSWbvIPngHm6fB+j6Pmyvnxf2eoCub277fGNjY77yla/kueeey9ixY8td64l9HmBRpri+iCmKIocffnguu+yy/Pvf/y4XUt7T2NiYxsbGVFS0/5+2srIyLS0tSVqLMtXV1Rk7dmx5ffLkyXn44YczYsSIJMmmm26a+vr63H333eWYu+66K/X19e1iHn744fIxxElyww03pKamJhtssEE55tZbb82sWbPaxQwcODArrbTSAvgTAVi8zG2ff7+6urost9xyeeqpp3Lvvfdm1113TWKfB1gUFUWRmTNnZujQoRkwYEC7PXzWrFm55ZZbyvuzfR5g0fTeXj8v7PUAi5737/PvFdafeuqp3Hjjjenbt2+7WPs8wCKsYJFy6KGHFnV1dcXNN99cTJ48ufx65513yjFbbbVVsdZaaxU33XRT8eyzzxajR48uunfvXpxxxhnlmEMOOaQYNGhQceONNxb3339/sfXWWxfrrbde0dTUVI75whe+UKy77rrFuHHjinHjxhXrrLNOsdNOO5XXm5qairXXXrvYZpttivvvv7+48cYbi0GDBhWHH354OWbatGlF//79i7333ruYMGFCcdlllxW1tbXFKaecspD/pAAWTfOyz//tb38rbrrppuKZZ54prrjiimLIkCHFHnvs0e459nmArmvUqFHFrbfeWjz33HPFQw89VPzwhz8sKioqihtuuKEoiqI46aSTirq6uuKyyy4rJkyYUOy9997F8ssvXzQ0NJSfYZ8H6NrmttdPmTKleOCBB4prrrmmSFKMGTOmeOCBB4rJkyeXn2GvB+i6Pmqfb2xsLHbZZZdi0KBBxYMPPtju5zszZ84sP8M+D7BoUlxfxCSZ42v06NHlmMmTJxdf//rXi4EDBxbdu3cvVl999eLUU08tWlpayjHvvvtucfjhhxfLLLNM0aNHj2KnnXYqXnjhhXbfa8qUKcW+++5b9O7du+jdu3ex7777FlOnTm0X8/zzzxc77rhj0aNHj2KZZZYpDj/88GLGjBntYh566KFiiy22KGpqaooBAwYUxx9/fLtcAGgzL/v8b3/722LQoEFFdXV1seKKKxbHHntsu/84Kwr7PEBXdsABBxRDhgwpunXrViy33HLFNttsUy62FEVRtLS0FD/5yU+KAQMGFDU1NcWWW25ZTJgwod0z7PMAXdvc9vrRo0fP8e/9P/nJT8ox9nqAruuj9vnnnnvuQ3++c9NNN5WfYZ8HWDSViqIoOq5PHgAAAAAAAAAWPe5cBwAAAAAAAIC5UFwHAAAAAAAAgLlQXAcAAAAAAACAuVBcBwAAAAAAAIC5UFwHAAAAAAAAgLlQXAcAAAAAAACAuVBcBwAAAAAAAIC5UFwHAAAAAAAAgLlQXAcAAAAAAACAuVBcBwAAAAAAAIC5UFwHAAAAAAAAgLn4/4+scO2iJpNsAAAAAElFTkSuQmCC",
|
| 317 |
+
"text/plain": [
|
| 318 |
+
"<Figure size 2500x1000 with 1 Axes>"
|
| 319 |
+
]
|
| 320 |
+
},
|
| 321 |
+
"metadata": {},
|
| 322 |
+
"output_type": "display_data"
|
| 323 |
+
}
|
| 324 |
+
],
|
| 325 |
+
"source": [
|
| 326 |
+
"# import seaborn as sns\n",
|
| 327 |
+
"\n",
|
| 328 |
+
"# dist_data = np.clip(dists, 0, 50)\n",
|
| 329 |
+
"\n",
|
| 330 |
+
"# plt.figure(figsize=(38, 6))\n",
|
| 331 |
+
"# plt.scatter(np.arange(len(dist_data)), dist_data)\n",
|
| 332 |
+
"# plt.ylim((0,60))\n",
|
| 333 |
+
"\n",
|
| 334 |
+
"# fig, ax = plt.subplots(1,1) #figsize=(19, 6)\n",
|
| 335 |
+
"# # sns.histplot(np.clip(dists[::6], 0, 50), kde=True, bins=30)\n",
|
| 336 |
+
"# sns.kdeplot(dist_data, fill=True)\n",
|
| 337 |
+
"# # plt.plot(np.clip(dists[::6], 0, 50))\n",
|
| 338 |
+
"# # plt.ylim((0,60))\n",
|
| 339 |
+
"# plt.xlim(0, 50)\n",
|
| 340 |
+
"# plt.title('VPR Match Distance Distribution')\n",
|
| 341 |
+
"# plt.xticks([0, 10, 20, 30, 40, 50], labels=['0m', '10m', '20m', '30m', '40m', '>50m'])\n",
|
| 342 |
+
"# # print(np.sum(np.array(dists)>5000))\n",
|
| 343 |
+
"# plt.figure(figsize=(38, 3))\n",
|
| 344 |
+
"# plt.plot(valid_qry)\n",
|
| 345 |
+
"# warra = [278500, 7017500]\n",
|
| 346 |
+
"# macalister = [375000, 7007000]\n",
|
| 347 |
+
"# brigalow = [265360, 7046313]\n",
|
| 348 |
+
"dalby_utm = [326893.216, 6992729.692]\n",
|
| 349 |
+
"macalister_utm = [309287.466, 7007270.149]\n",
|
| 350 |
+
"warra_utm = [293685.547, 7019462.582]\n",
|
| 351 |
+
"brigalow_utm = [280382.449, 7028979.359]\n",
|
| 352 |
+
"\n",
|
| 353 |
+
"plt.figure(figsize=(25,10))\n",
|
| 354 |
+
"plt.scatter(np.array(plot_utms)[::100,0], np.array(plot_utms)[::100,1], marker='.', c=colors[::100])\n",
|
| 355 |
+
"plt.scatter(warra_utm[0], warra_utm[1], marker='o', s=120, c='b')\n",
|
| 356 |
+
"plt.scatter(macalister_utm[0], macalister_utm[1], marker='o', s=120, c='b')\n",
|
| 357 |
+
"plt.scatter(brigalow_utm[0], brigalow_utm[1], marker='o', s=120, c='b')\n",
|
| 358 |
+
"plt.scatter(dalby_utm[0], dalby_utm[1], marker='o', s=120, c='b')"
|
| 359 |
+
]
|
| 360 |
+
}
|
| 361 |
+
],
|
| 362 |
+
"metadata": {
|
| 363 |
+
"kernelspec": {
|
| 364 |
+
"display_name": "CARRSQ",
|
| 365 |
+
"language": "python",
|
| 366 |
+
"name": "python3"
|
| 367 |
+
},
|
| 368 |
+
"language_info": {
|
| 369 |
+
"codemirror_mode": {
|
| 370 |
+
"name": "ipython",
|
| 371 |
+
"version": 3
|
| 372 |
+
},
|
| 373 |
+
"file_extension": ".py",
|
| 374 |
+
"mimetype": "text/x-python",
|
| 375 |
+
"name": "python",
|
| 376 |
+
"nbconvert_exporter": "python",
|
| 377 |
+
"pygments_lexer": "ipython3",
|
| 378 |
+
"version": "3.10.15"
|
| 379 |
+
}
|
| 380 |
+
},
|
| 381 |
+
"nbformat": 4,
|
| 382 |
+
"nbformat_minor": 2
|
| 383 |
+
}
|
localisation/VPR_eval.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.image as mpimg
|
| 4 |
+
import sys
|
| 5 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
import open3d as o3d
|
| 9 |
+
from scipy.spatial.transform import Rotation
|
| 10 |
+
from utils.lidar import PointCloud
|
| 11 |
+
from utils.camera import ImageData
|
| 12 |
+
import utils.utils as utils
|
| 13 |
+
from natsort import natsorted, index_natsorted
|
| 14 |
+
import torch
|
| 15 |
+
from tqdm.notebook import tqdm
|
| 16 |
+
|
| 17 |
+
################## set device based on cuda availability #################
|
| 18 |
+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
| 19 |
+
|
| 20 |
+
print('CUDA availability: ' + str(torch.cuda.is_available()))
|
| 21 |
+
|
| 22 |
+
####################### Functions for matching using numpy on CPU or Pytorch on GPU ###################
|
| 23 |
+
def getMatchIndsCPU(ft_ref,ft_qry,topK=20,metric='cosine'):
|
| 24 |
+
"""
|
| 25 |
+
metric: 'euclidean' or 'cosine'
|
| 26 |
+
"""
|
| 27 |
+
# dMat = cdist(ft_ref,ft_qry,metric)
|
| 28 |
+
|
| 29 |
+
ft_qry_norm = ft_qry / np.linalg.norm(ft_qry, axis=1, keepdims=True) # Shape (M, N)
|
| 30 |
+
ft_ref_norm = ft_ref / np.linalg.norm(ft_ref, axis=1, keepdims=True) # Shape (C, N)
|
| 31 |
+
|
| 32 |
+
# Step 2: Compute cosine similarity
|
| 33 |
+
dMat = 1 - (ft_ref_norm @ ft_qry_norm.T)
|
| 34 |
+
mInds = np.argsort(dMat,axis=0)[:topK].squeeze() # shape: K x ft_qry.shape[0]
|
| 35 |
+
return mInds, dMat
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def getMatchIndsGPU(ft_ref, ft_qry,topK=20, metric='cosine'):
|
| 39 |
+
# metric: 'euclidean' or 'cosine'
|
| 40 |
+
ft_qry_tensor = torch.Tensor(ft_qry).to(device)
|
| 41 |
+
ft_ref_tensor = torch.Tensor(ft_ref).to(device)
|
| 42 |
+
|
| 43 |
+
if metric == 'euclidean':
|
| 44 |
+
# Use torch's cdist for Euclidean distance
|
| 45 |
+
dMat = torch.cdist(ft_ref, ft_qry)
|
| 46 |
+
|
| 47 |
+
elif metric == 'cosine':
|
| 48 |
+
# # Normalize both the query and reference tensors
|
| 49 |
+
ft_qry_norm = ft_qry_tensor / ft_qry_tensor.norm(dim=1, keepdim=True)
|
| 50 |
+
ft_ref_norm = ft_ref_tensor / ft_ref_tensor.norm(dim=1, keepdim=True)
|
| 51 |
+
# Compute cosine similarity (1 - cosine similarity for distance)
|
| 52 |
+
dMat = 1 - ft_ref_norm @ ft_qry_norm.t()
|
| 53 |
+
|
| 54 |
+
# Get the indices of the top 5 closest matches
|
| 55 |
+
mInds = torch.argsort(dMat, dim=0)[:topK].squeeze()
|
| 56 |
+
|
| 57 |
+
return mInds, dMat
|
| 58 |
+
|
| 59 |
+
# User parameters
|
| 60 |
+
vpr_desc = 'salad'
|
| 61 |
+
# location = 'Holmview'
|
| 62 |
+
# location = 'Cambogan'
|
| 63 |
+
location = 'DairyCreek'
|
| 64 |
+
|
| 65 |
+
################ Query filenames and directories #################################
|
| 66 |
+
# qry_sequence = '20250820_130327'
|
| 67 |
+
# qry_sequence = '20250812_120856'
|
| 68 |
+
###
|
| 69 |
+
# qry_sequence = '20250811_113017'
|
| 70 |
+
# qry_sequence = '20250812_122621'
|
| 71 |
+
qry_condition = 'flooded'
|
| 72 |
+
# qry_sequence = '20250812_122339'
|
| 73 |
+
###
|
| 74 |
+
qry_sequence = '20250811_103318'
|
| 75 |
+
# qry_condition = 'dry'
|
| 76 |
+
qry_camera_pos = 'front'
|
| 77 |
+
qry_root_directory = f"../Datasets/FRED/{qry_condition}/KITTI-style"
|
| 78 |
+
qry_vpr_root = f"../Datasets/FRED/vpr_ftrs/{qry_condition}/KITTI-style"
|
| 79 |
+
|
| 80 |
+
qry_image_dir = f"{qry_root_directory}/{location}_{qry_sequence}/{qry_camera_pos}-imgs/"
|
| 81 |
+
qry_utm_dir = f"{qry_root_directory}/{location}_{qry_sequence}/utm/"
|
| 82 |
+
qry_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(qry_image_dir)) if os.path.isfile(qry_image_dir+filename)]
|
| 83 |
+
qry_name_sort_idx = index_natsorted(os.listdir(qry_image_dir))
|
| 84 |
+
qry_ftrs = np.load(f"{qry_vpr_root}/{location}_{qry_sequence}/{vpr_desc}/queries_descriptors.npy")
|
| 85 |
+
qry_ftrs = qry_ftrs[qry_name_sort_idx]
|
| 86 |
+
|
| 87 |
+
################ Reference filenames and directories #################################
|
| 88 |
+
# ref_sequence = '20250812_120100'
|
| 89 |
+
# 20250812_120856
|
| 90 |
+
# ref_sequence = '20250812_122339'
|
| 91 |
+
# 20250812_122621
|
| 92 |
+
if location == 'DairyCreek': location = 'Dairy-Creek'
|
| 93 |
+
ref_sequence = '20250812_122954'
|
| 94 |
+
ref_condition = 'dry'
|
| 95 |
+
ref_camera_pos = 'front'
|
| 96 |
+
ref_root_directory = f"../Datasets/FRED/{ref_condition}/KITTI-style"
|
| 97 |
+
ref_vpr_root = f"../Datasets/FRED/vpr_ftrs/{ref_condition}/KITTI-style"
|
| 98 |
+
|
| 99 |
+
ref_image_dir = f"{ref_root_directory}/{location}_{ref_sequence}/{ref_camera_pos}-imgs/"
|
| 100 |
+
ref_utm_dir = f"{ref_root_directory}/{location}_{ref_sequence}/utm/"
|
| 101 |
+
ref_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]
|
| 102 |
+
ref_name_sort_idx = index_natsorted(os.listdir(ref_image_dir))
|
| 103 |
+
ref_utms = np.array([np.loadtxt(ref_utm_dir+filename) for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])
|
| 104 |
+
print(ref_utms.shape)
|
| 105 |
+
ref_img_filenames = [filename for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]
|
| 106 |
+
ref_utm_filenames = np.array([filename for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])
|
| 107 |
+
ref_ftrs = np.load(f"{ref_vpr_root}/{location}_{ref_sequence}/{vpr_desc}/queries_descriptors.npy")
|
| 108 |
+
ref_ftrs = ref_ftrs[ref_name_sort_idx]
|
| 109 |
+
|
| 110 |
+
img_calib_file = f"./camera_calib.txt"
|
| 111 |
+
|
| 112 |
+
dist_tolerance = 10 # metres
|
| 113 |
+
# qry_idx = 4
|
| 114 |
+
|
| 115 |
+
mInds, dMat = getMatchIndsGPU(ref_ftrs,qry_ftrs,topK=1)
|
| 116 |
+
mInds = mInds.cpu().numpy()
|
| 117 |
+
in_tol = []
|
| 118 |
+
dists = []
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
plt.ion()
|
| 122 |
+
|
| 123 |
+
fig, ax = plt.subplots(1, 2, figsize=(19.4, 6))
|
| 124 |
+
|
| 125 |
+
# Dummy images to initialize artists
|
| 126 |
+
img0 = ax[0].imshow(np.zeros((10, 10, 3), dtype=np.uint8))
|
| 127 |
+
img1 = ax[1].imshow(np.zeros((10, 10, 3), dtype=np.uint8))
|
| 128 |
+
|
| 129 |
+
ax[0].axis("off")
|
| 130 |
+
ax[1].axis("off")
|
| 131 |
+
|
| 132 |
+
pressed_key = None
|
| 133 |
+
|
| 134 |
+
def on_key(event):
|
| 135 |
+
global pressed_key
|
| 136 |
+
pressed_key = event.key
|
| 137 |
+
|
| 138 |
+
cid = fig.canvas.mpl_connect("key_press_event", on_key)
|
| 139 |
+
|
| 140 |
+
for qry_idx in tqdm(range(len(qry_timestamps))):
|
| 141 |
+
|
| 142 |
+
pressed_key = None
|
| 143 |
+
|
| 144 |
+
qry_image_timestamp = qry_timestamps[qry_idx]
|
| 145 |
+
qry_image_filename = f"{qry_image_dir}/{qry_image_timestamp}.png"
|
| 146 |
+
qry_utm_timestamp = utils.get_corr_files(
|
| 147 |
+
qry_image_timestamp, [qry_utm_dir]
|
| 148 |
+
)
|
| 149 |
+
qry_utm = np.loadtxt(qry_utm_timestamp)
|
| 150 |
+
|
| 151 |
+
ref_utm_timestamp = utils.get_corr_files(
|
| 152 |
+
ref_timestamps[int(mInds[qry_idx])], [ref_utm_dir]
|
| 153 |
+
)
|
| 154 |
+
ref_utm = np.loadtxt(ref_utm_timestamp)
|
| 155 |
+
|
| 156 |
+
diff = ref_utm - qry_utm
|
| 157 |
+
dist = np.linalg.norm(diff)
|
| 158 |
+
|
| 159 |
+
dists.append(dist)
|
| 160 |
+
in_tol.append(1 if dist < dist_tolerance else 0)
|
| 161 |
+
|
| 162 |
+
# Load images
|
| 163 |
+
qry_image = ImageData(qry_image_filename, img_calib_file)
|
| 164 |
+
ref_image = ImageData(
|
| 165 |
+
f"{ref_image_dir}/{ref_timestamps[int(mInds[qry_idx])]}.png",
|
| 166 |
+
img_calib_file,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
# Update image data (NO re-plotting)
|
| 170 |
+
img0.set_data(qry_image.image[:, :, ::-1])
|
| 171 |
+
img1.set_data(ref_image.image[:, :, ::-1])
|
| 172 |
+
|
| 173 |
+
ax[0].set_title(f"{qry_image_timestamp}.png")
|
| 174 |
+
ax[1].set_title(
|
| 175 |
+
f"{ref_timestamps[int(mInds[qry_idx])]}\nDist = {dist:.2f} m"
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
fig.canvas.draw_idle()
|
| 179 |
+
fig.canvas.flush_events()
|
| 180 |
+
|
| 181 |
+
# print("Press any key for next (mouse click also works)")
|
| 182 |
+
# key = plt.waitforbuttonpress()
|
| 183 |
+
|
| 184 |
+
# print("Press any key for next, 'q' to quit")
|
| 185 |
+
|
| 186 |
+
print("Any key = next | q = quit")
|
| 187 |
+
|
| 188 |
+
while pressed_key is None:
|
| 189 |
+
plt.pause(0.05)
|
| 190 |
+
|
| 191 |
+
if pressed_key == "q":
|
| 192 |
+
break
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
# print(f"Recall: {np.sum(np.array(in_tol))/len(qry_timestamps):.02%}")
|
| 196 |
+
# plt.figure()
|
| 197 |
+
# plt.plot(np.clip(dists, 0, 30))
|
| 198 |
+
# plt.ylim((0,35))
|
| 199 |
+
|
localisation/__init__.py
ADDED
|
File without changes
|
localisation/create_VPR_eval_dataset.ipynb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 30,
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"outputs": [],
|
| 8 |
+
"source": [
|
| 9 |
+
"import os\n",
|
| 10 |
+
"import shutil\n",
|
| 11 |
+
"import matplotlib.pyplot as plt\n",
|
| 12 |
+
"import sys\n",
|
| 13 |
+
"from tqdm import tqdm_notebook as tqdm\n",
|
| 14 |
+
"sys.path.append(os.path.abspath(\"..\")) # one level up\n",
|
| 15 |
+
"import numpy as np\n",
|
| 16 |
+
"import utils.utils as utils\n",
|
| 17 |
+
"from natsort import natsorted\n",
|
| 18 |
+
"\n",
|
| 19 |
+
"cmap = plt.get_cmap(\"jet\")"
|
| 20 |
+
]
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"cell_type": "code",
|
| 24 |
+
"execution_count": 31,
|
| 25 |
+
"metadata": {},
|
| 26 |
+
"outputs": [],
|
| 27 |
+
"source": [
|
| 28 |
+
"# User parameters\n",
|
| 29 |
+
"# location = 'Holmview'\n",
|
| 30 |
+
"location = 'Cambogan'\n",
|
| 31 |
+
"\n",
|
| 32 |
+
"################ Query filenames and directories #################################\n",
|
| 33 |
+
"# qry_sequence = '20250820_130327'\n",
|
| 34 |
+
"qry_sequence = '20250811_113017' # '20250812_122339' '20250811_113017'\n",
|
| 35 |
+
"qry_condition = 'flooded' # 'dry' 'flooded'\n",
|
| 36 |
+
"qry_camera_pos = 'front'\n",
|
| 37 |
+
"qry_root_directory = f\"../../Datasets/FRED/{qry_condition}/KITTI-style\"\n",
|
| 38 |
+
"\n",
|
| 39 |
+
"qry_image_dir = f\"{qry_root_directory}/{location}_{qry_sequence}/{qry_camera_pos}-imgs/\"\n",
|
| 40 |
+
"qry_utm_dir = f\"{qry_root_directory}/{location}_{qry_sequence}/utm/\"\n",
|
| 41 |
+
"qry_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(qry_image_dir)) if os.path.isfile(qry_image_dir+filename)]\n",
|
| 42 |
+
"\n",
|
| 43 |
+
"# ################ Reference filenames and directories #################################\n",
|
| 44 |
+
"# # ref_sequence = '20250812_120100'\n",
|
| 45 |
+
"# ref_sequence = '20250812_122339'\n",
|
| 46 |
+
"# ref_condition = 'dry'\n",
|
| 47 |
+
"# ref_camera_pos = 'front'\n",
|
| 48 |
+
"# ref_root_directory = f\"../Datasets/FRED/{ref_condition}/KITTI-style\"\n",
|
| 49 |
+
"\n",
|
| 50 |
+
"# ref_image_dir = f\"{ref_root_directory}/{location}_{ref_sequence}/{ref_camera_pos}-imgs/\"\n",
|
| 51 |
+
"# ref_utm_dir = f\"{ref_root_directory}/{location}_{ref_sequence}/utm/\"\n",
|
| 52 |
+
"# ref_utms = np.array([np.loadtxt(ref_utm_dir+filename) for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])\n",
|
| 53 |
+
"# ref_img_filenames = [filename for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]\n",
|
| 54 |
+
"# ref_utm_filenames = np.array([filename for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])\n",
|
| 55 |
+
"\n",
|
| 56 |
+
"img_calib_file = f\"./camera_calib.txt\"\n",
|
| 57 |
+
"\n",
|
| 58 |
+
"# dist_tolerance = 10 # metres\n",
|
| 59 |
+
"\n",
|
| 60 |
+
"save_folder = f\"../../Datasets/FRED/VPR-eval/{qry_condition}_{location}_{qry_sequence}/\""
|
| 61 |
+
]
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"cell_type": "code",
|
| 65 |
+
"execution_count": 32,
|
| 66 |
+
"metadata": {},
|
| 67 |
+
"outputs": [
|
| 68 |
+
{
|
| 69 |
+
"name": "stderr",
|
| 70 |
+
"output_type": "stream",
|
| 71 |
+
"text": [
|
| 72 |
+
"C:\\Users\\malonecj\\AppData\\Local\\Temp\\ipykernel_30324\\3268383255.py:2: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0\n",
|
| 73 |
+
"Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook`\n",
|
| 74 |
+
" for i in tqdm(range(len(qry_timestamps))):\n"
|
| 75 |
+
]
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"data": {
|
| 79 |
+
"application/vnd.jupyter.widget-view+json": {
|
| 80 |
+
"model_id": "93387ddb94d84df5adba819c16412c8f",
|
| 81 |
+
"version_major": 2,
|
| 82 |
+
"version_minor": 0
|
| 83 |
+
},
|
| 84 |
+
"text/plain": [
|
| 85 |
+
" 0%| | 0/243 [00:00<?, ?it/s]"
|
| 86 |
+
]
|
| 87 |
+
},
|
| 88 |
+
"metadata": {},
|
| 89 |
+
"output_type": "display_data"
|
| 90 |
+
}
|
| 91 |
+
],
|
| 92 |
+
"source": [
|
| 93 |
+
"os.makedirs(save_folder, exist_ok=True)\n",
|
| 94 |
+
"for i in tqdm(range(len(qry_timestamps))):\n",
|
| 95 |
+
" qry_image_timestamp = qry_timestamps[i]\n",
|
| 96 |
+
" # try:\n",
|
| 97 |
+
" qry_image_filename = f\"{qry_image_dir}/{qry_image_timestamp}.png\"\n",
|
| 98 |
+
" qry_utm_timestamp = utils.get_corr_files(qry_image_timestamp, [qry_utm_dir,])\n",
|
| 99 |
+
" qry_utm = np.loadtxt(qry_utm_timestamp)\n",
|
| 100 |
+
"\n",
|
| 101 |
+
" # qry_image = ImageData(qry_image_filename, img_calib_file)\n",
|
| 102 |
+
"\n",
|
| 103 |
+
" shutil.copy(qry_image_filename, f\"{save_folder}/@{qry_utm[0]}@{qry_utm[1]}@.png\")\n",
|
| 104 |
+
" \n",
|
| 105 |
+
"\n",
|
| 106 |
+
"\n",
|
| 107 |
+
" "
|
| 108 |
+
]
|
| 109 |
+
}
|
| 110 |
+
],
|
| 111 |
+
"metadata": {
|
| 112 |
+
"kernelspec": {
|
| 113 |
+
"display_name": "CARRSQ",
|
| 114 |
+
"language": "python",
|
| 115 |
+
"name": "python3"
|
| 116 |
+
},
|
| 117 |
+
"language_info": {
|
| 118 |
+
"codemirror_mode": {
|
| 119 |
+
"name": "ipython",
|
| 120 |
+
"version": 3
|
| 121 |
+
},
|
| 122 |
+
"file_extension": ".py",
|
| 123 |
+
"mimetype": "text/x-python",
|
| 124 |
+
"name": "python",
|
| 125 |
+
"nbconvert_exporter": "python",
|
| 126 |
+
"pygments_lexer": "ipython3",
|
| 127 |
+
"version": "3.10.15"
|
| 128 |
+
}
|
| 129 |
+
},
|
| 130 |
+
"nbformat": 4,
|
| 131 |
+
"nbformat_minor": 2
|
| 132 |
+
}
|
localisation/groundtruth_utm-single.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
localisation/groundtruth_utm_checker-all.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.image as mpimg
|
| 4 |
+
import sys
|
| 5 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
import open3d as o3d
|
| 9 |
+
from scipy.spatial.transform import Rotation
|
| 10 |
+
from utils.lidar import PointCloud
|
| 11 |
+
from utils.camera import ImageData
|
| 12 |
+
import utils.utils as utils
|
| 13 |
+
from natsort import natsorted
|
| 14 |
+
|
| 15 |
+
cmap = plt.get_cmap("jet")
|
| 16 |
+
|
| 17 |
+
# User parameters
|
| 18 |
+
# location = 'Holmview'
|
| 19 |
+
location = 'Cambogan'
|
| 20 |
+
|
| 21 |
+
################ Query filenames and directories #################################
|
| 22 |
+
# qry_sequence = '20250820_130327'
|
| 23 |
+
qry_sequence = '20250811_113017'
|
| 24 |
+
qry_condition = 'flooded'
|
| 25 |
+
qry_camera_pos = 'front'
|
| 26 |
+
qry_root_directory = f"../Datasets/FRED/{qry_condition}/KITTI-style"
|
| 27 |
+
|
| 28 |
+
qry_image_dir = f"{qry_root_directory}/{location}_{qry_sequence}/{qry_camera_pos}-imgs/"
|
| 29 |
+
qry_utm_dir = f"{qry_root_directory}/{location}_{qry_sequence}/utm/"
|
| 30 |
+
qry_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(qry_image_dir)) if os.path.isfile(qry_image_dir+filename)]
|
| 31 |
+
|
| 32 |
+
################ Reference filenames and directories #################################
|
| 33 |
+
# ref_sequence = '20250812_120100'
|
| 34 |
+
ref_sequence = '20250812_122339'
|
| 35 |
+
ref_condition = 'dry'
|
| 36 |
+
ref_camera_pos = 'front'
|
| 37 |
+
ref_root_directory = f"../Datasets/FRED/{ref_condition}/KITTI-style"
|
| 38 |
+
|
| 39 |
+
ref_image_dir = f"{ref_root_directory}/{location}_{ref_sequence}/{ref_camera_pos}-imgs/"
|
| 40 |
+
ref_utm_dir = f"{ref_root_directory}/{location}_{ref_sequence}/utm/"
|
| 41 |
+
ref_utms = np.array([np.loadtxt(ref_utm_dir+filename) for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])
|
| 42 |
+
ref_img_filenames = [filename for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]
|
| 43 |
+
ref_utm_filenames = np.array([filename for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])
|
| 44 |
+
|
| 45 |
+
img_calib_file = f"./camera_calib.txt"
|
| 46 |
+
|
| 47 |
+
dist_tolerance = 10 # metres
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
fig, ax = plt.subplots(1, 2, figsize=(19.4, 6))
|
| 51 |
+
# idx = [0] # mutable index
|
| 52 |
+
idx = [183]
|
| 53 |
+
|
| 54 |
+
def show_image(i):
|
| 55 |
+
ax[0].clear()
|
| 56 |
+
ax[1].clear()
|
| 57 |
+
if i >= len(qry_timestamps):
|
| 58 |
+
plt.close(fig)
|
| 59 |
+
return
|
| 60 |
+
qry_image_timestamp = qry_timestamps[i]
|
| 61 |
+
# try:
|
| 62 |
+
qry_image_filename = f"{qry_image_dir}/{qry_image_timestamp}.png"
|
| 63 |
+
qry_utm_timestamp = utils.get_corr_files(qry_image_timestamp, [qry_utm_dir,])
|
| 64 |
+
qry_utm = np.loadtxt(qry_utm_timestamp)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
diffs = ref_utms - qry_utm # shape (N, 2)
|
| 68 |
+
dists = np.linalg.norm(diffs, axis=1) # shape (N,)
|
| 69 |
+
closest_idx = np.argmin(dists)
|
| 70 |
+
closest_dist = dists[closest_idx]
|
| 71 |
+
|
| 72 |
+
qry_image = ImageData(qry_image_filename, img_calib_file)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
ax[0].imshow(qry_image.image[:, :, ::-1])
|
| 76 |
+
ax[0].set_title(f"{qry_image_timestamp}.png")
|
| 77 |
+
ax[0].axis("off")
|
| 78 |
+
|
| 79 |
+
if closest_dist <= dist_tolerance:
|
| 80 |
+
# Show matching reference image
|
| 81 |
+
ref_img_timestamp = utils.get_corr_files(ref_utm_filenames[closest_idx].split('.txt')[0], [ref_image_dir,])
|
| 82 |
+
ref_image = ImageData(ref_img_timestamp, img_calib_file)
|
| 83 |
+
ax[1].imshow(ref_image.image[:, :, ::-1])
|
| 84 |
+
ax[1].set_title(f"{ref_img_timestamp.split('/')[-1]}\nDist={closest_dist:.2f}m")
|
| 85 |
+
else:
|
| 86 |
+
# Show black image with message
|
| 87 |
+
black_img = np.zeros_like(qry_image.image)
|
| 88 |
+
ax[1].imshow(black_img)
|
| 89 |
+
ax[1].text(
|
| 90 |
+
0.5, 0.5, "No reference image found\nwithin distance tolerance",
|
| 91 |
+
color="white", fontsize=16, ha="center", va="center", transform=ax[1].transAxes
|
| 92 |
+
)
|
| 93 |
+
ax[1].set_title(f"No Match (min dist={closest_dist:.2f}m)")
|
| 94 |
+
|
| 95 |
+
ax[1].axis("off")
|
| 96 |
+
plt.savefig('paper_figures/localization_check.pdf', format="pdf", bbox_inches='tight')
|
| 97 |
+
fig.canvas.draw()
|
| 98 |
+
|
| 99 |
+
def on_key(event):
|
| 100 |
+
if event.key in [' ', 'right']: # space or right arrow
|
| 101 |
+
idx[0] += 1
|
| 102 |
+
show_image(idx[0])
|
| 103 |
+
elif event.key in [' ', 'left']: # space or right arrow
|
| 104 |
+
if idx[0] > 0:
|
| 105 |
+
idx[0] -= 1
|
| 106 |
+
show_image(idx[0])
|
| 107 |
+
elif event.key in ['q', 'escape']: # q or Esc → quit
|
| 108 |
+
plt.close(fig)
|
| 109 |
+
|
| 110 |
+
fig.canvas.mpl_connect('key_press_event', on_key)
|
| 111 |
+
show_image(idx[0])
|
| 112 |
+
plt.show()
|
localisation/plot_utm_traj.ipynb
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 30,
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"outputs": [],
|
| 8 |
+
"source": [
|
| 9 |
+
"import os\n",
|
| 10 |
+
"import matplotlib.pyplot as plt\n",
|
| 11 |
+
"import matplotlib.image as mpimg\n",
|
| 12 |
+
"import sys\n",
|
| 13 |
+
"sys.path.append(os.path.abspath(\"..\")) # one level up\n",
|
| 14 |
+
"import numpy as np\n",
|
| 15 |
+
"import cv2\n",
|
| 16 |
+
"import open3d as o3d\n",
|
| 17 |
+
"from scipy.spatial.transform import Rotation\n",
|
| 18 |
+
"from utils.lidar import PointCloud\n",
|
| 19 |
+
"from utils.camera import ImageData\n",
|
| 20 |
+
"import utils.utils as utils\n",
|
| 21 |
+
"from natsort import natsorted\n",
|
| 22 |
+
"\n",
|
| 23 |
+
"cmap = plt.get_cmap(\"jet\")"
|
| 24 |
+
]
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"cell_type": "code",
|
| 28 |
+
"execution_count": 31,
|
| 29 |
+
"metadata": {},
|
| 30 |
+
"outputs": [
|
| 31 |
+
{
|
| 32 |
+
"name": "stdout",
|
| 33 |
+
"output_type": "stream",
|
| 34 |
+
"text": [
|
| 35 |
+
"81\n"
|
| 36 |
+
]
|
| 37 |
+
}
|
| 38 |
+
],
|
| 39 |
+
"source": [
|
| 40 |
+
"\n",
|
| 41 |
+
"# User parameters\n",
|
| 42 |
+
"location = 'Cambogan'\n",
|
| 43 |
+
"# sequence = '20250811_113017'\n",
|
| 44 |
+
"# location = 'Holmview'\n",
|
| 45 |
+
"\n",
|
| 46 |
+
"################ Query filenames and directories #################################\n",
|
| 47 |
+
"# qry_sequence = '20250820_130327'\n",
|
| 48 |
+
"qry_sequence = '20250811_113017'\n",
|
| 49 |
+
"qry_condition = 'flooded'\n",
|
| 50 |
+
"qry_camera_pos = 'front'\n",
|
| 51 |
+
"qry_root_directory = f\"../../Datasets/FRED/{qry_condition}/KITTI-style\"\n",
|
| 52 |
+
"\n",
|
| 53 |
+
"qry_image_dir = f\"{qry_root_directory}/{location}_{qry_sequence}/{qry_camera_pos}-imgs/\"\n",
|
| 54 |
+
"qry_utm_dir = f\"{qry_root_directory}/{location}_{qry_sequence}/utm/\"\n",
|
| 55 |
+
"qry_utms = np.array([np.loadtxt(qry_utm_dir+filename) for filename in natsorted(os.listdir(qry_utm_dir)) if os.path.isfile(qry_utm_dir+filename)])\n",
|
| 56 |
+
"qry_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(qry_image_dir)) if os.path.isfile(qry_image_dir+filename)]\n",
|
| 57 |
+
"\n",
|
| 58 |
+
"################ Reference filenames and directories #################################\n",
|
| 59 |
+
"# ref_sequence = '20250812_120100'\n",
|
| 60 |
+
"ref_sequence = '20250812_122339'\n",
|
| 61 |
+
"ref_condition = 'dry'\n",
|
| 62 |
+
"ref_camera_pos = 'front'\n",
|
| 63 |
+
"ref_root_directory = f\"../../Datasets/FRED/{ref_condition}/KITTI-style\"\n",
|
| 64 |
+
"\n",
|
| 65 |
+
"ref_image_dir = f\"{ref_root_directory}/{location}_{ref_sequence}/{ref_camera_pos}-imgs/\"\n",
|
| 66 |
+
"ref_utm_dir = f\"{ref_root_directory}/{location}_{ref_sequence}/utm/\"\n",
|
| 67 |
+
"ref_utms = np.array([np.loadtxt(ref_utm_dir+filename) for filename in natsorted(os.listdir(ref_utm_dir)) if os.path.isfile(ref_utm_dir+filename)])\n",
|
| 68 |
+
"ref_img_filenames = [filename for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]\n",
|
| 69 |
+
"ref_timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(ref_image_dir)) if os.path.isfile(ref_image_dir+filename)]\n",
|
| 70 |
+
"\n",
|
| 71 |
+
"end_ref = np.where((qry_utms[-1,0] < ref_utms[:,0]) * (qry_utms[-1,1] > ref_utms[:,1]))[0][0]\n",
|
| 72 |
+
"print(end_ref)\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"# img_calib_file = f\"./camera_calib.txt\""
|
| 75 |
+
]
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"cell_type": "code",
|
| 79 |
+
"execution_count": 33,
|
| 80 |
+
"metadata": {},
|
| 81 |
+
"outputs": [
|
| 82 |
+
{
|
| 83 |
+
"data": {
|
| 84 |
+
"image/png": "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",
|
| 85 |
+
"text/plain": [
|
| 86 |
+
"<Figure size 1200x400 with 1 Axes>"
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
"metadata": {},
|
| 90 |
+
"output_type": "display_data"
|
| 91 |
+
}
|
| 92 |
+
],
|
| 93 |
+
"source": [
|
| 94 |
+
"plt.figure(figsize=(12,4))\n",
|
| 95 |
+
"plt.plot(qry_utms[:,0]-qry_utms[0,0], qry_utms[:,1]-qry_utms[0,1])\n",
|
| 96 |
+
"plt.plot(ref_utms[:end_ref,0]-qry_utms[0,0], ref_utms[:end_ref,1]-qry_utms[0,1], '--')\n",
|
| 97 |
+
"plt.savefig('../paper_figures/trajectory_plot.pdf', format=\"pdf\", bbox_inches='tight')"
|
| 98 |
+
]
|
| 99 |
+
}
|
| 100 |
+
],
|
| 101 |
+
"metadata": {
|
| 102 |
+
"kernelspec": {
|
| 103 |
+
"display_name": "CARRSQ",
|
| 104 |
+
"language": "python",
|
| 105 |
+
"name": "python3"
|
| 106 |
+
},
|
| 107 |
+
"language_info": {
|
| 108 |
+
"codemirror_mode": {
|
| 109 |
+
"name": "ipython",
|
| 110 |
+
"version": 3
|
| 111 |
+
},
|
| 112 |
+
"file_extension": ".py",
|
| 113 |
+
"mimetype": "text/x-python",
|
| 114 |
+
"name": "python",
|
| 115 |
+
"nbconvert_exporter": "python",
|
| 116 |
+
"pygments_lexer": "ipython3",
|
| 117 |
+
"version": "3.10.15"
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
"nbformat": 4,
|
| 121 |
+
"nbformat_minor": 2
|
| 122 |
+
}
|
segmentation/.gitkeep
ADDED
|
File without changes
|
segmentation/__init__.py
ADDED
|
File without changes
|
segmentation/evaluate-predictions.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import argparse
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import matplotlib.image as mpimg
|
| 5 |
+
import sys
|
| 6 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 7 |
+
import numpy as np
|
| 8 |
+
import cv2
|
| 9 |
+
import open3d as o3d
|
| 10 |
+
from scipy.spatial.transform import Rotation
|
| 11 |
+
from utils.lidar import PointCloud
|
| 12 |
+
from utils.camera import ImageData
|
| 13 |
+
import utils.utils as utils
|
| 14 |
+
from natsort import natsorted
|
| 15 |
+
import json
|
| 16 |
+
import yaml # pip install pyyaml
|
| 17 |
+
from tqdm import tqdm
|
| 18 |
+
|
| 19 |
+
cmap = plt.get_cmap("jet")
|
| 20 |
+
|
| 21 |
+
# ---------------- Argument Parsing ---------------- #
|
| 22 |
+
parser = argparse.ArgumentParser()
|
| 23 |
+
|
| 24 |
+
parser.add_argument("--config", type=str, help="Path to config file (YAML or JSON). If provided, overrides other args.")
|
| 25 |
+
|
| 26 |
+
parser.add_argument("--location", type=str, default="Cambogan", help="Location name (e.g., Cambogan)")
|
| 27 |
+
parser.add_argument("--sequence", type=str, default="20250811_113017", help="Sequence ID (e.g., 20250811_113017)")
|
| 28 |
+
parser.add_argument("--condition", type=str, default="flooded", help="Condition (e.g., flooded)")
|
| 29 |
+
parser.add_argument("--camera_pos", type=str, default="front", help="Camera position (e.g., front)")
|
| 30 |
+
parser.add_argument("--root", type=str, default="../Datasets/FRED/", help="Root dataset directory (e.g., ../Datasets/FRED/)")
|
| 31 |
+
parser.add_argument("--masks", type=str, required=True, help="Where predicted masks are saved")
|
| 32 |
+
parser.add_argument("--img_calib_file", type=str, default="./camera_calib.txt", help="Path to camera calibration file (e.g., ./camera_calib.txt)")
|
| 33 |
+
parser.add_argument('--vis', action='store_true', help="Store visual comparisons of the predictions and labels")
|
| 34 |
+
parser.add_argument("--output", type=str, default=None, help="Where to save visual comparisons")
|
| 35 |
+
|
| 36 |
+
args = parser.parse_args()
|
| 37 |
+
|
| 38 |
+
# ---------------- Config Loading ---------------- #
|
| 39 |
+
if args.config:
|
| 40 |
+
if args.config.endswith(".yaml") or args.config.endswith(".yml"):
|
| 41 |
+
with open(args.config, "r") as f:
|
| 42 |
+
cfg = yaml.safe_load(f)
|
| 43 |
+
elif args.config.endswith(".json"):
|
| 44 |
+
with open(args.config, "r") as f:
|
| 45 |
+
cfg = json.load(f)
|
| 46 |
+
else:
|
| 47 |
+
raise ValueError("Config file must be .yaml, .yml, or .json")
|
| 48 |
+
|
| 49 |
+
location = cfg["location"]
|
| 50 |
+
sequence = cfg["sequence"]
|
| 51 |
+
condition = cfg["condition"]
|
| 52 |
+
camera_pos = cfg["camera_pos"]
|
| 53 |
+
root = cfg["root"]
|
| 54 |
+
root_directory = f"{root}/{condition}/KITTI-style"
|
| 55 |
+
img_calib_file = cfg["img_calib_file"]
|
| 56 |
+
|
| 57 |
+
else:
|
| 58 |
+
# Fallback: require all CLI args
|
| 59 |
+
required_args = ["location", "sequence", "condition", "camera_pos", "root", "img_calib_file"]
|
| 60 |
+
missing = [arg for arg in required_args if getattr(args, arg) is None]
|
| 61 |
+
if missing:
|
| 62 |
+
parser.error(f"Missing arguments: {', '.join(missing)} (or provide --config)")
|
| 63 |
+
|
| 64 |
+
location = args.location
|
| 65 |
+
sequence = args.sequence
|
| 66 |
+
condition = args.condition
|
| 67 |
+
camera_pos = args.camera_pos
|
| 68 |
+
root_directory = f"{args.root}/{args.condition}/KITTI-style"
|
| 69 |
+
img_calib_file = args.img_calib_file
|
| 70 |
+
|
| 71 |
+
# # User parameters
|
| 72 |
+
# # location = 'Cambogan'
|
| 73 |
+
# # sequence = '20250811_113017'
|
| 74 |
+
# # location = 'Holmview'
|
| 75 |
+
# # sequence = '20250820_130327'
|
| 76 |
+
# # location = 'Pullenvale'
|
| 77 |
+
# # sequence = '20250916_124105'
|
| 78 |
+
# location = 'Mount-Cotton'
|
| 79 |
+
# sequence = '20241217_113410'
|
| 80 |
+
# condition = 'flooded'
|
| 81 |
+
# camera_pos = 'front'
|
| 82 |
+
# root_directory = f"../Datasets/FRED/{condition}/KITTI-style"
|
| 83 |
+
# # 01000000
|
| 84 |
+
|
| 85 |
+
############ Define filenames and directories ####################################
|
| 86 |
+
|
| 87 |
+
image_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-imgs/"
|
| 88 |
+
label_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-labels/"
|
| 89 |
+
img_calib_file = f"./camera_calib.txt"
|
| 90 |
+
timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(image_dir)) if os.path.isfile(image_dir+filename)]
|
| 91 |
+
mask_filenames = [filename for filename in natsorted(os.listdir(args.masks)) if os.path.isfile(args.masks+filename)]
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
fig, ax = plt.subplots(figsize=(12.8, 8))
|
| 95 |
+
idx = 0 # mutable index
|
| 96 |
+
# idx = 183 # mutable index
|
| 97 |
+
|
| 98 |
+
def load_image_data(i):
|
| 99 |
+
image_timestamp = timestamps[i]
|
| 100 |
+
try:
|
| 101 |
+
image_filename = f"{image_dir}/{image_timestamp}.png"
|
| 102 |
+
label_filename = f"{label_dir}/{image_timestamp}.png"
|
| 103 |
+
image = ImageData(image_filename, img_calib_file, label_filename)
|
| 104 |
+
water_label = image.label_img == 1
|
| 105 |
+
|
| 106 |
+
return water_label.astype(int)
|
| 107 |
+
|
| 108 |
+
# label_mask = np.any(image.colour_label != image.semantic_classes['other'], axis=-1)
|
| 109 |
+
except Exception as e:
|
| 110 |
+
print(f"Could not show label for {image_timestamp}.png: {e}")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def calculate_iou(label: np.ndarray, prediction: np.ndarray, verbose=False) -> float:
|
| 116 |
+
"""
|
| 117 |
+
Calculate Intersection over Union (IOU) for two binary masks.
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
label: Ground truth binary mask (values 0 or 1)
|
| 121 |
+
prediction: Predicted binary mask (values 0 or 1)
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
IOU score as a float in [0, 1]. Returns 0.0 if both masks are empty.
|
| 125 |
+
"""
|
| 126 |
+
if label.shape != prediction.shape:
|
| 127 |
+
# raise ValueError(f"Shape mismatch: label {label.shape} vs prediction {prediction.shape}")
|
| 128 |
+
if verbose:
|
| 129 |
+
print(f"reshaping predictions from {prediction.shape} to {label.shape} to match labels.")
|
| 130 |
+
prediction = cv2.resize(prediction, (label.shape[1], label.shape[0]), interpolation=cv2.INTER_NEAREST)
|
| 131 |
+
|
| 132 |
+
intersection = np.logical_and(label, prediction).sum()
|
| 133 |
+
union = np.logical_or(label, prediction).sum()
|
| 134 |
+
|
| 135 |
+
if union == 0:
|
| 136 |
+
return 1.0 # or 1.0 if you want to treat two empty masks as a perfect match
|
| 137 |
+
|
| 138 |
+
return float(intersection / union)
|
| 139 |
+
|
| 140 |
+
def save_mask_comparison(label: np.ndarray, prediction: np.ndarray, save_path: str, iou: float | None = None) -> None:
|
| 141 |
+
"""
|
| 142 |
+
Save label and prediction masks side by side for visual comparison.
|
| 143 |
+
|
| 144 |
+
Args:
|
| 145 |
+
label: Ground truth binary mask
|
| 146 |
+
prediction: Predicted binary mask
|
| 147 |
+
save_path: Path to save the output image (e.g. 'comparison.png')
|
| 148 |
+
iou: Optional IOU score to display in the title
|
| 149 |
+
"""
|
| 150 |
+
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
|
| 151 |
+
|
| 152 |
+
axes[0].imshow(label, cmap='gray', vmin=0, vmax=1)
|
| 153 |
+
axes[0].set_title('Label')
|
| 154 |
+
axes[0].axis('off')
|
| 155 |
+
|
| 156 |
+
axes[1].imshow(prediction, cmap='gray', vmin=0, vmax=1)
|
| 157 |
+
axes[1].set_title('Prediction')
|
| 158 |
+
axes[1].axis('off')
|
| 159 |
+
|
| 160 |
+
title = 'Mask Comparison'
|
| 161 |
+
if iou is not None:
|
| 162 |
+
title += f' | IOU: {iou:.4f}'
|
| 163 |
+
fig.suptitle(title)
|
| 164 |
+
|
| 165 |
+
plt.tight_layout()
|
| 166 |
+
plt.savefig(save_path, bbox_inches='tight', dpi=150)
|
| 167 |
+
plt.close(fig)
|
| 168 |
+
|
| 169 |
+
iou_scores = []
|
| 170 |
+
|
| 171 |
+
for i in tqdm(range(idx, len(timestamps))):
|
| 172 |
+
|
| 173 |
+
img_label = load_image_data(i)
|
| 174 |
+
# print(mask_filenames[i])
|
| 175 |
+
pred_label = (cv2.cvtColor(cv2.imread(args.masks+mask_filenames[i]), cv2.COLOR_BGR2GRAY)/255).astype(int)
|
| 176 |
+
|
| 177 |
+
# cv2.imshow('pred', cv2.resize(cv2.cvtColor(cv2.imread(args.masks+mask_filenames[i]), cv2.COLOR_BGR2GRAY), (640, 480)))
|
| 178 |
+
# cv2.waitKey(0)
|
| 179 |
+
|
| 180 |
+
# print(f"label shape: {img_label.shape}, label values: {np.unique(img_label)}")
|
| 181 |
+
# print(f"pred shape: {pred_label.shape}, pred values: {np.unique(pred_label)}")
|
| 182 |
+
# break
|
| 183 |
+
iou = calculate_iou(img_label, pred_label)
|
| 184 |
+
iou_scores.append(iou)
|
| 185 |
+
if args.vis:
|
| 186 |
+
os.makedirs(args.output, exist_ok=True)
|
| 187 |
+
save_mask_comparison(img_label, pred_label, f"{args.output}/{timestamps[i]}.png", iou)
|
| 188 |
+
|
| 189 |
+
# break
|
| 190 |
+
|
| 191 |
+
print(f"Mean IOU for water predictions: {np.mean(np.array(iou_scores))}")
|
segmentation/show_labels-all.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import argparse
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import matplotlib.image as mpimg
|
| 5 |
+
import sys
|
| 6 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 7 |
+
import numpy as np
|
| 8 |
+
import cv2
|
| 9 |
+
import open3d as o3d
|
| 10 |
+
from scipy.spatial.transform import Rotation
|
| 11 |
+
from utils.lidar import PointCloud
|
| 12 |
+
from utils.camera import ImageData
|
| 13 |
+
import utils.utils as utils
|
| 14 |
+
from natsort import natsorted
|
| 15 |
+
import json
|
| 16 |
+
import yaml # pip install pyyaml
|
| 17 |
+
|
| 18 |
+
cmap = plt.get_cmap("jet")
|
| 19 |
+
|
| 20 |
+
# ---------------- Argument Parsing ---------------- #
|
| 21 |
+
parser = argparse.ArgumentParser()
|
| 22 |
+
|
| 23 |
+
parser.add_argument("--config", type=str, help="Path to config file (YAML or JSON). If provided, overrides other args.")
|
| 24 |
+
|
| 25 |
+
parser.add_argument("--location", type=str, default="Cambogan", help="Location name (e.g., Cambogan)")
|
| 26 |
+
parser.add_argument("--sequence", type=str, default="20250811_113017", help="Sequence ID (e.g., 20250811_113017)")
|
| 27 |
+
parser.add_argument("--condition", type=str, default="flooded", help="Condition (e.g., flooded)")
|
| 28 |
+
parser.add_argument("--camera_pos", type=str, default="front", help="Camera position (e.g., front)")
|
| 29 |
+
parser.add_argument("--root", type=str, default="../Datasets/FRED/", help="Root dataset directory (e.g., ../Datasets/FRED/)")
|
| 30 |
+
parser.add_argument("--img_calib_file", type=str, default="./camera_calib.txt", help="Path to camera calibration file (e.g., ./camera_calib.txt)")
|
| 31 |
+
|
| 32 |
+
args = parser.parse_args()
|
| 33 |
+
|
| 34 |
+
# ---------------- Config Loading ---------------- #
|
| 35 |
+
if args.config:
|
| 36 |
+
if args.config.endswith(".yaml") or args.config.endswith(".yml"):
|
| 37 |
+
with open(args.config, "r") as f:
|
| 38 |
+
cfg = yaml.safe_load(f)
|
| 39 |
+
elif args.config.endswith(".json"):
|
| 40 |
+
with open(args.config, "r") as f:
|
| 41 |
+
cfg = json.load(f)
|
| 42 |
+
else:
|
| 43 |
+
raise ValueError("Config file must be .yaml, .yml, or .json")
|
| 44 |
+
|
| 45 |
+
location = cfg["location"]
|
| 46 |
+
sequence = cfg["sequence"]
|
| 47 |
+
condition = cfg["condition"]
|
| 48 |
+
camera_pos = cfg["camera_pos"]
|
| 49 |
+
root = cfg["root"]
|
| 50 |
+
root_directory = f"{root}/{condition}/KITTI-style"
|
| 51 |
+
img_calib_file = cfg["img_calib_file"]
|
| 52 |
+
|
| 53 |
+
else:
|
| 54 |
+
# Fallback: require all CLI args
|
| 55 |
+
required_args = ["location", "sequence", "condition", "camera_pos", "root", "img_calib_file"]
|
| 56 |
+
missing = [arg for arg in required_args if getattr(args, arg) is None]
|
| 57 |
+
if missing:
|
| 58 |
+
parser.error(f"Missing arguments: {', '.join(missing)} (or provide --config)")
|
| 59 |
+
|
| 60 |
+
location = args.location
|
| 61 |
+
sequence = args.sequence
|
| 62 |
+
condition = args.condition
|
| 63 |
+
camera_pos = args.camera_pos
|
| 64 |
+
root_directory = f"{args.root}/{args.condition}/KITTI-style"
|
| 65 |
+
img_calib_file = args.img_calib_file
|
| 66 |
+
|
| 67 |
+
# # User parameters
|
| 68 |
+
# # location = 'Cambogan'
|
| 69 |
+
# # sequence = '20250811_113017'
|
| 70 |
+
# # location = 'Holmview'
|
| 71 |
+
# # sequence = '20250820_130327'
|
| 72 |
+
# # location = 'Pullenvale'
|
| 73 |
+
# # sequence = '20250916_124105'
|
| 74 |
+
# location = 'Mount-Cotton'
|
| 75 |
+
# sequence = '20241217_113410'
|
| 76 |
+
# condition = 'flooded'
|
| 77 |
+
# camera_pos = 'front'
|
| 78 |
+
# root_directory = f"../Datasets/FRED/{condition}/KITTI-style"
|
| 79 |
+
# # 01000000
|
| 80 |
+
|
| 81 |
+
############ Define filenames and directories ####################################
|
| 82 |
+
|
| 83 |
+
image_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-imgs/"
|
| 84 |
+
label_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-labels/"
|
| 85 |
+
img_calib_file = f"./camera_calib.txt"
|
| 86 |
+
timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(image_dir)) if os.path.isfile(image_dir+filename)]
|
| 87 |
+
|
| 88 |
+
fig, ax = plt.subplots(figsize=(12.8, 8))
|
| 89 |
+
# idx = [0] # mutable index
|
| 90 |
+
idx = [183] # mutable index
|
| 91 |
+
|
| 92 |
+
def show_image(i):
|
| 93 |
+
ax.clear()
|
| 94 |
+
if i >= len(timestamps):
|
| 95 |
+
plt.close(fig)
|
| 96 |
+
return
|
| 97 |
+
image_timestamp = timestamps[i]
|
| 98 |
+
try:
|
| 99 |
+
image_filename = f"{image_dir}/{image_timestamp}.png"
|
| 100 |
+
label_filename = f"{label_dir}/{image_timestamp}.png"
|
| 101 |
+
image = ImageData(image_filename, img_calib_file, label_filename)
|
| 102 |
+
|
| 103 |
+
label_mask = np.any(image.colour_label != image.semantic_classes['other'], axis=-1)
|
| 104 |
+
overlay_img = image.image.copy()
|
| 105 |
+
|
| 106 |
+
# Catch for when there is no label or all pixels are labelled as 'other' (i.e. not labelled) #
|
| 107 |
+
if not np.all(image.colour_label == image.semantic_classes['other']):
|
| 108 |
+
overlay_img[label_mask] = cv2.addWeighted(image.image[label_mask], 0.5, image.colour_label[label_mask], 0.5, 0)
|
| 109 |
+
|
| 110 |
+
ax.imshow(overlay_img[:, :, ::-1])
|
| 111 |
+
ax.set_title(f"{image_timestamp}.png")
|
| 112 |
+
ax.axis("off")
|
| 113 |
+
# plt.savefig('paper_figures/semantic_image_labels.pdf', format="pdf", bbox_inches='tight')
|
| 114 |
+
fig.canvas.draw()
|
| 115 |
+
except Exception as e:
|
| 116 |
+
print(f"Could not show label for {image_timestamp}.png: {e}")
|
| 117 |
+
idx[0] += 1
|
| 118 |
+
show_image(idx[0]) # skip bad one
|
| 119 |
+
|
| 120 |
+
def on_key(event):
|
| 121 |
+
if event.key in [' ', 'right']: # space or right arrow
|
| 122 |
+
idx[0] += 1
|
| 123 |
+
show_image(idx[0])
|
| 124 |
+
elif event.key in [' ', 'left']: # space or right arrow
|
| 125 |
+
if idx[0] > 0:
|
| 126 |
+
idx[0] -= 1
|
| 127 |
+
show_image(idx[0])
|
| 128 |
+
elif event.key in ['q', 'escape']: # q or Esc → quit
|
| 129 |
+
plt.close(fig)
|
| 130 |
+
|
| 131 |
+
fig.canvas.mpl_connect('key_press_event', on_key)
|
| 132 |
+
show_image(idx[0])
|
| 133 |
+
plt.show()
|
segmentation/show_labels-single.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
utils/.gitkeep
ADDED
|
File without changes
|
utils/__init__.py
ADDED
|
File without changes
|
utils/camera.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
import cv2
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import open3d as o3d
|
| 6 |
+
from utils.utils import read_calib_file #, fill_projected_os1_rings
|
| 7 |
+
|
| 8 |
+
color_key = {
|
| 9 |
+
0: [0, 0, 128], # road
|
| 10 |
+
1: [0, 128, 0], # water
|
| 11 |
+
2: [0, 0, 0] # other
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
class ImageData():
|
| 15 |
+
def __init__(self, file_path, calib_path, label_path=None):
|
| 16 |
+
self.image = cv2.imread(file_path)
|
| 17 |
+
|
| 18 |
+
intrinsics = read_calib_file(calib_path)
|
| 19 |
+
focal_len = intrinsics['focal_len'][0]
|
| 20 |
+
principal_x = intrinsics['principal_x'][0]
|
| 21 |
+
principal_y = intrinsics['principal_y'][0]
|
| 22 |
+
pp_mm_x = intrinsics['pp_mm_x'][0]
|
| 23 |
+
pp_mm_y = intrinsics['pp_mm_y'][0]
|
| 24 |
+
|
| 25 |
+
# print(f"{focal_len}, {principal_x}, {principal_y}, {pp_mm_x}, {pp_mm_y}")
|
| 26 |
+
|
| 27 |
+
self.camera_matrix = self.create_camera_matrix(focal_len, principal_x, principal_y, pp_mm_x, pp_mm_y)
|
| 28 |
+
self.dist_coeffs = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
|
| 29 |
+
|
| 30 |
+
self.semantic_classes = None
|
| 31 |
+
self.colour_label = None
|
| 32 |
+
self.semantic_classes = {'road': [0, 0, 128], 'water': [0, 128, 0], 'other': [0, 0, 0]} # In the opencv BGR convention
|
| 33 |
+
|
| 34 |
+
if label_path is not None:
|
| 35 |
+
if os.path.exists(label_path):
|
| 36 |
+
label_img = cv2.imread(label_path)
|
| 37 |
+
else:
|
| 38 |
+
# Catch for no label existing #
|
| 39 |
+
print(f"Could not load label \'{label_path}\'. Using blank labels.")
|
| 40 |
+
label_img = np.zeros(self.image.shape).astype(np.uint8)
|
| 41 |
+
self.colour_label = cv2.resize(label_img, (self.image.shape[1], self.image.shape[0]), interpolation=cv2.INTER_NEAREST)
|
| 42 |
+
|
| 43 |
+
H, W, _ = label_img.shape
|
| 44 |
+
self.label_img = np.full((H, W), 2, dtype=np.uint8) # unknown = 255
|
| 45 |
+
|
| 46 |
+
# Convert to uint8 in case it's float
|
| 47 |
+
img = label_img.astype(np.uint8)
|
| 48 |
+
|
| 49 |
+
for class_idx, color in color_key.items():
|
| 50 |
+
# Create mask where all 3 channels match
|
| 51 |
+
mask = np.all(img == color, axis=2)
|
| 52 |
+
self.label_img[mask] = class_idx
|
| 53 |
+
|
| 54 |
+
self.label_img = cv2.resize(self.label_img, (self.image.shape[1], self.image.shape[0]), interpolation=cv2.INTER_NEAREST)
|
| 55 |
+
|
| 56 |
+
def create_camera_matrix(self, focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
|
| 57 |
+
pixels_per_mm_x, pixels_per_mm_y):
|
| 58 |
+
"""
|
| 59 |
+
Create camera matrix from physical camera parameters.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
focal_length_mm: Focal length in millimeters
|
| 63 |
+
principal_point_x_pixels: Principal point X coordinate in pixels
|
| 64 |
+
principal_point_y_pixels: Principal point Y coordinate in pixels
|
| 65 |
+
pixels_per_mm_x: Pixels per millimeter in X direction
|
| 66 |
+
pixels_per_mm_y: Pixels per millimeter in Y direction
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
camera_matrix: 3x3 camera intrinsic matrix
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
# Check for None or invalid values
|
| 73 |
+
if any(x is None for x in [focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
|
| 74 |
+
pixels_per_mm_x, pixels_per_mm_y]):
|
| 75 |
+
print("ERROR: One or more input parameters is None!")
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
if pixels_per_mm_x == 0 or pixels_per_mm_y == 0:
|
| 79 |
+
print("ERROR: pixels_per_mm cannot be zero!")
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
# Convert focal length from mm to pixels
|
| 84 |
+
fx = focal_length_mm * pixels_per_mm_x
|
| 85 |
+
fy = focal_length_mm * pixels_per_mm_y
|
| 86 |
+
|
| 87 |
+
# Principal point is already in pixels
|
| 88 |
+
cx = principal_point_x_pixels
|
| 89 |
+
cy = principal_point_y_pixels
|
| 90 |
+
|
| 91 |
+
camera_matrix = np.array([
|
| 92 |
+
[fx, 0, cx],
|
| 93 |
+
[0, fy, cy],
|
| 94 |
+
[0, 0, 1]
|
| 95 |
+
], dtype=np.float64)
|
| 96 |
+
|
| 97 |
+
return camera_matrix
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(f"ERROR in create_camera_matrix: {e}")
|
| 101 |
+
return None
|
| 102 |
+
|
| 103 |
+
def project_points(self, points, colours, cmap, valid_cam, colour_norm=None):
|
| 104 |
+
# , beam_id, azimuth
|
| 105 |
+
# Project to image coordinates
|
| 106 |
+
rvec = np.zeros(3) # No additional rotation
|
| 107 |
+
tvec = np.zeros(3) # No additional translation
|
| 108 |
+
|
| 109 |
+
image_points, _ = cv2.projectPoints(points, rvec, tvec, self.camera_matrix, self.dist_coeffs)
|
| 110 |
+
|
| 111 |
+
image_points = image_points.reshape(-1, 2)
|
| 112 |
+
|
| 113 |
+
# Filter points within image bounds
|
| 114 |
+
h, w = self.image.shape[0], self.image.shape[1]
|
| 115 |
+
valid_img_mask = ((image_points[:, 0] >= 0) & (image_points[:, 0] < w) &
|
| 116 |
+
(image_points[:, 1] >= 0) & (image_points[:, 1] < h))
|
| 117 |
+
|
| 118 |
+
valid_mask = valid_img_mask & valid_cam
|
| 119 |
+
points2project = image_points[valid_mask]
|
| 120 |
+
if colour_norm is None:
|
| 121 |
+
colour2project = colours[valid_mask]/colours[valid_img_mask].max()
|
| 122 |
+
else:
|
| 123 |
+
colour2project = colours[valid_mask]/colour_norm
|
| 124 |
+
|
| 125 |
+
img_vis = self.image.copy()
|
| 126 |
+
# Draw points
|
| 127 |
+
for (point, c) in zip(points2project.astype(int), colour2project):
|
| 128 |
+
r, g, b, _ = cmap(c)
|
| 129 |
+
colour = (r*255, g*255, b*255)
|
| 130 |
+
cv2.circle(img_vis, (int(point[0]), int(point[1])), 5, colour, -1) # -1 = filled circle
|
| 131 |
+
|
| 132 |
+
return img_vis, image_points, valid_img_mask
|
| 133 |
+
|
| 134 |
+
def get_image_coords(self, points):
|
| 135 |
+
# Project to image coordinates
|
| 136 |
+
rvec = np.zeros(3) # No additional rotation
|
| 137 |
+
tvec = np.zeros(3) # No additional translation
|
| 138 |
+
|
| 139 |
+
image_points, _ = cv2.projectPoints(points, rvec, tvec, self.camera_matrix, self.dist_coeffs)
|
| 140 |
+
|
| 141 |
+
image_points = image_points.reshape(-1, 2)
|
| 142 |
+
|
| 143 |
+
return image_points
|
utils/lidar.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import os
|
| 3 |
+
from utils.utils import read_calib_file #, compute_os1_angles, elevation_to_beam_id
|
| 4 |
+
|
| 5 |
+
INLIER_BIT = 1 << 8
|
| 6 |
+
INSTANCE_SHIFT = 16
|
| 7 |
+
beam_altitude_angles_deg = np.array([
|
| 8 |
+
20.97, 18.51, 15.97, 13.37, 12.04, 10.70, 9.35, 8.70,
|
| 9 |
+
7.99, 7.34, 6.62, 5.96, 5.23, 4.55, 3.84, 3.51,
|
| 10 |
+
3.17, 2.82, 2.46, 2.11, 1.76, 1.43, 1.05, 0.70,
|
| 11 |
+
0.36, 0.03, -0.34, -0.70, -1.04, -1.37, -1.76, -2.10,
|
| 12 |
+
-2.43, -2.77, -3.15, -3.48, -3.84, -4.18, -4.54, -4.88,
|
| 13 |
+
-5.24, -5.55, -5.93, -6.29, -6.63, -6.94, -7.32, -7.67,
|
| 14 |
+
-8.00, -8.33, -9.04, -9.71, -10.42, -11.06, -11.77, -12.41,
|
| 15 |
+
-13.12, -13.74, -15.06, -16.36, -17.64, -18.91, -20.16, -21.38
|
| 16 |
+
])
|
| 17 |
+
|
| 18 |
+
class PointCloud:
|
| 19 |
+
def __init__(self, file_path, calib_path):
|
| 20 |
+
|
| 21 |
+
self.points = self.load_pointcloud(file_path)
|
| 22 |
+
ground_labels = np.fromfile(f"{file_path.split('/ouster')[0]}/ouster_ground_labels/{file_path.split('/')[-1].split('.bin')[0]}.label", dtype=np.uint32)
|
| 23 |
+
# points = self.load_pointcloud(file_path)
|
| 24 |
+
# self.points = points[self.ground_labels==0,:]
|
| 25 |
+
self.ground_semantic = ground_labels & 0xFF
|
| 26 |
+
self.ground_inlier = (ground_labels & INLIER_BIT) != 0
|
| 27 |
+
|
| 28 |
+
calibration = read_calib_file(calib_path)
|
| 29 |
+
self.P2, self.R0, self.Tr4 = self.get_matrices(calibration)
|
| 30 |
+
|
| 31 |
+
self.pixel_shift_by_row = [12, 12, 12, 12, 12, 12, 12, -4,
|
| 32 |
+
12, -4, 12, -4, 12, -4, 12, 4,
|
| 33 |
+
-4, -12, 12, 4, -4, -12, 12, 4,
|
| 34 |
+
-4, -12, 12, 4, -4, -12, 12, 4,
|
| 35 |
+
-4, -12, 12, 4, -4, -12, 12, 4,
|
| 36 |
+
-4, -12, 12, 4, -4, -12, 12, 4,
|
| 37 |
+
-4, -12, 4, -12, 4, -12, 4, -12,
|
| 38 |
+
4, -12, -12, -12, -12, -12, -12, -12]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def load_pointcloud(self, file_path):
|
| 43 |
+
"""
|
| 44 |
+
Load point cloud from various formats including .bin files
|
| 45 |
+
Assumes each point has 4 values (x, y, z, reflectivity)
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
file_path (str): Path to point cloud file
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
np.array: Nx4 array of 3D points with
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
| 55 |
+
|
| 56 |
+
if not file_ext == '.bin':
|
| 57 |
+
raise ValueError(f"Given file is not binary format: {file_path}")
|
| 58 |
+
|
| 59 |
+
points = np.fromfile(file_path, dtype=np.float32)
|
| 60 |
+
|
| 61 |
+
if len(points) % 4 == 0:
|
| 62 |
+
points_array = points.reshape(-1, 4)
|
| 63 |
+
|
| 64 |
+
return points_array
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_matrices(self,calib):
|
| 68 |
+
# P2 (3x4)
|
| 69 |
+
P2 = calib['P2'].reshape(3,4)
|
| 70 |
+
# R0_rect (3x3) or identity
|
| 71 |
+
R0 = calib.get('R0_rect', np.array([1,0,0,0,1,0,0,0,1])).reshape(3,3)
|
| 72 |
+
# Tr_ouster_to_cam (3x4)
|
| 73 |
+
Tr = calib['Tr_ouster_to_cam'].reshape(3,4)
|
| 74 |
+
# Make 4x4 homogeneous
|
| 75 |
+
Tr4 = np.vstack((Tr, [0,0,0,1]))
|
| 76 |
+
|
| 77 |
+
return P2, R0, Tr4
|
| 78 |
+
|
| 79 |
+
def points_ouster_to_cam(self,):
|
| 80 |
+
"""
|
| 81 |
+
pts_ouster: (N,3) numpy array in LiDAR frame.
|
| 82 |
+
Tr4: 4x4 homogeneous transform from ouster -> camera.
|
| 83 |
+
Returns pts_cam (N,3) in camera coordinates.
|
| 84 |
+
"""
|
| 85 |
+
n = self.points.shape[0]
|
| 86 |
+
|
| 87 |
+
pts_xyz = self.points[:, :3]
|
| 88 |
+
|
| 89 |
+
pts_h = np.column_stack((pts_xyz, np.ones((n)))) # (N,4)
|
| 90 |
+
pts_cam_h = (self.Tr4 @ pts_h.T).T[:,:3] # (N,4)
|
| 91 |
+
|
| 92 |
+
valid = pts_cam_h[:,2] > 1e-6
|
| 93 |
+
|
| 94 |
+
distances = np.linalg.norm(pts_cam_h[valid,:], axis=1)
|
| 95 |
+
|
| 96 |
+
return pts_cam_h[valid,:], np.linalg.norm(pts_cam_h, axis=1), self.points[:,3], pts_cam_h, valid
|
| 97 |
+
|
| 98 |
+
def select_points_ouster_to_cam(self, points):
|
| 99 |
+
"""
|
| 100 |
+
pts_ouster: (N,3) numpy array in LiDAR frame.
|
| 101 |
+
Tr4: 4x4 homogeneous transform from ouster -> camera.
|
| 102 |
+
Returns pts_cam (N,3) in camera coordinates.
|
| 103 |
+
"""
|
| 104 |
+
n = points.shape[0]
|
| 105 |
+
pts_h = np.column_stack((points[:,:3], np.ones((n)))) # (N,4)
|
| 106 |
+
pts_cam_h = (self.Tr4 @ pts_h.T).T[:,:3] # (N,4)
|
| 107 |
+
|
| 108 |
+
valid = pts_cam_h[:,2] > 1e-6
|
| 109 |
+
# valid = np.ones((pts_cam_h.shape[0]))
|
| 110 |
+
|
| 111 |
+
distances = np.linalg.norm(pts_cam_h[valid,:], axis=1)
|
| 112 |
+
|
| 113 |
+
return pts_cam_h[valid,:], distances, points[valid,3]
|
| 114 |
+
|
| 115 |
+
def destagger(self, points, ground_labels, inlier_labels):
|
| 116 |
+
|
| 117 |
+
pixel_shift_by_row = np.array(self.pixel_shift_by_row)
|
| 118 |
+
|
| 119 |
+
H = 64
|
| 120 |
+
W = 1024
|
| 121 |
+
|
| 122 |
+
# pc_img = points.reshape(W, H, 4).transpose(1, 0, 2)
|
| 123 |
+
# # shape: (64, 1024, 4)
|
| 124 |
+
|
| 125 |
+
# rows = np.arange(H)[:, None]
|
| 126 |
+
# cols = np.arange(W)[None, :]
|
| 127 |
+
|
| 128 |
+
# pc_destaggered = pc_img[
|
| 129 |
+
# rows,
|
| 130 |
+
# (cols - pixel_shift_by_row[:, None]) % W,
|
| 131 |
+
# :
|
| 132 |
+
# ]
|
| 133 |
+
|
| 134 |
+
# return pc_destaggered.reshape(-1, 4)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# --- reshape ---
|
| 138 |
+
pc_img = points.reshape(W, H, 4).transpose(1, 0, 2) # (64, 1024, 4)
|
| 139 |
+
lbl_img = ground_labels.reshape(W, H).T # (64, 1024)
|
| 140 |
+
inlier_img = inlier_labels.reshape(W, H).T
|
| 141 |
+
|
| 142 |
+
rows = np.arange(H)[:, None]
|
| 143 |
+
cols = np.arange(W)[None, :]
|
| 144 |
+
|
| 145 |
+
# --- destagger (IDENTICAL indexing) ---
|
| 146 |
+
pc_destaggered = pc_img[
|
| 147 |
+
rows,
|
| 148 |
+
(cols - pixel_shift_by_row[:, None]) % W,
|
| 149 |
+
:
|
| 150 |
+
]
|
| 151 |
+
|
| 152 |
+
lbl_destaggered = lbl_img[
|
| 153 |
+
rows,
|
| 154 |
+
(cols - pixel_shift_by_row[:, None]) % W
|
| 155 |
+
]
|
| 156 |
+
|
| 157 |
+
inlier_destaggered = inlier_img[
|
| 158 |
+
rows,
|
| 159 |
+
(cols - pixel_shift_by_row[:, None]) % W
|
| 160 |
+
]
|
| 161 |
+
|
| 162 |
+
return pc_destaggered.reshape(-1, 4), lbl_destaggered.reshape(-1), inlier_destaggered.reshape(-1)
|
| 163 |
+
|
utils/utils.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import os
|
| 3 |
+
import cv2
|
| 4 |
+
import open3d as o3d
|
| 5 |
+
from natsort import natsorted
|
| 6 |
+
|
| 7 |
+
beam_altitudes = np.deg2rad(np.array([
|
| 8 |
+
20.97, 18.51, 15.97, 13.37, 12.04, 10.70, 9.35, 8.70,
|
| 9 |
+
7.99, 7.34, 6.62, 5.96, 5.23, 4.55, 3.84, 3.51,
|
| 10 |
+
3.17, 2.82, 2.46, 2.11, 1.76, 1.43, 1.05, 0.70,
|
| 11 |
+
0.36, 0.03, -0.34, -0.70, -1.04, -1.37, -1.76, -2.10,
|
| 12 |
+
-2.43, -2.77, -3.15, -3.48, -3.84, -4.18, -4.54, -4.88,
|
| 13 |
+
-5.24, -5.55, -5.93, -6.29, -6.63, -6.94, -7.32, -7.67,
|
| 14 |
+
-8.00, -8.33, -9.04, -9.71, -10.42, -11.06, -11.77, -12.41,
|
| 15 |
+
-13.12, -13.74, -15.06, -16.36, -17.64, -18.91, -20.16, -21.38
|
| 16 |
+
]))
|
| 17 |
+
|
| 18 |
+
def read_calib_file(path):
|
| 19 |
+
"""
|
| 20 |
+
Read KITTI-style calibration file lines like:
|
| 21 |
+
parameter: <value>
|
| 22 |
+
parameter: <value>
|
| 23 |
+
...
|
| 24 |
+
Returns a dict mapping keys to numpy arrays.
|
| 25 |
+
"""
|
| 26 |
+
d = {}
|
| 27 |
+
with open(path, 'r') as f:
|
| 28 |
+
for line in f:
|
| 29 |
+
line = line.strip()
|
| 30 |
+
if not line or ':' not in line or line.startswith('#'):
|
| 31 |
+
continue
|
| 32 |
+
key, vals = line.split(':', 1)
|
| 33 |
+
nums = [float(x) for x in vals.strip().split()]
|
| 34 |
+
d[key.strip()] = np.array(nums)
|
| 35 |
+
return d
|
| 36 |
+
|
| 37 |
+
def get_all_corr_files(image_timestamps, dirs, tol=200000, adjust=None):
|
| 38 |
+
|
| 39 |
+
single = not isinstance(image_timestamps, (list, np.ndarray))
|
| 40 |
+
if single:
|
| 41 |
+
image_timestamps = [image_timestamps]
|
| 42 |
+
|
| 43 |
+
image_timestamps = np.array(image_timestamps, dtype=np.int64)
|
| 44 |
+
|
| 45 |
+
results_per_dir = []
|
| 46 |
+
indices_per_dir = []
|
| 47 |
+
for dir in dirs:
|
| 48 |
+
if adjust is None:
|
| 49 |
+
filenames = np.array([f for f in natsorted(os.listdir(dir)) if os.path.isfile(dir + f)])
|
| 50 |
+
else:
|
| 51 |
+
filenames = np.array([f for f in natsorted(os.listdir(dir)) if os.path.isfile(dir + f)])[:adjust]
|
| 52 |
+
|
| 53 |
+
timestamps = np.array([int(f.split('.')[0]) for f in filenames], dtype=np.int64)
|
| 54 |
+
|
| 55 |
+
diffs = np.abs(image_timestamps[:, None] - timestamps[None, :])
|
| 56 |
+
closest_indices = np.argmin(diffs, axis=1)
|
| 57 |
+
closest_diffs = diffs[np.arange(len(image_timestamps)), closest_indices]
|
| 58 |
+
|
| 59 |
+
violations = closest_diffs > tol
|
| 60 |
+
if violations.any():
|
| 61 |
+
bad_ts = image_timestamps[violations]
|
| 62 |
+
raise Exception(f"No timestamp in {dir} within tolerance for timestamps: {bad_ts}")
|
| 63 |
+
|
| 64 |
+
results_per_dir.append(filenames[closest_indices])
|
| 65 |
+
indices_per_dir.append(closest_indices)
|
| 66 |
+
|
| 67 |
+
if len(dirs) == 1:
|
| 68 |
+
results = [f"{dirs[0]}/{f}" for f in results_per_dir[0]]
|
| 69 |
+
indices = indices_per_dir[0].tolist()
|
| 70 |
+
else:
|
| 71 |
+
results = [
|
| 72 |
+
tuple(f"{dirs[i]}/{results_per_dir[i][j]}" for i in range(len(dirs)))
|
| 73 |
+
for j in range(len(image_timestamps))
|
| 74 |
+
]
|
| 75 |
+
indices = [
|
| 76 |
+
tuple(int(indices_per_dir[i][j]) for i in range(len(dirs)))
|
| 77 |
+
for j in range(len(image_timestamps))
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
if single:
|
| 81 |
+
return results[0], indices[0]
|
| 82 |
+
return results, indices
|
| 83 |
+
|
| 84 |
+
def get_corr_files(image_timestamp, dirs, tol=200000):
|
| 85 |
+
output_filenames = []
|
| 86 |
+
for dir in dirs:
|
| 87 |
+
############ Find closest point cloud and UTM position data ########################
|
| 88 |
+
filenames = np.array([filename for filename in os.listdir(dir) if os.path.isfile(dir+filename)])
|
| 89 |
+
timestamps = np.array([int(filename.split('.')[0]) for filename in os.listdir(dir) if os.path.isfile(dir+filename)])
|
| 90 |
+
|
| 91 |
+
closest_lidar = np.argmin(abs(timestamps-int(image_timestamp)))
|
| 92 |
+
timestamp_diff = abs(int(image_timestamp)-timestamps[closest_lidar])
|
| 93 |
+
|
| 94 |
+
timestamp_tolerance = tol # in microseconds (0.2 seconds)
|
| 95 |
+
|
| 96 |
+
if timestamp_diff > timestamp_tolerance:
|
| 97 |
+
raise Exception(f"No timestamp in {dir} in close enough proximity to {image_timestamp}")
|
| 98 |
+
else:
|
| 99 |
+
output_filenames.append(f"{dir}/{filenames[closest_lidar]}")
|
| 100 |
+
|
| 101 |
+
if len(dirs) == 1:
|
| 102 |
+
return output_filenames[0]
|
| 103 |
+
else:
|
| 104 |
+
return tuple(output_filenames)
|
| 105 |
+
|
| 106 |
+
NUM_COLS = 1024
|
| 107 |
+
|
| 108 |
+
def compute_column_index(points_xyz):
|
| 109 |
+
az = np.arctan2(points_xyz[:, 1], points_xyz[:, 0])
|
| 110 |
+
az = np.mod(az, 2 * np.pi)
|
| 111 |
+
col = np.round(az / (2 * np.pi) * NUM_COLS).astype(int)
|
| 112 |
+
return col % NUM_COLS
|
| 113 |
+
|
| 114 |
+
# beam_altitudes = np.deg2rad(np.array(beam_altitude_angles))
|
| 115 |
+
|
| 116 |
+
def compute_ring_ids(points_xyz):
|
| 117 |
+
xy_norm = np.linalg.norm(points_xyz[:, :2], axis=1)
|
| 118 |
+
elev = np.arctan2(points_xyz[:, 2], xy_norm)
|
| 119 |
+
|
| 120 |
+
return np.argmin(
|
| 121 |
+
np.abs(elev[:, None] - beam_altitudes[None, :]),
|
| 122 |
+
axis=1
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
def compute_ring_col_from_index(num_points, num_cols=1024):
|
| 126 |
+
idx = np.arange(num_points)
|
| 127 |
+
ring_ids = idx // num_cols
|
| 128 |
+
col_ids = idx % num_cols
|
| 129 |
+
return ring_ids, col_ids
|
| 130 |
+
|
| 131 |
+
def is_valid_point(points_xyz):
|
| 132 |
+
return np.linalg.norm(points_xyz, axis=1) > 1e-6
|
| 133 |
+
|
| 134 |
+
def fill_ring_known_cols_with_intensity(points_xyzi, ring_ids, col_ids):
|
| 135 |
+
"""
|
| 136 |
+
points_xyzi: (N,4) -> x,y,z,intensity
|
| 137 |
+
"""
|
| 138 |
+
filled_points = []
|
| 139 |
+
interp_flags = []
|
| 140 |
+
|
| 141 |
+
unique_ring_ids = np.array([i for i in range(0,64)])
|
| 142 |
+
|
| 143 |
+
for ring in unique_ring_ids:
|
| 144 |
+
ring_mask = ring_ids == ring
|
| 145 |
+
ring_pts = points_xyzi[ring_mask]
|
| 146 |
+
ring_cols = col_ids[ring_mask]
|
| 147 |
+
|
| 148 |
+
# storage per column
|
| 149 |
+
ring_grid = [None] * NUM_COLS
|
| 150 |
+
|
| 151 |
+
# place original points
|
| 152 |
+
for p, c in zip(ring_pts, ring_cols):
|
| 153 |
+
ring_grid[c] = p # keep intensity
|
| 154 |
+
|
| 155 |
+
elev = beam_altitudes[ring]
|
| 156 |
+
cos_e, sin_e = np.cos(elev), np.sin(elev)
|
| 157 |
+
|
| 158 |
+
for c in range(NUM_COLS):
|
| 159 |
+
if ring_grid[c] is not None:
|
| 160 |
+
filled_points.append(ring_grid[c])
|
| 161 |
+
interp_flags.append(False)
|
| 162 |
+
else:
|
| 163 |
+
# find neighbors cyclically
|
| 164 |
+
left = (c - 1) % NUM_COLS
|
| 165 |
+
right = (c + 1) % NUM_COLS
|
| 166 |
+
|
| 167 |
+
while ring_grid[left] is None:
|
| 168 |
+
left = (left - 1) % NUM_COLS
|
| 169 |
+
while ring_grid[right] is None:
|
| 170 |
+
right = (right + 1) % NUM_COLS
|
| 171 |
+
|
| 172 |
+
p0, p1 = ring_grid[left], ring_grid[right]
|
| 173 |
+
|
| 174 |
+
r0 = np.linalg.norm(p0[:3])
|
| 175 |
+
r1 = np.linalg.norm(p1[:3])
|
| 176 |
+
|
| 177 |
+
d = (c - left) % NUM_COLS
|
| 178 |
+
span = (right - left) % NUM_COLS
|
| 179 |
+
t = d / span if span > 0 else 0.0
|
| 180 |
+
r = (1 - t) * r0 + t * r1
|
| 181 |
+
|
| 182 |
+
az = 2 * np.pi * c / NUM_COLS
|
| 183 |
+
|
| 184 |
+
x = r * cos_e * np.cos(az)
|
| 185 |
+
y = r * cos_e * np.sin(az)
|
| 186 |
+
z = r * sin_e
|
| 187 |
+
|
| 188 |
+
filled_points.append([x, y, z, 255])
|
| 189 |
+
interp_flags.append(True)
|
| 190 |
+
|
| 191 |
+
return np.array(filled_points), np.array(interp_flags)
|
| 192 |
+
|
| 193 |
+
def fill_ring_known_cols_with_intensity_and_plane(
|
| 194 |
+
points_xyzi,
|
| 195 |
+
ring_ids,
|
| 196 |
+
col_ids,
|
| 197 |
+
plane,
|
| 198 |
+
max_range=200.0
|
| 199 |
+
):
|
| 200 |
+
"""
|
| 201 |
+
points_xyzi: (N,4) -> x,y,z,intensity
|
| 202 |
+
"""
|
| 203 |
+
a, b, c, d = plane
|
| 204 |
+
|
| 205 |
+
filled_points = []
|
| 206 |
+
interp_flags = []
|
| 207 |
+
|
| 208 |
+
unique_ring_ids = np.array([i for i in range(0,64)])
|
| 209 |
+
|
| 210 |
+
for ring in unique_ring_ids:
|
| 211 |
+
# print(f"ring {ring}")
|
| 212 |
+
ring_mask = ring_ids == ring
|
| 213 |
+
ring_pts = points_xyzi[ring_mask]
|
| 214 |
+
ring_cols = col_ids[ring_mask]
|
| 215 |
+
|
| 216 |
+
ring_grid = [None] * NUM_COLS
|
| 217 |
+
# print(f"ring grid shape: {np.array(ring_grid).shape}")
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
# place original points
|
| 221 |
+
for p, col in zip(ring_pts, ring_cols):
|
| 222 |
+
ring_grid[col] = p
|
| 223 |
+
|
| 224 |
+
elev = beam_altitudes[ring]
|
| 225 |
+
cos_e, sin_e = np.cos(elev), np.sin(elev)
|
| 226 |
+
|
| 227 |
+
for c_idx in range(NUM_COLS):
|
| 228 |
+
|
| 229 |
+
# print(f"c_idx: {c_idx}")
|
| 230 |
+
|
| 231 |
+
if ring_grid[c_idx] is not None:
|
| 232 |
+
filled_points.append(ring_grid[c_idx])
|
| 233 |
+
interp_flags.append(False)
|
| 234 |
+
continue
|
| 235 |
+
|
| 236 |
+
# find neighbors cyclically
|
| 237 |
+
# print(f"Find neighbours")
|
| 238 |
+
# left = (c_idx - 1) % NUM_COLS
|
| 239 |
+
# right = (c_idx + 1) % NUM_COLS
|
| 240 |
+
|
| 241 |
+
# while ring_grid[left] is None:
|
| 242 |
+
# left = (left - 1) % NUM_COLS
|
| 243 |
+
# while ring_grid[right] is None:
|
| 244 |
+
# right = (right + 1) % NUM_COLS
|
| 245 |
+
|
| 246 |
+
# print(f"p0, p1")
|
| 247 |
+
|
| 248 |
+
# p0, p1 = ring_grid[left], ring_grid[right]
|
| 249 |
+
|
| 250 |
+
# r0 = np.linalg.norm(p0[:3])
|
| 251 |
+
# r1 = np.linalg.norm(p1[:3])
|
| 252 |
+
|
| 253 |
+
# print(f"dcol, span")
|
| 254 |
+
|
| 255 |
+
# dcol = (c_idx - left) % NUM_COLS
|
| 256 |
+
# span = (right - left) % NUM_COLS
|
| 257 |
+
# t_interp = dcol / span if span > 0 else 0.0
|
| 258 |
+
|
| 259 |
+
# r_guess = (1 - t_interp) * r0 + t_interp * r1
|
| 260 |
+
|
| 261 |
+
# print(f"az")
|
| 262 |
+
|
| 263 |
+
az = 2 * np.pi * c_idx / NUM_COLS
|
| 264 |
+
|
| 265 |
+
# Ray direction
|
| 266 |
+
dx = cos_e * np.cos(az)
|
| 267 |
+
dy = cos_e * np.sin(az)
|
| 268 |
+
dz = sin_e
|
| 269 |
+
|
| 270 |
+
denom = a * dx + b * dy + c * dz
|
| 271 |
+
if abs(denom) < 1e-6:
|
| 272 |
+
# print(f"point in rang {ring} is parallel")
|
| 273 |
+
continue # ray parallel to plane
|
| 274 |
+
|
| 275 |
+
t_plane = -d / denom
|
| 276 |
+
# if t_plane <= 0 or t_plane > max_range:
|
| 277 |
+
# # print(f"point in rang {ring} is {t_plane}m away")
|
| 278 |
+
# continue
|
| 279 |
+
|
| 280 |
+
# print(f"Fill point")
|
| 281 |
+
|
| 282 |
+
x = t_plane * dx
|
| 283 |
+
y = t_plane * dy
|
| 284 |
+
z = t_plane * dz
|
| 285 |
+
|
| 286 |
+
filled_points.append([x, y, z, 255])
|
| 287 |
+
interp_flags.append(True)
|
| 288 |
+
|
| 289 |
+
return np.asarray(filled_points), np.asarray(interp_flags)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def complete_cloud(points_xyzi, plane):
|
| 293 |
+
# ring_ids = compute_ring_ids(points_xyzi[:, :3])
|
| 294 |
+
# col_ids = compute_column_index(points_xyzi[:, :3])
|
| 295 |
+
ring_ids, col_ids = compute_ring_col_from_index(
|
| 296 |
+
len(points_xyzi), NUM_COLS
|
| 297 |
+
)
|
| 298 |
+
valid_mask = is_valid_point(points_xyzi[:, :3])
|
| 299 |
+
|
| 300 |
+
# return fill_ring_known_cols_with_intensity_and_plane(points_xyzi, ring_ids, col_ids, plane)
|
| 301 |
+
return fill_ring_known_cols_with_intensity_and_heightfield(points_xyzi, ring_ids, col_ids, valid_mask, plane)
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def assign_semantic_labels(
|
| 305 |
+
points_xyz,
|
| 306 |
+
uv,
|
| 307 |
+
valid,
|
| 308 |
+
semantic_image,
|
| 309 |
+
interp_flags=None,
|
| 310 |
+
unknown_label=-1
|
| 311 |
+
):
|
| 312 |
+
"""
|
| 313 |
+
semantic_image: (H, W) or (H, W, 1) integer labels
|
| 314 |
+
"""
|
| 315 |
+
H, W = semantic_image.shape[:2]
|
| 316 |
+
N = points_xyz.shape[0]
|
| 317 |
+
|
| 318 |
+
labels = np.full(N, unknown_label, dtype=np.uint8)
|
| 319 |
+
|
| 320 |
+
# round to nearest pixel
|
| 321 |
+
u = np.round(uv[:, 0]).astype(int)
|
| 322 |
+
v = np.round(uv[:, 1]).astype(int)
|
| 323 |
+
|
| 324 |
+
inside = (
|
| 325 |
+
valid &
|
| 326 |
+
(u >= 0) & (u < W) &
|
| 327 |
+
(v >= 0) & (v < H)
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
labels[inside] = semantic_image[v[inside], u[inside]]
|
| 331 |
+
|
| 332 |
+
# Optional: mark interpolated points as unknown
|
| 333 |
+
if interp_flags is not None:
|
| 334 |
+
labels[interp_flags] = unknown_label
|
| 335 |
+
|
| 336 |
+
return labels
|
| 337 |
+
|
| 338 |
+
def fit_height_field_linear(ground_points):
|
| 339 |
+
"""
|
| 340 |
+
Fit z = ax + by + c to ground points
|
| 341 |
+
"""
|
| 342 |
+
X = ground_points[:, 0]
|
| 343 |
+
Y = ground_points[:, 1]
|
| 344 |
+
Z = ground_points[:, 2]
|
| 345 |
+
|
| 346 |
+
A = np.column_stack((X, Y, np.ones_like(X)))
|
| 347 |
+
coef, _, _, _ = np.linalg.lstsq(A, Z, rcond=None)
|
| 348 |
+
|
| 349 |
+
a, b, c = coef
|
| 350 |
+
return a, b, c
|
| 351 |
+
|
| 352 |
+
def fill_ring_known_cols_with_intensity_and_heightfield(
|
| 353 |
+
points_xyzi,
|
| 354 |
+
ring_ids,
|
| 355 |
+
col_ids,
|
| 356 |
+
valid_mask,
|
| 357 |
+
height_field, # (a, b, c)
|
| 358 |
+
max_range=200.0
|
| 359 |
+
):
|
| 360 |
+
"""
|
| 361 |
+
points_xyzi: (N,4) -> x,y,z,intensity
|
| 362 |
+
"""
|
| 363 |
+
a, b, c = height_field
|
| 364 |
+
|
| 365 |
+
filled_points = []
|
| 366 |
+
interp_flags = []
|
| 367 |
+
|
| 368 |
+
for ring in range(64):
|
| 369 |
+
ring_mask = ring_ids == ring
|
| 370 |
+
|
| 371 |
+
ring_pts = points_xyzi[ring_mask]
|
| 372 |
+
ring_cols = col_ids[ring_mask]
|
| 373 |
+
ring_valid = valid_mask[ring_mask]
|
| 374 |
+
|
| 375 |
+
ring_grid = [None] * NUM_COLS
|
| 376 |
+
|
| 377 |
+
# Place ONLY valid original points
|
| 378 |
+
for p, col, v in zip(ring_pts, ring_cols, ring_valid):
|
| 379 |
+
if v:
|
| 380 |
+
ring_grid[col] = p
|
| 381 |
+
|
| 382 |
+
elev = beam_altitudes[ring]
|
| 383 |
+
cos_e, sin_e = np.cos(elev), np.sin(elev)
|
| 384 |
+
|
| 385 |
+
for c_idx in range(NUM_COLS):
|
| 386 |
+
|
| 387 |
+
if ring_grid[c_idx] is not None:
|
| 388 |
+
filled_points.append(ring_grid[c_idx])
|
| 389 |
+
interp_flags.append(False)
|
| 390 |
+
continue
|
| 391 |
+
|
| 392 |
+
# Missing return → ray cast
|
| 393 |
+
az = 2 * np.pi * c_idx / NUM_COLS
|
| 394 |
+
|
| 395 |
+
dx = cos_e * np.cos(az)
|
| 396 |
+
dy = cos_e * np.sin(az)
|
| 397 |
+
dz = sin_e
|
| 398 |
+
|
| 399 |
+
denom = dz - a * dx - b * dy
|
| 400 |
+
if abs(denom) < 1e-6:
|
| 401 |
+
continue
|
| 402 |
+
|
| 403 |
+
t = c / denom
|
| 404 |
+
if t <= 0 or t > max_range:
|
| 405 |
+
continue
|
| 406 |
+
|
| 407 |
+
x = t * dx
|
| 408 |
+
y = t * dy
|
| 409 |
+
z = t * dz
|
| 410 |
+
|
| 411 |
+
filled_points.append([x, y, z, 255])
|
| 412 |
+
interp_flags.append(True)
|
| 413 |
+
|
| 414 |
+
return np.asarray(filled_points), np.asarray(interp_flags)
|
| 415 |
+
# a, b, c = height_field
|
| 416 |
+
|
| 417 |
+
# filled_points = []
|
| 418 |
+
# interp_flags = []
|
| 419 |
+
|
| 420 |
+
# unique_ring_ids = np.arange(64)
|
| 421 |
+
|
| 422 |
+
# for ring in unique_ring_ids:
|
| 423 |
+
# ring_mask = ring_ids == ring
|
| 424 |
+
# ring_pts = points_xyzi[ring_mask]
|
| 425 |
+
# ring_cols = col_ids[ring_mask]
|
| 426 |
+
|
| 427 |
+
# ring_grid = [None] * NUM_COLS
|
| 428 |
+
|
| 429 |
+
# # place original points
|
| 430 |
+
# for p, col in zip(ring_pts, ring_cols):
|
| 431 |
+
# ring_grid[col] = p
|
| 432 |
+
|
| 433 |
+
# elev = beam_altitudes[ring]
|
| 434 |
+
# cos_e, sin_e = np.cos(elev), np.sin(elev)
|
| 435 |
+
|
| 436 |
+
# for c_idx in range(NUM_COLS):
|
| 437 |
+
|
| 438 |
+
# if ring_grid[c_idx] is not None:
|
| 439 |
+
# filled_points.append(ring_grid[c_idx])
|
| 440 |
+
# interp_flags.append(False)
|
| 441 |
+
# continue
|
| 442 |
+
|
| 443 |
+
# az = 2 * np.pi * c_idx / NUM_COLS
|
| 444 |
+
|
| 445 |
+
# # Unit ray direction
|
| 446 |
+
# dx = cos_e * np.cos(az)
|
| 447 |
+
# dy = cos_e * np.sin(az)
|
| 448 |
+
# dz = sin_e
|
| 449 |
+
|
| 450 |
+
# denom = dz - a * dx - b * dy
|
| 451 |
+
# if abs(denom) < 1e-6:
|
| 452 |
+
# continue # ray parallel to height field
|
| 453 |
+
|
| 454 |
+
# t = c / denom
|
| 455 |
+
# if t <= 0 or t > max_range:
|
| 456 |
+
# continue
|
| 457 |
+
|
| 458 |
+
# x = t * dx
|
| 459 |
+
# y = t * dy
|
| 460 |
+
# z = t * dz
|
| 461 |
+
|
| 462 |
+
# filled_points.append([x, y, z, 255])
|
| 463 |
+
# interp_flags.append(True)
|
| 464 |
+
|
| 465 |
+
# return np.asarray(filled_points), np.asarray(interp_flags)
|
| 466 |
+
|
| 467 |
+
def create_height_field_mesh(plane, xlim, ylim, resolution=1.0):
|
| 468 |
+
a, b, c = plane
|
| 469 |
+
xs = np.arange(xlim[0], xlim[1], resolution)
|
| 470 |
+
ys = np.arange(ylim[0], ylim[1], resolution)
|
| 471 |
+
|
| 472 |
+
xx, yy = np.meshgrid(xs, ys)
|
| 473 |
+
zz = a * xx + b * yy + c
|
| 474 |
+
|
| 475 |
+
points = np.column_stack((xx.ravel(), yy.ravel(), zz.ravel()))
|
| 476 |
+
|
| 477 |
+
mesh = o3d.geometry.TriangleMesh()
|
| 478 |
+
mesh.vertices = o3d.utility.Vector3dVector(points)
|
| 479 |
+
|
| 480 |
+
triangles = []
|
| 481 |
+
w = len(xs)
|
| 482 |
+
h = len(ys)
|
| 483 |
+
|
| 484 |
+
for y in range(h - 1):
|
| 485 |
+
for x in range(w - 1):
|
| 486 |
+
i = y * w + x
|
| 487 |
+
triangles.append([i, i + 1, i + w])
|
| 488 |
+
triangles.append([i + 1, i + w + 1, i + w])
|
| 489 |
+
|
| 490 |
+
mesh.triangles = o3d.utility.Vector3iVector(triangles)
|
| 491 |
+
mesh.compute_vertex_normals()
|
| 492 |
+
mesh.paint_uniform_color([0.8, 0.8, 0.8])
|
| 493 |
+
|
| 494 |
+
return mesh
|
visualisation/.gitkeep
ADDED
|
File without changes
|
visualisation/__init__.py
ADDED
|
File without changes
|
visualisation/convert_imgs2video.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
images_to_mp4.py — Combine a directory of images into an MP4 video.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python images_to_mp4.py <image_dir> [options]
|
| 7 |
+
|
| 8 |
+
Options:
|
| 9 |
+
--fps FPS Frames per second (default: 10)
|
| 10 |
+
--output OUTPUT Output file path (default: output.mp4)
|
| 11 |
+
--pattern PATTERN Glob pattern for images (default: auto-detect)
|
| 12 |
+
--sort {name,time} Sort order for frames (default: name)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import sys
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from natsort import natsorted
|
| 19 |
+
import cv2
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif", ".webp"}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def collect_images(directory: Path, pattern: str | None, sort: str) -> list[Path]:
|
| 26 |
+
if pattern:
|
| 27 |
+
images = sorted(directory.glob(pattern))
|
| 28 |
+
else:
|
| 29 |
+
images = [
|
| 30 |
+
p for p in directory.iterdir()
|
| 31 |
+
if p.suffix.lower() in SUPPORTED_EXTENSIONS
|
| 32 |
+
]
|
| 33 |
+
if sort == "time":
|
| 34 |
+
images.sort(key=lambda p: p.stat().st_mtime)
|
| 35 |
+
else:
|
| 36 |
+
images.sort(key=lambda p: p.name)
|
| 37 |
+
|
| 38 |
+
return natsorted(images)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def build_video(images: list[Path], output: Path, fps: int) -> None:
|
| 42 |
+
# Read the first frame to get dimensions
|
| 43 |
+
first = cv2.imread(str(images[0]))
|
| 44 |
+
if first is None:
|
| 45 |
+
sys.exit(f"Error: could not read image '{images[0]}'")
|
| 46 |
+
|
| 47 |
+
height, width = first.shape[:2]
|
| 48 |
+
print(f"Frame size : {width}x{height}")
|
| 49 |
+
print(f"Frame count: {len(images)}")
|
| 50 |
+
print(f"FPS : {fps}")
|
| 51 |
+
print(f"Output : {output}")
|
| 52 |
+
|
| 53 |
+
fourcc = cv2.VideoWriter_fourcc(*"avc1") # H.264
|
| 54 |
+
writer = cv2.VideoWriter(str(output), fourcc, fps, (width, height))
|
| 55 |
+
|
| 56 |
+
if not writer.isOpened():
|
| 57 |
+
# Fallback: try mp4v codec if avc1 unavailable
|
| 58 |
+
print("Warning: avc1 (H.264) unavailable, falling back to mp4v codec.")
|
| 59 |
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
| 60 |
+
writer = cv2.VideoWriter(str(output), fourcc, fps, (width, height))
|
| 61 |
+
|
| 62 |
+
if not writer.isOpened():
|
| 63 |
+
sys.exit("Error: could not open VideoWriter. Check OpenCV build & codecs.")
|
| 64 |
+
|
| 65 |
+
for i, path in enumerate(images, 1):
|
| 66 |
+
frame = cv2.imread(str(path))
|
| 67 |
+
if frame is None:
|
| 68 |
+
print(f" Warning: skipping unreadable file '{path.name}'")
|
| 69 |
+
continue
|
| 70 |
+
# Resize if a frame differs from the first frame's dimensions
|
| 71 |
+
if (frame.shape[1], frame.shape[0]) != (width, height):
|
| 72 |
+
frame = cv2.resize(frame, (width, height))
|
| 73 |
+
writer.write(frame)
|
| 74 |
+
if i % 50 == 0 or i == len(images):
|
| 75 |
+
print(f" Encoded {i}/{len(images)} frames...")
|
| 76 |
+
|
| 77 |
+
writer.release()
|
| 78 |
+
print(f"\nDone! Video saved to: {output}")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def main():
|
| 82 |
+
parser = argparse.ArgumentParser(
|
| 83 |
+
description="Combine images in a directory into an MP4 video."
|
| 84 |
+
)
|
| 85 |
+
parser.add_argument("image_dir", help="Path to directory containing images")
|
| 86 |
+
parser.add_argument("--fps", type=int, default=10, help="Frames per second (default: 10)")
|
| 87 |
+
parser.add_argument("--output", default="output.mp4", help="Output video file (default: output.mp4)")
|
| 88 |
+
parser.add_argument("--pattern", default=None, help="Glob pattern, e.g. 'frame_*.png'")
|
| 89 |
+
parser.add_argument("--sort", choices=["name", "time"], default="name",
|
| 90 |
+
help="Sort frames by name (default) or modification time")
|
| 91 |
+
args = parser.parse_args()
|
| 92 |
+
|
| 93 |
+
directory = Path(args.image_dir)
|
| 94 |
+
if not directory.is_dir():
|
| 95 |
+
sys.exit(f"Error: '{directory}' is not a valid directory.")
|
| 96 |
+
|
| 97 |
+
images = collect_images(directory, args.pattern, args.sort)
|
| 98 |
+
if not images:
|
| 99 |
+
sys.exit(f"Error: no supported images found in '{directory}'.")
|
| 100 |
+
|
| 101 |
+
print(f"Found {len(images)} image(s) in '{directory}'")
|
| 102 |
+
build_video(images, Path(args.output), args.fps)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
main()
|
visualisation/points2image-all.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.image as mpimg
|
| 4 |
+
import sys
|
| 5 |
+
sys.path.append(os.path.abspath(".")) # one level up
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
import open3d as o3d
|
| 9 |
+
from scipy.spatial.transform import Rotation
|
| 10 |
+
from utils.lidar import PointCloud
|
| 11 |
+
from utils.camera import ImageData
|
| 12 |
+
import utils.utils as utils
|
| 13 |
+
from natsort import natsorted
|
| 14 |
+
|
| 15 |
+
cmap = plt.get_cmap("jet")
|
| 16 |
+
|
| 17 |
+
# User parameters
|
| 18 |
+
location = 'Cambogan'
|
| 19 |
+
sequence = '20250811_113017'
|
| 20 |
+
# sequence = '20250812_122101'
|
| 21 |
+
# location = 'Holmview'
|
| 22 |
+
# sequence = '20250820_130327'
|
| 23 |
+
# location = 'Mount-Cotton'
|
| 24 |
+
# sequence = '20241217_113410'
|
| 25 |
+
condition = 'flooded'
|
| 26 |
+
# condition = 'dry'
|
| 27 |
+
camera_pos = 'front'
|
| 28 |
+
root_directory = f"../Datasets/FRED/{condition}/KITTI-style"
|
| 29 |
+
# 01000000
|
| 30 |
+
|
| 31 |
+
############ Define filenames and directories ####################################
|
| 32 |
+
|
| 33 |
+
image_dir = f"{root_directory}/{location}_{sequence}/{camera_pos}-imgs/"
|
| 34 |
+
lidar_dir = f"{root_directory}/{location}_{sequence}/ouster/"
|
| 35 |
+
utm_dir = f"{root_directory}/{location}_{sequence}/utm/"
|
| 36 |
+
|
| 37 |
+
img_calib_file = f"./camera_calib.txt"
|
| 38 |
+
lidar_calib_file = f"./calib.txt"
|
| 39 |
+
|
| 40 |
+
timestamps = [filename.split('.png')[0] for filename in natsorted(os.listdir(image_dir)) if os.path.isfile(image_dir+filename)]
|
| 41 |
+
|
| 42 |
+
# timestamps.sort()
|
| 43 |
+
|
| 44 |
+
fig, ax = plt.subplots(figsize=(12.8, 8))
|
| 45 |
+
# idx = [0] # mutable index
|
| 46 |
+
idx = [183]
|
| 47 |
+
|
| 48 |
+
def show_image(i):
|
| 49 |
+
ax.clear()
|
| 50 |
+
if i >= len(timestamps):
|
| 51 |
+
plt.close(fig)
|
| 52 |
+
return
|
| 53 |
+
image_timestamp = timestamps[i]
|
| 54 |
+
try:
|
| 55 |
+
image_filename = f"{image_dir}/{image_timestamp}.png"
|
| 56 |
+
lidar_filename, utm_filename = utils.get_corr_files(image_timestamp, [lidar_dir, utm_dir])
|
| 57 |
+
|
| 58 |
+
image = ImageData(image_filename, img_calib_file)
|
| 59 |
+
pointcloud = PointCloud(lidar_filename, lidar_calib_file)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
point_cam, distances_cam, intensities_cam, all_points_cam, valid_cam = pointcloud.points_ouster_to_cam() #, beam_id, azimuth
|
| 63 |
+
img_vis, _, _ = image.project_points(all_points_cam, distances_cam, cmap, valid_cam, colour_norm=None) #, beam_id, azimuth
|
| 64 |
+
|
| 65 |
+
ax.imshow(img_vis[:, :, ::-1])
|
| 66 |
+
ax.set_title(f"{image_timestamp}.png")
|
| 67 |
+
ax.axis("off")
|
| 68 |
+
# plt.savefig('paper_figures/CADRRAS/projected_pointcloud_distance_flooded.svg', format="svg", bbox_inches='tight')
|
| 69 |
+
fig.canvas.draw()
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"Could not project pointcloud onto {image_timestamp}.png: {e}")
|
| 72 |
+
idx[0] += 1
|
| 73 |
+
show_image(idx[0]) # skip bad one
|
| 74 |
+
|
| 75 |
+
def on_key(event):
|
| 76 |
+
if event.key in [' ', 'right']: # space or right arrow
|
| 77 |
+
idx[0] += 1
|
| 78 |
+
show_image(idx[0])
|
| 79 |
+
elif event.key in [' ', 'left']: # space or right arrow
|
| 80 |
+
if idx[0] > 0:
|
| 81 |
+
idx[0] -= 1
|
| 82 |
+
show_image(idx[0])
|
| 83 |
+
elif event.key in ['q', 'escape']: # q or Esc → quit
|
| 84 |
+
plt.close(fig)
|
| 85 |
+
|
| 86 |
+
fig.canvas.mpl_connect('key_press_event', on_key)
|
| 87 |
+
show_image(idx[0])
|
| 88 |
+
plt.show()
|