tse-tools 1.0.0__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.
- tse_tools-1.0.0/LICENSE.md +21 -0
- tse_tools-1.0.0/PKG-INFO +87 -0
- tse_tools-1.0.0/README.md +51 -0
- tse_tools-1.0.0/pyproject.toml +63 -0
- tse_tools-1.0.0/setup.cfg +4 -0
- tse_tools-1.0.0/src/eclipsetools/__init__.py +0 -0
- tse_tools-1.0.0/src/eclipsetools/alignment/__init__.py +0 -0
- tse_tools-1.0.0/src/eclipsetools/alignment/phase_correlation.py +101 -0
- tse_tools-1.0.0/src/eclipsetools/alignment/transform.py +104 -0
- tse_tools-1.0.0/src/eclipsetools/cli.py +20 -0
- tse_tools-1.0.0/src/eclipsetools/commands/__init__.py +0 -0
- tse_tools-1.0.0/src/eclipsetools/commands/align.py +444 -0
- tse_tools-1.0.0/src/eclipsetools/commands/filter.py +285 -0
- tse_tools-1.0.0/src/eclipsetools/commands/stack.py +232 -0
- tse_tools-1.0.0/src/eclipsetools/commands/utils.py +166 -0
- tse_tools-1.0.0/src/eclipsetools/common/__init__.py +0 -0
- tse_tools-1.0.0/src/eclipsetools/common/circle_finder.py +113 -0
- tse_tools-1.0.0/src/eclipsetools/common/image_reader.py +57 -0
- tse_tools-1.0.0/src/eclipsetools/common/image_writer.py +26 -0
- tse_tools-1.0.0/src/eclipsetools/common/moon_masker.py +111 -0
- tse_tools-1.0.0/src/eclipsetools/filtering/__init__.py +0 -0
- tse_tools-1.0.0/src/eclipsetools/filtering/convolution.py +129 -0
- tse_tools-1.0.0/src/eclipsetools/filtering/inpainting.py +100 -0
- tse_tools-1.0.0/src/eclipsetools/filtering/kernels.py +42 -0
- tse_tools-1.0.0/src/eclipsetools/preprocessing/__init__.py +0 -0
- tse_tools-1.0.0/src/eclipsetools/preprocessing/filtering.py +41 -0
- tse_tools-1.0.0/src/eclipsetools/preprocessing/masking.py +178 -0
- tse_tools-1.0.0/src/eclipsetools/preprocessing/workflows.py +71 -0
- tse_tools-1.0.0/src/eclipsetools/stacking/__init__.py +0 -0
- tse_tools-1.0.0/src/eclipsetools/stacking/linear_fit.py +196 -0
- tse_tools-1.0.0/src/eclipsetools/stacking/sorting.py +66 -0
- tse_tools-1.0.0/src/eclipsetools/stacking/weighting.py +8 -0
- tse_tools-1.0.0/src/eclipsetools/validation/__init__.py +0 -0
- tse_tools-1.0.0/src/eclipsetools/validation/sigma_validator.py +21 -0
- tse_tools-1.0.0/src/tse_tools.egg-info/PKG-INFO +87 -0
- tse_tools-1.0.0/src/tse_tools.egg-info/SOURCES.txt +38 -0
- tse_tools-1.0.0/src/tse_tools.egg-info/dependency_links.txt +1 -0
- tse_tools-1.0.0/src/tse_tools.egg-info/entry_points.txt +2 -0
- tse_tools-1.0.0/src/tse_tools.egg-info/requires.txt +13 -0
- tse_tools-1.0.0/src/tse_tools.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Samuli Vuorinen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tse_tools-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tse-tools
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Toolkit for aligning, stacking, and filtering total solar eclipse photographs.
|
|
5
|
+
Author-email: Samuli Vuorinen <samuli.vuorinen@iki.fi>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/naavis/eclipsetools/
|
|
8
|
+
Project-URL: Repository, https://github.com/naavis/eclipsetools/
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE.md
|
|
22
|
+
Requires-Dist: rawpy~=0.25.0
|
|
23
|
+
Requires-Dist: matplotlib~=3.10.3
|
|
24
|
+
Requires-Dist: numpy~=2.2.6
|
|
25
|
+
Requires-Dist: opencv-python-headless~=4.11.0
|
|
26
|
+
Requires-Dist: joblib~=1.5.1
|
|
27
|
+
Requires-Dist: click~=8.1.7
|
|
28
|
+
Requires-Dist: tifffile~=2025.6.11
|
|
29
|
+
Requires-Dist: tqdm~=4.67.1
|
|
30
|
+
Requires-Dist: scipy~=1.15.2
|
|
31
|
+
Requires-Dist: scikit-learn~=1.7.0
|
|
32
|
+
Requires-Dist: numba~=0.61.2
|
|
33
|
+
Requires-Dist: numba-progress~=1.1.0
|
|
34
|
+
Requires-Dist: scikit-image~=0.25.2
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Total Solar Eclipse Toolkit
|
|
38
|
+
|
|
39
|
+
Toolkit for aligning and processing total solar eclipse photographs.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install tse-tools
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
tse-tools --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
### Setting up
|
|
56
|
+
|
|
57
|
+
Create a virtual environment and install the package in editable mode with the
|
|
58
|
+
development dependencies:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m venv .venv
|
|
62
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
63
|
+
pip install -e . --group dev
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This installs the runtime dependencies, the test tooling, and `black`, and puts
|
|
67
|
+
the `tse-tools` command on your `PATH` (pointing at your working tree).
|
|
68
|
+
|
|
69
|
+
### Running in development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
tse-tools --help # or: python -m eclipsetools.cli --help
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Formatting
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
black src tests
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Tests
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pytest -n auto
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The alignment tests depend on the binary asset `tests/images/eclipse_5ms.CR3`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Total Solar Eclipse Toolkit
|
|
2
|
+
|
|
3
|
+
Toolkit for aligning and processing total solar eclipse photographs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install tse-tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
tse-tools --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
### Setting up
|
|
20
|
+
|
|
21
|
+
Create a virtual environment and install the package in editable mode with the
|
|
22
|
+
development dependencies:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m venv .venv
|
|
26
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
27
|
+
pip install -e . --group dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This installs the runtime dependencies, the test tooling, and `black`, and puts
|
|
31
|
+
the `tse-tools` command on your `PATH` (pointing at your working tree).
|
|
32
|
+
|
|
33
|
+
### Running in development
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
tse-tools --help # or: python -m eclipsetools.cli --help
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Formatting
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
black src tests
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Tests
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pytest -n auto
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The alignment tests depend on the binary asset `tests/images/eclipse_5ms.CR3`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tse-tools"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Toolkit for aligning, stacking, and filtering total solar eclipse photographs."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Samuli Vuorinen", email = "samuli.vuorinen@iki.fi" }
|
|
7
|
+
]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 5 - Production/Stable",
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Astronomy",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
readme = 'README.md'
|
|
21
|
+
license = 'MIT'
|
|
22
|
+
license-files = ['LICENSE.md']
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"rawpy ~= 0.25.0",
|
|
26
|
+
"matplotlib ~= 3.10.3",
|
|
27
|
+
"numpy ~= 2.2.6",
|
|
28
|
+
"opencv-python-headless ~= 4.11.0",
|
|
29
|
+
"joblib ~= 1.5.1",
|
|
30
|
+
"click ~= 8.1.7",
|
|
31
|
+
"tifffile ~= 2025.6.11",
|
|
32
|
+
"tqdm ~= 4.67.1",
|
|
33
|
+
"scipy ~= 1.15.2",
|
|
34
|
+
"scikit-learn ~= 1.7.0",
|
|
35
|
+
"numba ~= 0.61.2",
|
|
36
|
+
"numba-progress ~= 1.1.0",
|
|
37
|
+
"scikit-image ~= 0.25.2",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
test = [
|
|
42
|
+
"pytest ~= 8.4.0",
|
|
43
|
+
"pytest-xdist ~= 3.7.0",
|
|
44
|
+
]
|
|
45
|
+
dev = [
|
|
46
|
+
{ include-group = "test" },
|
|
47
|
+
"black ~= 25.1.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://github.com/naavis/eclipsetools/"
|
|
52
|
+
Repository = "https://github.com/naavis/eclipsetools/"
|
|
53
|
+
|
|
54
|
+
[project.scripts]
|
|
55
|
+
tse-tools = "eclipsetools.cli:main"
|
|
56
|
+
|
|
57
|
+
[build-system]
|
|
58
|
+
requires = ["setuptools", "wheel"]
|
|
59
|
+
build-backend = "setuptools.build_meta"
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["src"]
|
|
63
|
+
include = ["eclipsetools*"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from eclipsetools.preprocessing.masking import hann_window_mask
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def phase_correlate_with_low_pass(
|
|
9
|
+
img_a: np.ndarray,
|
|
10
|
+
img_b: np.ndarray,
|
|
11
|
+
low_pass_sigma: float | None = None,
|
|
12
|
+
) -> np.ndarray:
|
|
13
|
+
"""
|
|
14
|
+
Phase correlate two images with optional low-pass filtering in the frequency domain. This method was suggested
|
|
15
|
+
by Druckmüller and Druckmüllerová. The correlation peak is further refined by calculating the center of mass
|
|
16
|
+
in a 5x5 window around the initial peak.
|
|
17
|
+
:param img_a: First image
|
|
18
|
+
:param img_b: Second image
|
|
19
|
+
:param low_pass_sigma: Standard deviation for Gaussian low-pass filter in frequency domain. The lower the value,
|
|
20
|
+
the stronger the low-pass filtering. If None, no low-pass filtering is applied.
|
|
21
|
+
:return: Translation vector (dy, dx) indicating how much the second image is shifted relative to the first.
|
|
22
|
+
"""
|
|
23
|
+
assert img_a.shape == img_b.shape
|
|
24
|
+
|
|
25
|
+
window = hann_window_mask(img_a.shape)
|
|
26
|
+
|
|
27
|
+
img_a_win = window * img_a
|
|
28
|
+
img_a_win -= img_a_win.mean()
|
|
29
|
+
img_a_win /= img_a_win.std()
|
|
30
|
+
|
|
31
|
+
img_b_win = window * img_b
|
|
32
|
+
img_b_win -= img_b_win.mean()
|
|
33
|
+
img_b_win /= img_b_win.std()
|
|
34
|
+
|
|
35
|
+
fft1 = np.fft.rfft2(img_a_win)
|
|
36
|
+
fft2 = np.fft.rfft2(img_b_win)
|
|
37
|
+
|
|
38
|
+
offset = 0.01 * np.max(np.abs(fft1))
|
|
39
|
+
cross_power_spectrum = (
|
|
40
|
+
fft1 * np.conjugate(fft2) / ((np.abs(fft1) + offset) * (np.abs(fft2) + offset))
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if low_pass_sigma:
|
|
44
|
+
cross_power_spectrum *= _gaussian_weights(low_pass_sigma, img_a.shape)
|
|
45
|
+
|
|
46
|
+
phase_correlation = np.abs(np.fft.irfft2(cross_power_spectrum, s=img_a.shape))
|
|
47
|
+
phase_correlation = np.fft.ifftshift(phase_correlation)
|
|
48
|
+
|
|
49
|
+
initial_peak = np.unravel_index(np.argmax(phase_correlation), img_a.shape)
|
|
50
|
+
subpixel_peak = _center_of_mass(
|
|
51
|
+
phase_correlation,
|
|
52
|
+
(int(initial_peak[0]), int(initial_peak[1])),
|
|
53
|
+
5,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return np.array(img_a.shape) / 2 - subpixel_peak
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@functools.cache
|
|
60
|
+
def _gaussian_weights(sigma: float, image_shape: tuple) -> np.ndarray:
|
|
61
|
+
fy = np.fft.rfftfreq(image_shape[1])
|
|
62
|
+
fx = np.fft.fftfreq(image_shape[0])
|
|
63
|
+
fy_grid, fx_grid = np.meshgrid(fy, fx)
|
|
64
|
+
freq_squared = fy_grid**2 + fx_grid**2
|
|
65
|
+
gaussian_weighting = np.exp(-0.5 * freq_squared / (sigma**2), dtype=np.float32)
|
|
66
|
+
return gaussian_weighting
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _center_of_mass(
|
|
70
|
+
image: np.ndarray,
|
|
71
|
+
center_point: tuple[int, int],
|
|
72
|
+
window_size: int,
|
|
73
|
+
) -> tuple[float, float]:
|
|
74
|
+
height, width = image.shape
|
|
75
|
+
y0, x0 = center_point
|
|
76
|
+
half_size = window_size // 2
|
|
77
|
+
|
|
78
|
+
# Generate wrapped indices
|
|
79
|
+
y_indices = np.arange(y0 - half_size, y0 + half_size + 1) % height
|
|
80
|
+
x_indices = np.arange(x0 - half_size, x0 + half_size + 1) % width
|
|
81
|
+
|
|
82
|
+
# Extract wrapped region
|
|
83
|
+
region = image[np.ix_(y_indices, x_indices)]
|
|
84
|
+
|
|
85
|
+
# Create coordinate grid (local to the region)
|
|
86
|
+
y_grid, x_grid = np.indices(region.shape)
|
|
87
|
+
|
|
88
|
+
# Compute total intensity
|
|
89
|
+
total_mass = region.sum()
|
|
90
|
+
if total_mass == 0:
|
|
91
|
+
return float(y0), float(x0) # or None
|
|
92
|
+
|
|
93
|
+
# Compute local CoM
|
|
94
|
+
y_com_local = (y_grid * region).sum() / total_mass
|
|
95
|
+
x_com_local = (x_grid * region).sum() / total_mass
|
|
96
|
+
|
|
97
|
+
# Map back to global coordinates (using wrapping)
|
|
98
|
+
y_com_global = np.float32((y0 - half_size + y_com_local) % height)
|
|
99
|
+
x_com_global = np.float32((x0 - half_size + x_com_local) % width)
|
|
100
|
+
|
|
101
|
+
return y_com_global, x_com_global
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from eclipsetools.alignment.phase_correlation import phase_correlate_with_low_pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_transform(
|
|
8
|
+
ref_image: np.ndarray,
|
|
9
|
+
image: np.ndarray,
|
|
10
|
+
low_pass_sigma: float,
|
|
11
|
+
allow_scale: bool,
|
|
12
|
+
) -> tuple[float, float, tuple[float, float]]:
|
|
13
|
+
"""
|
|
14
|
+
Find the scale, rotation, and translation between two images.
|
|
15
|
+
|
|
16
|
+
This function uses a multistep approach:
|
|
17
|
+
1. Find scale and rotation using log-polar transform and phase correlation
|
|
18
|
+
2. Apply the found scale and rotation to the image
|
|
19
|
+
3. Find the translation using phase correlation on the transformed image
|
|
20
|
+
|
|
21
|
+
:param ref_image: Reference image to align against
|
|
22
|
+
:param image: Image to be aligned
|
|
23
|
+
:param low_pass_sigma: Standard deviation for Gaussian low-pass filter in frequency domain when finding translation.
|
|
24
|
+
:param allow_scale: Allow estimating scale. If False, scale is assumed to be 1.0.
|
|
25
|
+
:return: Tuple containing (scale, rotation_angle_degrees, (dy, dx))
|
|
26
|
+
where dy, dx is the translation vector from the reference image to the aligned image.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
ref_image_pad = _pad_with_zeros(ref_image)
|
|
30
|
+
image_pad = _pad_with_zeros(image)
|
|
31
|
+
|
|
32
|
+
shape = ref_image_pad.shape
|
|
33
|
+
shortest_side = np.min(shape)
|
|
34
|
+
radius = shortest_side // 8
|
|
35
|
+
|
|
36
|
+
# Step 1: Find scale and rotation
|
|
37
|
+
# We only need half of the log-polar FFTs, as they are symmetric
|
|
38
|
+
ref_fft_log_polar = _log_polar_fft(ref_image_pad, radius)[: shape[0] // 2, :]
|
|
39
|
+
image_fft_log_polar = _log_polar_fft(image_pad, radius)[: shape[0] // 2, :]
|
|
40
|
+
|
|
41
|
+
# Find shifts in the log-polar FFTs using phase correlation
|
|
42
|
+
shift_y, shift_x = phase_correlate_with_low_pass(
|
|
43
|
+
ref_fft_log_polar, image_fft_log_polar
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Recover rotation from the correlation result
|
|
47
|
+
rotation_degrees = -360.0 * shift_y / ref_fft_log_polar.shape[0]
|
|
48
|
+
|
|
49
|
+
# Recover scale from the correlation result
|
|
50
|
+
k_log = radius / np.log(radius)
|
|
51
|
+
scale = np.exp(-shift_x / k_log) if allow_scale else 1.0
|
|
52
|
+
|
|
53
|
+
# Step 2: Apply scale and rotation to the image
|
|
54
|
+
rotate_scale_matrix = cv2.getRotationMatrix2D(
|
|
55
|
+
(image_pad.shape[1] // 2, image_pad.shape[0] // 2),
|
|
56
|
+
-rotation_degrees,
|
|
57
|
+
1.0 / scale,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
translated_image = cv2.warpAffine(
|
|
61
|
+
image_pad,
|
|
62
|
+
rotate_scale_matrix,
|
|
63
|
+
dsize=(image_pad.shape[1], image_pad.shape[0]),
|
|
64
|
+
flags=cv2.INTER_LINEAR,
|
|
65
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
66
|
+
borderValue=[0, 0, 0],
|
|
67
|
+
).astype(np.float32)
|
|
68
|
+
|
|
69
|
+
# Step 3: Find translation between reference and transformed image
|
|
70
|
+
translation_y, translation_x = phase_correlate_with_low_pass(
|
|
71
|
+
ref_image_pad, translated_image, low_pass_sigma
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
float(scale),
|
|
76
|
+
float(rotation_degrees),
|
|
77
|
+
(float(translation_y), float(translation_x)),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _pad_with_zeros(image: np.ndarray) -> np.ndarray:
|
|
82
|
+
assert len(image.shape) == 2, "Input image must be a 2D array (grayscale image)."
|
|
83
|
+
h, w = image.shape
|
|
84
|
+
longer_side = max(h, w)
|
|
85
|
+
padded = np.zeros((longer_side, longer_side), dtype=image.dtype)
|
|
86
|
+
y_offset = (longer_side - h) // 2
|
|
87
|
+
x_offset = (longer_side - w) // 2
|
|
88
|
+
padded[y_offset : y_offset + h, x_offset : x_offset + w] = image
|
|
89
|
+
return padded
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _log_polar_fft(image: np.ndarray, radius: float) -> np.ndarray:
|
|
93
|
+
assert len(image.shape) == 2, "Input image must be a 2D array (grayscale image)."
|
|
94
|
+
fft_mag = np.abs(np.fft.fftshift(np.fft.fft2(image)))
|
|
95
|
+
# Amplitude spectra have very high values in the low frequencies, so we use logarithm to compress the range
|
|
96
|
+
fft_mag = np.log(1.0 + fft_mag)
|
|
97
|
+
log_polar = cv2.warpPolar(
|
|
98
|
+
src=fft_mag,
|
|
99
|
+
dsize=(0, 0),
|
|
100
|
+
center=(fft_mag.shape[1] // 2, fft_mag.shape[0] // 2),
|
|
101
|
+
maxRadius=radius,
|
|
102
|
+
flags=cv2.INTER_LINEAR | cv2.WARP_POLAR_LOG,
|
|
103
|
+
)
|
|
104
|
+
return log_polar
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from eclipsetools.commands.align import align_group
|
|
4
|
+
from eclipsetools.commands.filter import filter_group
|
|
5
|
+
from eclipsetools.commands.stack import stack_group
|
|
6
|
+
from eclipsetools.commands.utils import utils_group
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(context_settings={"show_default": True})
|
|
10
|
+
def main():
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
main.add_command(align_group) # type: ignore
|
|
15
|
+
main.add_command(filter_group) # type: ignore
|
|
16
|
+
main.add_command(stack_group) # type: ignore
|
|
17
|
+
main.add_command(utils_group) # type: ignore
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
File without changes
|