teareduce 0.4.9__py3-none-any.whl → 0.5.1__py3-none-any.whl

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.
@@ -0,0 +1,51 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Definitions for the cleanest module."""
11
+
12
+ # Default parameters for L.A.Cosmic algorithm
13
+ # Note that 'type' is set to the expected data type for each parameter
14
+ # using the intrinsic Python types, so that they can be easily cast
15
+ # when reading user input.
16
+ lacosmic_default_dict = {
17
+ # L.A.Cosmic parameters
18
+ 'gain': {'value': 1.0, 'type': float, 'positive': True},
19
+ 'readnoise': {'value': 6.5, 'type': float, 'positive': True},
20
+ 'sigclip': {'value': 4.5, 'type': float, 'positive': True},
21
+ 'sigfrac': {'value': 0.3, 'type': float, 'positive': True},
22
+ 'objlim': {'value': 5.0, 'type': float, 'positive': True},
23
+ 'niter': {'value': 4, 'type': int, 'positive': True},
24
+ 'verbose': {'value': False, 'type': bool},
25
+ # Dilation of the mask
26
+ 'dilation': {'value': 0, 'type': int, 'positive': True},
27
+ # Limits for the image section to process (pixels start at 1)
28
+ 'xmin': {'value': 1, 'type': int, 'positive': True},
29
+ 'xmax': {'value': None, 'type': int, 'positive': True},
30
+ 'ymin': {'value': 1, 'type': int, 'positive': True},
31
+ 'ymax': {'value': None, 'type': int, 'positive': True}
32
+ }
33
+
34
+ # Default parameters for cleaning methods
35
+ VALID_CLEANING_METHODS = [
36
+ 'x interp.',
37
+ 'y interp.',
38
+ 'surface interp.',
39
+ 'median',
40
+ 'lacosmic',
41
+ 'auxdata'
42
+ ]
43
+
44
+ # Maximum pixel distance to consider when finding closest CR pixel
45
+ MAX_PIXEL_DISTANCE_TO_CR = 15
46
+
47
+ # Default number of points for interpolation
48
+ DEFAULT_NPOINTS_INTERP = 2
49
+
50
+ # Default degree for interpolation
51
+ DEFAULT_DEGREE_INTERP = 1
@@ -0,0 +1,44 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Find the closest true (CR) pixel to a given (x, y) position."""
11
+
12
+ import numpy as np
13
+
14
+
15
+ def find_closest_true(mask, x, y):
16
+ """Find the closest True pixel in a boolean mask to the given (x, y) position.
17
+
18
+ Parameters
19
+ ----------
20
+ mask : 2D numpy.ndarray of bool
21
+ Boolean mask where True indicates the presence of a cosmic ray pixel.
22
+ x : int
23
+ X-coordinate (column index) of the reference position (0-based).
24
+ y : int
25
+ Y-coordinate (row index) of the reference position (0-based).
26
+
27
+ Returns
28
+ -------
29
+ (closest_x, closest_y) : tuple of int
30
+ Coordinates (0-based) of the closest True pixel in the mask.
31
+ Returns (None, None) if no True pixels are found.
32
+ min_distance : float
33
+ Euclidean distance to the closest True pixel.
34
+ """
35
+ true_indices = np.argwhere(mask)
36
+ if true_indices.size == 0:
37
+ return None, None
38
+
39
+ distances = np.sqrt((true_indices[:, 1] - x) ** 2 + (true_indices[:, 0] - y) ** 2)
40
+ min_index = np.argmin(distances)
41
+ closest_y, closest_x = true_indices[min_index]
42
+ min_distance = distances[min_index]
43
+
44
+ return (closest_x, closest_y), min_distance
@@ -0,0 +1,155 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Base class for image display with min/max and zscale controls."""
11
+
12
+ from tkinter import simpledialog
13
+ import numpy as np
14
+
15
+ from ..sliceregion import SliceRegion2D
16
+ from ..zscale import zscale
17
+
18
+
19
+ # The functionality defined here is used in multiple classes
20
+ class ImageDisplay:
21
+ """Class to handle image display with min/max and zscale controls.
22
+
23
+ Methods
24
+ -------
25
+ set_vmin()
26
+ Prompt user to set a new minimum display value (vmin).
27
+ set_vmax()
28
+ Prompt user to set a new maximum display value (vmax).
29
+ get_vmin()
30
+ Get the current minimum display value (vmin).
31
+ get_vmax()
32
+ Get the current maximum display value (vmax).
33
+ get_displayed_region()
34
+ Get the currently displayed region of the image.
35
+ set_minmax()
36
+ Set vmin and vmax based on the currently displayed region.
37
+ set_zscale()
38
+ Set vmin and vmax using zscale on the currently displayed region.
39
+
40
+ Attributes
41
+ ----------
42
+ vmin_button : tkinter.Button
43
+ Button to display and set the minimum display value (vmin).
44
+ vmax_button : tkinter.Button
45
+ Button to display and set the maximum display value (vmax).
46
+ image : matplotlib.image.AxesImage
47
+ The main image being displayed.
48
+ image_aux : matplotlib.image.AxesImage, optional
49
+ An auxiliary image being displayed (if any).
50
+ canvas : matplotlib.backends.backend_tkagg.FigureCanvasTkAgg
51
+ The canvas on which the image is drawn.
52
+
53
+ Notes
54
+ -----
55
+ This class is intented to be used as a parent class for different
56
+ classes that display images and need functionality to adjust the
57
+ display limits (vmin and vmax) interactively.
58
+
59
+ This class assumes that the image data is stored in `self.data` and that
60
+ the displayed region can be determined from either the axes limits or a
61
+ predefined region attribute.
62
+ """
63
+ def set_vmin(self):
64
+ """Prompt user to set a new minimum display value (vmin)."""
65
+ old_vmin = self.get_vmin()
66
+ old_vmax = self.get_vmax()
67
+ new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
68
+ if new_vmin is None:
69
+ return
70
+ if new_vmin >= old_vmax:
71
+ print("Error: vmin must be less than vmax.")
72
+ return
73
+ self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
74
+ self.image.set_clim(vmin=new_vmin)
75
+ if hasattr(self, 'image_aux'):
76
+ self.image_aux.set_clim(vmin=new_vmin)
77
+ self.canvas.draw_idle()
78
+
79
+ def set_vmax(self):
80
+ """Prompt user to set a new maximum display value (vmax)."""
81
+ old_vmin = self.get_vmin()
82
+ old_vmax = self.get_vmax()
83
+ new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
84
+ if new_vmax is None:
85
+ return
86
+ if new_vmax <= old_vmin:
87
+ print("Error: vmax must be greater than vmin.")
88
+ return
89
+ self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
90
+ self.image.set_clim(vmax=new_vmax)
91
+ if hasattr(self, 'image_aux'):
92
+ self.image_aux.set_clim(vmax=new_vmax)
93
+ self.canvas.draw_idle()
94
+
95
+ def get_vmin(self):
96
+ """Get the current minimum display value (vmin)."""
97
+ return float(self.vmin_button.cget("text").split(":")[1])
98
+
99
+ def get_vmax(self):
100
+ """Get the current maximum display value (vmax)."""
101
+ return float(self.vmax_button.cget("text").split(":")[1])
102
+
103
+ def get_displayed_region(self):
104
+ """Get the currently displayed region of the image."""
105
+ if hasattr(self, 'ax'):
106
+ xmin, xmax = self.ax.get_xlim()
107
+ xmin = int(xmin + 0.5)
108
+ if xmin < 1:
109
+ xmin = 1
110
+ xmax = int(xmax + 0.5)
111
+ if xmax > self.data.shape[1]:
112
+ xmax = self.data.shape[1]
113
+ ymin, ymax = self.ax.get_ylim()
114
+ ymin = int(ymin + 0.5)
115
+ if ymin < 1:
116
+ ymin = 1
117
+ ymax = int(ymax + 0.5)
118
+ if ymax > self.data.shape[0]:
119
+ ymax = self.data.shape[0]
120
+ print(f"Setting min/max using axis limits: x=({xmin:.2f}, {xmax:.2f}), y=({ymin:.2f}, {ymax:.2f})")
121
+ region = self.region = SliceRegion2D(
122
+ f'[{xmin}:{xmax}, {ymin}:{ymax}]', mode='fits'
123
+ ).python
124
+ elif hasattr(self, 'region'):
125
+ region = self.region
126
+ else:
127
+ raise AttributeError("No axis or region defined for set_minmax.")
128
+ return region
129
+
130
+ def set_minmax(self):
131
+ """Set vmin and vmax based on the currently displayed region."""
132
+ region = self.get_displayed_region()
133
+ vmin_new = np.min(self.data[region])
134
+ vmax_new = np.max(self.data[region])
135
+ self.vmin_button.config(text=f"vmin: {vmin_new:.2f}")
136
+ self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
137
+ self.image.set_clim(vmin=vmin_new)
138
+ self.image.set_clim(vmax=vmax_new)
139
+ if hasattr(self, 'image_aux'):
140
+ self.image_aux.set_clim(vmin=vmin_new)
141
+ self.image_aux.set_clim(vmax=vmax_new)
142
+ self.canvas.draw_idle()
143
+
144
+ def set_zscale(self):
145
+ """Set vmin and vmax using zscale on the currently displayed region."""
146
+ region = self.get_displayed_region()
147
+ vmin_new, vmax_new = zscale(self.data[region])
148
+ self.vmin_button.config(text=f"vmin: {vmin_new:.2f}")
149
+ self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
150
+ self.image.set_clim(vmin=vmin_new)
151
+ self.image.set_clim(vmax=vmax_new)
152
+ if hasattr(self, 'image_aux'):
153
+ self.image_aux.set_clim(vmin=vmin_new)
154
+ self.image_aux.set_clim(vmax=vmax_new)
155
+ self.canvas.draw_idle()
@@ -0,0 +1,83 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Surface interpolation (plane fit) or median interpolation"""
11
+
12
+ import numpy as np
13
+ from scipy.ndimage import binary_dilation
14
+
15
+
16
+ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
17
+ """Interpolate cosmic ray pixels using surface fit or median of border pixels.
18
+
19
+ Parameters
20
+ ----------
21
+ data : 2D numpy.ndarray
22
+ The image data array where cosmic rays are to be interpolated.
23
+ mask_fixed : 2D numpy.ndarray of bool
24
+ A boolean mask array indicating which pixels have been fixed.
25
+ cr_labels : 2D numpy.ndarray
26
+ An array labeling cosmic ray features.
27
+ cr_index : int
28
+ The index of the current cosmic ray feature to interpolate.
29
+ npoints : int
30
+ The number of points to use for interpolation.
31
+ method : str
32
+ The interpolation method to use ('surface' or 'median').
33
+
34
+ Returns
35
+ -------
36
+ interpolation_performed : bool
37
+ True if interpolation was performed, False otherwise.
38
+ xfit_all : list
39
+ X-coordinates of border pixels used for interpolation.
40
+ yfit_all : list
41
+ Y-coordinates of border pixels used for interpolation.
42
+ """
43
+ # Mask of CR pixels
44
+ mask = (cr_labels == cr_index)
45
+ # Dilate the mask to find border pixels
46
+ dilated_mask = binary_dilation(mask, structure=np.ones((3, 3)), iterations=npoints)
47
+ # Border pixels are those in the dilated mask but not in the original mask
48
+ border_mask = dilated_mask & (~mask)
49
+ # Get coordinates of border pixels
50
+ yfit_all, xfit_all = np.where(border_mask)
51
+ zfit_all = data[yfit_all, xfit_all].tolist()
52
+ # Perform interpolation
53
+ interpolation_performed = False
54
+ if method == 'surface':
55
+ if len(xfit_all) > 3:
56
+ # Construct the design matrix for a 2D polynomial fit to a plane,
57
+ # where each row corresponds to a point (x, y, z) and the model
58
+ # is z = C[0]*x + C[1]*y + C[2]
59
+ A = np.c_[xfit_all, yfit_all, np.ones(len(xfit_all))]
60
+ # Least squares polynomial fit
61
+ C, _, _, _ = np.linalg.lstsq(A, zfit_all, rcond=None)
62
+ # recompute all CR pixels to take into account "holes" between marked pixels
63
+ ycr_list, xcr_list = np.where(cr_labels == cr_index)
64
+ for iy, ix in zip(ycr_list, xcr_list):
65
+ data[iy, ix] = C[0] * ix + C[1] * iy + C[2]
66
+ mask_fixed[iy, ix] = True
67
+ interpolation_performed = True
68
+ else:
69
+ print("Not enough points to fit a plane")
70
+ elif method == 'median':
71
+ # Compute median of all surrounding points
72
+ if len(zfit_all) > 0:
73
+ zmed = np.median(zfit_all)
74
+ # recompute all CR pixels to take into account "holes" between marked pixels
75
+ ycr_list, xcr_list = np.where(cr_labels == cr_index)
76
+ for iy, ix in zip(ycr_list, xcr_list):
77
+ data[iy, ix] = zmed
78
+ mask_fixed[iy, ix] = True
79
+ interpolation_performed = True
80
+ else:
81
+ print(f"Unknown interpolation method: {method}")
82
+
83
+ return interpolation_performed, xfit_all, yfit_all
@@ -0,0 +1,80 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Polynomial nterpolation in the X direction"""
11
+
12
+ import numpy as np
13
+
14
+
15
+ def interpolation_x(data, mask_fixed, cr_labels, cr_index, npoints, degree):
16
+ """Interpolate cosmic ray affected pixels in the X direction.
17
+ Parameters
18
+ ----------
19
+ data : 2D numpy.ndarray
20
+ The image data array to be processed.
21
+ mask_fixed : 2D numpy.ndarray of bool
22
+ A boolean mask array indicating which pixels have been fixed.
23
+ cr_labels : 2D numpy.ndarray
24
+ An array labeling cosmic ray features.
25
+ cr_index : int
26
+ The index of the current cosmic ray feature to interpolate.
27
+ npoints : int
28
+ The number of points to use for interpolation.
29
+ degree : int
30
+ The degree of the polynomial to fit.
31
+
32
+ Returns
33
+ -------
34
+ interpolation_performed : bool
35
+ True if interpolation was performed, False otherwise.
36
+ xfit_all : list
37
+ X-coordinates of border pixels used for interpolation.
38
+ yfit_all : list
39
+ Y-coordinates of border pixels used for interpolation.
40
+ """
41
+ ycr_list, xcr_list = np.where(cr_labels == cr_index)
42
+ ycr_min = np.min(ycr_list)
43
+ ycr_max = np.max(ycr_list)
44
+ xfit_all = []
45
+ yfit_all = []
46
+ interpolation_performed = False
47
+ for ycr in range(ycr_min, ycr_max + 1):
48
+ xmarked = xcr_list[np.where(ycr_list == ycr)]
49
+ if len(xmarked) > 0:
50
+ jmin = np.min(xmarked)
51
+ jmax = np.max(xmarked)
52
+ # mark intermediate pixels too
53
+ for ix in range(jmin, jmax + 1):
54
+ cr_labels[ycr, ix] = cr_index
55
+ xmarked = xcr_list[np.where(ycr_list == ycr)]
56
+ xfit = []
57
+ zfit = []
58
+ for i in range(jmin - npoints, jmin):
59
+ if 0 <= i < data.shape[1]:
60
+ xfit.append(i)
61
+ xfit_all.append(i)
62
+ yfit_all.append(ycr)
63
+ zfit.append(data[ycr, i])
64
+ for i in range(jmax + 1, jmax + 1 + npoints):
65
+ if 0 <= i < data.shape[1]:
66
+ xfit.append(i)
67
+ xfit_all.append(i)
68
+ yfit_all.append(ycr)
69
+ zfit.append(data[ycr, i])
70
+ if len(xfit) > degree:
71
+ p = np.polyfit(xfit, zfit, degree)
72
+ for i in range(jmin, jmax + 1):
73
+ if 0 <= i < data.shape[1]:
74
+ data[ycr, i] = np.polyval(p, i)
75
+ mask_fixed[ycr, i] = True
76
+ interpolation_performed = True
77
+ else:
78
+ print(f"Not enough points to fit at y={ycr+1}")
79
+
80
+ return interpolation_performed, xfit_all, yfit_all
@@ -0,0 +1,81 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Polynomial nterpolation in the Y direction"""
11
+
12
+ import numpy as np
13
+
14
+
15
+ def interpolation_y(data, mask_fixed, cr_labels, cr_index, npoints, degree):
16
+ """Interpolate cosmic ray affected pixels in Y direction.
17
+ Parameters
18
+ ----------
19
+ data : 2D numpy.ndarray
20
+ The image data array to be processed.
21
+ mask_fixed : 2D numpy.ndarray of bool
22
+ A boolean mask array indicating which pixels have been fixed.
23
+ cr_labels : 2D numpy.ndarray
24
+ An array labeling cosmic ray features.
25
+ cr_index : int
26
+ The index of the current cosmic ray feature to interpolate.
27
+ npoints : int
28
+ The number of points to use for interpolation.
29
+ degree : int
30
+ The degree of the polynomial to fit.
31
+
32
+ Returns
33
+ -------
34
+ interpolation_performed : bool
35
+ True if interpolation was performed, False otherwise.
36
+ xfit_all : list
37
+ X-coordinates of border pixels used for interpolation.
38
+ yfit_all : list
39
+ Y-coordinates of border pixels used for interpolation.
40
+ """
41
+ ycr_list, xcr_list = np.where(cr_labels == cr_index)
42
+ xcr_min = np.min(xcr_list)
43
+ xcr_max = np.max(xcr_list)
44
+ xfit_all = []
45
+ yfit_all = []
46
+ interpolation_performed = False
47
+ for xcr in range(xcr_min, xcr_max + 1):
48
+ ymarked = ycr_list[np.where(xcr_list == xcr)]
49
+ if len(ymarked) > 0:
50
+ imin = np.min(ymarked)
51
+ imax = np.max(ymarked)
52
+ # mark intermediate pixels too
53
+ for iy in range(imin, imax + 1):
54
+ cr_labels[iy, xcr] = cr_index
55
+ ymarked = ycr_list[np.where(xcr_list == xcr)]
56
+ yfit = []
57
+ zfit = []
58
+ for i in range(imin - npoints, imin):
59
+ if 0 <= i < data.shape[0]:
60
+ yfit.append(i)
61
+ yfit_all.append(i)
62
+ xfit_all.append(xcr)
63
+ zfit.append(data[i, xcr])
64
+ for i in range(imax + 1, imax + 1 + npoints):
65
+ if 0 <= i < data.shape[0]:
66
+ yfit.append(i)
67
+ yfit_all.append(i)
68
+ xfit_all.append(xcr)
69
+ zfit.append(data[i, xcr])
70
+ if len(yfit) > degree:
71
+ p = np.polyfit(yfit, zfit, degree)
72
+ for i in range(imin, imax + 1):
73
+ if 0 <= i < data.shape[1]:
74
+ data[i, xcr] = np.polyval(p, i)
75
+ mask_fixed[i, xcr] = True
76
+ interpolation_performed = True
77
+ else:
78
+ print(f"Not enough points to fit at x={xcr+1}")
79
+ return
80
+
81
+ return interpolation_performed, xfit_all, yfit_all