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.
Files changed (40) hide show
  1. tse_tools-1.0.0/LICENSE.md +21 -0
  2. tse_tools-1.0.0/PKG-INFO +87 -0
  3. tse_tools-1.0.0/README.md +51 -0
  4. tse_tools-1.0.0/pyproject.toml +63 -0
  5. tse_tools-1.0.0/setup.cfg +4 -0
  6. tse_tools-1.0.0/src/eclipsetools/__init__.py +0 -0
  7. tse_tools-1.0.0/src/eclipsetools/alignment/__init__.py +0 -0
  8. tse_tools-1.0.0/src/eclipsetools/alignment/phase_correlation.py +101 -0
  9. tse_tools-1.0.0/src/eclipsetools/alignment/transform.py +104 -0
  10. tse_tools-1.0.0/src/eclipsetools/cli.py +20 -0
  11. tse_tools-1.0.0/src/eclipsetools/commands/__init__.py +0 -0
  12. tse_tools-1.0.0/src/eclipsetools/commands/align.py +444 -0
  13. tse_tools-1.0.0/src/eclipsetools/commands/filter.py +285 -0
  14. tse_tools-1.0.0/src/eclipsetools/commands/stack.py +232 -0
  15. tse_tools-1.0.0/src/eclipsetools/commands/utils.py +166 -0
  16. tse_tools-1.0.0/src/eclipsetools/common/__init__.py +0 -0
  17. tse_tools-1.0.0/src/eclipsetools/common/circle_finder.py +113 -0
  18. tse_tools-1.0.0/src/eclipsetools/common/image_reader.py +57 -0
  19. tse_tools-1.0.0/src/eclipsetools/common/image_writer.py +26 -0
  20. tse_tools-1.0.0/src/eclipsetools/common/moon_masker.py +111 -0
  21. tse_tools-1.0.0/src/eclipsetools/filtering/__init__.py +0 -0
  22. tse_tools-1.0.0/src/eclipsetools/filtering/convolution.py +129 -0
  23. tse_tools-1.0.0/src/eclipsetools/filtering/inpainting.py +100 -0
  24. tse_tools-1.0.0/src/eclipsetools/filtering/kernels.py +42 -0
  25. tse_tools-1.0.0/src/eclipsetools/preprocessing/__init__.py +0 -0
  26. tse_tools-1.0.0/src/eclipsetools/preprocessing/filtering.py +41 -0
  27. tse_tools-1.0.0/src/eclipsetools/preprocessing/masking.py +178 -0
  28. tse_tools-1.0.0/src/eclipsetools/preprocessing/workflows.py +71 -0
  29. tse_tools-1.0.0/src/eclipsetools/stacking/__init__.py +0 -0
  30. tse_tools-1.0.0/src/eclipsetools/stacking/linear_fit.py +196 -0
  31. tse_tools-1.0.0/src/eclipsetools/stacking/sorting.py +66 -0
  32. tse_tools-1.0.0/src/eclipsetools/stacking/weighting.py +8 -0
  33. tse_tools-1.0.0/src/eclipsetools/validation/__init__.py +0 -0
  34. tse_tools-1.0.0/src/eclipsetools/validation/sigma_validator.py +21 -0
  35. tse_tools-1.0.0/src/tse_tools.egg-info/PKG-INFO +87 -0
  36. tse_tools-1.0.0/src/tse_tools.egg-info/SOURCES.txt +38 -0
  37. tse_tools-1.0.0/src/tse_tools.egg-info/dependency_links.txt +1 -0
  38. tse_tools-1.0.0/src/tse_tools.egg-info/entry_points.txt +2 -0
  39. tse_tools-1.0.0/src/tse_tools.egg-info/requires.txt +13 -0
  40. 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.
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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