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.
@@ -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__(self, root, input_fits, extension=0, auxfile=None, extension_auxfile=0):
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 : int, optional
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 : int, optional
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.root.minsize(800, 800)
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
- with fits.open(self.input_fits, mode='readonly') as hdul:
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 'CRMASK' in hdul:
184
- self.mask_fixed = hdul['CRMASK'].data.astype(bool)
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'[1:{naxis1}, 1:{naxis2}]', mode='fits').python
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
- with fits.open(self.auxfile, mode='readonly') as hdul_aux:
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='readonly') as hdul:
413
+ with fits.open(self.input_fits, mode="readonly") as hdul:
236
414
  hdul[self.extension].data = self.data
237
- if 'CRMASK' in hdul:
238
- hdul['CRMASK'].data = self.mask_fixed.astype(np.uint8)
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='CRMASK')
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
- if self.overplot_cr_pixels:
273
- self.overplot_cr_button = tk.Button(self.button_frame1, text="CR overlay: On",
274
- command=self.toggle_cr_overlay)
275
- else:
276
- self.overplot_cr_button = tk.Button(self.button_frame1, text="CR overlay: Off",
277
- command=self.toggle_cr_overlay)
278
- self.overplot_cr_button.pack(side=tk.LEFT, padx=5)
279
- self.apply_lacosmic_button = tk.Button(self.button_frame1, text="Replace all detected CRs",
280
- command=self.apply_lacosmic)
281
- self.apply_lacosmic_button.pack(side=tk.LEFT, padx=5)
282
- self.apply_lacosmic_button.config(state=tk.DISABLED) # Initially disabled
283
- self.examine_detected_cr_button = tk.Button(self.button_frame1, text="Examine detected CRs",
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.fig, self.ax = plt.subplots(figsize=(7, 5.5))
312
- self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
313
- self.canvas.get_tk_widget().pack(padx=5, pady=5)
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 = 'X pixel (from 1 to NAXIS1)'
330
- ylabel = 'Y pixel (from 1 to NAXIS2)'
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.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
333
- title=os.path.basename(self.input_fits),
334
- xlabel=xlabel, ylabel=ylabel,
335
- extent=extent)
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='Cosmic Ray Mask Generation Parameters',
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['xmin']['value']
363
- self.last_xmax = updated_params['xmax']['value']
364
- self.last_ymin = updated_params['ymin']['value']
365
- self.last_ymax = updated_params['ymax']['value']
366
- usefulregion = SliceRegion2D(f"[{self.last_xmin}:{self.last_xmax},{self.last_ymin}:{self.last_ymax}]",
367
- mode="fits").python
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
- print("Parameters updated:")
373
- for key, info in self.lacosmic_params.items():
374
- print(f" {key}: {info['value']}")
375
- if self.lacosmic_params['nruns']['value'] not in [1, 2]:
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
- cleandata_lacosmic, mask_crfound = cosmicray_lacosmic(
379
- self.data,
380
- gain=self.lacosmic_params['run1_gain']['value'],
381
- readnoise=self.lacosmic_params['run1_readnoise']['value'],
382
- sigclip=self.lacosmic_params['run1_sigclip']['value'],
383
- sigfrac=self.lacosmic_params['run1_sigfrac']['value'],
384
- objlim=self.lacosmic_params['run1_objlim']['value'],
385
- niter=self.lacosmic_params['run1_niter']['value'],
386
- verbose=self.lacosmic_params['run1_verbose']['value']
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['nruns']['value'] == 2:
393
- cleandata_lacosmic2, mask_crfound2 = cosmicray_lacosmic(
394
- self.data,
395
- gain=self.lacosmic_params['run2_gain']['value'],
396
- readnoise=self.lacosmic_params['run2_readnoise']['value'],
397
- sigclip=self.lacosmic_params['run2_sigclip']['value'],
398
- sigfrac=self.lacosmic_params['run2_sigfrac']['value'],
399
- objlim=self.lacosmic_params['run2_objlim']['value'],
400
- niter=self.lacosmic_params['run2_niter']['value'],
401
- verbose=self.lacosmic_params['run2_verbose']['value']
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
- # find features in second run
411
- structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
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
- if np.any(self.mask_crfound):
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: On")
698
+ self.overplot_cr_button.config(text="CR overlay: ON ")
475
699
  else:
476
- self.overplot_cr_button.config(text="CR overlay: Off")
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, 'scatter_cr'):
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='red', marker='o')
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, 'scatter_cr'):
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['dilation']['value'],
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
- if cleaning_method == 'lacosmic':
531
- # Replace all detected CR pixels with L.A.Cosmic values
532
- self.data[self.mask_crfound] = self.cleandata_lacosmic[self.mask_crfound]
533
- # update mask_fixed to include the newly fixed pixels
534
- self.mask_fixed[self.mask_crfound] = True
535
- # upate mask_crfound by eliminating the cleaned pixels
536
- self.mask_crfound[self.mask_crfound] = False
537
- num_cr_cleaned = self.num_features
538
- elif cleaning_method == 'auxdata':
539
- if self.auxdata is None:
540
- print("No auxiliary data available. Cleaning skipped!")
541
- return
542
- # Replace all detected CR pixels with auxiliary data values
543
- self.data[self.mask_crfound] = self.auxdata[self.mask_crfound]
544
- # update mask_fixed to include the newly fixed pixels
545
- self.mask_fixed[self.mask_crfound] = True
546
- # upate mask_crfound by eliminating the cleaned pixels
547
- self.mask_crfound[self.mask_crfound] = False
548
- num_cr_cleaned = self.num_features
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
- for i in tqdm(range(1, self.num_features + 1)):
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
- sdum = str(np.sum(self.mask_crfound))
610
- print(f"Remaining number of cosmic ray pixels...........: {sdum}")
611
- print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
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 num_cr_cleaned > 0:
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.apply_lacosmic_button.config(state=tk.DISABLED)
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['dilation']['value'],
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['dilation']['value'],
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
- sdum = str(np.sum(self.mask_crfound))
675
- print(f"Remaining number of cosmic ray pixels...........: {sdum}")
676
- print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
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.apply_lacosmic_button.config(state=tk.DISABLED)
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['state'] == tk.NORMAL:
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 == 'q':
704
- pass # Ignore the "q" key to prevent closing the window
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
- else:
710
- print(f"Key pressed: {event.key}")
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