teareduce 0.4.9__py3-none-any.whl → 0.5.0__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 +18 -6
- teareduce/cleanest/cosmicraycleanerapp.py +586 -85
- teareduce/cleanest/definitions.py +51 -0
- teareduce/cleanest/find_closest_true.py +44 -0
- teareduce/cleanest/imagedisplay.py +155 -0
- teareduce/cleanest/interpolation_a.py +83 -0
- teareduce/cleanest/interpolation_x.py +80 -0
- teareduce/cleanest/interpolation_y.py +81 -0
- teareduce/cleanest/interpolationeditor.py +207 -0
- teareduce/cleanest/parametereditor.py +251 -0
- teareduce/cleanest/reviewcosmicray.py +351 -267
- teareduce/cookbook/get_cookbook_file.py +5 -0
- teareduce/version.py +1 -1
- {teareduce-0.4.9.dist-info → teareduce-0.5.0.dist-info}/METADATA +3 -1
- {teareduce-0.4.9.dist-info → teareduce-0.5.0.dist-info}/RECORD +19 -11
- {teareduce-0.4.9.dist-info → teareduce-0.5.0.dist-info}/WHEEL +0 -0
- {teareduce-0.4.9.dist-info → teareduce-0.5.0.dist-info}/entry_points.txt +0 -0
- {teareduce-0.4.9.dist-info → teareduce-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
- {teareduce-0.4.9.dist-info → teareduce-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import tkinter as tk
|
|
13
13
|
from tkinter import filedialog
|
|
14
|
-
from tkinter import
|
|
14
|
+
from tkinter import messagebox
|
|
15
|
+
import sys
|
|
15
16
|
|
|
16
17
|
from astropy.io import fits
|
|
17
18
|
from ccdproc import cosmicray_lacosmic
|
|
@@ -20,20 +21,34 @@ from matplotlib.backend_bases import key_press_handler
|
|
|
20
21
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
21
22
|
import numpy as np
|
|
22
23
|
import os
|
|
24
|
+
from rich import print
|
|
25
|
+
from scipy import ndimage
|
|
23
26
|
|
|
27
|
+
from .definitions import lacosmic_default_dict
|
|
28
|
+
from .definitions import DEFAULT_NPOINTS_INTERP
|
|
29
|
+
from .definitions import DEFAULT_DEGREE_INTERP
|
|
30
|
+
from .definitions import MAX_PIXEL_DISTANCE_TO_CR
|
|
31
|
+
from .find_closest_true import find_closest_true
|
|
32
|
+
from .interpolation_a import interpolation_a
|
|
33
|
+
from .interpolation_x import interpolation_x
|
|
34
|
+
from .interpolation_y import interpolation_y
|
|
35
|
+
from .interpolationeditor import InterpolationEditor
|
|
36
|
+
from .imagedisplay import ImageDisplay
|
|
37
|
+
from .parametereditor import ParameterEditor
|
|
24
38
|
from .reviewcosmicray import ReviewCosmicRay
|
|
25
39
|
|
|
26
40
|
from ..imshow import imshow
|
|
41
|
+
from ..sliceregion import SliceRegion2D
|
|
27
42
|
from ..zscale import zscale
|
|
28
43
|
|
|
29
44
|
import matplotlib
|
|
30
45
|
matplotlib.use("TkAgg")
|
|
31
46
|
|
|
32
47
|
|
|
33
|
-
class CosmicRayCleanerApp():
|
|
48
|
+
class CosmicRayCleanerApp(ImageDisplay):
|
|
34
49
|
"""Main application class for cosmic ray cleaning."""
|
|
35
50
|
|
|
36
|
-
def __init__(self, root, input_fits, extension=0,
|
|
51
|
+
def __init__(self, root, input_fits, extension=0, auxfile=None, extension_auxfile=0):
|
|
37
52
|
"""
|
|
38
53
|
Initialize the application.
|
|
39
54
|
|
|
@@ -45,20 +60,118 @@ class CosmicRayCleanerApp():
|
|
|
45
60
|
Path to the FITS file to be cleaned.
|
|
46
61
|
extension : int, optional
|
|
47
62
|
FITS extension to use (default is 0).
|
|
48
|
-
|
|
49
|
-
Path to
|
|
50
|
-
|
|
63
|
+
auxfile : str, optional
|
|
64
|
+
Path to an auxiliary FITS file (default is None).
|
|
65
|
+
extension_auxfile : int, optional
|
|
66
|
+
FITS extension for auxiliary file (default is 0).
|
|
67
|
+
|
|
68
|
+
Methods
|
|
69
|
+
-------
|
|
70
|
+
load_fits_file()
|
|
71
|
+
Load the FITS file and auxiliary file (if provided).
|
|
72
|
+
save_fits_file()
|
|
73
|
+
Save the cleaned data to a FITS file.
|
|
74
|
+
create_widgets()
|
|
75
|
+
Create the GUI widgets.
|
|
76
|
+
run_lacosmic()
|
|
77
|
+
Run the L.A.Cosmic algorithm.
|
|
78
|
+
toggle_cr_overlay()
|
|
79
|
+
Toggle the overlay of cosmic ray pixels on the image.
|
|
80
|
+
update_cr_overlay()
|
|
81
|
+
Update the overlay of cosmic ray pixels on the image.
|
|
82
|
+
apply_lacosmic()
|
|
83
|
+
Apply the L.A.Cosmic algorithm to the data.
|
|
84
|
+
examine_detected_cr()
|
|
85
|
+
Examine detected cosmic rays.
|
|
86
|
+
stop_app()
|
|
87
|
+
Stop the application.
|
|
88
|
+
on_key(event)
|
|
89
|
+
Handle key press events.
|
|
90
|
+
on_click(event)
|
|
91
|
+
Handle mouse click events.
|
|
92
|
+
|
|
93
|
+
Attributes
|
|
94
|
+
----------
|
|
95
|
+
root : tk.Tk
|
|
96
|
+
The main Tkinter window.
|
|
97
|
+
lacosmic_params : dict
|
|
98
|
+
Dictionary of L.A.Cosmic parameters.
|
|
99
|
+
input_fits : str
|
|
100
|
+
Path to the FITS file to be cleaned.
|
|
101
|
+
extension : int
|
|
102
|
+
FITS extension to use.
|
|
103
|
+
data : np.ndarray
|
|
104
|
+
The image data from the FITS file.
|
|
105
|
+
auxfile : str
|
|
106
|
+
Path to an auxiliary FITS file.
|
|
107
|
+
extension_auxfile : int
|
|
108
|
+
FITS extension for auxiliary file.
|
|
109
|
+
auxdata : np.ndarray
|
|
110
|
+
The image data from the auxiliary FITS file.
|
|
111
|
+
overplot_cr_pixels : bool
|
|
112
|
+
Flag to indicate whether to overlay cosmic ray pixels.
|
|
113
|
+
mask_crfound : np.ndarray
|
|
114
|
+
Boolean mask of detected cosmic ray pixels.
|
|
115
|
+
last_xmin : int
|
|
116
|
+
Last used minimum x-coordinate for region selection.
|
|
117
|
+
last_xmax : int
|
|
118
|
+
Last used maximum x-coordinate for region selection.
|
|
119
|
+
last_ymin : int
|
|
120
|
+
Last used minimum y-coordinate for region selection.
|
|
121
|
+
last_ymax : int
|
|
122
|
+
Last used maximum y-coordinate for region selection.
|
|
123
|
+
last_npoints : int
|
|
124
|
+
Last used number of points for interpolation.
|
|
125
|
+
last_degree : int
|
|
126
|
+
Last used degree for interpolation.
|
|
127
|
+
cleandata_lacosmic : np.ndarray
|
|
128
|
+
The cleaned data returned from L.A.Cosmic.
|
|
129
|
+
cr_labels : np.ndarray
|
|
130
|
+
Labeled cosmic ray features.
|
|
131
|
+
num_features : int
|
|
132
|
+
Number of detected cosmic ray features.
|
|
133
|
+
working_in_review_window : bool
|
|
134
|
+
Flag to indicate if the review window is active.
|
|
51
135
|
"""
|
|
52
136
|
self.root = root
|
|
53
137
|
self.root.title("Cosmic Ray Cleaner")
|
|
54
138
|
self.root.geometry("800x700+50+0")
|
|
139
|
+
self.lacosmic_params = lacosmic_default_dict.copy()
|
|
55
140
|
self.input_fits = input_fits
|
|
56
141
|
self.extension = extension
|
|
57
|
-
self.
|
|
142
|
+
self.data = None
|
|
143
|
+
self.auxfile = auxfile
|
|
144
|
+
self.extension_auxfile = extension_auxfile
|
|
145
|
+
self.auxdata = None
|
|
146
|
+
self.overplot_cr_pixels = True
|
|
147
|
+
self.mask_crfound = None
|
|
58
148
|
self.load_fits_file()
|
|
149
|
+
self.last_xmin = 1
|
|
150
|
+
self.last_xmax = self.data.shape[1]
|
|
151
|
+
self.last_ymin = 1
|
|
152
|
+
self.last_ymax = self.data.shape[0]
|
|
153
|
+
self.last_npoints = DEFAULT_NPOINTS_INTERP
|
|
154
|
+
self.last_degree = DEFAULT_DEGREE_INTERP
|
|
59
155
|
self.create_widgets()
|
|
156
|
+
self.cleandata_lacosmic = None
|
|
157
|
+
self.cr_labels = None
|
|
158
|
+
self.num_features = 0
|
|
159
|
+
self.working_in_review_window = False
|
|
60
160
|
|
|
61
161
|
def load_fits_file(self):
|
|
162
|
+
"""Load the FITS file and auxiliary file (if provided).
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
None
|
|
167
|
+
|
|
168
|
+
Notes
|
|
169
|
+
-----
|
|
170
|
+
This method loads the FITS file specified by `self.input_fits` and
|
|
171
|
+
reads the data from the specified extension. If an auxiliary file is
|
|
172
|
+
provided, it also loads the auxiliary data from the specified extension.
|
|
173
|
+
The loaded data is stored in `self.data` and `self.auxdata` attributes.
|
|
174
|
+
"""
|
|
62
175
|
try:
|
|
63
176
|
with fits.open(self.input_fits, mode='readonly') as hdul:
|
|
64
177
|
self.data = hdul[self.extension].data
|
|
@@ -68,14 +181,45 @@ class CosmicRayCleanerApp():
|
|
|
68
181
|
self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
|
|
69
182
|
except Exception as e:
|
|
70
183
|
print(f"Error loading FITS file: {e}")
|
|
184
|
+
self.mask_crfound = np.zeros(self.data.shape, dtype=bool)
|
|
185
|
+
naxis2, naxis1 = self.data.shape
|
|
186
|
+
self.region = SliceRegion2D(f'[1:{naxis1}, 1:{naxis2}]', mode='fits').python
|
|
187
|
+
# Read auxiliary file if provided
|
|
188
|
+
if self.auxfile is not None:
|
|
189
|
+
try:
|
|
190
|
+
with fits.open(self.auxfile, mode='readonly') as hdul_aux:
|
|
191
|
+
self.auxdata = hdul_aux[self.extension_auxfile].data
|
|
192
|
+
if self.auxdata.shape != self.data.shape:
|
|
193
|
+
print(f"data shape...: {self.data.shape}")
|
|
194
|
+
print(f"auxdata shape: {self.auxdata.shape}")
|
|
195
|
+
raise ValueError("Auxiliary file has different shape.")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
sys.exit(f"Error loading auxiliary FITS file: {e}")
|
|
71
198
|
|
|
72
199
|
def save_fits_file(self):
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
200
|
+
"""Save the cleaned FITS file.
|
|
201
|
+
|
|
202
|
+
This method prompts the user to select a location and filename to
|
|
203
|
+
save the cleaned FITS file. It writes the cleaned data and
|
|
204
|
+
the cosmic ray mask to the specified FITS file.
|
|
205
|
+
|
|
206
|
+
If the initial file contains a 'CRMASK' extension, it updates
|
|
207
|
+
that extension with the new mask. Otherwise, it creates a new
|
|
208
|
+
'CRMASK' extension to store the mask.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
None
|
|
213
|
+
|
|
214
|
+
Notes
|
|
215
|
+
-----
|
|
216
|
+
After successfully saving the cleaned FITS file, the chosen output
|
|
217
|
+
filename is stored in `self.input_fits`, and the save button is disabled
|
|
218
|
+
to prevent multiple saves without further modifications.
|
|
219
|
+
"""
|
|
220
|
+
base, ext = os.path.splitext(self.input_fits)
|
|
221
|
+
suggested_name = f"{base}_cleaned"
|
|
222
|
+
output_fits = filedialog.asksaveasfilename(
|
|
79
223
|
initialdir=os.getcwd(),
|
|
80
224
|
title="Save cleaned FITS file",
|
|
81
225
|
defaultextension=".fits",
|
|
@@ -90,114 +234,471 @@ class CosmicRayCleanerApp():
|
|
|
90
234
|
else:
|
|
91
235
|
crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name='CRMASK')
|
|
92
236
|
hdul.append(crmask_hdu)
|
|
93
|
-
hdul.writeto(
|
|
94
|
-
print(f"Cleaned data saved to {
|
|
237
|
+
hdul.writeto(output_fits, overwrite=True)
|
|
238
|
+
print(f"Cleaned data saved to {output_fits}")
|
|
239
|
+
self.ax.set_title(os.path.basename(output_fits))
|
|
240
|
+
self.canvas.draw_idle()
|
|
241
|
+
self.input_fits = os.path.basename(output_fits)
|
|
242
|
+
self.save_button.config(state=tk.DISABLED)
|
|
95
243
|
except Exception as e:
|
|
96
244
|
print(f"Error saving FITS file: {e}")
|
|
97
245
|
|
|
98
246
|
def create_widgets(self):
|
|
99
|
-
|
|
247
|
+
"""Create the GUI widgets.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
None
|
|
252
|
+
|
|
253
|
+
Notes
|
|
254
|
+
-----
|
|
255
|
+
This method sets up the GUI layout, including buttons for running
|
|
256
|
+
L.A.Cosmic, toggling cosmic ray overlay, applying cleaning methods,
|
|
257
|
+
examining detected cosmic rays, saving the cleaned FITS file, and
|
|
258
|
+
stopping the application. It also initializes the matplotlib figure
|
|
259
|
+
and canvas for image display, along with the toolbar for navigation.
|
|
260
|
+
The relevant attributes are stored in the instance for later use.
|
|
261
|
+
"""
|
|
262
|
+
# Row 1 of buttons
|
|
100
263
|
self.button_frame1 = tk.Frame(self.root)
|
|
101
|
-
self.button_frame1.
|
|
264
|
+
self.button_frame1.pack(pady=5)
|
|
102
265
|
self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
|
|
103
266
|
self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
|
|
104
|
-
|
|
105
|
-
|
|
267
|
+
if self.overplot_cr_pixels:
|
|
268
|
+
self.overplot_cr_button = tk.Button(self.button_frame1, text="CR overlay: On",
|
|
269
|
+
command=self.toggle_cr_overlay)
|
|
270
|
+
else:
|
|
271
|
+
self.overplot_cr_button = tk.Button(self.button_frame1, text="CR overlay: Off",
|
|
272
|
+
command=self.toggle_cr_overlay)
|
|
273
|
+
self.overplot_cr_button.pack(side=tk.LEFT, padx=5)
|
|
274
|
+
self.apply_lacosmic_button = tk.Button(self.button_frame1, text="Replace all detected CRs",
|
|
275
|
+
command=self.apply_lacosmic)
|
|
276
|
+
self.apply_lacosmic_button.pack(side=tk.LEFT, padx=5)
|
|
277
|
+
self.apply_lacosmic_button.config(state=tk.DISABLED) # Initially disabled
|
|
278
|
+
self.examine_detected_cr_button = tk.Button(self.button_frame1, text="Examine detected CRs",
|
|
279
|
+
command=lambda: self.examine_detected_cr(1))
|
|
280
|
+
self.examine_detected_cr_button.pack(side=tk.LEFT, padx=5)
|
|
281
|
+
self.examine_detected_cr_button.config(state=tk.DISABLED) # Initially disabled
|
|
106
282
|
|
|
107
|
-
# Row 2
|
|
283
|
+
# Row 2 of buttons
|
|
108
284
|
self.button_frame2 = tk.Frame(self.root)
|
|
109
|
-
self.button_frame2.
|
|
110
|
-
|
|
111
|
-
self.
|
|
112
|
-
self.
|
|
113
|
-
self.vmax_button = tk.Button(self.button_frame2, text=f"vmax: {vmax:.2f}", command=self.set_vmax)
|
|
114
|
-
self.vmax_button.pack(side=tk.LEFT, padx=5)
|
|
285
|
+
self.button_frame2.pack(pady=5)
|
|
286
|
+
self.save_button = tk.Button(self.button_frame2, text="Save cleaned FITS", command=self.save_fits_file)
|
|
287
|
+
self.save_button.pack(side=tk.LEFT, padx=5)
|
|
288
|
+
self.save_button.config(state=tk.DISABLED) # Initially disabled
|
|
115
289
|
self.stop_button = tk.Button(self.button_frame2, text="Stop program", command=self.stop_app)
|
|
116
290
|
self.stop_button.pack(side=tk.LEFT, padx=5)
|
|
117
291
|
|
|
118
|
-
#
|
|
119
|
-
self.
|
|
120
|
-
self.
|
|
121
|
-
|
|
122
|
-
self.
|
|
123
|
-
self.
|
|
124
|
-
self.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
self.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
|
|
131
|
-
self.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
|
|
132
|
-
xlabel=xlabel, ylabel=ylabel, extent=extent)
|
|
133
|
-
# Note: tight_layout should be called before defining the canvas
|
|
134
|
-
self.fig.tight_layout()
|
|
292
|
+
# Row 3 of buttons
|
|
293
|
+
self.button_frame3 = tk.Frame(self.root)
|
|
294
|
+
self.button_frame3.pack(pady=5)
|
|
295
|
+
vmin, vmax = zscale(self.data)
|
|
296
|
+
self.vmin_button = tk.Button(self.button_frame3, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
|
|
297
|
+
self.vmin_button.pack(side=tk.LEFT, padx=5)
|
|
298
|
+
self.vmax_button = tk.Button(self.button_frame3, text=f"vmax: {vmax:.2f}", command=self.set_vmax)
|
|
299
|
+
self.vmax_button.pack(side=tk.LEFT, padx=5)
|
|
300
|
+
self.set_minmax_button = tk.Button(self.button_frame3, text="minmax [,]", command=self.set_minmax)
|
|
301
|
+
self.set_minmax_button.pack(side=tk.LEFT, padx=5)
|
|
302
|
+
self.set_zscale_button = tk.Button(self.button_frame3, text="zscale [/]", command=self.set_zscale)
|
|
303
|
+
self.set_zscale_button.pack(side=tk.LEFT, padx=5)
|
|
135
304
|
|
|
136
|
-
#
|
|
137
|
-
self.
|
|
305
|
+
# Figure
|
|
306
|
+
self.fig, self.ax = plt.subplots(figsize=(7, 5.5))
|
|
307
|
+
self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
|
|
308
|
+
self.canvas.get_tk_widget().pack(padx=5, pady=5)
|
|
138
309
|
# The next two instructions prevent a segmentation fault when pressing "q"
|
|
139
310
|
self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
|
|
140
311
|
self.canvas.mpl_connect("key_press_event", self.on_key)
|
|
141
312
|
self.canvas.mpl_connect("button_press_event", self.on_click)
|
|
142
313
|
canvas_widget = self.canvas.get_tk_widget()
|
|
143
|
-
canvas_widget.
|
|
314
|
+
canvas_widget.pack(fill=tk.BOTH, expand=True)
|
|
144
315
|
|
|
145
316
|
# Matplotlib toolbar
|
|
146
|
-
self.toolbar_frame = tk.Frame(self.
|
|
147
|
-
self.toolbar_frame.
|
|
317
|
+
self.toolbar_frame = tk.Frame(self.root)
|
|
318
|
+
self.toolbar_frame.pack(fill=tk.X, expand=False, pady=5)
|
|
148
319
|
self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
|
|
149
320
|
self.toolbar.update()
|
|
150
321
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def set_vmax(self):
|
|
161
|
-
old_vmax = self.get_vmax()
|
|
162
|
-
new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
|
|
163
|
-
if new_vmax is None:
|
|
164
|
-
return
|
|
165
|
-
self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
|
|
166
|
-
self.image.set_clim(vmax=new_vmax)
|
|
167
|
-
self.canvas.draw()
|
|
168
|
-
|
|
169
|
-
def get_vmin(self):
|
|
170
|
-
return float(self.vmin_button.cget("text").split(":")[1])
|
|
171
|
-
|
|
172
|
-
def get_vmax(self):
|
|
173
|
-
return float(self.vmax_button.cget("text").split(":")[1])
|
|
322
|
+
# update the image display
|
|
323
|
+
xlabel = 'X pixel (from 1 to NAXIS1)'
|
|
324
|
+
ylabel = 'Y pixel (from 1 to NAXIS2)'
|
|
325
|
+
extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
|
|
326
|
+
self.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
|
|
327
|
+
title=os.path.basename(self.input_fits),
|
|
328
|
+
xlabel=xlabel, ylabel=ylabel,
|
|
329
|
+
extent=extent)
|
|
330
|
+
self.fig.tight_layout()
|
|
174
331
|
|
|
175
332
|
def run_lacosmic(self):
|
|
333
|
+
"""Run L.A.Cosmic to detect cosmic rays."""
|
|
176
334
|
self.run_lacosmic_button.config(state=tk.DISABLED)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
335
|
+
# Define parameters for L.A.Cosmic from default dictionary
|
|
336
|
+
editor_window = tk.Toplevel(self.root)
|
|
337
|
+
editor = ParameterEditor(
|
|
338
|
+
root=editor_window,
|
|
339
|
+
param_dict=self.lacosmic_params,
|
|
340
|
+
window_title='Cosmic Ray Mask Generation Parameters',
|
|
341
|
+
xmin=self.last_xmin,
|
|
342
|
+
xmax=self.last_xmax,
|
|
343
|
+
ymin=self.last_ymin,
|
|
344
|
+
ymax=self.last_ymax,
|
|
345
|
+
imgshape=self.data.shape
|
|
185
346
|
)
|
|
186
|
-
|
|
347
|
+
# Make it modal (blocks interaction with main window)
|
|
348
|
+
editor_window.transient(self.root)
|
|
349
|
+
editor_window.grab_set()
|
|
350
|
+
# Wait for the editor window to close
|
|
351
|
+
self.root.wait_window(editor_window)
|
|
352
|
+
# Get the result after window closes
|
|
353
|
+
updated_params = editor.get_result()
|
|
354
|
+
if updated_params is not None:
|
|
355
|
+
# Update last used region values
|
|
356
|
+
self.last_xmin = updated_params['xmin']['value']
|
|
357
|
+
self.last_xmax = updated_params['xmax']['value']
|
|
358
|
+
self.last_ymin = updated_params['ymin']['value']
|
|
359
|
+
self.last_ymax = updated_params['ymax']['value']
|
|
360
|
+
# Update parameter dictionary with new values
|
|
361
|
+
self.lacosmic_params = updated_params
|
|
362
|
+
print("Parameters updated:")
|
|
363
|
+
for key, info in self.lacosmic_params.items():
|
|
364
|
+
print(f" {key}: {info['value']}")
|
|
365
|
+
# Execute L.A.Cosmic with updated parameters
|
|
366
|
+
cleandata_lacosmic, mask_crfound = cosmicray_lacosmic(
|
|
367
|
+
self.data,
|
|
368
|
+
gain=self.lacosmic_params['gain']['value'],
|
|
369
|
+
readnoise=self.lacosmic_params['readnoise']['value'],
|
|
370
|
+
sigclip=self.lacosmic_params['sigclip']['value'],
|
|
371
|
+
sigfrac=self.lacosmic_params['sigfrac']['value'],
|
|
372
|
+
objlim=self.lacosmic_params['objlim']['value'],
|
|
373
|
+
niter=self.lacosmic_params['niter']['value'],
|
|
374
|
+
verbose=self.lacosmic_params['verbose']['value']
|
|
375
|
+
)
|
|
376
|
+
# Select the image region to process
|
|
377
|
+
fits_region = f"[{updated_params['xmin']['value']}:{updated_params['xmax']['value']}"
|
|
378
|
+
fits_region += f",{updated_params['ymin']['value']}:{updated_params['ymax']['value']}]"
|
|
379
|
+
region = SliceRegion2D(fits_region, mode="fits").python
|
|
380
|
+
self.cleandata_lacosmic = self.data.copy()
|
|
381
|
+
self.cleandata_lacosmic[region] = cleandata_lacosmic[region]
|
|
382
|
+
self.mask_crfound = np.zeros_like(self.data, dtype=bool)
|
|
383
|
+
self.mask_crfound[region] = mask_crfound[region]
|
|
384
|
+
# Process the mask: dilation and labeling
|
|
385
|
+
if np.any(self.mask_crfound):
|
|
386
|
+
num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
|
|
387
|
+
dilation = self.lacosmic_params['dilation']['value']
|
|
388
|
+
if dilation > 0:
|
|
389
|
+
# Dilate the mask by the specified number of pixels
|
|
390
|
+
structure = ndimage.generate_binary_structure(2, 2) # 8-connectivity
|
|
391
|
+
self.mask_crfound = ndimage.binary_dilation(
|
|
392
|
+
self.mask_crfound,
|
|
393
|
+
structure=structure,
|
|
394
|
+
iterations=self.lacosmic_params['dilation']['value']
|
|
395
|
+
)
|
|
396
|
+
num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
|
|
397
|
+
sdum = str(num_cr_pixels_after_dilation)
|
|
398
|
+
else:
|
|
399
|
+
sdum = str(num_cr_pixels_before_dilation)
|
|
400
|
+
print("Number of cosmic ray pixels detected by L.A.Cosmic: "
|
|
401
|
+
f"{num_cr_pixels_before_dilation:{len(sdum)}}")
|
|
402
|
+
if dilation > 0:
|
|
403
|
+
print(f"Number of cosmic ray pixels after dilation........: "
|
|
404
|
+
f"{num_cr_pixels_after_dilation:{len(sdum)}}")
|
|
405
|
+
# Label connected components in the mask; note that by default,
|
|
406
|
+
# structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
|
|
407
|
+
# diagonal connections too, so we define a 3x3 square.
|
|
408
|
+
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
409
|
+
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
410
|
+
print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features:{len(sdum)}}")
|
|
411
|
+
self.apply_lacosmic_button.config(state=tk.NORMAL)
|
|
412
|
+
self.examine_detected_cr_button.config(state=tk.NORMAL)
|
|
413
|
+
self.update_cr_overlay()
|
|
414
|
+
else:
|
|
415
|
+
print("No cosmic ray pixels detected by L.A.Cosmic.")
|
|
416
|
+
self.cr_labels = None
|
|
417
|
+
self.num_features = 0
|
|
418
|
+
self.apply_lacosmic_button.config(state=tk.DISABLED)
|
|
419
|
+
self.examine_detected_cr_button.config(state=tk.DISABLED)
|
|
420
|
+
else:
|
|
421
|
+
print("Parameter editing cancelled. L.A.Cosmic detection skipped!")
|
|
187
422
|
self.run_lacosmic_button.config(state=tk.NORMAL)
|
|
188
|
-
|
|
423
|
+
|
|
424
|
+
def toggle_cr_overlay(self):
|
|
425
|
+
"""Toggle the overlay of cosmic ray pixels on the image."""
|
|
426
|
+
self.overplot_cr_pixels = not self.overplot_cr_pixels
|
|
427
|
+
if self.overplot_cr_pixels:
|
|
428
|
+
self.overplot_cr_button.config(text="CR overlay: On")
|
|
429
|
+
else:
|
|
430
|
+
self.overplot_cr_button.config(text="CR overlay: Off")
|
|
431
|
+
self.update_cr_overlay()
|
|
432
|
+
|
|
433
|
+
def update_cr_overlay(self):
|
|
434
|
+
"""Update the overlay of cosmic ray pixels on the image."""
|
|
435
|
+
if self.overplot_cr_pixels:
|
|
436
|
+
# Remove previous CR pixel overlay (if any)
|
|
437
|
+
if hasattr(self, 'scatter_cr'):
|
|
438
|
+
self.scatter_cr.remove()
|
|
439
|
+
del self.scatter_cr
|
|
440
|
+
# Overlay CR pixels in red
|
|
441
|
+
if np.any(self.mask_crfound):
|
|
442
|
+
y_indices, x_indices = np.where(self.mask_crfound)
|
|
443
|
+
self.scatter_cr = self.ax.scatter(x_indices + 1, y_indices + 1, s=1, c='red', marker='o')
|
|
444
|
+
else:
|
|
445
|
+
# Remove CR pixel overlay
|
|
446
|
+
if hasattr(self, 'scatter_cr'):
|
|
447
|
+
self.scatter_cr.remove()
|
|
448
|
+
del self.scatter_cr
|
|
449
|
+
self.canvas.draw_idle()
|
|
450
|
+
|
|
451
|
+
def apply_lacosmic(self):
|
|
452
|
+
"""Apply the selected cleaning method to the detected cosmic rays."""
|
|
453
|
+
if np.any(self.mask_crfound):
|
|
454
|
+
# recalculate labels and number of features
|
|
455
|
+
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
456
|
+
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
457
|
+
print(f"Number of cosmic ray pixels detected by L.A.Cosmic...........: {np.sum(self.mask_crfound)}")
|
|
458
|
+
print(f"Number of cosmic rays (grouped pixels) detected by L.A.Cosmic: {self.num_features}")
|
|
459
|
+
# Define parameters for L.A.Cosmic from default dictionary
|
|
460
|
+
editor_window = tk.Toplevel(self.root)
|
|
461
|
+
editor = InterpolationEditor(
|
|
462
|
+
root=editor_window,
|
|
463
|
+
last_dilation=self.lacosmic_params['dilation']['value'],
|
|
464
|
+
last_npoints=self.last_npoints,
|
|
465
|
+
last_degree=self.last_degree,
|
|
466
|
+
auxdata=self.auxdata
|
|
467
|
+
)
|
|
468
|
+
# Make it modal (blocks interaction with main window)
|
|
469
|
+
editor_window.transient(self.root)
|
|
470
|
+
editor_window.grab_set()
|
|
471
|
+
# Wait for the editor window to close
|
|
472
|
+
self.root.wait_window(editor_window)
|
|
473
|
+
# Get the result after window closes
|
|
474
|
+
cleaning_method = editor.cleaning_method
|
|
475
|
+
num_cr_cleaned = 0
|
|
476
|
+
if cleaning_method is None:
|
|
477
|
+
print("Interpolation method selection cancelled. No cleaning applied!")
|
|
478
|
+
return
|
|
479
|
+
self.last_npoints = editor.npoints
|
|
480
|
+
self.last_degree = editor.degree
|
|
481
|
+
if cleaning_method == 'lacosmic':
|
|
482
|
+
# Replace all detected CR pixels with L.A.Cosmic values
|
|
483
|
+
self.data[self.mask_crfound] = self.cleandata_lacosmic[self.mask_crfound]
|
|
484
|
+
# update mask_fixed to include the newly fixed pixels
|
|
485
|
+
self.mask_fixed[self.mask_crfound] = True
|
|
486
|
+
# upate mask_crfound by eliminating the cleaned pixels
|
|
487
|
+
self.mask_crfound[self.mask_crfound] = False
|
|
488
|
+
num_cr_cleaned = self.num_features
|
|
489
|
+
elif cleaning_method == 'auxdata':
|
|
490
|
+
if self.auxdata is None:
|
|
491
|
+
print("No auxiliary data available. Cleaning skipped!")
|
|
492
|
+
return
|
|
493
|
+
# Replace all detected CR pixels with auxiliary data values
|
|
494
|
+
self.data[self.mask_crfound] = self.auxdata[self.mask_crfound]
|
|
495
|
+
# update mask_fixed to include the newly fixed pixels
|
|
496
|
+
self.mask_fixed[self.mask_crfound] = True
|
|
497
|
+
# upate mask_crfound by eliminating the cleaned pixels
|
|
498
|
+
self.mask_crfound[self.mask_crfound] = False
|
|
499
|
+
num_cr_cleaned = self.num_features
|
|
500
|
+
else:
|
|
501
|
+
for i in range(1, self.num_features + 1):
|
|
502
|
+
tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
|
|
503
|
+
if cleaning_method == 'x':
|
|
504
|
+
interpolation_performed, _, _ = interpolation_x(
|
|
505
|
+
data=self.data,
|
|
506
|
+
mask_fixed=tmp_mask_fixed,
|
|
507
|
+
cr_labels=self.cr_labels,
|
|
508
|
+
cr_index=i,
|
|
509
|
+
npoints=editor.npoints,
|
|
510
|
+
degree=editor.degree
|
|
511
|
+
)
|
|
512
|
+
elif cleaning_method == 'y':
|
|
513
|
+
interpolation_performed, _, _ = interpolation_y(
|
|
514
|
+
data=self.data,
|
|
515
|
+
mask_fixed=tmp_mask_fixed,
|
|
516
|
+
cr_labels=self.cr_labels,
|
|
517
|
+
cr_index=i,
|
|
518
|
+
npoints=editor.npoints,
|
|
519
|
+
degree=editor.degree
|
|
520
|
+
)
|
|
521
|
+
elif cleaning_method == 'a-plane':
|
|
522
|
+
interpolation_performed, _, _ = interpolation_a(
|
|
523
|
+
data=self.data,
|
|
524
|
+
mask_fixed=tmp_mask_fixed,
|
|
525
|
+
cr_labels=self.cr_labels,
|
|
526
|
+
cr_index=i,
|
|
527
|
+
npoints=editor.npoints,
|
|
528
|
+
method='surface'
|
|
529
|
+
)
|
|
530
|
+
elif cleaning_method == 'a-median':
|
|
531
|
+
interpolation_performed, _, _ = interpolation_a(
|
|
532
|
+
data=self.data,
|
|
533
|
+
mask_fixed=tmp_mask_fixed,
|
|
534
|
+
cr_labels=self.cr_labels,
|
|
535
|
+
cr_index=i,
|
|
536
|
+
npoints=editor.npoints,
|
|
537
|
+
method='median'
|
|
538
|
+
)
|
|
539
|
+
else:
|
|
540
|
+
raise ValueError(f"Unknown cleaning method: {cleaning_method}")
|
|
541
|
+
if interpolation_performed:
|
|
542
|
+
num_cr_cleaned += 1
|
|
543
|
+
# update mask_fixed to include the newly fixed pixels
|
|
544
|
+
self.mask_fixed[tmp_mask_fixed] = True
|
|
545
|
+
# upate mask_crfound by eliminating the cleaned pixels
|
|
546
|
+
self.mask_crfound[tmp_mask_fixed] = False
|
|
547
|
+
print(f"Number of cosmic rays identified and cleaned: {num_cr_cleaned}")
|
|
548
|
+
# recalculate labels and number of features
|
|
549
|
+
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
550
|
+
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
551
|
+
print(f"Remaining number of cosmic ray pixels...........: {np.sum(self.mask_crfound)}")
|
|
552
|
+
print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features}")
|
|
553
|
+
# redraw image to show the changes
|
|
554
|
+
self.image.set_data(self.data)
|
|
555
|
+
self.canvas.draw_idle()
|
|
556
|
+
if num_cr_cleaned > 0:
|
|
557
|
+
self.save_button.config(state=tk.NORMAL)
|
|
558
|
+
if self.num_features == 0:
|
|
559
|
+
self.examine_detected_cr_button.config(state=tk.DISABLED)
|
|
560
|
+
self.apply_lacosmic_button.config(state=tk.DISABLED)
|
|
561
|
+
self.update_cr_overlay()
|
|
562
|
+
|
|
563
|
+
def examine_detected_cr(self, first_cr_index=1, single_cr=False, ixpix=None, iypix=None):
|
|
564
|
+
"""Open a window to examine and possibly clean detected cosmic rays."""
|
|
565
|
+
self.working_in_review_window = True
|
|
566
|
+
review_window = tk.Toplevel(self.root)
|
|
567
|
+
if ixpix is not None and iypix is not None:
|
|
568
|
+
# select single pixel based on provided coordinates
|
|
569
|
+
tmp_cr_labels = np.zeros_like(self.data, dtype=int)
|
|
570
|
+
tmp_cr_labels[iypix - 1, ixpix - 1] = 1
|
|
571
|
+
review = ReviewCosmicRay(
|
|
572
|
+
root=review_window,
|
|
573
|
+
data=self.data,
|
|
574
|
+
auxdata=self.auxdata,
|
|
575
|
+
cleandata_lacosmic=self.cleandata_lacosmic,
|
|
576
|
+
cr_labels=tmp_cr_labels,
|
|
577
|
+
num_features=1,
|
|
578
|
+
first_cr_index=1,
|
|
579
|
+
single_cr=True,
|
|
580
|
+
last_dilation=self.lacosmic_params['dilation']['value'],
|
|
581
|
+
last_npoints=self.last_npoints,
|
|
582
|
+
last_degree=self.last_degree
|
|
583
|
+
)
|
|
584
|
+
else:
|
|
585
|
+
review = ReviewCosmicRay(
|
|
586
|
+
root=review_window,
|
|
587
|
+
data=self.data,
|
|
588
|
+
auxdata=self.auxdata,
|
|
589
|
+
cleandata_lacosmic=self.cleandata_lacosmic,
|
|
590
|
+
cr_labels=self.cr_labels,
|
|
591
|
+
num_features=self.num_features,
|
|
592
|
+
first_cr_index=first_cr_index,
|
|
593
|
+
single_cr=single_cr,
|
|
594
|
+
last_dilation=self.lacosmic_params['dilation']['value'],
|
|
595
|
+
last_npoints=self.last_npoints,
|
|
596
|
+
last_degree=self.last_degree
|
|
597
|
+
)
|
|
598
|
+
# Make it modal (blocks interaction with main window)
|
|
599
|
+
review_window.transient(self.root)
|
|
600
|
+
review_window.grab_set()
|
|
601
|
+
self.root.wait_window(review_window)
|
|
602
|
+
self.working_in_review_window = False
|
|
603
|
+
# Get the result after window closes
|
|
604
|
+
if review.num_cr_cleaned > 0:
|
|
605
|
+
self.last_npoints = review.npoints
|
|
606
|
+
self.last_degree = review.degree
|
|
607
|
+
print(f"Number of cosmic rays identified and cleaned: {review.num_cr_cleaned}")
|
|
608
|
+
# update mask_fixed to include the newly fixed pixels
|
|
609
|
+
self.mask_fixed[review.mask_fixed] = True
|
|
610
|
+
# upate mask_crfound by eliminating the cleaned pixels
|
|
611
|
+
self.mask_crfound[review.mask_fixed] = False
|
|
612
|
+
# recalculate labels and number of features
|
|
613
|
+
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
614
|
+
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
615
|
+
print(f"Remaining number of cosmic ray pixels...........: {np.sum(self.mask_crfound)}")
|
|
616
|
+
print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features}")
|
|
617
|
+
# redraw image to show the changes
|
|
618
|
+
self.image.set_data(self.data)
|
|
619
|
+
self.canvas.draw_idle()
|
|
620
|
+
if review.num_cr_cleaned > 0:
|
|
621
|
+
self.save_button.config(state=tk.NORMAL)
|
|
622
|
+
if self.num_features == 0:
|
|
623
|
+
self.examine_detected_cr_button.config(state=tk.DISABLED)
|
|
624
|
+
self.apply_lacosmic_button.config(state=tk.DISABLED)
|
|
625
|
+
self.update_cr_overlay()
|
|
189
626
|
|
|
190
627
|
def stop_app(self):
|
|
191
|
-
|
|
192
|
-
|
|
628
|
+
"""Stop the application, prompting to save if there are unsaved changes."""
|
|
629
|
+
proceed_with_stop = True
|
|
630
|
+
if self.save_button['state'] == tk.NORMAL:
|
|
631
|
+
print("Warning: There are unsaved changes!")
|
|
632
|
+
proceed_with_stop = messagebox.askyesno(
|
|
633
|
+
"Unsaved Changes",
|
|
634
|
+
"You have unsaved changes.\nDo you really want to quit?",
|
|
635
|
+
default=messagebox.NO
|
|
636
|
+
)
|
|
637
|
+
if proceed_with_stop:
|
|
638
|
+
self.root.quit()
|
|
639
|
+
self.root.destroy()
|
|
193
640
|
|
|
194
641
|
def on_key(self, event):
|
|
642
|
+
"""Handle key press events."""
|
|
195
643
|
if event.key == 'q':
|
|
196
644
|
pass # Ignore the "q" key to prevent closing the window
|
|
645
|
+
elif event.key == ',':
|
|
646
|
+
self.set_minmax()
|
|
647
|
+
elif event.key == '/':
|
|
648
|
+
self.set_zscale()
|
|
197
649
|
else:
|
|
198
650
|
print(f"Key pressed: {event.key}")
|
|
199
651
|
|
|
200
652
|
def on_click(self, event):
|
|
201
|
-
|
|
653
|
+
"""Handle mouse click events on the image."""
|
|
654
|
+
# ignore clicks if we are working in the review window
|
|
655
|
+
if self.working_in_review_window:
|
|
656
|
+
print("Currently working in review window; click ignored.")
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
# check the toolbar is not active
|
|
660
|
+
toolbar = self.fig.canvas.toolbar
|
|
661
|
+
if toolbar.mode != "":
|
|
662
|
+
print(f"Toolbar mode '{toolbar.mode}' active; click ignored.")
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
# ignore clicks outside the expected axes
|
|
666
|
+
# (note that the color bar is a different axes)
|
|
667
|
+
if event.inaxes == self.ax:
|
|
202
668
|
x, y = event.xdata, event.ydata
|
|
203
|
-
|
|
669
|
+
ix = int(x + 0.5)
|
|
670
|
+
iy = int(y + 0.5)
|
|
671
|
+
print(f"Clicked at image coordinates: ({ix}, {iy})")
|
|
672
|
+
label_at_click = 0
|
|
673
|
+
if self.mask_crfound is None:
|
|
674
|
+
print("No cosmic ray pixels detected (mask_crfound is None)")
|
|
675
|
+
elif not np.any(self.mask_crfound):
|
|
676
|
+
print("No remaining cosmic ray pixels in mask_crfound")
|
|
677
|
+
else:
|
|
678
|
+
label_at_click = self.cr_labels[iy - 1, ix - 1]
|
|
679
|
+
if label_at_click == 0:
|
|
680
|
+
(closest_x, closest_y), min_distance = find_closest_true(self.mask_crfound, ix - 1, iy - 1)
|
|
681
|
+
if closest_x is None and closest_y is None:
|
|
682
|
+
print("No remaining cosmic ray pixels")
|
|
683
|
+
elif min_distance > MAX_PIXEL_DISTANCE_TO_CR * 1.4142135:
|
|
684
|
+
print("No nearby cosmic ray pixels found in searching square")
|
|
685
|
+
else:
|
|
686
|
+
label_at_click = self.cr_labels[closest_y, closest_x]
|
|
687
|
+
print(f"Clicked pixel is part of cosmic ray number {label_at_click}.")
|
|
688
|
+
if label_at_click == 0:
|
|
689
|
+
# Find pixel with maximum value within a square region around the click
|
|
690
|
+
semiwidth = MAX_PIXEL_DISTANCE_TO_CR
|
|
691
|
+
jmin = (ix - 1) - semiwidth if (ix - 1) - semiwidth >= 0 else 0
|
|
692
|
+
jmax = (ix - 1) + semiwidth if (ix - 1) + semiwidth < self.data.shape[1] else self.data.shape[1] - 1
|
|
693
|
+
imin = (iy - 1) - semiwidth if (iy - 1) - semiwidth >= 0 else 0
|
|
694
|
+
imax = (iy - 1) + semiwidth if (iy - 1) + semiwidth < self.data.shape[0] else self.data.shape[0] - 1
|
|
695
|
+
ijmax = np.unravel_index(
|
|
696
|
+
np.argmax(self.data[imin:imax+1, jmin:jmax+1]),
|
|
697
|
+
self.data[imin:imax+1, jmin:jmax+1].shape
|
|
698
|
+
)
|
|
699
|
+
ixpix = ijmax[1] + jmin + 1
|
|
700
|
+
iypix = ijmax[0] + imin + 1
|
|
701
|
+
else:
|
|
702
|
+
ixpix = None
|
|
703
|
+
iypix = None
|
|
704
|
+
self.examine_detected_cr(label_at_click, single_cr=True, ixpix=ixpix, iypix=iypix)
|