teareduce 0.4.8__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.
@@ -11,7 +11,8 @@
11
11
 
12
12
  import tkinter as tk
13
13
  from tkinter import filedialog
14
- from tkinter import simpledialog
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, output_fits=None):
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
- output_fits : str, optional
49
- Path to save the cleaned FITS file (default is None, which prompts
50
- for a save location).
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.output_fits = output_fits
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
- if self.output_fits is None:
74
- base, ext = os.path.splitext(self.input_fits)
75
- suggested_name = f"{base}_cleaned"
76
- else:
77
- suggested_name, _ = os.path.splitext(self.output_fits)
78
- self.output_fits = filedialog.asksaveasfilename(
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(self.output_fits, overwrite=True)
94
- print(f"Cleaned data saved to {self.output_fits}")
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
- # Row 1
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.grid(row=0, column=0, pady=5)
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
- self.save_button = tk.Button(self.button_frame1, text="Save cleaned FITS", command=self.save_fits_file)
105
- self.save_button.pack(side=tk.LEFT, padx=5)
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.grid(row=1, column=0, pady=5)
110
- vmin, vmax = zscale(self.data)
111
- self.vmin_button = tk.Button(self.button_frame2, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
112
- self.vmin_button.pack(side=tk.LEFT, padx=5)
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
- # Main frame for figure and toolbar
119
- self.main_frame = tk.Frame(self.root)
120
- self.main_frame.grid(row=2, column=0, sticky="nsew")
121
- self.root.grid_rowconfigure(2, weight=1)
122
- self.root.grid_columnconfigure(0, weight=1)
123
- self.main_frame.grid_rowconfigure(0, weight=1)
124
- self.main_frame.grid_columnconfigure(0, weight=1)
125
-
126
- # Create figure and axis
127
- self.fig, self.ax = plt.subplots(figsize=(8, 6))
128
- xlabel = 'X pixel (from 1 to NAXIS1)'
129
- ylabel = 'Y pixel (from 1 to NAXIS2)'
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
- # Create canvas and toolbar
137
- self.canvas = FigureCanvasTkAgg(self.fig, master=self.main_frame)
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.grid(row=0, column=0, sticky="nsew")
314
+ canvas_widget.pack(fill=tk.BOTH, expand=True)
144
315
 
145
316
  # Matplotlib toolbar
146
- self.toolbar_frame = tk.Frame(self.main_frame)
147
- self.toolbar_frame.grid(row=1, column=0, sticky="ew")
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
- def set_vmin(self):
152
- old_vmin = self.get_vmin()
153
- new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
154
- if new_vmin is None:
155
- return
156
- self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
157
- self.image.set_clim(vmin=new_vmin)
158
- self.canvas.draw()
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
- self.stop_button.config(state=tk.DISABLED)
178
- # Parameters for L.A.Cosmic can be adjusted as needed
179
- _, mask_crfound = cosmicray_lacosmic(self.data, sigclip=4.5, sigfrac=0.3, objlim=5.0, verbose=True)
180
- ReviewCosmicRay(
181
- root=self.root,
182
- data=self.data,
183
- mask_fixed=self.mask_fixed,
184
- mask_crfound=mask_crfound
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
- print("L.A.Cosmic cleaning applied.")
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
- self.stop_button.config(state=tk.NORMAL)
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
- self.root.quit()
192
- self.root.destroy()
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
- if event.inaxes:
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
- print(f"Clicked at image coordinates: ({x:.2f}, {y:.2f})")
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)