ruststartracker 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/PKG-INFO +47 -8
- ruststartracker-0.2.3/README.md +93 -0
- ruststartracker-0.2.3/build_script.py +65 -0
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/pyproject.toml +11 -5
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/catalog.py +14 -18
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/libruststartracker.pyi +38 -7
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/star.py +30 -5
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/test_backend.py +17 -6
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/test_catalog.py +14 -0
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/test_integration.py +4 -28
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/test_star.py +47 -12
- ruststartracker-0.2.1/README.md +0 -57
- ruststartracker-0.2.1/rust_build.py +0 -21
- ruststartracker-0.2.1/ruststartracker/star_catalog.tsv +0 -118010
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/LICENSE +0 -0
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/__init__.py +0 -0
- {ruststartracker-0.2.1 → ruststartracker-0.2.3}/ruststartracker/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ruststartracker
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Lightweight Python Star Tracker With Rust Backend
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Nicolas Tobler
|
|
@@ -14,6 +14,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Dist: numpy (>=1.26.0,<2.0.0)
|
|
16
16
|
Requires-Dist: opencv-python-headless (>=4.9.0,<5.0.0)
|
|
17
|
+
Project-URL: GaiaAttribution, https://www.cosmos.esa.int/gaia
|
|
18
|
+
Project-URL: Homepage, https://github.com/ntobler/ruststartracker
|
|
19
|
+
Project-URL: License, https://opensource.org/licenses/MIT
|
|
17
20
|
Description-Content-Type: text/markdown
|
|
18
21
|
|
|
19
22
|
# Lightweight Python Star Tracker With Rust Backend
|
|
@@ -31,12 +34,41 @@ Features:
|
|
|
31
34
|
|
|
32
35
|
## Example
|
|
33
36
|
|
|
37
|
+
### Rust
|
|
38
|
+
|
|
39
|
+
See [examples/basic.rs](examples/basic.rs)
|
|
40
|
+
|
|
41
|
+
```rust
|
|
42
|
+
// Get catalog positions
|
|
43
|
+
let catalog: StarCatalog = StarCatalog::from_gaia(max_magnitude: ...).unwrap();
|
|
44
|
+
let stars_xyz: Vec<[f32; 3]> = catalog.normalized_positions(epoch: ..., observer_position: ...);
|
|
45
|
+
let stars_mag: Vec<f32> = catalog.magnitudes();
|
|
46
|
+
|
|
47
|
+
// Create StarTracker instance (reuse this)
|
|
48
|
+
let star_matcher = StarMatcher::new(
|
|
49
|
+
stars_xyz,
|
|
50
|
+
stars_mag,
|
|
51
|
+
max_lookup_magnitude: ...
|
|
52
|
+
max_inter_star_angle: ...,
|
|
53
|
+
inter_star_angle_tolerance: ...,
|
|
54
|
+
min_matches: ...,
|
|
55
|
+
timeout: ...
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Normalized observation in the camera frame
|
|
59
|
+
let obs_xyz_camera: Vec<[f32; 3]> = ...
|
|
60
|
+
|
|
61
|
+
let result = star_matcher.find(&obs_xyz_camera);
|
|
62
|
+
println!("Result: {:?}", result);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Python
|
|
34
66
|
```python
|
|
35
67
|
import ruststartracker
|
|
36
68
|
|
|
37
69
|
# Get catalog positions
|
|
38
|
-
catalog = ruststartracker.StarCatalog()
|
|
39
|
-
star_catalog_vecs = catalog.normalized_positions()
|
|
70
|
+
catalog = ruststartracker.StarCatalog(max_magnitude=...)
|
|
71
|
+
star_catalog_vecs = catalog.normalized_positions(epoch=...)
|
|
40
72
|
|
|
41
73
|
# Define opencv camera parameters, see https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html
|
|
42
74
|
camera_params = ruststartracker.CameraParameters(
|
|
@@ -52,6 +84,7 @@ st = ruststartracker.StarTracker(
|
|
|
52
84
|
max_inter_star_angle=...,
|
|
53
85
|
inter_star_angle_tolerance=...,
|
|
54
86
|
n_minimum_matches=...,
|
|
87
|
+
timeout_secs=...,
|
|
55
88
|
)
|
|
56
89
|
|
|
57
90
|
# Obtain numpy array image
|
|
@@ -66,11 +99,17 @@ print(result)
|
|
|
66
99
|
|
|
67
100
|
## Installation
|
|
68
101
|
|
|
69
|
-
-
|
|
70
|
-
|
|
102
|
+
- Install with `pip install ruststartracker` (Currently only ARM/x86 Linux wheels available).
|
|
103
|
+
|
|
104
|
+
## Attributions
|
|
105
|
+
|
|
106
|
+
### Gaia Data
|
|
107
|
+
|
|
108
|
+
This project includes data from the European Space Agency (ESA) mission [**Gaia**](https://www.cosmos.esa.int/gaia), processed by the **Gaia Data Processing and Analysis Consortium (DPAC)**.
|
|
109
|
+
Funding for the DPAC has been provided by national institutions, in particular the institutions participating in the Gaia Multilateral Agreement.
|
|
71
110
|
|
|
72
|
-
|
|
111
|
+
Gaia DR3 data is © European Space Agency and is released under the [**Creative Commons Attribution 4.0 International License (CC BY 4.0)**](https://creativecommons.org/licenses/by/4.0/).
|
|
73
112
|
|
|
74
|
-
|
|
75
|
-
|
|
113
|
+
> Gaia Collaboration, Vallenari et al. (2022), *A\&A* **674**, A1.
|
|
114
|
+
> [DOI: 10.1051/0004-6361/202243940](https://doi.org/10.1051/0004-6361/202243940)
|
|
76
115
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Lightweight Python Star Tracker With Rust Backend
|
|
2
|
+
|
|
3
|
+
Based on the methodology used in https://github.com/nasa/COTS-Star-Tracker, with following improvements:
|
|
4
|
+
- Reduced dependencies to opencv and numpy for lightweight usage in a Raspberry Pi.
|
|
5
|
+
- Reimplemented computationally expensive parts in rust. This includes most parts that are not image processing related.
|
|
6
|
+
- Added quadratic inter star angle index look up polynomial for faster triangle search.
|
|
7
|
+
- Added spatial index to look up neighboring stars.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Attitude estimation from image and camera calibration parameters.
|
|
11
|
+
- Attitude estimation from list of star observation coordinates.
|
|
12
|
+
- Star catalog creation with temporal corrections.
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
### Rust
|
|
17
|
+
|
|
18
|
+
See [examples/basic.rs](examples/basic.rs)
|
|
19
|
+
|
|
20
|
+
```rust
|
|
21
|
+
// Get catalog positions
|
|
22
|
+
let catalog: StarCatalog = StarCatalog::from_gaia(max_magnitude: ...).unwrap();
|
|
23
|
+
let stars_xyz: Vec<[f32; 3]> = catalog.normalized_positions(epoch: ..., observer_position: ...);
|
|
24
|
+
let stars_mag: Vec<f32> = catalog.magnitudes();
|
|
25
|
+
|
|
26
|
+
// Create StarTracker instance (reuse this)
|
|
27
|
+
let star_matcher = StarMatcher::new(
|
|
28
|
+
stars_xyz,
|
|
29
|
+
stars_mag,
|
|
30
|
+
max_lookup_magnitude: ...
|
|
31
|
+
max_inter_star_angle: ...,
|
|
32
|
+
inter_star_angle_tolerance: ...,
|
|
33
|
+
min_matches: ...,
|
|
34
|
+
timeout: ...
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Normalized observation in the camera frame
|
|
38
|
+
let obs_xyz_camera: Vec<[f32; 3]> = ...
|
|
39
|
+
|
|
40
|
+
let result = star_matcher.find(&obs_xyz_camera);
|
|
41
|
+
println!("Result: {:?}", result);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Python
|
|
45
|
+
```python
|
|
46
|
+
import ruststartracker
|
|
47
|
+
|
|
48
|
+
# Get catalog positions
|
|
49
|
+
catalog = ruststartracker.StarCatalog(max_magnitude=...)
|
|
50
|
+
star_catalog_vecs = catalog.normalized_positions(epoch=...)
|
|
51
|
+
|
|
52
|
+
# Define opencv camera parameters, see https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html
|
|
53
|
+
camera_params = ruststartracker.CameraParameters(
|
|
54
|
+
camera_matrix=...,
|
|
55
|
+
cam_resolution=...,
|
|
56
|
+
dist_coefs=...,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create StarTracker instance (reuse this)
|
|
60
|
+
st = ruststartracker.StarTracker(
|
|
61
|
+
star_catalog_vecs,
|
|
62
|
+
camera_params,
|
|
63
|
+
max_inter_star_angle=...,
|
|
64
|
+
inter_star_angle_tolerance=...,
|
|
65
|
+
n_minimum_matches=...,
|
|
66
|
+
timeout_secs=...,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Obtain numpy array image
|
|
70
|
+
img = ...
|
|
71
|
+
|
|
72
|
+
# Find attitude from given image
|
|
73
|
+
result = st.process_image(img)
|
|
74
|
+
|
|
75
|
+
print(result)
|
|
76
|
+
# StarTrackerResult(quat=[-0.43977802991867065, -0.439766526222229, -0.4398997128009796, 0.6478340029716492], match_ids=[1435, 1272, 1140, 2035, 1070, 1438, 1338, 903, 260, 2141, 1771, 1727, 385, 1717, 2204, 2062, 1989, 1634, 708, 1357], n_matches=20, duration_s=0.0003700880042742938)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
- Install with `pip install ruststartracker` (Currently only ARM/x86 Linux wheels available).
|
|
82
|
+
|
|
83
|
+
## Attributions
|
|
84
|
+
|
|
85
|
+
### Gaia Data
|
|
86
|
+
|
|
87
|
+
This project includes data from the European Space Agency (ESA) mission [**Gaia**](https://www.cosmos.esa.int/gaia), processed by the **Gaia Data Processing and Analysis Consortium (DPAC)**.
|
|
88
|
+
Funding for the DPAC has been provided by national institutions, in particular the institutions participating in the Gaia Multilateral Agreement.
|
|
89
|
+
|
|
90
|
+
Gaia DR3 data is © European Space Agency and is released under the [**Creative Commons Attribution 4.0 International License (CC BY 4.0)**](https://creativecommons.org/licenses/by/4.0/).
|
|
91
|
+
|
|
92
|
+
> Gaia Collaboration, Vallenari et al. (2022), *A\&A* **674**, A1.
|
|
93
|
+
> [DOI: 10.1051/0004-6361/202243940](https://doi.org/10.1051/0004-6361/202243940)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Build rust backend.
|
|
2
|
+
|
|
3
|
+
This script is automatically run when the pyproject is installed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pathlib
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def download_gaia_data(output_file: pathlib.Path) -> None:
|
|
12
|
+
"""Download Gaia data and save it to the specified output file.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
output_file: Path to save the downloaded Gaia data (csv).
|
|
16
|
+
"""
|
|
17
|
+
from astroquery.gaia import Gaia
|
|
18
|
+
|
|
19
|
+
Gaia.ROW_LIMIT = 10000
|
|
20
|
+
|
|
21
|
+
query = """
|
|
22
|
+
SELECT source_id, ra, dec, parallax, pmra, pmdec, phot_g_mean_mag
|
|
23
|
+
FROM gaiadr3.gaia_source
|
|
24
|
+
WHERE phot_g_mean_mag < 7
|
|
25
|
+
AND parallax > 0
|
|
26
|
+
AND astrometric_params_solved = 31
|
|
27
|
+
AND visibility_periods_used >= 8
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
print("Executing query on Gaia database...")
|
|
31
|
+
job = Gaia.launch_job_async(
|
|
32
|
+
query,
|
|
33
|
+
dump_to_file=True,
|
|
34
|
+
output_format="csv",
|
|
35
|
+
output_file=str(output_file.absolute()),
|
|
36
|
+
verbose=True,
|
|
37
|
+
)
|
|
38
|
+
if not job.is_finished():
|
|
39
|
+
print("Job did not finish successfully.")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
print(f"Saved to: {output_file}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_script() -> None:
|
|
46
|
+
"""Build rust backend and move shared library to correct folder."""
|
|
47
|
+
cwd = pathlib.Path(__file__).parent.expanduser().absolute()
|
|
48
|
+
|
|
49
|
+
gaia_file = cwd / "ruststartracker/gaia_data_j2016.csv"
|
|
50
|
+
if not gaia_file.exists():
|
|
51
|
+
download_gaia_data(gaia_file)
|
|
52
|
+
|
|
53
|
+
subprocess.check_call( # noqa: S603
|
|
54
|
+
["cargo", "build", "--release", "--features", "improc,gaia"], # noqa: S607
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
stdout=None,
|
|
57
|
+
stderr=None,
|
|
58
|
+
)
|
|
59
|
+
shutil.copy(
|
|
60
|
+
cwd / "target/release/libruststartracker.so", cwd / "ruststartracker/libruststartracker.so"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
build_script()
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ruststartracker"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "Lightweight Python Star Tracker With Rust Backend"
|
|
5
5
|
authors = ["Nicolas Tobler <nitobler@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
include = [
|
|
9
|
-
{ path = "ruststartracker/
|
|
9
|
+
{ path = "ruststartracker/gaia_data_j2016.csv", format = ["sdist", "wheel"] },
|
|
10
10
|
{ path = "ruststartracker/libruststartracker.so", format = ["wheel"] },
|
|
11
11
|
]
|
|
12
12
|
|
|
13
|
+
[tool.poetry.urls]
|
|
14
|
+
Homepage = "https://github.com/ntobler/ruststartracker"
|
|
15
|
+
License = "https://opensource.org/licenses/MIT"
|
|
16
|
+
GaiaAttribution = "https://www.cosmos.esa.int/gaia"
|
|
17
|
+
|
|
13
18
|
[tool.poetry.dependencies]
|
|
14
19
|
python = "^3.10"
|
|
15
20
|
numpy = "^1.26.0"
|
|
@@ -27,16 +32,17 @@ mypy = "^1.13.0"
|
|
|
27
32
|
scipy-stubs = "^1.14.1.5"
|
|
28
33
|
ruff = "^0.9.3"
|
|
29
34
|
bump-my-version = "^1.2.0"
|
|
35
|
+
astroquery = "^0.4.10"
|
|
30
36
|
|
|
31
37
|
[tool.poetry.build]
|
|
32
|
-
script = "
|
|
38
|
+
script = "build_script.py"
|
|
33
39
|
|
|
34
40
|
[build-system]
|
|
35
|
-
requires = ["poetry-core"]
|
|
41
|
+
requires = ["poetry-core", "astroquery>=0.4.10"]
|
|
36
42
|
build-backend = "poetry.core.masonry.api"
|
|
37
43
|
|
|
38
44
|
[tool.bumpversion]
|
|
39
|
-
current_version = "0.2.
|
|
45
|
+
current_version = "0.2.3"
|
|
40
46
|
commit = true
|
|
41
47
|
tag = true
|
|
42
48
|
tag_name = "v{new_version}"
|
|
@@ -11,7 +11,9 @@ import numpy.typing as npt
|
|
|
11
11
|
AU: float = 149597870.693
|
|
12
12
|
"""Astronomical unit."""
|
|
13
13
|
|
|
14
|
-
INTERNAL_CATALOG_FILE =
|
|
14
|
+
INTERNAL_CATALOG_FILE = (
|
|
15
|
+
pathlib.Path(__file__).parent.expanduser().absolute() / "gaia_data_j2016.csv"
|
|
16
|
+
)
|
|
15
17
|
"""Location of the internal star catalog file."""
|
|
16
18
|
|
|
17
19
|
|
|
@@ -26,7 +28,7 @@ def time_to_epoch(t: datetime.datetime) -> float:
|
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
class StarCatalog:
|
|
29
|
-
"""Star catalog from
|
|
31
|
+
"""Star catalog from Gaia data."""
|
|
30
32
|
|
|
31
33
|
_data: npt.NDArray[np.float32]
|
|
32
34
|
"""Underlying data array."""
|
|
@@ -42,7 +44,7 @@ class StarCatalog:
|
|
|
42
44
|
"""Proper motion of the declination in mad."""
|
|
43
45
|
magnitude: npt.NDArray[np.float32]
|
|
44
46
|
"""Magnitude values."""
|
|
45
|
-
epoch: float =
|
|
47
|
+
epoch: float = 2016.0
|
|
46
48
|
"""Epoch of the catalog in years."""
|
|
47
49
|
|
|
48
50
|
def __init__(
|
|
@@ -57,13 +59,10 @@ class StarCatalog:
|
|
|
57
59
|
# Use locally included star catalog is no file is given
|
|
58
60
|
filename = INTERNAL_CATALOG_FILE if filename is None else pathlib.Path(filename)
|
|
59
61
|
|
|
60
|
-
keep_columns = ("
|
|
62
|
+
keep_columns = ("ra", "dec", "parallax", "pmra", "pmdec", "phot_g_mean_mag")
|
|
61
63
|
|
|
62
64
|
with filename.open("r") as f:
|
|
63
|
-
it = csv.reader(f, delimiter="
|
|
64
|
-
|
|
65
|
-
# skip head
|
|
66
|
-
[next(it) for _ in range(52)]
|
|
65
|
+
it = csv.reader(f, delimiter=",", strict=True)
|
|
67
66
|
|
|
68
67
|
# Get columns
|
|
69
68
|
columns = next(it)
|
|
@@ -71,10 +70,7 @@ class StarCatalog:
|
|
|
71
70
|
keep_columns_indices = tuple(columns.index(x) for x in keep_columns)
|
|
72
71
|
min_length = len(keep_columns_indices)
|
|
73
72
|
|
|
74
|
-
mag_column_index = columns.index("
|
|
75
|
-
|
|
76
|
-
# Skip unit and horizontal bar
|
|
77
|
-
[next(it) for _ in range(2)]
|
|
73
|
+
mag_column_index = columns.index("phot_g_mean_mag")
|
|
78
74
|
|
|
79
75
|
rows = [
|
|
80
76
|
[float(line[j]) for j in keep_columns_indices]
|
|
@@ -83,12 +79,12 @@ class StarCatalog:
|
|
|
83
79
|
]
|
|
84
80
|
self._data = np.array(rows, dtype=np.float32)
|
|
85
81
|
|
|
86
|
-
self.ra = self._data[:, keep_columns.index("
|
|
87
|
-
self.de = self._data[:, keep_columns.index("
|
|
88
|
-
self.parallax = self._data[:, keep_columns.index("
|
|
89
|
-
self.proper_motion_ra = self._data[:, keep_columns.index("
|
|
90
|
-
self.proper_motion_de = self._data[:, keep_columns.index("
|
|
91
|
-
self.magnitude = self._data[:, keep_columns.index("
|
|
82
|
+
self.ra = self._data[:, keep_columns.index("ra")]
|
|
83
|
+
self.de = self._data[:, keep_columns.index("dec")]
|
|
84
|
+
self.parallax = self._data[:, keep_columns.index("parallax")]
|
|
85
|
+
self.proper_motion_ra = self._data[:, keep_columns.index("pmra")]
|
|
86
|
+
self.proper_motion_de = self._data[:, keep_columns.index("pmdec")]
|
|
87
|
+
self.magnitude = self._data[:, keep_columns.index("phot_g_mean_mag")]
|
|
92
88
|
|
|
93
89
|
deg2rad = math.pi / 180
|
|
94
90
|
arcsec2deg = 1 / 3600
|
|
@@ -2,12 +2,15 @@ from collections.abc import Iterator
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import numpy.typing as npt
|
|
5
|
+
from typing_extensions import Self
|
|
5
6
|
|
|
6
7
|
class StarMatcher:
|
|
7
8
|
def __init__(
|
|
8
9
|
self,
|
|
9
10
|
stars_xyz: npt.NDArray[np.float32],
|
|
11
|
+
stars_mag: npt.NDArray[np.float32],
|
|
10
12
|
max_inter_star_angle: float,
|
|
13
|
+
max_lookup_magnitude: float,
|
|
11
14
|
inter_star_angle_tolerance: float,
|
|
12
15
|
n_minimum_matches: int,
|
|
13
16
|
timeout_secs: float,
|
|
@@ -19,7 +22,7 @@ class StarMatcher:
|
|
|
19
22
|
npt.NDArray[np.uint32],
|
|
20
23
|
npt.NDArray[np.uint32],
|
|
21
24
|
int,
|
|
22
|
-
|
|
25
|
+
npt.NDArray[np.float32],
|
|
23
26
|
float,
|
|
24
27
|
]: ...
|
|
25
28
|
|
|
@@ -44,15 +47,43 @@ class IterTriangleFinder:
|
|
|
44
47
|
class UnitVectorLookup:
|
|
45
48
|
def __init__(self, vec: npt.NDArray[np.float32]) -> None: ...
|
|
46
49
|
def lookup_nearest(self, key: npt.NDArray[np.float32]) -> int: ...
|
|
47
|
-
def get_inter_star_index_numpy(
|
|
48
|
-
self, vec: npt.NDArray[np.float32], angle_threshold: float
|
|
49
|
-
) -> tuple[list[list[int]], list[float], list[float]]: ...
|
|
50
50
|
def get_inter_star_index(
|
|
51
|
-
self,
|
|
51
|
+
self,
|
|
52
|
+
stars: npt.NDArray[np.float32],
|
|
53
|
+
magnitudes: npt.NDArray[np.float32],
|
|
54
|
+
max_angle_rad: float,
|
|
55
|
+
max_magnitude: float,
|
|
52
56
|
) -> tuple[list[list[int]], list[float], list[float]]: ...
|
|
53
57
|
def look_up_close_angles(
|
|
54
|
-
self,
|
|
58
|
+
self,
|
|
59
|
+
vectors: npt.NDArray[np.float32],
|
|
60
|
+
magnitudes: npt.NDArray[np.float32],
|
|
61
|
+
max_angle_rad: float,
|
|
62
|
+
max_magnitude: float,
|
|
55
63
|
) -> list[tuple[list[float], float]]: ...
|
|
56
64
|
def look_up_close_angles_naive(
|
|
57
|
-
self,
|
|
65
|
+
self,
|
|
66
|
+
vectors: npt.NDArray[np.float32],
|
|
67
|
+
magnitudes: npt.NDArray[np.float32],
|
|
68
|
+
max_angle_rad: float,
|
|
69
|
+
max_magnitude: float,
|
|
58
70
|
) -> list[tuple[list[float], float]]: ...
|
|
71
|
+
|
|
72
|
+
def get_threshold_from_histogram(
|
|
73
|
+
img: npt.NDArray[np.uint8],
|
|
74
|
+
*,
|
|
75
|
+
fraction: float,
|
|
76
|
+
) -> int: ...
|
|
77
|
+
def extract_observations(
|
|
78
|
+
img: npt.NDArray[np.uint8],
|
|
79
|
+
threshold: int,
|
|
80
|
+
min_star_area: int,
|
|
81
|
+
max_star_area: int,
|
|
82
|
+
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]: ...
|
|
83
|
+
|
|
84
|
+
class StarCatalog:
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_gaia(cls, *, max_magnitude: float | None) -> Self: ...
|
|
87
|
+
def normalized_positions(
|
|
88
|
+
self, *, epoch: float | None, observer_position: np.ndarray | None
|
|
89
|
+
) -> npt.NDArray[np.float32]: ...
|
|
@@ -87,8 +87,10 @@ class StarTracker:
|
|
|
87
87
|
def __init__(
|
|
88
88
|
self,
|
|
89
89
|
stars_xyz: npt.NDArray[np.float32],
|
|
90
|
+
stars_mag: npt.NDArray[np.float32],
|
|
90
91
|
camera_params: CameraParameters,
|
|
91
92
|
*,
|
|
93
|
+
max_lookup_magnitude: float | None = None,
|
|
92
94
|
max_inter_star_angle: float | None = None,
|
|
93
95
|
inter_star_angle_tolerance: float = 0.0008,
|
|
94
96
|
n_minimum_matches: int = 10,
|
|
@@ -98,11 +100,15 @@ class StarTracker:
|
|
|
98
100
|
|
|
99
101
|
Args:
|
|
100
102
|
stars_xyz: Positions of catalog stars
|
|
103
|
+
stars_mag: Magnitudes of catalog stars
|
|
101
104
|
camera_params: Calibrated camera parameters
|
|
105
|
+
max_lookup_magnitude: Maximum magnitude of stars used in the triangulation. Reducing
|
|
106
|
+
this number means only bright stars are used for triangulation. This results in
|
|
107
|
+
faster lookup performance.
|
|
102
108
|
max_inter_star_angle: Maximum angle between stars that should be indexed.
|
|
103
109
|
Calculating large inter star angles is expensive. If None, the angle is
|
|
104
110
|
calculated from the camera field of view.
|
|
105
|
-
inter_star_angle_tolerance: Tolerance for inter star angle matching.
|
|
111
|
+
inter_star_angle_tolerance: Tolerance for inter star angle matching in rad.
|
|
106
112
|
n_minimum_matches: Minimum amount of required matches for a successful
|
|
107
113
|
attitude estimation
|
|
108
114
|
timeout_secs: Maximum allowed search time in seconds. A StarTrackerError is raised
|
|
@@ -127,8 +133,13 @@ class StarTracker:
|
|
|
127
133
|
dot_products = (corner_coords_xyz * np.array([0, 0, 1], dtype=np.float32)).sum(axis=-1)
|
|
128
134
|
max_inter_star_angle = float(np.arccos(dot_products.max())) * 2
|
|
129
135
|
|
|
136
|
+
if max_lookup_magnitude is None:
|
|
137
|
+
max_lookup_magnitude = 100.0 # A very faint star. Almost infinity
|
|
138
|
+
|
|
130
139
|
self._star_matcher = ruststartracker.libruststartracker.StarMatcher(
|
|
131
140
|
np.ascontiguousarray(stars_xyz, dtype=np.float32),
|
|
141
|
+
np.ascontiguousarray(stars_mag, dtype=np.float32),
|
|
142
|
+
float(max_lookup_magnitude),
|
|
132
143
|
float(max_inter_star_angle),
|
|
133
144
|
float(inter_star_angle_tolerance),
|
|
134
145
|
int(n_minimum_matches),
|
|
@@ -178,6 +189,20 @@ class StarTracker:
|
|
|
178
189
|
max_star_area=max_star_area,
|
|
179
190
|
)
|
|
180
191
|
|
|
192
|
+
if threshold is None:
|
|
193
|
+
threshold = ruststartracker.libruststartracker.get_threshold_from_histogram(
|
|
194
|
+
img, fraction=0.99
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
centroids, intensities = ruststartracker.libruststartracker.extract_observations(
|
|
198
|
+
img,
|
|
199
|
+
threshold,
|
|
200
|
+
min_star_area,
|
|
201
|
+
max_star_area,
|
|
202
|
+
)
|
|
203
|
+
centroids = np.array(centroids, dtype=np.float32)
|
|
204
|
+
intensities = np.array(intensities, dtype=np.float32)
|
|
205
|
+
|
|
181
206
|
# At least 3 observations are required (one triangle)
|
|
182
207
|
if len(centroids) < 3:
|
|
183
208
|
raise StarTrackerError("Found too few star candidates (< 3) to continue.")
|
|
@@ -238,12 +263,12 @@ class StarTracker:
|
|
|
238
263
|
quat, match_ids, obs_indices, n_matches, matched_obs, duration_s = result
|
|
239
264
|
|
|
240
265
|
return StarTrackerResult(
|
|
241
|
-
quat=
|
|
242
|
-
match_ids=
|
|
266
|
+
quat=quat,
|
|
267
|
+
match_ids=match_ids,
|
|
243
268
|
n_matches=n_matches,
|
|
244
269
|
duration_s=duration_s,
|
|
245
|
-
mached_obs_x=
|
|
246
|
-
obs_indices=
|
|
270
|
+
mached_obs_x=matched_obs,
|
|
271
|
+
obs_indices=obs_indices,
|
|
247
272
|
)
|
|
248
273
|
|
|
249
274
|
|
|
@@ -54,9 +54,12 @@ def test_unit_vector_lookup():
|
|
|
54
54
|
close_indices_gt = np.concatenate(results, axis=-1).T[args]
|
|
55
55
|
angles_gt = angles[args]
|
|
56
56
|
|
|
57
|
-
close_indices, angles, poly = uvl.
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
close_indices, angles, poly = uvl.get_inter_star_index(
|
|
58
|
+
np.array(vec[:, :3], dtype=np.float32),
|
|
59
|
+
np.ones(len(vec), dtype=np.float32),
|
|
60
|
+
angle_threshold,
|
|
61
|
+
10,
|
|
62
|
+
)
|
|
60
63
|
close_indices = np.array(close_indices)
|
|
61
64
|
angles = np.array(angles)
|
|
62
65
|
poly = np.array(poly)
|
|
@@ -101,6 +104,8 @@ def test_star_matcher():
|
|
|
101
104
|
vec = rng.normal(size=[n_cat_stars, 3]).astype(np.float32)
|
|
102
105
|
vec /= np.linalg.norm(vec, axis=-1, keepdims=True)
|
|
103
106
|
|
|
107
|
+
magnitudes = rng.uniform(0, 10, size=vec.shape[:1]).astype(np.float32)
|
|
108
|
+
|
|
104
109
|
key = rng.normal(size=[3]).astype(np.float32)
|
|
105
110
|
key /= np.linalg.norm(key, axis=-1, keepdims=True)
|
|
106
111
|
|
|
@@ -113,10 +118,16 @@ def test_star_matcher():
|
|
|
113
118
|
|
|
114
119
|
rot = scipy.spatial.transform.Rotation.from_rotvec([1, 1, 1])
|
|
115
120
|
|
|
116
|
-
obs_rotated = rot.apply(obs)
|
|
121
|
+
obs_rotated = rot.apply(obs).astype(np.float32)
|
|
117
122
|
|
|
118
123
|
index = libruststartracker.StarMatcher(
|
|
119
|
-
vec,
|
|
124
|
+
vec,
|
|
125
|
+
magnitudes,
|
|
126
|
+
10,
|
|
127
|
+
np.radians(10).item(),
|
|
128
|
+
np.radians(0.1).item(),
|
|
129
|
+
4,
|
|
130
|
+
999.0,
|
|
120
131
|
)
|
|
121
132
|
|
|
122
133
|
res = index.find(obs_rotated)
|
|
@@ -124,7 +135,7 @@ def test_star_matcher():
|
|
|
124
135
|
assert res is not None
|
|
125
136
|
|
|
126
137
|
quat, match_ids, obs_indices, n_matches, matched_obs, time_s = res
|
|
127
|
-
np.testing.assert_allclose(quat, rot.inv().as_quat())
|
|
138
|
+
np.testing.assert_allclose(quat, rot.inv().as_quat(), rtol=1e-6)
|
|
128
139
|
assert n_matches >= 4
|
|
129
140
|
assert len(obs_index) == len(match_ids)
|
|
130
141
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import time
|
|
2
3
|
|
|
3
4
|
import astropy.time # type: ignore[import]
|
|
4
5
|
import numpy as np
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
7
8
|
import ruststartracker.catalog
|
|
9
|
+
import ruststartracker.libruststartracker
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def test_time_to_epoch():
|
|
@@ -43,5 +45,17 @@ def test_extract_observations():
|
|
|
43
45
|
np.testing.assert_allclose(np.linalg.norm(positions, axis=-1), 1.0, rtol=1e-5)
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
def test_python_rust():
|
|
49
|
+
t0 = time.monotonic()
|
|
50
|
+
positions = ruststartracker.catalog.StarCatalog().normalized_positions(epoch=2025.0)
|
|
51
|
+
print(f"Python catalog took {time.monotonic() - t0:.3f} seconds")
|
|
52
|
+
t0 = time.monotonic()
|
|
53
|
+
positions2 = ruststartracker.libruststartracker.StarCatalog.from_gaia(
|
|
54
|
+
max_magnitude=6.0
|
|
55
|
+
).normalized_positions(epoch=2025.0, observer_position=None)
|
|
56
|
+
print(f"Rust catalog took {time.monotonic() - t0:.3f} seconds")
|
|
57
|
+
np.testing.assert_allclose(positions, positions2, rtol=1e-5, atol=1e-5)
|
|
58
|
+
|
|
59
|
+
|
|
46
60
|
if __name__ == "__main__":
|
|
47
61
|
pytest.main([__file__])
|
|
@@ -25,7 +25,8 @@ def prepare() -> tuple[ruststartracker.StarTracker, np.ndarray]:
|
|
|
25
25
|
dist_coefs = np.array([-0.44120807, -0.15954202, 0.00767012, -0.00213292, -1.64788247])
|
|
26
26
|
|
|
27
27
|
catalog = ruststartracker.StarCatalog()
|
|
28
|
-
star_catalog_vecs = catalog.normalized_positions()
|
|
28
|
+
star_catalog_vecs = catalog.normalized_positions(epoch=2024)
|
|
29
|
+
star_catalog_magnitudes = catalog.magnitude
|
|
29
30
|
|
|
30
31
|
camera_params = ruststartracker.CameraParameters(
|
|
31
32
|
camera_matrix=camera_matrix,
|
|
@@ -35,40 +36,15 @@ def prepare() -> tuple[ruststartracker.StarTracker, np.ndarray]:
|
|
|
35
36
|
|
|
36
37
|
st = ruststartracker.StarTracker(
|
|
37
38
|
star_catalog_vecs,
|
|
39
|
+
star_catalog_magnitudes,
|
|
38
40
|
camera_params,
|
|
39
|
-
inter_star_angle_tolerance=np.radians(0.
|
|
41
|
+
inter_star_angle_tolerance=np.radians(0.05).item(),
|
|
40
42
|
n_minimum_matches=5,
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
return st, star_catalog_vecs
|
|
44
46
|
|
|
45
47
|
|
|
46
|
-
def test_example(prepare: tuple[ruststartracker.StarTracker, np.ndarray]):
|
|
47
|
-
os.environ["RUST_BACKTRACE"] = "1"
|
|
48
|
-
|
|
49
|
-
st, _ = prepare
|
|
50
|
-
|
|
51
|
-
obs = np.array(
|
|
52
|
-
[
|
|
53
|
-
[0.11975033, -0.02227603, 0.9925541],
|
|
54
|
-
[0.03917335, 0.04533212, 0.99820361],
|
|
55
|
-
[0.05137746, -0.01717139, 0.99853167],
|
|
56
|
-
[-0.14742009, 0.00734109, 0.98904673],
|
|
57
|
-
[0.03396359, 0.05851033, 0.99770888],
|
|
58
|
-
[-0.10286126, 0.04479652, 0.99368649],
|
|
59
|
-
[-0.050927, -0.06002669, 0.9968968],
|
|
60
|
-
[0.02815389, 0.02852981, 0.99919638],
|
|
61
|
-
[-0.1390861, 0.05684676, 0.98864731],
|
|
62
|
-
[-0.14276463, -0.13402131, 0.98064089],
|
|
63
|
-
]
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
result = st.process_observation_vectors(obs)
|
|
67
|
-
np.testing.assert_allclose(
|
|
68
|
-
result.quat, [0.1722, -0.4309, 0.8818, 0.08396], rtol=1e-3, atol=1e-3
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
48
|
def test_star_matcher(prepare: tuple[ruststartracker.StarTracker, np.ndarray]):
|
|
73
49
|
os.environ["RUST_BACKTRACE"] = "1"
|
|
74
50
|
|