teareduce 0.5.4__py3-none-any.whl → 0.5.7__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/__init__.py +3 -1
- teareduce/cleanest/__main__.py +119 -11
- teareduce/cleanest/centerchildparent.py +46 -0
- teareduce/cleanest/cosmicraycleanerapp.py +517 -232
- teareduce/cleanest/definitions.py +30 -29
- teareduce/cleanest/imagedisplay.py +8 -9
- teareduce/cleanest/{cleanest.py → interpolate.py} +25 -26
- teareduce/cleanest/interpolation_a.py +5 -9
- teareduce/cleanest/interpolationeditor.py +156 -35
- teareduce/cleanest/lacosmicpad.py +57 -0
- teareduce/cleanest/mergemasks.py +58 -0
- teareduce/cleanest/modalprogressbar.py +182 -0
- teareduce/cleanest/parametereditor.py +112 -79
- teareduce/cleanest/reviewcosmicray.py +90 -62
- teareduce/tests/test_cleanest.py +11 -11
- teareduce/version.py +1 -1
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/METADATA +1 -1
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/RECORD +22 -18
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/WHEEL +0 -0
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/entry_points.txt +0 -0
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/licenses/LICENSE.txt +0 -0
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/top_level.txt +0 -0
|
@@ -11,24 +11,30 @@
|
|
|
11
11
|
|
|
12
12
|
import tkinter as tk
|
|
13
13
|
from tkinter import filedialog
|
|
14
|
+
from tkinter import font as tkfont
|
|
14
15
|
from tkinter import messagebox
|
|
16
|
+
from tkinter import simpledialog
|
|
15
17
|
import sys
|
|
16
18
|
|
|
17
19
|
from astropy.io import fits
|
|
18
|
-
from ccdproc import cosmicray_lacosmic
|
|
19
20
|
import matplotlib.pyplot as plt
|
|
20
21
|
from matplotlib.backend_bases import key_press_handler
|
|
21
22
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
22
23
|
from scipy import ndimage
|
|
23
24
|
import numpy as np
|
|
24
25
|
import os
|
|
26
|
+
from pathlib import Path
|
|
25
27
|
from rich import print
|
|
26
|
-
from tqdm import tqdm
|
|
27
28
|
|
|
29
|
+
from .centerchildparent import center_on_parent
|
|
28
30
|
from .definitions import lacosmic_default_dict
|
|
29
31
|
from .definitions import DEFAULT_NPOINTS_INTERP
|
|
30
32
|
from .definitions import DEFAULT_DEGREE_INTERP
|
|
31
33
|
from .definitions import MAX_PIXEL_DISTANCE_TO_CR
|
|
34
|
+
from .definitions import DEFAULT_TK_WINDOW_SIZE_X
|
|
35
|
+
from .definitions import DEFAULT_TK_WINDOW_SIZE_Y
|
|
36
|
+
from .definitions import DEFAULT_FONT_FAMILY
|
|
37
|
+
from .definitions import DEFAULT_FONT_SIZE
|
|
32
38
|
from .dilatemask import dilatemask
|
|
33
39
|
from .find_closest_true import find_closest_true
|
|
34
40
|
from .interpolation_a import interpolation_a
|
|
@@ -36,21 +42,37 @@ from .interpolation_x import interpolation_x
|
|
|
36
42
|
from .interpolation_y import interpolation_y
|
|
37
43
|
from .interpolationeditor import InterpolationEditor
|
|
38
44
|
from .imagedisplay import ImageDisplay
|
|
45
|
+
from .lacosmicpad import lacosmicpad
|
|
46
|
+
from .mergemasks import merge_peak_tail_masks
|
|
39
47
|
from .parametereditor import ParameterEditor
|
|
40
48
|
from .reviewcosmicray import ReviewCosmicRay
|
|
49
|
+
from .modalprogressbar import ModalProgressBar
|
|
41
50
|
|
|
42
51
|
from ..imshow import imshow
|
|
43
52
|
from ..sliceregion import SliceRegion2D
|
|
44
53
|
from ..zscale import zscale
|
|
45
54
|
|
|
46
55
|
import matplotlib
|
|
56
|
+
|
|
47
57
|
matplotlib.use("TkAgg")
|
|
48
58
|
|
|
49
59
|
|
|
50
60
|
class CosmicRayCleanerApp(ImageDisplay):
|
|
51
61
|
"""Main application class for cosmic ray cleaning."""
|
|
52
62
|
|
|
53
|
-
def __init__(
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
root,
|
|
66
|
+
input_fits,
|
|
67
|
+
extension="0",
|
|
68
|
+
auxfile=None,
|
|
69
|
+
extension_auxfile="0",
|
|
70
|
+
fontfamily=DEFAULT_FONT_FAMILY,
|
|
71
|
+
fontsize=DEFAULT_FONT_SIZE,
|
|
72
|
+
width=DEFAULT_TK_WINDOW_SIZE_X,
|
|
73
|
+
height=DEFAULT_TK_WINDOW_SIZE_Y,
|
|
74
|
+
verbose=False,
|
|
75
|
+
):
|
|
54
76
|
"""
|
|
55
77
|
Initialize the application.
|
|
56
78
|
|
|
@@ -60,15 +82,29 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
60
82
|
The main Tkinter window.
|
|
61
83
|
input_fits : str
|
|
62
84
|
Path to the FITS file to be cleaned.
|
|
63
|
-
extension :
|
|
64
|
-
FITS extension to use (default is 0).
|
|
85
|
+
extension : str, optional
|
|
86
|
+
FITS extension to use (default is "0").
|
|
65
87
|
auxfile : str, optional
|
|
66
88
|
Path to an auxiliary FITS file (default is None).
|
|
67
|
-
extension_auxfile :
|
|
68
|
-
FITS extension for auxiliary file (default is 0).
|
|
89
|
+
extension_auxfile : str, optional
|
|
90
|
+
FITS extension for auxiliary file (default is "0").
|
|
91
|
+
fontfamily : str, optional
|
|
92
|
+
Font family for the GUI (default is "Helvetica").
|
|
93
|
+
fontsize : int, optional
|
|
94
|
+
Font size for the GUI (default is 14).
|
|
95
|
+
width : int, optional
|
|
96
|
+
Width of the GUI window in pixels (default is 800).
|
|
97
|
+
height : int, optional
|
|
98
|
+
Height of the GUI window in pixels (default is 600).
|
|
99
|
+
verbose : bool, optional
|
|
100
|
+
Enable verbose output (default is False).
|
|
69
101
|
|
|
70
102
|
Methods
|
|
71
103
|
-------
|
|
104
|
+
process_detected_cr(dilation)
|
|
105
|
+
Process the detected cosmic ray mask.
|
|
106
|
+
load_detected_cr_from_file()
|
|
107
|
+
Load detected cosmic ray mask from a FITS file.
|
|
72
108
|
load_fits_file()
|
|
73
109
|
Load the FITS file and auxiliary file (if provided).
|
|
74
110
|
save_fits_file()
|
|
@@ -96,6 +132,18 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
96
132
|
----------
|
|
97
133
|
root : tk.Tk
|
|
98
134
|
The main Tkinter window.
|
|
135
|
+
fontfamily : str
|
|
136
|
+
Font family for the GUI.
|
|
137
|
+
fontsize : int
|
|
138
|
+
Font size for the GUI.
|
|
139
|
+
default_font : tkfont.Font
|
|
140
|
+
The default font used in the GUI.
|
|
141
|
+
width : int
|
|
142
|
+
Width of the GUI window in pixels.
|
|
143
|
+
height : int
|
|
144
|
+
Height of the GUI window in pixels.
|
|
145
|
+
verbose : bool
|
|
146
|
+
Enable verbose output.
|
|
99
147
|
lacosmic_params : dict
|
|
100
148
|
Dictionary of L.A.Cosmic parameters.
|
|
101
149
|
input_fits : str
|
|
@@ -116,12 +164,16 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
116
164
|
Boolean mask of detected cosmic ray pixels.
|
|
117
165
|
last_xmin : int
|
|
118
166
|
Last used minimum x-coordinate for region selection.
|
|
167
|
+
From 1 to NAXIS1.
|
|
119
168
|
last_xmax : int
|
|
120
169
|
Last used maximum x-coordinate for region selection.
|
|
170
|
+
From 1 to NAXIS1.
|
|
121
171
|
last_ymin : int
|
|
122
172
|
Last used minimum y-coordinate for region selection.
|
|
173
|
+
From 1 to NAXIS2.
|
|
123
174
|
last_ymax : int
|
|
124
175
|
Last used maximum y-coordinate for region selection.
|
|
176
|
+
From 1 to NAXIS2.
|
|
125
177
|
last_npoints : int
|
|
126
178
|
Last used number of points for interpolation.
|
|
127
179
|
last_degree : int
|
|
@@ -137,11 +189,21 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
137
189
|
"""
|
|
138
190
|
self.root = root
|
|
139
191
|
# self.root.geometry("800x800+50+0") # This does not work in Fedora
|
|
140
|
-
self.
|
|
192
|
+
self.width = width
|
|
193
|
+
self.height = height
|
|
194
|
+
self.verbose = verbose
|
|
195
|
+
self.root.minsize(self.width, self.height)
|
|
141
196
|
self.root.update_idletasks()
|
|
142
|
-
self.root.geometry("+50+0")
|
|
143
197
|
self.root.title("Cosmic Ray Cleaner")
|
|
198
|
+
self.fontfamily = fontfamily
|
|
199
|
+
self.fontsize = fontsize
|
|
200
|
+
self.default_font = tkfont.nametofont("TkDefaultFont")
|
|
201
|
+
self.default_font.configure(
|
|
202
|
+
family=fontfamily, size=fontsize, weight="normal", slant="roman", underline=0, overstrike=0
|
|
203
|
+
)
|
|
144
204
|
self.lacosmic_params = lacosmic_default_dict.copy()
|
|
205
|
+
self.lacosmic_params["run1_verbose"]["value"] = self.verbose
|
|
206
|
+
self.lacosmic_params["run2_verbose"]["value"] = self.verbose
|
|
145
207
|
self.input_fits = input_fits
|
|
146
208
|
self.extension = extension
|
|
147
209
|
self.data = None
|
|
@@ -163,6 +225,93 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
163
225
|
self.num_features = 0
|
|
164
226
|
self.working_in_review_window = False
|
|
165
227
|
|
|
228
|
+
def process_detected_cr(self, dilation):
|
|
229
|
+
"""Process the detected cosmic ray mask.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
dilation : int
|
|
234
|
+
Number of pixels to dilate the cosmic ray mask.
|
|
235
|
+
"""
|
|
236
|
+
# Process the mask: dilation and labeling
|
|
237
|
+
if np.any(self.mask_crfound):
|
|
238
|
+
num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
|
|
239
|
+
if dilation > 0:
|
|
240
|
+
# Dilate the mask by the specified number of pixels
|
|
241
|
+
self.mask_crfound = dilatemask(
|
|
242
|
+
mask=self.mask_crfound, iterations=dilation, connectivity=1
|
|
243
|
+
)
|
|
244
|
+
num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
|
|
245
|
+
sdum = str(num_cr_pixels_after_dilation)
|
|
246
|
+
else:
|
|
247
|
+
sdum = str(num_cr_pixels_before_dilation)
|
|
248
|
+
print(
|
|
249
|
+
"Number of cosmic ray pixels detected..........: "
|
|
250
|
+
f"{num_cr_pixels_before_dilation:>{len(sdum)}}"
|
|
251
|
+
)
|
|
252
|
+
if dilation > 0:
|
|
253
|
+
print(
|
|
254
|
+
f"Number of cosmic ray pixels after dilation....: "
|
|
255
|
+
f"{num_cr_pixels_after_dilation:>{len(sdum)}}"
|
|
256
|
+
)
|
|
257
|
+
# Label connected components in the mask; note that by default,
|
|
258
|
+
# structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
|
|
259
|
+
# diagonal connections too, so we define a 3x3 square.
|
|
260
|
+
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
261
|
+
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
262
|
+
print(f"Number of cosmic ray features (grouped pixels): {self.num_features:>{len(sdum)}}")
|
|
263
|
+
self.replace_detected_cr_button.config(state=tk.NORMAL)
|
|
264
|
+
self.examine_detected_cr_button.config(state=tk.NORMAL)
|
|
265
|
+
self.update_cr_overlay()
|
|
266
|
+
self.use_cursor = True
|
|
267
|
+
self.use_cursor_button.config(text="[c]ursor: ON ")
|
|
268
|
+
else:
|
|
269
|
+
print("No cosmic ray pixels detected!")
|
|
270
|
+
self.cr_labels = None
|
|
271
|
+
self.num_features = 0
|
|
272
|
+
self.replace_detected_cr_button.config(state=tk.DISABLED)
|
|
273
|
+
self.examine_detected_cr_button.config(state=tk.DISABLED)
|
|
274
|
+
|
|
275
|
+
def load_detected_cr_from_file(self):
|
|
276
|
+
"""Load detected cosmic ray mask from a FITS file."""
|
|
277
|
+
crmask_file = filedialog.askopenfilename(
|
|
278
|
+
initialdir=os.getcwd(),
|
|
279
|
+
title="Select FITS file with cosmic ray mask",
|
|
280
|
+
filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
|
|
281
|
+
)
|
|
282
|
+
if crmask_file:
|
|
283
|
+
print(f"Selected input FITS file: {crmask_file}")
|
|
284
|
+
extension = simpledialog.askstring(
|
|
285
|
+
"Select Extension",
|
|
286
|
+
f"\nEnter extension number or name for file:\n{Path(crmask_file).name}",
|
|
287
|
+
initialvalue=None,
|
|
288
|
+
)
|
|
289
|
+
try:
|
|
290
|
+
extension = int(extension)
|
|
291
|
+
except ValueError:
|
|
292
|
+
pass # Keep as string
|
|
293
|
+
dilation = simpledialog.askinteger(
|
|
294
|
+
"Dilation", "Enter Dilation (min=0):", initialvalue=0, minvalue=0
|
|
295
|
+
)
|
|
296
|
+
try:
|
|
297
|
+
with fits.open(crmask_file, mode="readonly") as hdul:
|
|
298
|
+
if isinstance(extension, int):
|
|
299
|
+
if extension < 0 or extension >= len(hdul):
|
|
300
|
+
raise IndexError(f"Extension index {extension} out of range.")
|
|
301
|
+
else:
|
|
302
|
+
if extension not in hdul:
|
|
303
|
+
raise KeyError(f"Extension name '{extension}' not found.")
|
|
304
|
+
mask_crfound_loaded = hdul[extension].data.astype(bool)
|
|
305
|
+
if mask_crfound_loaded.shape != self.data.shape:
|
|
306
|
+
print(f"data shape...: {self.data.shape}")
|
|
307
|
+
print(f"mask shape...: {mask_crfound_loaded.shape}")
|
|
308
|
+
raise ValueError("Cosmic ray mask has different shape.")
|
|
309
|
+
self.mask_crfound = mask_crfound_loaded
|
|
310
|
+
print(f"Loaded cosmic ray mask from {crmask_file}")
|
|
311
|
+
self.process_detected_cr(dilation=dilation)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
print(f"Error loading cosmic ray mask: {e}")
|
|
314
|
+
|
|
166
315
|
def load_fits_file(self):
|
|
167
316
|
"""Load the FITS file and auxiliary file (if provided).
|
|
168
317
|
|
|
@@ -177,22 +326,51 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
177
326
|
provided, it also loads the auxiliary data from the specified extension.
|
|
178
327
|
The loaded data is stored in `self.data` and `self.auxdata` attributes.
|
|
179
328
|
"""
|
|
329
|
+
# check if extension is compatible with an integer
|
|
180
330
|
try:
|
|
181
|
-
|
|
331
|
+
extnum = int(self.extension)
|
|
332
|
+
self.extension = extnum
|
|
333
|
+
except ValueError:
|
|
334
|
+
# Keep as string (delaying checking until opening the file)
|
|
335
|
+
self.extension = self.extension.upper() # Convert to uppercase
|
|
336
|
+
try:
|
|
337
|
+
with fits.open(self.input_fits, mode="readonly") as hdul:
|
|
338
|
+
if isinstance(self.extension, int):
|
|
339
|
+
if self.extension < 0 or self.extension >= len(hdul):
|
|
340
|
+
raise IndexError(f"Extension index {self.extension} out of range.")
|
|
341
|
+
else:
|
|
342
|
+
if self.extension not in hdul:
|
|
343
|
+
raise KeyError(f"Extension name '{self.extension}' not found.")
|
|
344
|
+
print(f"Reading file [bold green]{self.input_fits}[/bold green], extension {self.extension}")
|
|
182
345
|
self.data = hdul[self.extension].data
|
|
183
|
-
if
|
|
184
|
-
self.mask_fixed = hdul[
|
|
346
|
+
if "CRMASK" in hdul:
|
|
347
|
+
self.mask_fixed = hdul["CRMASK"].data.astype(bool)
|
|
185
348
|
else:
|
|
186
349
|
self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
|
|
187
350
|
except Exception as e:
|
|
188
351
|
print(f"Error loading FITS file: {e}")
|
|
352
|
+
sys.exit(1)
|
|
189
353
|
self.mask_crfound = np.zeros(self.data.shape, dtype=bool)
|
|
190
354
|
naxis2, naxis1 = self.data.shape
|
|
191
|
-
self.region = SliceRegion2D(f
|
|
355
|
+
self.region = SliceRegion2D(f"[1:{naxis1}, 1:{naxis2}]", mode="fits").python
|
|
192
356
|
# Read auxiliary file if provided
|
|
193
357
|
if self.auxfile is not None:
|
|
358
|
+
# check if extension_auxfile is compatible with an integer
|
|
194
359
|
try:
|
|
195
|
-
|
|
360
|
+
extnum_aux = int(self.extension_auxfile)
|
|
361
|
+
self.extension_auxfile = extnum_aux
|
|
362
|
+
except ValueError:
|
|
363
|
+
# Keep as string (delaying checking until opening the file)
|
|
364
|
+
self.extension_auxfile = self.extension_auxfile.upper() # Convert to uppercase
|
|
365
|
+
try:
|
|
366
|
+
with fits.open(self.auxfile, mode="readonly") as hdul_aux:
|
|
367
|
+
if isinstance(self.extension_auxfile, int):
|
|
368
|
+
if self.extension_auxfile < 0 or self.extension_auxfile >= len(hdul_aux):
|
|
369
|
+
raise IndexError(f"Extension index {self.extension_auxfile} out of range.")
|
|
370
|
+
else:
|
|
371
|
+
if self.extension_auxfile not in hdul_aux:
|
|
372
|
+
raise KeyError(f"Extension name '{self.extension_auxfile}' not found.")
|
|
373
|
+
print(f"Reading auxiliary file [bold green]{self.auxfile}[/bold green], extension {self.extension_auxfile}")
|
|
196
374
|
self.auxdata = hdul_aux[self.extension_auxfile].data
|
|
197
375
|
if self.auxdata.shape != self.data.shape:
|
|
198
376
|
print(f"data shape...: {self.data.shape}")
|
|
@@ -229,15 +407,15 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
229
407
|
title="Save cleaned FITS file",
|
|
230
408
|
defaultextension=".fits",
|
|
231
409
|
filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
|
|
232
|
-
initialfile=suggested_name
|
|
410
|
+
initialfile=suggested_name,
|
|
233
411
|
)
|
|
234
412
|
try:
|
|
235
|
-
with fits.open(self.input_fits, mode=
|
|
413
|
+
with fits.open(self.input_fits, mode="readonly") as hdul:
|
|
236
414
|
hdul[self.extension].data = self.data
|
|
237
|
-
if
|
|
238
|
-
hdul[
|
|
415
|
+
if "CRMASK" in hdul:
|
|
416
|
+
hdul["CRMASK"].data = self.mask_fixed.astype(np.uint8)
|
|
239
417
|
else:
|
|
240
|
-
crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name=
|
|
418
|
+
crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name="CRMASK")
|
|
241
419
|
hdul.append(crmask_hdu)
|
|
242
420
|
hdul.writeto(output_fits, overwrite=True)
|
|
243
421
|
print(f"Cleaned data saved to {output_fits}")
|
|
@@ -269,25 +447,35 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
269
447
|
self.button_frame1.pack(pady=5)
|
|
270
448
|
self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
|
|
271
449
|
self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
|
|
272
|
-
|
|
273
|
-
self.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
self.
|
|
280
|
-
|
|
281
|
-
self.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
command=lambda: self.examine_detected_cr(1))
|
|
450
|
+
self.load_detected_cr_button = tk.Button(
|
|
451
|
+
self.button_frame1, text="Load detected CRs", command=self.load_detected_cr_from_file
|
|
452
|
+
)
|
|
453
|
+
self.load_detected_cr_button.pack(side=tk.LEFT, padx=5)
|
|
454
|
+
self.replace_detected_cr_button = tk.Button(
|
|
455
|
+
self.button_frame1, text="Replace detected CRs", command=self.apply_lacosmic
|
|
456
|
+
)
|
|
457
|
+
self.replace_detected_cr_button.pack(side=tk.LEFT, padx=5)
|
|
458
|
+
self.replace_detected_cr_button.config(state=tk.DISABLED) # Initially disabled
|
|
459
|
+
self.examine_detected_cr_button = tk.Button(
|
|
460
|
+
self.button_frame1, text="Examine detected CRs", command=lambda: self.examine_detected_cr(1)
|
|
461
|
+
)
|
|
285
462
|
self.examine_detected_cr_button.pack(side=tk.LEFT, padx=5)
|
|
286
463
|
self.examine_detected_cr_button.config(state=tk.DISABLED) # Initially disabled
|
|
287
464
|
|
|
288
465
|
# Row 2 of buttons
|
|
289
466
|
self.button_frame2 = tk.Frame(self.root)
|
|
290
467
|
self.button_frame2.pack(pady=5)
|
|
468
|
+
self.toggle_auxdata_button = tk.Button(self.button_frame2, text="[t]oggle data", command=self.toggle_auxdata)
|
|
469
|
+
self.toggle_auxdata_button.pack(side=tk.LEFT, padx=5)
|
|
470
|
+
if self.auxdata is None:
|
|
471
|
+
self.toggle_auxdata_button.config(state=tk.DISABLED)
|
|
472
|
+
else:
|
|
473
|
+
self.toggle_auxdata_button.config(state=tk.NORMAL)
|
|
474
|
+
self.image_aspect = "equal"
|
|
475
|
+
self.toggle_aspect_button = tk.Button(
|
|
476
|
+
self.button_frame2, text=f"[a]spect: {self.image_aspect}", command=self.toggle_aspect
|
|
477
|
+
)
|
|
478
|
+
self.toggle_aspect_button.pack(side=tk.LEFT, padx=5)
|
|
291
479
|
self.save_button = tk.Button(self.button_frame2, text="Save cleaned FITS", command=self.save_fits_file)
|
|
292
480
|
self.save_button.pack(side=tk.LEFT, padx=5)
|
|
293
481
|
self.save_button.config(state=tk.DISABLED) # Initially disabled
|
|
@@ -297,6 +485,9 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
297
485
|
# Row 3 of buttons
|
|
298
486
|
self.button_frame3 = tk.Frame(self.root)
|
|
299
487
|
self.button_frame3.pack(pady=5)
|
|
488
|
+
self.use_cursor = False
|
|
489
|
+
self.use_cursor_button = tk.Button(self.button_frame3, text="[c]ursor: OFF", command=self.set_cursor_onoff)
|
|
490
|
+
self.use_cursor_button.pack(side=tk.LEFT, padx=5)
|
|
300
491
|
vmin, vmax = zscale(self.data)
|
|
301
492
|
self.vmin_button = tk.Button(self.button_frame3, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
|
|
302
493
|
self.vmin_button.pack(side=tk.LEFT, padx=5)
|
|
@@ -306,18 +497,37 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
306
497
|
self.set_minmax_button.pack(side=tk.LEFT, padx=5)
|
|
307
498
|
self.set_zscale_button = tk.Button(self.button_frame3, text="zscale [/]", command=self.set_zscale)
|
|
308
499
|
self.set_zscale_button.pack(side=tk.LEFT, padx=5)
|
|
500
|
+
if self.overplot_cr_pixels:
|
|
501
|
+
self.overplot_cr_button = tk.Button(
|
|
502
|
+
self.button_frame3,
|
|
503
|
+
text="CR overlay: ON ",
|
|
504
|
+
command=self.toggle_cr_overlay,
|
|
505
|
+
)
|
|
506
|
+
else:
|
|
507
|
+
self.overplot_cr_button = tk.Button(
|
|
508
|
+
self.button_frame3,
|
|
509
|
+
text="CR overlay: OFF",
|
|
510
|
+
command=self.toggle_cr_overlay,
|
|
511
|
+
)
|
|
512
|
+
self.overplot_cr_button.pack(side=tk.LEFT, padx=5)
|
|
309
513
|
|
|
310
514
|
# Figure
|
|
311
|
-
self.
|
|
312
|
-
self.
|
|
313
|
-
|
|
515
|
+
self.plot_frame = tk.Frame(self.root)
|
|
516
|
+
self.plot_frame.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
|
|
517
|
+
fig_dpi = 100
|
|
518
|
+
image_ratio = 480 / 640 # Default image ratio
|
|
519
|
+
fig_width_inches = self.width / fig_dpi
|
|
520
|
+
fig_height_inches = self.height * image_ratio / fig_dpi
|
|
521
|
+
self.fig, self.ax = plt.subplots(figsize=(fig_width_inches, fig_height_inches), dpi=fig_dpi)
|
|
522
|
+
self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
|
|
523
|
+
canvas_widget = self.canvas.get_tk_widget()
|
|
524
|
+
canvas_widget.config(width=self.width, height=self.height * image_ratio)
|
|
525
|
+
canvas_widget.pack(expand=True)
|
|
314
526
|
# The next two instructions prevent a segmentation fault when pressing "q"
|
|
315
527
|
self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
|
|
316
528
|
self.canvas.mpl_connect("key_press_event", self.on_key)
|
|
317
529
|
self.canvas.mpl_connect("button_press_event", self.on_click)
|
|
318
530
|
canvas_widget = self.canvas.get_tk_widget()
|
|
319
|
-
# canvas_widget.pack(fill=tk.BOTH, expand=True) # This does not work in Fedora
|
|
320
|
-
canvas_widget.pack(expand=True)
|
|
321
531
|
|
|
322
532
|
# Matplotlib toolbar
|
|
323
533
|
self.toolbar_frame = tk.Frame(self.root)
|
|
@@ -326,29 +536,81 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
326
536
|
self.toolbar.update()
|
|
327
537
|
|
|
328
538
|
# update the image display
|
|
329
|
-
xlabel =
|
|
330
|
-
ylabel =
|
|
539
|
+
xlabel = "X pixel (from 1 to NAXIS1)"
|
|
540
|
+
ylabel = "Y pixel (from 1 to NAXIS2)"
|
|
331
541
|
extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
|
|
332
|
-
self.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
542
|
+
self.image_aspect = "equal"
|
|
543
|
+
self.displaying_auxdata = False
|
|
544
|
+
self.image, _, _ = imshow(
|
|
545
|
+
fig=self.fig,
|
|
546
|
+
ax=self.ax,
|
|
547
|
+
data=self.data,
|
|
548
|
+
vmin=vmin,
|
|
549
|
+
vmax=vmax,
|
|
550
|
+
title=f"data: {os.path.basename(self.input_fits)}",
|
|
551
|
+
xlabel=xlabel,
|
|
552
|
+
ylabel=ylabel,
|
|
553
|
+
extent=extent,
|
|
554
|
+
aspect=self.image_aspect,
|
|
555
|
+
)
|
|
336
556
|
self.fig.tight_layout()
|
|
337
557
|
|
|
558
|
+
def set_cursor_onoff(self):
|
|
559
|
+
"""Toggle cursor selection mode on or off."""
|
|
560
|
+
if not self.use_cursor:
|
|
561
|
+
self.use_cursor = True
|
|
562
|
+
self.use_cursor_button.config(text="[c]ursor: ON ")
|
|
563
|
+
else:
|
|
564
|
+
self.use_cursor = False
|
|
565
|
+
self.use_cursor_button.config(text="[c]ursor: OFF")
|
|
566
|
+
|
|
567
|
+
def toggle_auxdata(self):
|
|
568
|
+
"""Toggle between main data and auxiliary data for display."""
|
|
569
|
+
if self.displaying_auxdata:
|
|
570
|
+
# Switch to main data
|
|
571
|
+
vmin = self.get_vmin()
|
|
572
|
+
vmax = self.get_vmax()
|
|
573
|
+
self.image.set_data(self.data)
|
|
574
|
+
self.image.set_clim(vmin=vmin, vmax=vmax)
|
|
575
|
+
self.displaying_auxdata = False
|
|
576
|
+
self.ax.set_title(f"data: {os.path.basename(self.input_fits)}")
|
|
577
|
+
else:
|
|
578
|
+
# Switch to auxiliary data
|
|
579
|
+
vmin = self.get_vmin()
|
|
580
|
+
vmax = self.get_vmax()
|
|
581
|
+
self.image.set_data(self.auxdata)
|
|
582
|
+
self.image.set_clim(vmin=vmin, vmax=vmax)
|
|
583
|
+
self.displaying_auxdata = True
|
|
584
|
+
self.ax.set_title(f"auxdata: {os.path.basename(self.auxfile)}")
|
|
585
|
+
self.canvas.draw_idle()
|
|
586
|
+
|
|
587
|
+
def toggle_aspect(self):
|
|
588
|
+
"""Toggle the aspect ratio of the image display."""
|
|
589
|
+
if self.image_aspect == "equal":
|
|
590
|
+
self.image_aspect = "auto"
|
|
591
|
+
else:
|
|
592
|
+
self.image_aspect = "equal"
|
|
593
|
+
print(f"Setting image aspect to: {self.image_aspect}")
|
|
594
|
+
self.toggle_aspect_button.config(text=f"[a]spect: {self.image_aspect}")
|
|
595
|
+
self.ax.set_aspect(self.image_aspect)
|
|
596
|
+
self.fig.tight_layout()
|
|
597
|
+
self.canvas.draw_idle()
|
|
598
|
+
|
|
338
599
|
def run_lacosmic(self):
|
|
339
600
|
"""Run L.A.Cosmic to detect cosmic rays."""
|
|
340
601
|
self.run_lacosmic_button.config(state=tk.DISABLED)
|
|
341
602
|
# Define parameters for L.A.Cosmic from default dictionary
|
|
342
603
|
editor_window = tk.Toplevel(self.root)
|
|
604
|
+
center_on_parent(child=editor_window, parent=self.root)
|
|
343
605
|
editor = ParameterEditor(
|
|
344
606
|
root=editor_window,
|
|
345
607
|
param_dict=self.lacosmic_params,
|
|
346
|
-
window_title=
|
|
608
|
+
window_title="Cosmic Ray Mask Generation Parameters",
|
|
347
609
|
xmin=self.last_xmin,
|
|
348
610
|
xmax=self.last_xmax,
|
|
349
611
|
ymin=self.last_ymin,
|
|
350
612
|
ymax=self.last_ymax,
|
|
351
|
-
imgshape=self.data.shape
|
|
613
|
+
imgshape=self.data.shape,
|
|
352
614
|
)
|
|
353
615
|
# Make it modal (blocks interaction with main window)
|
|
354
616
|
editor_window.transient(self.root)
|
|
@@ -359,46 +621,53 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
359
621
|
updated_params = editor.get_result()
|
|
360
622
|
if updated_params is not None:
|
|
361
623
|
# Update last used region values
|
|
362
|
-
self.last_xmin = updated_params[
|
|
363
|
-
self.last_xmax = updated_params[
|
|
364
|
-
self.last_ymin = updated_params[
|
|
365
|
-
self.last_ymax = updated_params[
|
|
366
|
-
usefulregion = SliceRegion2D(
|
|
367
|
-
|
|
624
|
+
self.last_xmin = updated_params["xmin"]["value"]
|
|
625
|
+
self.last_xmax = updated_params["xmax"]["value"]
|
|
626
|
+
self.last_ymin = updated_params["ymin"]["value"]
|
|
627
|
+
self.last_ymax = updated_params["ymax"]["value"]
|
|
628
|
+
usefulregion = SliceRegion2D(
|
|
629
|
+
f"[{self.last_xmin}:{self.last_xmax},{self.last_ymin}:{self.last_ymax}]", mode="fits"
|
|
630
|
+
).python
|
|
368
631
|
usefulmask = np.zeros_like(self.data)
|
|
369
632
|
usefulmask[usefulregion] = 1.0
|
|
370
633
|
# Update parameter dictionary with new values
|
|
371
634
|
self.lacosmic_params = updated_params
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
635
|
+
if self.verbose:
|
|
636
|
+
print("Parameters updated:")
|
|
637
|
+
for key, info in self.lacosmic_params.items():
|
|
638
|
+
print(f" {key}: {info['value']}")
|
|
639
|
+
if self.lacosmic_params["nruns"]["value"] not in [1, 2]:
|
|
376
640
|
raise ValueError("nruns must be 1 or 2")
|
|
377
641
|
# Execute L.A.Cosmic with updated parameters
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
642
|
+
print("[bold green]Executing L.A.Cosmic (run 1)...[/bold green]")
|
|
643
|
+
borderpadd = updated_params["borderpadd"]["value"]
|
|
644
|
+
cleandata_lacosmic, mask_crfound = lacosmicpad(
|
|
645
|
+
pad_width=borderpadd,
|
|
646
|
+
ccd=self.data,
|
|
647
|
+
gain=self.lacosmic_params["run1_gain"]["value"],
|
|
648
|
+
readnoise=self.lacosmic_params["run1_readnoise"]["value"],
|
|
649
|
+
sigclip=self.lacosmic_params["run1_sigclip"]["value"],
|
|
650
|
+
sigfrac=self.lacosmic_params["run1_sigfrac"]["value"],
|
|
651
|
+
objlim=self.lacosmic_params["run1_objlim"]["value"],
|
|
652
|
+
niter=self.lacosmic_params["run1_niter"]["value"],
|
|
653
|
+
verbose=self.lacosmic_params["run1_verbose"]["value"],
|
|
387
654
|
)
|
|
388
655
|
# Apply usefulmask to consider only selected region
|
|
389
656
|
cleandata_lacosmic *= usefulmask
|
|
390
657
|
mask_crfound = mask_crfound & (usefulmask.astype(bool))
|
|
391
658
|
# Second execution if nruns == 2
|
|
392
|
-
if self.lacosmic_params[
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
659
|
+
if self.lacosmic_params["nruns"]["value"] == 2:
|
|
660
|
+
print("[bold green]Executing L.A.Cosmic (run 2)...[/bold green]")
|
|
661
|
+
cleandata_lacosmic2, mask_crfound2 = lacosmicpad(
|
|
662
|
+
pad_width=borderpadd,
|
|
663
|
+
ccd=self.data,
|
|
664
|
+
gain=self.lacosmic_params["run2_gain"]["value"],
|
|
665
|
+
readnoise=self.lacosmic_params["run2_readnoise"]["value"],
|
|
666
|
+
sigclip=self.lacosmic_params["run2_sigclip"]["value"],
|
|
667
|
+
sigfrac=self.lacosmic_params["run2_sigfrac"]["value"],
|
|
668
|
+
objlim=self.lacosmic_params["run2_objlim"]["value"],
|
|
669
|
+
niter=self.lacosmic_params["run2_niter"]["value"],
|
|
670
|
+
verbose=self.lacosmic_params["run2_verbose"]["value"],
|
|
402
671
|
)
|
|
403
672
|
# Apply usefulmask to consider only selected region
|
|
404
673
|
cleandata_lacosmic2 *= usefulmask
|
|
@@ -407,20 +676,8 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
407
676
|
if np.any(mask_crfound):
|
|
408
677
|
print(f"Number of cosmic ray pixels (run1).......: {np.sum(mask_crfound)}")
|
|
409
678
|
print(f"Number of cosmic ray pixels (run2).......: {np.sum(mask_crfound2)}")
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
cr_labels2, num_features2 = ndimage.label(mask_crfound2, structure=structure)
|
|
413
|
-
# generate mask of ones at CR pixels found in first run
|
|
414
|
-
mask_peaks = np.zeros(mask_crfound.shape, dtype=float)
|
|
415
|
-
mask_peaks[mask_crfound] = 1.0
|
|
416
|
-
# preserve only those CR pixels in second run that are in the first run
|
|
417
|
-
cr_labels2_preserved = mask_peaks * cr_labels2
|
|
418
|
-
# generate new mask with preserved CR pixels from second run
|
|
419
|
-
mask_crfound = np.zeros_like(mask_crfound, dtype=bool)
|
|
420
|
-
for icr in np.unique(cr_labels2_preserved):
|
|
421
|
-
if icr > 0:
|
|
422
|
-
mask_crfound[cr_labels2 == icr] = True
|
|
423
|
-
print(f'Number of cosmic ray pixels (run1 & run2): {np.sum(mask_crfound)}')
|
|
679
|
+
mask_crfound = merge_peak_tail_masks(mask_crfound, mask_crfound2)
|
|
680
|
+
print(f"Number of cosmic ray pixels (run1 & run2): {np.sum(mask_crfound)}")
|
|
424
681
|
# Use the cleandata from the second run
|
|
425
682
|
cleandata_lacosmic = cleandata_lacosmic2
|
|
426
683
|
# Select the image region to process
|
|
@@ -429,40 +686,7 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
429
686
|
self.mask_crfound = np.zeros_like(self.data, dtype=bool)
|
|
430
687
|
self.mask_crfound[usefulregion] = mask_crfound[usefulregion]
|
|
431
688
|
# Process the mask: dilation and labeling
|
|
432
|
-
|
|
433
|
-
num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
|
|
434
|
-
dilation = self.lacosmic_params['dilation']['value']
|
|
435
|
-
if dilation > 0:
|
|
436
|
-
# Dilate the mask by the specified number of pixels
|
|
437
|
-
self.mask_crfound = dilatemask(
|
|
438
|
-
mask=self.mask_crfound,
|
|
439
|
-
iterations=self.lacosmic_params['dilation']['value'],
|
|
440
|
-
connectivity=1
|
|
441
|
-
)
|
|
442
|
-
num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
|
|
443
|
-
sdum = str(num_cr_pixels_after_dilation)
|
|
444
|
-
else:
|
|
445
|
-
sdum = str(num_cr_pixels_before_dilation)
|
|
446
|
-
print("Number of cosmic ray pixels detected by L.A.Cosmic: "
|
|
447
|
-
f"{num_cr_pixels_before_dilation:{len(sdum)}}")
|
|
448
|
-
if dilation > 0:
|
|
449
|
-
print(f"Number of cosmic ray pixels after dilation........: "
|
|
450
|
-
f"{num_cr_pixels_after_dilation:{len(sdum)}}")
|
|
451
|
-
# Label connected components in the mask; note that by default,
|
|
452
|
-
# structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
|
|
453
|
-
# diagonal connections too, so we define a 3x3 square.
|
|
454
|
-
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
455
|
-
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
456
|
-
print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features:>{len(sdum)}}")
|
|
457
|
-
self.apply_lacosmic_button.config(state=tk.NORMAL)
|
|
458
|
-
self.examine_detected_cr_button.config(state=tk.NORMAL)
|
|
459
|
-
self.update_cr_overlay()
|
|
460
|
-
else:
|
|
461
|
-
print("No cosmic ray pixels detected by L.A.Cosmic.")
|
|
462
|
-
self.cr_labels = None
|
|
463
|
-
self.num_features = 0
|
|
464
|
-
self.apply_lacosmic_button.config(state=tk.DISABLED)
|
|
465
|
-
self.examine_detected_cr_button.config(state=tk.DISABLED)
|
|
689
|
+
self.process_detected_cr(dilation=self.lacosmic_params["dilation"]["value"])
|
|
466
690
|
else:
|
|
467
691
|
print("Parameter editing cancelled. L.A.Cosmic detection skipped!")
|
|
468
692
|
self.run_lacosmic_button.config(state=tk.NORMAL)
|
|
@@ -471,25 +695,25 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
471
695
|
"""Toggle the overlay of cosmic ray pixels on the image."""
|
|
472
696
|
self.overplot_cr_pixels = not self.overplot_cr_pixels
|
|
473
697
|
if self.overplot_cr_pixels:
|
|
474
|
-
self.overplot_cr_button.config(text="CR overlay:
|
|
698
|
+
self.overplot_cr_button.config(text="CR overlay: ON ")
|
|
475
699
|
else:
|
|
476
|
-
self.overplot_cr_button.config(text="CR overlay:
|
|
700
|
+
self.overplot_cr_button.config(text="CR overlay: OFF")
|
|
477
701
|
self.update_cr_overlay()
|
|
478
702
|
|
|
479
703
|
def update_cr_overlay(self):
|
|
480
704
|
"""Update the overlay of cosmic ray pixels on the image."""
|
|
481
705
|
if self.overplot_cr_pixels:
|
|
482
706
|
# Remove previous CR pixel overlay (if any)
|
|
483
|
-
if hasattr(self,
|
|
707
|
+
if hasattr(self, "scatter_cr"):
|
|
484
708
|
self.scatter_cr.remove()
|
|
485
709
|
del self.scatter_cr
|
|
486
710
|
# Overlay CR pixels in red
|
|
487
711
|
if np.any(self.mask_crfound):
|
|
488
712
|
y_indices, x_indices = np.where(self.mask_crfound)
|
|
489
|
-
self.scatter_cr = self.ax.scatter(x_indices + 1, y_indices + 1, s=1, c=
|
|
713
|
+
self.scatter_cr = self.ax.scatter(x_indices + 1, y_indices + 1, s=1, c="red", marker="o")
|
|
490
714
|
else:
|
|
491
715
|
# Remove CR pixel overlay
|
|
492
|
-
if hasattr(self,
|
|
716
|
+
if hasattr(self, "scatter_cr"):
|
|
493
717
|
self.scatter_cr.remove()
|
|
494
718
|
del self.scatter_cr
|
|
495
719
|
self.canvas.draw_idle()
|
|
@@ -498,21 +722,25 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
498
722
|
"""Apply the selected cleaning method to the detected cosmic rays."""
|
|
499
723
|
if np.any(self.mask_crfound):
|
|
500
724
|
# recalculate labels and number of features
|
|
501
|
-
structure = [[1, 1, 1],
|
|
502
|
-
[1, 1, 1],
|
|
503
|
-
[1, 1, 1]]
|
|
725
|
+
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
504
726
|
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
505
727
|
sdum = str(np.sum(self.mask_crfound))
|
|
506
728
|
print(f"Number of cosmic ray pixels detected by L.A.Cosmic...........: {sdum}")
|
|
507
729
|
print(f"Number of cosmic rays (grouped pixels) detected by L.A.Cosmic: {self.num_features:>{len(sdum)}}")
|
|
508
730
|
# Define parameters for L.A.Cosmic from default dictionary
|
|
509
731
|
editor_window = tk.Toplevel(self.root)
|
|
732
|
+
center_on_parent(child=editor_window, parent=self.root)
|
|
510
733
|
editor = InterpolationEditor(
|
|
511
734
|
root=editor_window,
|
|
512
|
-
last_dilation=self.lacosmic_params[
|
|
735
|
+
last_dilation=self.lacosmic_params["dilation"]["value"],
|
|
513
736
|
last_npoints=self.last_npoints,
|
|
514
737
|
last_degree=self.last_degree,
|
|
515
738
|
auxdata=self.auxdata,
|
|
739
|
+
xmin=self.last_xmin,
|
|
740
|
+
xmax=self.last_xmax,
|
|
741
|
+
ymin=self.last_ymin,
|
|
742
|
+
ymax=self.last_ymax,
|
|
743
|
+
imgshape=self.data.shape,
|
|
516
744
|
)
|
|
517
745
|
# Make it modal (blocks interaction with main window)
|
|
518
746
|
editor_window.transient(self.root)
|
|
@@ -521,108 +749,134 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
521
749
|
self.root.wait_window(editor_window)
|
|
522
750
|
# Get the result after window closes
|
|
523
751
|
cleaning_method = editor.cleaning_method
|
|
524
|
-
num_cr_cleaned = 0
|
|
525
752
|
if cleaning_method is None:
|
|
526
753
|
print("Interpolation method selection cancelled. No cleaning applied!")
|
|
527
754
|
return
|
|
528
755
|
self.last_npoints = editor.npoints
|
|
529
756
|
self.last_degree = editor.degree
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
757
|
+
cleaning_region = SliceRegion2D(
|
|
758
|
+
f"[{editor.xmin}:{editor.xmax},{editor.ymin}:{editor.ymax}]", mode="fits"
|
|
759
|
+
).python
|
|
760
|
+
print(
|
|
761
|
+
"Applying cleaning method to region "
|
|
762
|
+
f"x=[{editor.xmin},{editor.xmax}], y=[{editor.ymin},{editor.ymax}]"
|
|
763
|
+
)
|
|
764
|
+
mask_crfound_region = np.zeros_like(self.mask_crfound, dtype=bool)
|
|
765
|
+
mask_crfound_region[cleaning_region] = self.mask_crfound[cleaning_region]
|
|
766
|
+
data_has_been_modified = False
|
|
767
|
+
if np.any(mask_crfound_region):
|
|
768
|
+
if cleaning_method == "lacosmic":
|
|
769
|
+
# Replace detected CR pixels with L.A.Cosmic values
|
|
770
|
+
self.data[mask_crfound_region] = self.cleandata_lacosmic[mask_crfound_region]
|
|
771
|
+
# update mask_fixed to include the newly fixed pixels
|
|
772
|
+
self.mask_fixed[mask_crfound_region] = True
|
|
773
|
+
# upate mask_crfound by eliminating the cleaned pixels
|
|
774
|
+
self.mask_crfound[mask_crfound_region] = False
|
|
775
|
+
data_has_been_modified = True
|
|
776
|
+
elif cleaning_method == "auxdata":
|
|
777
|
+
if self.auxdata is None:
|
|
778
|
+
print("No auxiliary data available. Cleaning skipped!")
|
|
779
|
+
return
|
|
780
|
+
# Replace detected CR pixels with auxiliary data values
|
|
781
|
+
self.data[mask_crfound_region] = self.auxdata[mask_crfound_region]
|
|
782
|
+
# update mask_fixed to include the newly fixed pixels
|
|
783
|
+
self.mask_fixed[mask_crfound_region] = True
|
|
784
|
+
# upate mask_crfound by eliminating the cleaned pixels
|
|
785
|
+
self.mask_crfound[mask_crfound_region] = False
|
|
786
|
+
data_has_been_modified = True
|
|
787
|
+
else:
|
|
788
|
+
# Determine features to process within the selected region
|
|
789
|
+
features_in_region = np.unique(self.cr_labels[mask_crfound_region])
|
|
790
|
+
with ModalProgressBar(
|
|
791
|
+
parent=self.root, iterable=range(1, self.num_features + 1), desc="Cleaning cosmic rays"
|
|
792
|
+
) as pbar:
|
|
793
|
+
for i in pbar:
|
|
794
|
+
if i in features_in_region:
|
|
795
|
+
tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
|
|
796
|
+
if cleaning_method == "x":
|
|
797
|
+
interpolation_performed, _, _ = interpolation_x(
|
|
798
|
+
data=self.data,
|
|
799
|
+
mask_fixed=tmp_mask_fixed,
|
|
800
|
+
cr_labels=self.cr_labels,
|
|
801
|
+
cr_index=i,
|
|
802
|
+
npoints=editor.npoints,
|
|
803
|
+
degree=editor.degree,
|
|
804
|
+
)
|
|
805
|
+
elif cleaning_method == "y":
|
|
806
|
+
interpolation_performed, _, _ = interpolation_y(
|
|
807
|
+
data=self.data,
|
|
808
|
+
mask_fixed=tmp_mask_fixed,
|
|
809
|
+
cr_labels=self.cr_labels,
|
|
810
|
+
cr_index=i,
|
|
811
|
+
npoints=editor.npoints,
|
|
812
|
+
degree=editor.degree,
|
|
813
|
+
)
|
|
814
|
+
elif cleaning_method == "a-plane":
|
|
815
|
+
interpolation_performed, _, _ = interpolation_a(
|
|
816
|
+
data=self.data,
|
|
817
|
+
mask_fixed=tmp_mask_fixed,
|
|
818
|
+
cr_labels=self.cr_labels,
|
|
819
|
+
cr_index=i,
|
|
820
|
+
npoints=editor.npoints,
|
|
821
|
+
method="surface",
|
|
822
|
+
)
|
|
823
|
+
elif cleaning_method == "a-median":
|
|
824
|
+
interpolation_performed, _, _ = interpolation_a(
|
|
825
|
+
data=self.data,
|
|
826
|
+
mask_fixed=tmp_mask_fixed,
|
|
827
|
+
cr_labels=self.cr_labels,
|
|
828
|
+
cr_index=i,
|
|
829
|
+
npoints=editor.npoints,
|
|
830
|
+
method="median",
|
|
831
|
+
)
|
|
832
|
+
elif cleaning_method == "a-mean":
|
|
833
|
+
interpolation_performed, _, _ = interpolation_a(
|
|
834
|
+
data=self.data,
|
|
835
|
+
mask_fixed=tmp_mask_fixed,
|
|
836
|
+
cr_labels=self.cr_labels,
|
|
837
|
+
cr_index=i,
|
|
838
|
+
npoints=editor.npoints,
|
|
839
|
+
method="mean",
|
|
840
|
+
)
|
|
841
|
+
else:
|
|
842
|
+
raise ValueError(f"Unknown cleaning method: {cleaning_method}")
|
|
843
|
+
if interpolation_performed:
|
|
844
|
+
# update mask_fixed to include the newly fixed pixels
|
|
845
|
+
self.mask_fixed[tmp_mask_fixed] = True
|
|
846
|
+
# upate mask_crfound by eliminating the cleaned pixels
|
|
847
|
+
self.mask_crfound[tmp_mask_fixed] = False
|
|
848
|
+
# mark that data has been modified
|
|
849
|
+
data_has_been_modified = True
|
|
850
|
+
# If any pixels were cleaned, print message
|
|
851
|
+
if data_has_been_modified:
|
|
852
|
+
print("Cosmic ray cleaning applied.")
|
|
549
853
|
else:
|
|
550
|
-
|
|
551
|
-
tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
|
|
552
|
-
if cleaning_method == 'x':
|
|
553
|
-
interpolation_performed, _, _ = interpolation_x(
|
|
554
|
-
data=self.data,
|
|
555
|
-
mask_fixed=tmp_mask_fixed,
|
|
556
|
-
cr_labels=self.cr_labels,
|
|
557
|
-
cr_index=i,
|
|
558
|
-
npoints=editor.npoints,
|
|
559
|
-
degree=editor.degree
|
|
560
|
-
)
|
|
561
|
-
elif cleaning_method == 'y':
|
|
562
|
-
interpolation_performed, _, _ = interpolation_y(
|
|
563
|
-
data=self.data,
|
|
564
|
-
mask_fixed=tmp_mask_fixed,
|
|
565
|
-
cr_labels=self.cr_labels,
|
|
566
|
-
cr_index=i,
|
|
567
|
-
npoints=editor.npoints,
|
|
568
|
-
degree=editor.degree
|
|
569
|
-
)
|
|
570
|
-
elif cleaning_method == 'a-plane':
|
|
571
|
-
interpolation_performed, _, _ = interpolation_a(
|
|
572
|
-
data=self.data,
|
|
573
|
-
mask_fixed=tmp_mask_fixed,
|
|
574
|
-
cr_labels=self.cr_labels,
|
|
575
|
-
cr_index=i,
|
|
576
|
-
npoints=editor.npoints,
|
|
577
|
-
method='surface'
|
|
578
|
-
)
|
|
579
|
-
elif cleaning_method == 'a-median':
|
|
580
|
-
interpolation_performed, _, _ = interpolation_a(
|
|
581
|
-
data=self.data,
|
|
582
|
-
mask_fixed=tmp_mask_fixed,
|
|
583
|
-
cr_labels=self.cr_labels,
|
|
584
|
-
cr_index=i,
|
|
585
|
-
npoints=editor.npoints,
|
|
586
|
-
method='median'
|
|
587
|
-
)
|
|
588
|
-
elif cleaning_method == 'a-mean':
|
|
589
|
-
interpolation_performed, _, _ = interpolation_a(
|
|
590
|
-
data=self.data,
|
|
591
|
-
mask_fixed=tmp_mask_fixed,
|
|
592
|
-
cr_labels=self.cr_labels,
|
|
593
|
-
cr_index=i,
|
|
594
|
-
npoints=editor.npoints,
|
|
595
|
-
method='mean'
|
|
596
|
-
)
|
|
597
|
-
else:
|
|
598
|
-
raise ValueError(f"Unknown cleaning method: {cleaning_method}")
|
|
599
|
-
if interpolation_performed:
|
|
600
|
-
num_cr_cleaned += 1
|
|
601
|
-
# update mask_fixed to include the newly fixed pixels
|
|
602
|
-
self.mask_fixed[tmp_mask_fixed] = True
|
|
603
|
-
# upate mask_crfound by eliminating the cleaned pixels
|
|
604
|
-
self.mask_crfound[tmp_mask_fixed] = False
|
|
605
|
-
print(f"Number of cosmic rays identified and cleaned: {num_cr_cleaned}")
|
|
854
|
+
print("No cosmic ray pixels cleaned.")
|
|
606
855
|
# recalculate labels and number of features
|
|
607
856
|
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
608
857
|
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
print(f"Remaining number of cosmic
|
|
858
|
+
num_cr_remaining = np.sum(self.mask_crfound)
|
|
859
|
+
sdum = str(num_cr_remaining)
|
|
860
|
+
print(f"Remaining number of cosmic ray pixels...................: {sdum}")
|
|
861
|
+
print(f"Remaining number of cosmic ray features (grouped pixels): {self.num_features:>{len(sdum)}}")
|
|
862
|
+
if num_cr_remaining == 0:
|
|
863
|
+
self.use_cursor = False
|
|
864
|
+
self.use_cursor_button.config(text="[c]ursor: OFF")
|
|
612
865
|
# redraw image to show the changes
|
|
613
866
|
self.image.set_data(self.data)
|
|
614
867
|
self.canvas.draw_idle()
|
|
615
|
-
if
|
|
868
|
+
if data_has_been_modified:
|
|
616
869
|
self.save_button.config(state=tk.NORMAL)
|
|
617
870
|
if self.num_features == 0:
|
|
618
871
|
self.examine_detected_cr_button.config(state=tk.DISABLED)
|
|
619
|
-
self.
|
|
872
|
+
self.replace_detected_cr_button.config(state=tk.DISABLED)
|
|
620
873
|
self.update_cr_overlay()
|
|
621
874
|
|
|
622
875
|
def examine_detected_cr(self, first_cr_index=1, single_cr=False, ixpix=None, iypix=None):
|
|
623
876
|
"""Open a window to examine and possibly clean detected cosmic rays."""
|
|
624
877
|
self.working_in_review_window = True
|
|
625
878
|
review_window = tk.Toplevel(self.root)
|
|
879
|
+
center_on_parent(child=review_window, parent=self.root)
|
|
626
880
|
if ixpix is not None and iypix is not None:
|
|
627
881
|
# select single pixel based on provided coordinates
|
|
628
882
|
tmp_cr_labels = np.zeros_like(self.data, dtype=int)
|
|
@@ -636,9 +890,9 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
636
890
|
num_features=1,
|
|
637
891
|
first_cr_index=1,
|
|
638
892
|
single_cr=True,
|
|
639
|
-
last_dilation=self.lacosmic_params[
|
|
893
|
+
last_dilation=self.lacosmic_params["dilation"]["value"],
|
|
640
894
|
last_npoints=self.last_npoints,
|
|
641
|
-
last_degree=self.last_degree
|
|
895
|
+
last_degree=self.last_degree,
|
|
642
896
|
)
|
|
643
897
|
else:
|
|
644
898
|
review = ReviewCosmicRay(
|
|
@@ -650,9 +904,9 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
650
904
|
num_features=self.num_features,
|
|
651
905
|
first_cr_index=first_cr_index,
|
|
652
906
|
single_cr=single_cr,
|
|
653
|
-
last_dilation=self.lacosmic_params[
|
|
907
|
+
last_dilation=self.lacosmic_params["dilation"]["value"],
|
|
654
908
|
last_npoints=self.last_npoints,
|
|
655
|
-
last_degree=self.last_degree
|
|
909
|
+
last_degree=self.last_degree,
|
|
656
910
|
)
|
|
657
911
|
# Make it modal (blocks interaction with main window)
|
|
658
912
|
review_window.transient(self.root)
|
|
@@ -671,9 +925,13 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
671
925
|
# recalculate labels and number of features
|
|
672
926
|
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
673
927
|
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
print(f"Remaining number of cosmic
|
|
928
|
+
num_remaining = np.sum(self.mask_crfound)
|
|
929
|
+
sdum = str(num_remaining)
|
|
930
|
+
print(f"Remaining number of cosmic ray pixels...................: {sdum}")
|
|
931
|
+
print(f"Remaining number of cosmic ray features (grouped pixels): {self.num_features:>{len(sdum)}}")
|
|
932
|
+
if num_remaining == 0:
|
|
933
|
+
self.use_cursor = False
|
|
934
|
+
self.use_cursor_button.config(text="[c]ursor: OFF")
|
|
677
935
|
# redraw image to show the changes
|
|
678
936
|
self.image.set_data(self.data)
|
|
679
937
|
self.canvas.draw_idle()
|
|
@@ -681,18 +939,16 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
681
939
|
self.save_button.config(state=tk.NORMAL)
|
|
682
940
|
if self.num_features == 0:
|
|
683
941
|
self.examine_detected_cr_button.config(state=tk.DISABLED)
|
|
684
|
-
self.
|
|
942
|
+
self.replace_detected_cr_button.config(state=tk.DISABLED)
|
|
685
943
|
self.update_cr_overlay()
|
|
686
944
|
|
|
687
945
|
def stop_app(self):
|
|
688
946
|
"""Stop the application, prompting to save if there are unsaved changes."""
|
|
689
947
|
proceed_with_stop = True
|
|
690
|
-
if self.save_button[
|
|
948
|
+
if self.save_button["state"] == tk.NORMAL:
|
|
691
949
|
print("Warning: There are unsaved changes!")
|
|
692
950
|
proceed_with_stop = messagebox.askyesno(
|
|
693
|
-
"Unsaved Changes",
|
|
694
|
-
"You have unsaved changes.\nDo you really want to quit?",
|
|
695
|
-
default=messagebox.NO
|
|
951
|
+
"Unsaved Changes", "You have unsaved changes.\nDo you really want to quit?", default=messagebox.NO
|
|
696
952
|
)
|
|
697
953
|
if proceed_with_stop:
|
|
698
954
|
self.root.quit()
|
|
@@ -700,14 +956,39 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
700
956
|
|
|
701
957
|
def on_key(self, event):
|
|
702
958
|
"""Handle key press events."""
|
|
703
|
-
if event.key ==
|
|
704
|
-
|
|
705
|
-
elif event.key ==
|
|
959
|
+
if event.key == "c":
|
|
960
|
+
self.set_cursor_onoff()
|
|
961
|
+
elif event.key == "a":
|
|
962
|
+
self.toggle_aspect()
|
|
963
|
+
elif event.key == "t" and self.auxdata is not None:
|
|
964
|
+
self.toggle_auxdata()
|
|
965
|
+
elif event.key == ",":
|
|
706
966
|
self.set_minmax()
|
|
707
|
-
elif event.key ==
|
|
967
|
+
elif event.key == "/":
|
|
708
968
|
self.set_zscale()
|
|
709
|
-
|
|
710
|
-
|
|
969
|
+
elif event.key == "o":
|
|
970
|
+
self.toolbar.zoom()
|
|
971
|
+
elif event.key == "h":
|
|
972
|
+
self.toolbar.home()
|
|
973
|
+
elif event.key == "p":
|
|
974
|
+
self.toolbar.pan()
|
|
975
|
+
elif event.key == "s":
|
|
976
|
+
self.toolbar.save_figure()
|
|
977
|
+
elif event.key == "?":
|
|
978
|
+
# Display list of keyboard shortcuts
|
|
979
|
+
print("[bold blue]Keyboard Shortcuts:[/bold blue]")
|
|
980
|
+
print("[red] c [/red]: Toggle cursor selection mode on/off")
|
|
981
|
+
print("[red] t [/red]: Toggle between main data and auxiliary data")
|
|
982
|
+
print("[red] a [/red]: Toggle image aspect ratio equal/auto")
|
|
983
|
+
print("[red] , [/red]: Set vmin and vmax to minmax")
|
|
984
|
+
print("[red] / [/red]: Set vmin and vmax using zscale")
|
|
985
|
+
print("[red] h [/red]: Go to home view \\[toolbar]")
|
|
986
|
+
print("[red] o [/red]: Activate zoom mode \\[toolbar]")
|
|
987
|
+
print("[red] p [/red]: Activate pan mode \\[toolbar]")
|
|
988
|
+
print("[red] s [/red]: Save the current figure \\[toolbar]")
|
|
989
|
+
print("[red] q [/red]: (ignored) prevent closing the window")
|
|
990
|
+
elif event.key == "q":
|
|
991
|
+
pass # Ignore the "q" key to prevent closing the window
|
|
711
992
|
|
|
712
993
|
def on_click(self, event):
|
|
713
994
|
"""Handle mouse click events on the image."""
|
|
@@ -722,6 +1003,10 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
722
1003
|
print(f"Toolbar mode '{toolbar.mode}' active; click ignored.")
|
|
723
1004
|
return
|
|
724
1005
|
|
|
1006
|
+
# proceed only if cursor selection mode is on
|
|
1007
|
+
if not self.use_cursor:
|
|
1008
|
+
return
|
|
1009
|
+
|
|
725
1010
|
# ignore clicks outside the expected axes
|
|
726
1011
|
# (note that the color bar is a different axes)
|
|
727
1012
|
if event.inaxes == self.ax:
|
|
@@ -753,8 +1038,8 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
753
1038
|
imin = (iy - 1) - semiwidth if (iy - 1) - semiwidth >= 0 else 0
|
|
754
1039
|
imax = (iy - 1) + semiwidth if (iy - 1) + semiwidth < self.data.shape[0] else self.data.shape[0] - 1
|
|
755
1040
|
ijmax = np.unravel_index(
|
|
756
|
-
np.argmax(self.data[imin:imax+1, jmin:jmax+1]),
|
|
757
|
-
self.data[imin:imax+1, jmin:jmax+1].shape
|
|
1041
|
+
np.argmax(self.data[imin : imax + 1, jmin : jmax + 1]),
|
|
1042
|
+
self.data[imin : imax + 1, jmin : jmax + 1].shape,
|
|
758
1043
|
)
|
|
759
1044
|
ixpix = ijmax[1] + jmin + 1
|
|
760
1045
|
iypix = ijmax[0] + imin + 1
|