teareduce 0.5.3__py3-none-any.whl → 0.5.5__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.
- teareduce/cleanest/__main__.py +57 -8
- teareduce/cleanest/centerchildparent.py +46 -0
- teareduce/cleanest/cleanest.py +16 -19
- teareduce/cleanest/cosmicraycleanerapp.py +264 -168
- teareduce/cleanest/definitions.py +30 -29
- teareduce/cleanest/imagedisplay.py +8 -9
- teareduce/cleanest/interpolation_a.py +5 -9
- teareduce/cleanest/interpolationeditor.py +156 -35
- teareduce/cleanest/modalprogressbar.py +182 -0
- teareduce/cleanest/parametereditor.py +98 -76
- teareduce/cleanest/reviewcosmicray.py +99 -66
- teareduce/version.py +1 -1
- {teareduce-0.5.3.dist-info → teareduce-0.5.5.dist-info}/METADATA +1 -1
- {teareduce-0.5.3.dist-info → teareduce-0.5.5.dist-info}/RECORD +18 -16
- {teareduce-0.5.3.dist-info → teareduce-0.5.5.dist-info}/WHEEL +0 -0
- {teareduce-0.5.3.dist-info → teareduce-0.5.5.dist-info}/entry_points.txt +0 -0
- {teareduce-0.5.3.dist-info → teareduce-0.5.5.dist-info}/licenses/LICENSE.txt +0 -0
- {teareduce-0.5.3.dist-info → teareduce-0.5.5.dist-info}/top_level.txt +0 -0
|
@@ -15,42 +15,35 @@
|
|
|
15
15
|
# when reading user input.
|
|
16
16
|
lacosmic_default_dict = {
|
|
17
17
|
# L.A.Cosmic parameters for run 1
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
"run1_gain": {"value": 1.0, "type": float, "positive": True},
|
|
19
|
+
"run1_readnoise": {"value": 6.5, "type": float, "positive": True},
|
|
20
|
+
"run1_sigclip": {"value": 5.0, "type": float, "positive": True},
|
|
21
|
+
"run1_sigfrac": {"value": 0.3, "type": float, "positive": True},
|
|
22
|
+
"run1_objlim": {"value": 5.0, "type": float, "positive": True},
|
|
23
|
+
"run1_niter": {"value": 4, "type": int, "positive": True},
|
|
24
|
+
"run1_verbose": {"value": True, "type": bool},
|
|
25
25
|
# L.A.Cosmic parameters for run 2
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
"run2_gain": {"value": 1.0, "type": float, "positive": True},
|
|
27
|
+
"run2_readnoise": {"value": 6.5, "type": float, "positive": True},
|
|
28
|
+
"run2_sigclip": {"value": 3.0, "type": float, "positive": True},
|
|
29
|
+
"run2_sigfrac": {"value": 0.3, "type": float, "positive": True},
|
|
30
|
+
"run2_objlim": {"value": 5.0, "type": float, "positive": True},
|
|
31
|
+
"run2_niter": {"value": 4, "type": int, "positive": True},
|
|
32
|
+
"run2_verbose": {"value": True, "type": bool},
|
|
33
33
|
# Dilation of the mask
|
|
34
|
-
|
|
34
|
+
"dilation": {"value": 0, "type": int, "positive": True},
|
|
35
|
+
"borderpadd": {"value": 10, "type": int, "positive": True},
|
|
35
36
|
# Limits for the image section to process (pixels start at 1)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
"xmin": {"value": 1, "type": int, "positive": True},
|
|
38
|
+
"xmax": {"value": None, "type": int, "positive": True},
|
|
39
|
+
"ymin": {"value": 1, "type": int, "positive": True},
|
|
40
|
+
"ymax": {"value": None, "type": int, "positive": True},
|
|
40
41
|
# Number of runs to execute L.A.Cosmic
|
|
41
|
-
|
|
42
|
+
"nruns": {"value": 1, "type": int, "positive": True},
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
# Default parameters for cleaning methods
|
|
45
|
-
VALID_CLEANING_METHODS = [
|
|
46
|
-
'x interp.',
|
|
47
|
-
'y interp.',
|
|
48
|
-
'surface interp.',
|
|
49
|
-
'median',
|
|
50
|
-
'mean',
|
|
51
|
-
'lacosmic',
|
|
52
|
-
'auxdata'
|
|
53
|
-
]
|
|
46
|
+
VALID_CLEANING_METHODS = ["x interp.", "y interp.", "surface interp.", "median", "mean", "lacosmic", "auxdata"]
|
|
54
47
|
|
|
55
48
|
# Maximum pixel distance to consider when finding closest CR pixel
|
|
56
49
|
MAX_PIXEL_DISTANCE_TO_CR = 15
|
|
@@ -60,3 +53,11 @@ DEFAULT_NPOINTS_INTERP = 2
|
|
|
60
53
|
|
|
61
54
|
# Default degree for interpolation
|
|
62
55
|
DEFAULT_DEGREE_INTERP = 1
|
|
56
|
+
|
|
57
|
+
# Default Tk window size
|
|
58
|
+
DEFAULT_TK_WINDOW_SIZE_X = 800
|
|
59
|
+
DEFAULT_TK_WINDOW_SIZE_Y = 700
|
|
60
|
+
|
|
61
|
+
# Default font settings
|
|
62
|
+
DEFAULT_FONT_FAMILY = "Helvetica"
|
|
63
|
+
DEFAULT_FONT_SIZE = 14
|
|
@@ -60,6 +60,7 @@ class ImageDisplay:
|
|
|
60
60
|
the displayed region can be determined from either the axes limits or a
|
|
61
61
|
predefined region attribute.
|
|
62
62
|
"""
|
|
63
|
+
|
|
63
64
|
def set_vmin(self):
|
|
64
65
|
"""Prompt user to set a new minimum display value (vmin)."""
|
|
65
66
|
old_vmin = self.get_vmin()
|
|
@@ -72,7 +73,7 @@ class ImageDisplay:
|
|
|
72
73
|
return
|
|
73
74
|
self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
|
|
74
75
|
self.image.set_clim(vmin=new_vmin)
|
|
75
|
-
if hasattr(self,
|
|
76
|
+
if hasattr(self, "image_aux"):
|
|
76
77
|
self.image_aux.set_clim(vmin=new_vmin)
|
|
77
78
|
self.canvas.draw_idle()
|
|
78
79
|
|
|
@@ -88,7 +89,7 @@ class ImageDisplay:
|
|
|
88
89
|
return
|
|
89
90
|
self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
|
|
90
91
|
self.image.set_clim(vmax=new_vmax)
|
|
91
|
-
if hasattr(self,
|
|
92
|
+
if hasattr(self, "image_aux"):
|
|
92
93
|
self.image_aux.set_clim(vmax=new_vmax)
|
|
93
94
|
self.canvas.draw_idle()
|
|
94
95
|
|
|
@@ -102,7 +103,7 @@ class ImageDisplay:
|
|
|
102
103
|
|
|
103
104
|
def get_displayed_region(self):
|
|
104
105
|
"""Get the currently displayed region of the image."""
|
|
105
|
-
if hasattr(self,
|
|
106
|
+
if hasattr(self, "ax"):
|
|
106
107
|
xmin, xmax = self.ax.get_xlim()
|
|
107
108
|
xmin = int(xmin + 0.5)
|
|
108
109
|
if xmin < 1:
|
|
@@ -118,10 +119,8 @@ class ImageDisplay:
|
|
|
118
119
|
if ymax > self.data.shape[0]:
|
|
119
120
|
ymax = self.data.shape[0]
|
|
120
121
|
print(f"Setting min/max using axis limits: x=({xmin:.2f}, {xmax:.2f}), y=({ymin:.2f}, {ymax:.2f})")
|
|
121
|
-
region = self.region = SliceRegion2D(
|
|
122
|
-
|
|
123
|
-
).python
|
|
124
|
-
elif hasattr(self, 'region'):
|
|
122
|
+
region = self.region = SliceRegion2D(f"[{xmin}:{xmax}, {ymin}:{ymax}]", mode="fits").python
|
|
123
|
+
elif hasattr(self, "region"):
|
|
125
124
|
region = self.region
|
|
126
125
|
else:
|
|
127
126
|
raise AttributeError("No axis or region defined for set_minmax.")
|
|
@@ -136,7 +135,7 @@ class ImageDisplay:
|
|
|
136
135
|
self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
|
|
137
136
|
self.image.set_clim(vmin=vmin_new)
|
|
138
137
|
self.image.set_clim(vmax=vmax_new)
|
|
139
|
-
if hasattr(self,
|
|
138
|
+
if hasattr(self, "image_aux"):
|
|
140
139
|
self.image_aux.set_clim(vmin=vmin_new)
|
|
141
140
|
self.image_aux.set_clim(vmax=vmax_new)
|
|
142
141
|
self.canvas.draw_idle()
|
|
@@ -149,7 +148,7 @@ class ImageDisplay:
|
|
|
149
148
|
self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
|
|
150
149
|
self.image.set_clim(vmin=vmin_new)
|
|
151
150
|
self.image.set_clim(vmax=vmax_new)
|
|
152
|
-
if hasattr(self,
|
|
151
|
+
if hasattr(self, "image_aux"):
|
|
153
152
|
self.image_aux.set_clim(vmin=vmin_new)
|
|
154
153
|
self.image_aux.set_clim(vmax=vmax_new)
|
|
155
154
|
self.canvas.draw_idle()
|
|
@@ -53,14 +53,10 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
|
|
|
53
53
|
by cosmic rays are interpolated.
|
|
54
54
|
"""
|
|
55
55
|
# Mask of CR pixels
|
|
56
|
-
mask =
|
|
56
|
+
mask = cr_labels == cr_index
|
|
57
57
|
# Dilate the mask to find border pixels
|
|
58
58
|
# dilated_mask = binary_dilation(mask, structure=np.ones((3, 3)), iterations=npoints)
|
|
59
|
-
dilated_mask = dilatemask(
|
|
60
|
-
mask=mask,
|
|
61
|
-
iterations=npoints,
|
|
62
|
-
connectivity=1
|
|
63
|
-
)
|
|
59
|
+
dilated_mask = dilatemask(mask=mask, iterations=npoints, connectivity=1)
|
|
64
60
|
# Border pixels are those in the dilated mask but not in the original mask
|
|
65
61
|
border_mask = dilated_mask & (~mask)
|
|
66
62
|
# Get coordinates of border pixels
|
|
@@ -68,7 +64,7 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
|
|
|
68
64
|
zfit_all = data[yfit_all, xfit_all].tolist()
|
|
69
65
|
# Perform interpolation
|
|
70
66
|
interpolation_performed = False
|
|
71
|
-
if method ==
|
|
67
|
+
if method == "surface":
|
|
72
68
|
if len(xfit_all) > 3:
|
|
73
69
|
# Construct the design matrix for a 2D polynomial fit to a plane,
|
|
74
70
|
# where each row corresponds to a point (x, y, z) and the model
|
|
@@ -84,10 +80,10 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
|
|
|
84
80
|
interpolation_performed = True
|
|
85
81
|
else:
|
|
86
82
|
print("Not enough points to fit a plane")
|
|
87
|
-
elif method in [
|
|
83
|
+
elif method in ["median", "mean"]:
|
|
88
84
|
# Compute median of all surrounding points
|
|
89
85
|
if len(zfit_all) > 0:
|
|
90
|
-
if method ==
|
|
86
|
+
if method == "median":
|
|
91
87
|
zval = np.median(zfit_all)
|
|
92
88
|
else:
|
|
93
89
|
zval = np.mean(zfit_all)
|
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
import tkinter as tk
|
|
13
13
|
from tkinter import messagebox
|
|
14
|
+
from tkinter import ttk
|
|
14
15
|
|
|
16
|
+
from .centerchildparent import center_on_parent
|
|
15
17
|
from .definitions import VALID_CLEANING_METHODS
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class InterpolationEditor:
|
|
19
21
|
"""Dialog to select interpolation cleaning parameters."""
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
def __init__(self, root, last_dilation, last_npoints, last_degree, auxdata, xmin, xmax, ymin, ymax, imgshape):
|
|
21
24
|
"""Initialize the interpolation editor dialog.
|
|
22
25
|
|
|
23
26
|
Parameters
|
|
@@ -32,6 +35,16 @@ class InterpolationEditor:
|
|
|
32
35
|
The last used degree for interpolation.
|
|
33
36
|
auxdata : array-like or None
|
|
34
37
|
Auxiliary data for cleaning, if available.
|
|
38
|
+
xmin : float
|
|
39
|
+
Minimum x value of the data. From 1 to NAXIS1.
|
|
40
|
+
xmax : float
|
|
41
|
+
Maximum x value of the data. From 1 to NAXIS1.
|
|
42
|
+
ymin : float
|
|
43
|
+
Minimum y value of the data. From 1 to NAXIS2.
|
|
44
|
+
ymax : float
|
|
45
|
+
Maximum y value of the data. From 1 to NAXIS2.
|
|
46
|
+
imgshape : tuple
|
|
47
|
+
Shape of the image data (height, width).
|
|
35
48
|
|
|
36
49
|
Methods
|
|
37
50
|
-------
|
|
@@ -62,6 +75,16 @@ class InterpolationEditor:
|
|
|
62
75
|
The number of points for interpolation.
|
|
63
76
|
degree : int
|
|
64
77
|
The degree for interpolation.
|
|
78
|
+
xmin : float
|
|
79
|
+
Minimum x value of the data. From 1 to NAXIS1.
|
|
80
|
+
xmax : float
|
|
81
|
+
Maximum x value of the data. From 1 to NAXIS1.
|
|
82
|
+
ymin : float
|
|
83
|
+
Minimum y value of the data. From 1 to NAXIS2.
|
|
84
|
+
ymax : float
|
|
85
|
+
Maximum y value of the data. From 1 to NAXIS2.
|
|
86
|
+
imgshape : tuple
|
|
87
|
+
Shape of the image data (height, width).
|
|
65
88
|
"""
|
|
66
89
|
self.root = root
|
|
67
90
|
self.root.title("Cleaning Parameters")
|
|
@@ -74,15 +97,23 @@ class InterpolationEditor:
|
|
|
74
97
|
"median": "a-median",
|
|
75
98
|
"mean": "a-mean",
|
|
76
99
|
"lacosmic": "lacosmic",
|
|
77
|
-
"auxdata": "auxdata"
|
|
100
|
+
"auxdata": "auxdata",
|
|
78
101
|
}
|
|
79
102
|
self.check_interp_methods()
|
|
80
103
|
# Initialize parameters
|
|
81
104
|
self.cleaning_method = None
|
|
82
105
|
self.npoints = last_npoints
|
|
83
106
|
self.degree = last_degree
|
|
107
|
+
self.xmin = xmin
|
|
108
|
+
self.xmax = xmax
|
|
109
|
+
self.ymin = ymin
|
|
110
|
+
self.ymax = ymax
|
|
111
|
+
self.imgshape = imgshape
|
|
112
|
+
# Dictionary to hold entry widgets for region parameters
|
|
113
|
+
self.entries = {}
|
|
84
114
|
# Create the form
|
|
85
115
|
self.create_widgets()
|
|
116
|
+
center_on_parent(child=self.root, parent=self.root.master)
|
|
86
117
|
|
|
87
118
|
def create_widgets(self):
|
|
88
119
|
"""Create the widgets for the dialog."""
|
|
@@ -90,9 +121,15 @@ class InterpolationEditor:
|
|
|
90
121
|
main_frame = tk.Frame(self.root, padx=10, pady=10)
|
|
91
122
|
main_frame.pack()
|
|
92
123
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
row = 0
|
|
125
|
+
|
|
126
|
+
# Subtitle for cleaning method selection
|
|
127
|
+
default_font = tk.font.nametofont("TkDefaultFont")
|
|
128
|
+
bold_font = default_font.copy()
|
|
129
|
+
bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
|
|
130
|
+
subtitle_label = tk.Label(main_frame, text="Select Cleaning Method", font=bold_font)
|
|
131
|
+
subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
|
|
132
|
+
row += 1
|
|
96
133
|
|
|
97
134
|
# Create labels and entry fields for each cleaning method
|
|
98
135
|
row = 1
|
|
@@ -111,35 +148,80 @@ class InterpolationEditor:
|
|
|
111
148
|
text=interp_method,
|
|
112
149
|
variable=self.cleaning_method_var,
|
|
113
150
|
value=interp_method,
|
|
114
|
-
command=self.action_on_method_change
|
|
115
|
-
).grid(row=row, column=
|
|
151
|
+
command=self.action_on_method_change,
|
|
152
|
+
).grid(row=row, column=1, sticky="w")
|
|
116
153
|
row += 1
|
|
117
154
|
|
|
155
|
+
# Separator
|
|
156
|
+
separator1 = ttk.Separator(main_frame, orient="horizontal")
|
|
157
|
+
separator1.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
|
|
158
|
+
row += 1
|
|
159
|
+
|
|
160
|
+
# Subtitle for additional parameters
|
|
161
|
+
subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=bold_font)
|
|
162
|
+
subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
|
|
163
|
+
row += 1
|
|
164
|
+
|
|
118
165
|
# Create labels and entry fields for each additional parameter
|
|
119
|
-
label = tk.Label(main_frame, text=
|
|
120
|
-
label.grid(row=row, column=0, sticky=
|
|
166
|
+
label = tk.Label(main_frame, text="Npoints:")
|
|
167
|
+
label.grid(row=row, column=0, sticky="e", padx=(0, 10))
|
|
121
168
|
self.entry_npoints = tk.Entry(main_frame, width=10)
|
|
122
169
|
self.entry_npoints.insert(0, self.npoints)
|
|
123
|
-
self.entry_npoints.grid(row=row, column=1, sticky=
|
|
170
|
+
self.entry_npoints.grid(row=row, column=1, sticky="w")
|
|
124
171
|
row += 1
|
|
125
|
-
label = tk.Label(main_frame, text=
|
|
126
|
-
label.grid(row=row, column=0, sticky=
|
|
172
|
+
label = tk.Label(main_frame, text="Degree:")
|
|
173
|
+
label.grid(row=row, column=0, sticky="e", padx=(0, 10))
|
|
127
174
|
self.entry_degree = tk.Entry(main_frame, width=10)
|
|
128
175
|
self.entry_degree.insert(0, self.degree)
|
|
129
|
-
self.entry_degree.grid(row=row, column=1, sticky=
|
|
176
|
+
self.entry_degree.grid(row=row, column=1, sticky="w")
|
|
177
|
+
row += 1
|
|
178
|
+
|
|
179
|
+
# Separator
|
|
180
|
+
separator2 = ttk.Separator(main_frame, orient="horizontal")
|
|
181
|
+
separator2.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
|
|
182
|
+
row += 1
|
|
183
|
+
|
|
184
|
+
# Subtitle for region to be examined
|
|
185
|
+
subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=bold_font)
|
|
186
|
+
subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
|
|
187
|
+
row += 1
|
|
188
|
+
|
|
189
|
+
# Region to be examined label and entries
|
|
190
|
+
for key in ["xmin", "xmax", "ymin", "ymax"]:
|
|
191
|
+
# Parameter name label
|
|
192
|
+
label = tk.Label(main_frame, text=f"{key}:", anchor="e", width=15)
|
|
193
|
+
label.grid(row=row, column=0, sticky="w", pady=5)
|
|
194
|
+
# Entry field
|
|
195
|
+
entry = tk.Entry(main_frame, width=10)
|
|
196
|
+
entry.insert(0, str(self.__dict__[key]))
|
|
197
|
+
entry.grid(row=row, column=1, padx=10, pady=5)
|
|
198
|
+
self.entries[key] = entry # dictionary to hold entry widgets
|
|
199
|
+
# Type label
|
|
200
|
+
dumtext = "(int)"
|
|
201
|
+
if key in ["xmin", "xmax"]:
|
|
202
|
+
dumtext += f" --> [1, {self.imgshape[1]}]"
|
|
203
|
+
else:
|
|
204
|
+
dumtext += f" --> [1, {self.imgshape[0]}]"
|
|
205
|
+
type_label = tk.Label(main_frame, text=dumtext, fg="gray", anchor="w", width=15)
|
|
206
|
+
type_label.grid(row=row, column=2, sticky="w", pady=5)
|
|
207
|
+
row += 1
|
|
208
|
+
|
|
209
|
+
# Separator
|
|
210
|
+
separator3 = ttk.Separator(main_frame, orient="horizontal")
|
|
211
|
+
separator3.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
|
|
130
212
|
row += 1
|
|
131
213
|
|
|
132
214
|
# Button frame
|
|
133
215
|
self.button_frame = tk.Frame(main_frame)
|
|
134
|
-
self.button_frame.grid(row=row, column=0, columnspan=
|
|
216
|
+
self.button_frame.grid(row=row, column=0, columnspan=3, pady=(15, 0))
|
|
135
217
|
|
|
136
218
|
# OK button
|
|
137
219
|
self.ok_button = tk.Button(self.button_frame, text="OK", width=5, command=self.on_ok)
|
|
138
|
-
self.ok_button.pack(side=
|
|
220
|
+
self.ok_button.pack(side="left", padx=5)
|
|
139
221
|
|
|
140
222
|
# Cancel button
|
|
141
223
|
self.cancel_button = tk.Button(self.button_frame, text="Cancel", width=5, command=self.on_cancel)
|
|
142
|
-
self.cancel_button.pack(side=
|
|
224
|
+
self.cancel_button.pack(side="left", padx=5)
|
|
143
225
|
|
|
144
226
|
# Initial action depending on the default method
|
|
145
227
|
self.action_on_method_change()
|
|
@@ -165,10 +247,49 @@ class InterpolationEditor:
|
|
|
165
247
|
messagebox.showerror("Input Error", "Degree must be non-negative.")
|
|
166
248
|
return
|
|
167
249
|
|
|
168
|
-
if self.cleaning_method in [
|
|
250
|
+
if self.cleaning_method in ["x", "y"] and 2 * self.npoints <= self.degree:
|
|
169
251
|
messagebox.showerror("Input Error", "2*Npoints must be greater than Degree for x and y interpolation.")
|
|
170
252
|
return
|
|
171
253
|
|
|
254
|
+
# Retrieve and validate region parameters
|
|
255
|
+
try:
|
|
256
|
+
xmin = int(self.entries["xmin"].get())
|
|
257
|
+
except ValueError:
|
|
258
|
+
messagebox.showerror("Input Error", "xmin must be an integer.")
|
|
259
|
+
return
|
|
260
|
+
try:
|
|
261
|
+
xmax = int(self.entries["xmax"].get())
|
|
262
|
+
except ValueError:
|
|
263
|
+
messagebox.showerror("Input Error", "xmax must be an integer.")
|
|
264
|
+
return
|
|
265
|
+
if xmin >= xmax:
|
|
266
|
+
messagebox.showerror("Input Error", "xmin must be less than xmax.")
|
|
267
|
+
return
|
|
268
|
+
try:
|
|
269
|
+
ymin = int(self.entries["ymin"].get())
|
|
270
|
+
except ValueError:
|
|
271
|
+
messagebox.showerror("Input Error", "ymin must be an integer.")
|
|
272
|
+
return
|
|
273
|
+
try:
|
|
274
|
+
ymax = int(self.entries["ymax"].get())
|
|
275
|
+
except ValueError:
|
|
276
|
+
messagebox.showerror("Input Error", "ymax must be an integer.")
|
|
277
|
+
return
|
|
278
|
+
if ymin >= ymax:
|
|
279
|
+
messagebox.showerror("Input Error", "ymin must be less than ymax.")
|
|
280
|
+
return
|
|
281
|
+
for key, entry in self.entries.items():
|
|
282
|
+
value = int(entry.get())
|
|
283
|
+
if key in ["xmin", "xmax"]:
|
|
284
|
+
if not (1 <= value <= self.imgshape[1]):
|
|
285
|
+
messagebox.showerror("Input Error", f"{key} must be in the range [1, {self.imgshape[1]}].")
|
|
286
|
+
return
|
|
287
|
+
else:
|
|
288
|
+
if not (1 <= value <= self.imgshape[0]):
|
|
289
|
+
messagebox.showerror("Input Error", f"{key} must be in the range [1, {self.imgshape[0]}].")
|
|
290
|
+
return
|
|
291
|
+
self.__dict__[key] = value
|
|
292
|
+
|
|
172
293
|
self.root.destroy()
|
|
173
294
|
|
|
174
295
|
def on_cancel(self):
|
|
@@ -182,24 +303,24 @@ class InterpolationEditor:
|
|
|
182
303
|
"""Handle changes in the selected cleaning method."""
|
|
183
304
|
selected_method = self.cleaning_method_var.get()
|
|
184
305
|
print(f"Selected cleaning method: {selected_method}")
|
|
185
|
-
if selected_method in [
|
|
186
|
-
self.entry_npoints.config(state=
|
|
187
|
-
self.entry_degree.config(state=
|
|
188
|
-
elif selected_method ==
|
|
189
|
-
self.entry_npoints.config(state=
|
|
190
|
-
self.entry_degree.config(state=
|
|
191
|
-
elif selected_method ==
|
|
192
|
-
self.entry_npoints.config(state=
|
|
193
|
-
self.entry_degree.config(state=
|
|
194
|
-
elif selected_method ==
|
|
195
|
-
self.entry_npoints.config(state=
|
|
196
|
-
self.entry_degree.config(state=
|
|
197
|
-
elif selected_method ==
|
|
198
|
-
self.entry_npoints.config(state=
|
|
199
|
-
self.entry_degree.config(state=
|
|
200
|
-
elif selected_method ==
|
|
201
|
-
self.entry_npoints.config(state=
|
|
202
|
-
self.entry_degree.config(state=
|
|
306
|
+
if selected_method in ["x interp.", "y interp."]:
|
|
307
|
+
self.entry_npoints.config(state="normal")
|
|
308
|
+
self.entry_degree.config(state="normal")
|
|
309
|
+
elif selected_method == "surface interp.":
|
|
310
|
+
self.entry_npoints.config(state="normal")
|
|
311
|
+
self.entry_degree.config(state="disabled")
|
|
312
|
+
elif selected_method == "median":
|
|
313
|
+
self.entry_npoints.config(state="normal")
|
|
314
|
+
self.entry_degree.config(state="disabled")
|
|
315
|
+
elif selected_method == "mean":
|
|
316
|
+
self.entry_npoints.config(state="normal")
|
|
317
|
+
self.entry_degree.config(state="disabled")
|
|
318
|
+
elif selected_method == "lacosmic":
|
|
319
|
+
self.entry_npoints.config(state="disabled")
|
|
320
|
+
self.entry_degree.config(state="disabled")
|
|
321
|
+
elif selected_method == "auxdata":
|
|
322
|
+
self.entry_npoints.config(state="disabled")
|
|
323
|
+
self.entry_degree.config(state="disabled")
|
|
203
324
|
|
|
204
325
|
def check_interp_methods(self):
|
|
205
326
|
"""Check that all interpolation methods are valid."""
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
"""Module defining a progress bar widget for Tkinter."""
|
|
11
|
+
|
|
12
|
+
import tkinter as tk
|
|
13
|
+
from tkinter import ttk
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ModalProgressBar:
|
|
18
|
+
def __init__(
|
|
19
|
+
self, parent, iterable=None, total=None, desc="Processing", completion_msg="Processing completed successfully!"
|
|
20
|
+
):
|
|
21
|
+
self.parent = parent
|
|
22
|
+
self.iterable = iterable
|
|
23
|
+
self.total = total if total is not None else (len(iterable) if iterable is not None else 100)
|
|
24
|
+
self.current = 0
|
|
25
|
+
self.start_time = time.time()
|
|
26
|
+
self.window = None
|
|
27
|
+
self.desc = desc
|
|
28
|
+
self.completion_msg = completion_msg
|
|
29
|
+
self.continue_clicked = False
|
|
30
|
+
|
|
31
|
+
def __enter__(self):
|
|
32
|
+
# Create the modal window when entering context
|
|
33
|
+
self.window = tk.Toplevel(self.parent)
|
|
34
|
+
self.window.title("Progress")
|
|
35
|
+
|
|
36
|
+
# Make it modal
|
|
37
|
+
self.window.transient(self.parent)
|
|
38
|
+
self.window.grab_set()
|
|
39
|
+
self.window.protocol("WM_DELETE_WINDOW", lambda: None)
|
|
40
|
+
|
|
41
|
+
# Set geometry and force it
|
|
42
|
+
minwinsize_x = 400
|
|
43
|
+
minwinsize_y = 120
|
|
44
|
+
self.window.minsize(minwinsize_x, minwinsize_y)
|
|
45
|
+
self.window.update_idletasks()
|
|
46
|
+
self.window.update()
|
|
47
|
+
|
|
48
|
+
# Center on parent
|
|
49
|
+
self._center_on_parent()
|
|
50
|
+
|
|
51
|
+
# UI elements
|
|
52
|
+
default_font = tk.font.nametofont("TkDefaultFont")
|
|
53
|
+
bold_font = default_font.copy()
|
|
54
|
+
bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
|
|
55
|
+
self.desc_label = tk.Label(self.window, text=self.desc, font=bold_font)
|
|
56
|
+
self.desc_label.pack(padx=10, pady=5)
|
|
57
|
+
|
|
58
|
+
self.progress = ttk.Progressbar(self.window, length=minwinsize_x - 20, mode="determinate", maximum=self.total)
|
|
59
|
+
self.progress.pack(padx=10, pady=10)
|
|
60
|
+
|
|
61
|
+
self.status_label = tk.Label(self.window, text=f"0/{self.total} (0.0%)")
|
|
62
|
+
self.status_label.pack(padx=10, pady=2)
|
|
63
|
+
|
|
64
|
+
self.time_label = tk.Label(self.window, text="Elapsed: 0s | ETA: --")
|
|
65
|
+
self.time_label.pack(padx=10, pady=2)
|
|
66
|
+
|
|
67
|
+
# Continue button (to close the dialog after completion; hidden until done)
|
|
68
|
+
self.continue_button = tk.Button(self.window, text="Continue", command=self._on_continue)
|
|
69
|
+
self.continue_button.pack(padx=10, pady=10)
|
|
70
|
+
self.continue_button.pack_forget() # Hide initially
|
|
71
|
+
|
|
72
|
+
# Force another update
|
|
73
|
+
self.window.update_idletasks()
|
|
74
|
+
self.window.update()
|
|
75
|
+
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
79
|
+
if self.window:
|
|
80
|
+
# Show completion screen instead of closing
|
|
81
|
+
self._show_completion()
|
|
82
|
+
|
|
83
|
+
# Wait for user to click Continue
|
|
84
|
+
self.window.wait_variable(self._continue_var)
|
|
85
|
+
|
|
86
|
+
# Now close
|
|
87
|
+
self._destroy()
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def __iter__(self):
|
|
91
|
+
"""Allow iteration like tqdm"""
|
|
92
|
+
if self.iterable is None:
|
|
93
|
+
raise ValueError("No iterable provided for iteration")
|
|
94
|
+
|
|
95
|
+
for item in self.iterable:
|
|
96
|
+
yield item
|
|
97
|
+
self.update(1)
|
|
98
|
+
|
|
99
|
+
def _center_on_parent(self):
|
|
100
|
+
self.window.update_idletasks()
|
|
101
|
+
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.window.winfo_width() // 2)
|
|
102
|
+
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.window.winfo_height() // 2)
|
|
103
|
+
self.window.geometry(f"+{x}+{y}")
|
|
104
|
+
|
|
105
|
+
def update(self, n=1):
|
|
106
|
+
self.current += n
|
|
107
|
+
self.progress["value"] = self.current
|
|
108
|
+
percentage = (self.current / self.total) * 100
|
|
109
|
+
|
|
110
|
+
elapsed = time.time() - self.start_time
|
|
111
|
+
|
|
112
|
+
if self.current > 0:
|
|
113
|
+
rate = self.current / elapsed
|
|
114
|
+
remaining = self.total - self.current
|
|
115
|
+
eta_seconds = remaining / rate if rate > 0 else 0
|
|
116
|
+
|
|
117
|
+
elapsed_str = self._format_time(elapsed)
|
|
118
|
+
eta_str = self._format_time(eta_seconds)
|
|
119
|
+
rate_str = f"{rate:.2f} CR/s" if rate >= 1 else f"{1/rate:.2f} s/CR"
|
|
120
|
+
|
|
121
|
+
self.status_label.config(text=f"{self.current}/{self.total} ({percentage:.1f}%) | {rate_str}")
|
|
122
|
+
self.time_label.config(text=f"Elapsed: {elapsed_str} | ETA: {eta_str}")
|
|
123
|
+
|
|
124
|
+
self.window.update_idletasks()
|
|
125
|
+
self.window.update()
|
|
126
|
+
|
|
127
|
+
def _format_time(self, seconds):
|
|
128
|
+
if seconds < 60:
|
|
129
|
+
return f"{seconds:.1f} s"
|
|
130
|
+
elif seconds < 3600:
|
|
131
|
+
minutes = int(seconds // 60)
|
|
132
|
+
secs = int(seconds % 60)
|
|
133
|
+
return f"{minutes}m {secs} s"
|
|
134
|
+
else:
|
|
135
|
+
hours = int(seconds // 3600)
|
|
136
|
+
minutes = int((seconds % 3600) // 60)
|
|
137
|
+
return f"{hours}h {minutes} m"
|
|
138
|
+
|
|
139
|
+
def _show_completion(self):
|
|
140
|
+
"""Transform the window into a completion dialog"""
|
|
141
|
+
elapsed = time.time() - self.start_time
|
|
142
|
+
elapsed_str = self._format_time(elapsed)
|
|
143
|
+
|
|
144
|
+
# Update title
|
|
145
|
+
self.window.title("Completed")
|
|
146
|
+
|
|
147
|
+
# Update description to show completion message
|
|
148
|
+
default_font = tk.font.nametofont("TkDefaultFont")
|
|
149
|
+
bold_font = default_font.copy()
|
|
150
|
+
bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
|
|
151
|
+
self.desc_label.config(text=self.completion_msg, fg="green", font=bold_font)
|
|
152
|
+
|
|
153
|
+
# Hide progress bar
|
|
154
|
+
self.progress.pack_forget()
|
|
155
|
+
|
|
156
|
+
# Update status to show final stats
|
|
157
|
+
avg_rate = self.current / elapsed if elapsed > 0 else 0
|
|
158
|
+
rate_str = f"{avg_rate:.2f} CR/s" if avg_rate >= 1 else f"{1/avg_rate:.2f} s/CR"
|
|
159
|
+
self.status_label.config(text=f"Processed {self.current} CRs | {rate_str}")
|
|
160
|
+
|
|
161
|
+
# Update time label
|
|
162
|
+
self.time_label.config(text=f"Total time: {elapsed_str}")
|
|
163
|
+
|
|
164
|
+
# Show the Continue button
|
|
165
|
+
self.continue_button.pack(padx=10, pady=15)
|
|
166
|
+
|
|
167
|
+
# Create a variable to track when Continue is clicked
|
|
168
|
+
self._continue_var = tk.BooleanVar(value=False)
|
|
169
|
+
|
|
170
|
+
# Re-enable close button to work like Continue
|
|
171
|
+
self.window.protocol("WM_DELETE_WINDOW", self._on_continue)
|
|
172
|
+
|
|
173
|
+
self.window.update()
|
|
174
|
+
self._center_on_parent()
|
|
175
|
+
|
|
176
|
+
def _on_continue(self):
|
|
177
|
+
"""Called when Continue button is clicked"""
|
|
178
|
+
self._continue_var.set(True)
|
|
179
|
+
|
|
180
|
+
def _destroy(self):
|
|
181
|
+
self.window.grab_release()
|
|
182
|
+
self.window.destroy()
|