teareduce 0.5.5__py3-none-any.whl → 0.5.8__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.
@@ -7,4 +7,6 @@
7
7
  # License-Filename: LICENSE.txt
8
8
  #
9
9
 
10
- from .cleanest import cleanest
10
+ from .interpolate import interpolate
11
+ from .lacosmicpad import lacosmicpad
12
+ from .mergemasks import merge_peak_tail_masks
@@ -10,8 +10,12 @@
10
10
  """Interactive Cosmic Ray cleaning tool."""
11
11
 
12
12
  import argparse
13
+ from ast import arg
13
14
  import tkinter as tk
15
+ from tkinter import filedialog
16
+ from tkinter import simpledialog
14
17
  import os
18
+ from pathlib import Path
15
19
  import platform
16
20
  from rich import print
17
21
  from rich_argparse import RichHelpFormatter
@@ -21,6 +25,7 @@ from .definitions import DEFAULT_FONT_SIZE
21
25
  from .definitions import DEFAULT_TK_WINDOW_SIZE_X
22
26
  from .definitions import DEFAULT_TK_WINDOW_SIZE_Y
23
27
  from .cosmicraycleanerapp import CosmicRayCleanerApp
28
+ from ..version import VERSION
24
29
 
25
30
  import matplotlib
26
31
 
@@ -32,13 +37,13 @@ def main():
32
37
  description="Interactive cosmic ray cleaner for FITS images.",
33
38
  formatter_class=RichHelpFormatter,
34
39
  )
35
- parser.add_argument("input_fits", help="Path to the FITS file to be cleaned.")
36
- parser.add_argument("--extension", type=int, default=0, help="FITS extension to use (default: 0).")
40
+ parser.add_argument("input_fits", nargs="?", default=None, help="Path to the FITS file to be cleaned.")
41
+ parser.add_argument("--extension", type=str, default="0", help="FITS extension to use (default: 0).")
37
42
  parser.add_argument("--auxfile", type=str, default=None, help="Auxiliary FITS file")
38
43
  parser.add_argument(
39
44
  "--extension_auxfile",
40
- type=int,
41
- default=0,
45
+ type=str,
46
+ default="0",
42
47
  help="FITS extension for auxiliary file (default: 0).",
43
48
  )
44
49
  parser.add_argument(
@@ -65,14 +70,67 @@ def main():
65
70
  default=DEFAULT_TK_WINDOW_SIZE_Y,
66
71
  help=f"Height of the GUI window in pixels (default: {DEFAULT_TK_WINDOW_SIZE_Y}).",
67
72
  )
73
+ parser.add_argument("--version", action="version", version=f"%(prog)s {VERSION}")
74
+ parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output.")
68
75
  args = parser.parse_args()
69
76
 
77
+ # Welcome message
78
+ print("[bold green]Cosmic Ray Cleaner[/bold green]")
79
+ print("Interactive tool to clean cosmic rays from FITS images.")
80
+ print("teareduce version: " + VERSION)
81
+ print(f"https://nicocardiel.github.io/teareduce-cookbook/docs/cleanest/cleanest.html\n")
82
+
83
+ # If input_file is not provided, ask for it using a file dialog
84
+ if args.input_fits is None:
85
+ root = tk.Tk()
86
+ root.withdraw() # Hide the root window
87
+ args.input_fits = filedialog.askopenfilename(
88
+ title="Select FITS file to be cleaned",
89
+ filetypes=[("FITS files", "*.fits *.fit *.fts"), ("All files", "*.*")],
90
+ )
91
+ if not args.input_fits:
92
+ print("No input FITS file selected. Exiting.")
93
+ exit(1)
94
+ print(f"Selected input FITS file: {args.input_fits}")
95
+ args.extension = simpledialog.askstring(
96
+ "Select Extension",
97
+ f"\nEnter extension number or name for file:\n{Path(args.input_fits).name}",
98
+ initialvalue=args.extension,
99
+ )
100
+ # Ask for auxiliary file if not provided
101
+ if args.auxfile is None:
102
+ use_auxfile = tk.messagebox.askyesno(
103
+ "Auxiliary File",
104
+ "Do you want to use an auxiliary FITS file?",
105
+ default=tk.messagebox.NO,
106
+ )
107
+ if use_auxfile:
108
+ args.auxfile = filedialog.askopenfilename(
109
+ title="Select Auxiliary FITS file",
110
+ filetypes=[("FITS files", "*.fits *.fit *.fts"), ("All files", "*.*")],
111
+ initialfile=args.auxfile,
112
+ )
113
+ if not args.auxfile:
114
+ print("No auxiliary FITS file selected. Exiting.")
115
+ exit(1)
116
+ else:
117
+ use_auxfile = True
118
+ if use_auxfile:
119
+ print(f"Selected auxiliary FITS file: {args.auxfile}")
120
+ args.extension_auxfile = simpledialog.askstring(
121
+ "Select Extension for Auxiliary File",
122
+ f"\nEnter extension number or name for auxiliary file:\n{Path(args.auxfile).name}",
123
+ initialvalue=args.extension_auxfile,
124
+ )
125
+ root.destroy()
126
+
127
+ # Check that input files, and the corresponding extensions, exist
70
128
  if not os.path.isfile(args.input_fits):
71
129
  print(f"Error: File '{args.input_fits}' does not exist.")
72
- return
130
+ exit(1)
73
131
  if args.auxfile is not None and not os.path.isfile(args.auxfile):
74
132
  print(f"Error: Auxiliary file '{args.auxfile}' does not exist.")
75
- return
133
+ exit(1)
76
134
 
77
135
  # Initialize Tkinter root
78
136
  root = tk.Tk()
@@ -100,6 +158,7 @@ def main():
100
158
  fontsize=args.fontsize,
101
159
  width=args.width,
102
160
  height=args.height,
161
+ verbose=args.verbose,
103
162
  )
104
163
 
105
164
  # Execute
@@ -13,16 +13,18 @@ import tkinter as tk
13
13
  from tkinter import filedialog
14
14
  from tkinter import font as tkfont
15
15
  from tkinter import messagebox
16
+ from tkinter import simpledialog
16
17
  import sys
17
18
 
18
19
  from astropy.io import fits
19
- from ccdproc import cosmicray_lacosmic
20
+ from maskfill import maskfill
20
21
  import matplotlib.pyplot as plt
21
22
  from matplotlib.backend_bases import key_press_handler
22
23
  from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
23
24
  from scipy import ndimage
24
25
  import numpy as np
25
26
  import os
27
+ from pathlib import Path
26
28
  from rich import print
27
29
 
28
30
  from .centerchildparent import center_on_parent
@@ -41,6 +43,8 @@ from .interpolation_x import interpolation_x
41
43
  from .interpolation_y import interpolation_y
42
44
  from .interpolationeditor import InterpolationEditor
43
45
  from .imagedisplay import ImageDisplay
46
+ from .lacosmicpad import lacosmicpad
47
+ from .mergemasks import merge_peak_tail_masks
44
48
  from .parametereditor import ParameterEditor
45
49
  from .reviewcosmicray import ReviewCosmicRay
46
50
  from .modalprogressbar import ModalProgressBar
@@ -61,13 +65,14 @@ class CosmicRayCleanerApp(ImageDisplay):
61
65
  self,
62
66
  root,
63
67
  input_fits,
64
- extension=0,
68
+ extension="0",
65
69
  auxfile=None,
66
- extension_auxfile=0,
70
+ extension_auxfile="0",
67
71
  fontfamily=DEFAULT_FONT_FAMILY,
68
72
  fontsize=DEFAULT_FONT_SIZE,
69
73
  width=DEFAULT_TK_WINDOW_SIZE_X,
70
74
  height=DEFAULT_TK_WINDOW_SIZE_Y,
75
+ verbose=False,
71
76
  ):
72
77
  """
73
78
  Initialize the application.
@@ -78,19 +83,29 @@ class CosmicRayCleanerApp(ImageDisplay):
78
83
  The main Tkinter window.
79
84
  input_fits : str
80
85
  Path to the FITS file to be cleaned.
81
- extension : int, optional
82
- FITS extension to use (default is 0).
86
+ extension : str, optional
87
+ FITS extension to use (default is "0").
83
88
  auxfile : str, optional
84
89
  Path to an auxiliary FITS file (default is None).
85
- extension_auxfile : int, optional
86
- FITS extension for auxiliary file (default is 0).
90
+ extension_auxfile : str, optional
91
+ FITS extension for auxiliary file (default is "0").
87
92
  fontfamily : str, optional
88
93
  Font family for the GUI (default is "Helvetica").
89
94
  fontsize : int, optional
90
95
  Font size for the GUI (default is 14).
96
+ width : int, optional
97
+ Width of the GUI window in pixels (default is 800).
98
+ height : int, optional
99
+ Height of the GUI window in pixels (default is 600).
100
+ verbose : bool, optional
101
+ Enable verbose output (default is False).
91
102
 
92
103
  Methods
93
104
  -------
105
+ process_detected_cr(dilation)
106
+ Process the detected cosmic ray mask.
107
+ load_detected_cr_from_file()
108
+ Load detected cosmic ray mask from a FITS file.
94
109
  load_fits_file()
95
110
  Load the FITS file and auxiliary file (if provided).
96
111
  save_fits_file()
@@ -124,6 +139,12 @@ class CosmicRayCleanerApp(ImageDisplay):
124
139
  Font size for the GUI.
125
140
  default_font : tkfont.Font
126
141
  The default font used in the GUI.
142
+ width : int
143
+ Width of the GUI window in pixels.
144
+ height : int
145
+ Height of the GUI window in pixels.
146
+ verbose : bool
147
+ Enable verbose output.
127
148
  lacosmic_params : dict
128
149
  Dictionary of L.A.Cosmic parameters.
129
150
  input_fits : str
@@ -171,6 +192,7 @@ class CosmicRayCleanerApp(ImageDisplay):
171
192
  # self.root.geometry("800x800+50+0") # This does not work in Fedora
172
193
  self.width = width
173
194
  self.height = height
195
+ self.verbose = verbose
174
196
  self.root.minsize(self.width, self.height)
175
197
  self.root.update_idletasks()
176
198
  self.root.title("Cosmic Ray Cleaner")
@@ -181,6 +203,8 @@ class CosmicRayCleanerApp(ImageDisplay):
181
203
  family=fontfamily, size=fontsize, weight="normal", slant="roman", underline=0, overstrike=0
182
204
  )
183
205
  self.lacosmic_params = lacosmic_default_dict.copy()
206
+ self.lacosmic_params["run1_verbose"]["value"] = self.verbose
207
+ self.lacosmic_params["run2_verbose"]["value"] = self.verbose
184
208
  self.input_fits = input_fits
185
209
  self.extension = extension
186
210
  self.data = None
@@ -202,6 +226,93 @@ class CosmicRayCleanerApp(ImageDisplay):
202
226
  self.num_features = 0
203
227
  self.working_in_review_window = False
204
228
 
229
+ def process_detected_cr(self, dilation):
230
+ """Process the detected cosmic ray mask.
231
+
232
+ Parameters
233
+ ----------
234
+ dilation : int
235
+ Number of pixels to dilate the cosmic ray mask.
236
+ """
237
+ # Process the mask: dilation and labeling
238
+ if np.any(self.mask_crfound):
239
+ num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
240
+ if dilation > 0:
241
+ # Dilate the mask by the specified number of pixels
242
+ self.mask_crfound = dilatemask(
243
+ mask=self.mask_crfound, iterations=dilation, connectivity=1
244
+ )
245
+ num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
246
+ sdum = str(num_cr_pixels_after_dilation)
247
+ else:
248
+ sdum = str(num_cr_pixels_before_dilation)
249
+ print(
250
+ "Number of cosmic ray pixels detected..........: "
251
+ f"{num_cr_pixels_before_dilation:>{len(sdum)}}"
252
+ )
253
+ if dilation > 0:
254
+ print(
255
+ f"Number of cosmic ray pixels after dilation....: "
256
+ f"{num_cr_pixels_after_dilation:>{len(sdum)}}"
257
+ )
258
+ # Label connected components in the mask; note that by default,
259
+ # structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
260
+ # diagonal connections too, so we define a 3x3 square.
261
+ structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
262
+ self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
263
+ print(f"Number of cosmic ray features (grouped pixels): {self.num_features:>{len(sdum)}}")
264
+ self.replace_detected_cr_button.config(state=tk.NORMAL)
265
+ self.examine_detected_cr_button.config(state=tk.NORMAL)
266
+ self.update_cr_overlay()
267
+ self.use_cursor = True
268
+ self.use_cursor_button.config(text="[c]ursor: ON ")
269
+ else:
270
+ print("No cosmic ray pixels detected!")
271
+ self.cr_labels = None
272
+ self.num_features = 0
273
+ self.replace_detected_cr_button.config(state=tk.DISABLED)
274
+ self.examine_detected_cr_button.config(state=tk.DISABLED)
275
+
276
+ def load_detected_cr_from_file(self):
277
+ """Load detected cosmic ray mask from a FITS file."""
278
+ crmask_file = filedialog.askopenfilename(
279
+ initialdir=os.getcwd(),
280
+ title="Select FITS file with cosmic ray mask",
281
+ filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
282
+ )
283
+ if crmask_file:
284
+ print(f"Selected input FITS file: {crmask_file}")
285
+ extension = simpledialog.askstring(
286
+ "Select Extension",
287
+ f"\nEnter extension number or name for file:\n{Path(crmask_file).name}",
288
+ initialvalue=None,
289
+ )
290
+ try:
291
+ extension = int(extension)
292
+ except ValueError:
293
+ pass # Keep as string
294
+ dilation = simpledialog.askinteger(
295
+ "Dilation", "Enter Dilation (min=0):", initialvalue=0, minvalue=0
296
+ )
297
+ try:
298
+ with fits.open(crmask_file, mode="readonly") as hdul:
299
+ if isinstance(extension, int):
300
+ if extension < 0 or extension >= len(hdul):
301
+ raise IndexError(f"Extension index {extension} out of range.")
302
+ else:
303
+ if extension not in hdul:
304
+ raise KeyError(f"Extension name '{extension}' not found.")
305
+ mask_crfound_loaded = hdul[extension].data.astype(bool)
306
+ if mask_crfound_loaded.shape != self.data.shape:
307
+ print(f"data shape...: {self.data.shape}")
308
+ print(f"mask shape...: {mask_crfound_loaded.shape}")
309
+ raise ValueError("Cosmic ray mask has different shape.")
310
+ self.mask_crfound = mask_crfound_loaded
311
+ print(f"Loaded cosmic ray mask from {crmask_file}")
312
+ self.process_detected_cr(dilation=dilation)
313
+ except Exception as e:
314
+ print(f"Error loading cosmic ray mask: {e}")
315
+
205
316
  def load_fits_file(self):
206
317
  """Load the FITS file and auxiliary file (if provided).
207
318
 
@@ -216,8 +327,22 @@ class CosmicRayCleanerApp(ImageDisplay):
216
327
  provided, it also loads the auxiliary data from the specified extension.
217
328
  The loaded data is stored in `self.data` and `self.auxdata` attributes.
218
329
  """
330
+ # check if extension is compatible with an integer
331
+ try:
332
+ extnum = int(self.extension)
333
+ self.extension = extnum
334
+ except ValueError:
335
+ # Keep as string (delaying checking until opening the file)
336
+ self.extension = self.extension.upper() # Convert to uppercase
219
337
  try:
220
338
  with fits.open(self.input_fits, mode="readonly") as hdul:
339
+ if isinstance(self.extension, int):
340
+ if self.extension < 0 or self.extension >= len(hdul):
341
+ raise IndexError(f"Extension index {self.extension} out of range.")
342
+ else:
343
+ if self.extension not in hdul:
344
+ raise KeyError(f"Extension name '{self.extension}' not found.")
345
+ print(f"Reading file [bold green]{self.input_fits}[/bold green], extension {self.extension}")
221
346
  self.data = hdul[self.extension].data
222
347
  if "CRMASK" in hdul:
223
348
  self.mask_fixed = hdul["CRMASK"].data.astype(bool)
@@ -225,13 +350,28 @@ class CosmicRayCleanerApp(ImageDisplay):
225
350
  self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
226
351
  except Exception as e:
227
352
  print(f"Error loading FITS file: {e}")
353
+ sys.exit(1)
228
354
  self.mask_crfound = np.zeros(self.data.shape, dtype=bool)
229
355
  naxis2, naxis1 = self.data.shape
230
356
  self.region = SliceRegion2D(f"[1:{naxis1}, 1:{naxis2}]", mode="fits").python
231
357
  # Read auxiliary file if provided
232
358
  if self.auxfile is not None:
359
+ # check if extension_auxfile is compatible with an integer
360
+ try:
361
+ extnum_aux = int(self.extension_auxfile)
362
+ self.extension_auxfile = extnum_aux
363
+ except ValueError:
364
+ # Keep as string (delaying checking until opening the file)
365
+ self.extension_auxfile = self.extension_auxfile.upper() # Convert to uppercase
233
366
  try:
234
367
  with fits.open(self.auxfile, mode="readonly") as hdul_aux:
368
+ if isinstance(self.extension_auxfile, int):
369
+ if self.extension_auxfile < 0 or self.extension_auxfile >= len(hdul_aux):
370
+ raise IndexError(f"Extension index {self.extension_auxfile} out of range.")
371
+ else:
372
+ if self.extension_auxfile not in hdul_aux:
373
+ raise KeyError(f"Extension name '{self.extension_auxfile}' not found.")
374
+ print(f"Reading auxiliary file [bold green]{self.auxfile}[/bold green], extension {self.extension_auxfile}")
235
375
  self.auxdata = hdul_aux[self.extension_auxfile].data
236
376
  if self.auxdata.shape != self.data.shape:
237
377
  print(f"data shape...: {self.data.shape}")
@@ -308,20 +448,15 @@ class CosmicRayCleanerApp(ImageDisplay):
308
448
  self.button_frame1.pack(pady=5)
309
449
  self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
310
450
  self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
311
- if self.overplot_cr_pixels:
312
- self.overplot_cr_button = tk.Button(
313
- self.button_frame1, text="CR overlay: On", command=self.toggle_cr_overlay
314
- )
315
- else:
316
- self.overplot_cr_button = tk.Button(
317
- self.button_frame1, text="CR overlay: Off", command=self.toggle_cr_overlay
318
- )
319
- self.overplot_cr_button.pack(side=tk.LEFT, padx=5)
320
- self.apply_lacosmic_button = tk.Button(
451
+ self.load_detected_cr_button = tk.Button(
452
+ self.button_frame1, text="Load detected CRs", command=self.load_detected_cr_from_file
453
+ )
454
+ self.load_detected_cr_button.pack(side=tk.LEFT, padx=5)
455
+ self.replace_detected_cr_button = tk.Button(
321
456
  self.button_frame1, text="Replace detected CRs", command=self.apply_lacosmic
322
457
  )
323
- self.apply_lacosmic_button.pack(side=tk.LEFT, padx=5)
324
- self.apply_lacosmic_button.config(state=tk.DISABLED) # Initially disabled
458
+ self.replace_detected_cr_button.pack(side=tk.LEFT, padx=5)
459
+ self.replace_detected_cr_button.config(state=tk.DISABLED) # Initially disabled
325
460
  self.examine_detected_cr_button = tk.Button(
326
461
  self.button_frame1, text="Examine detected CRs", command=lambda: self.examine_detected_cr(1)
327
462
  )
@@ -331,6 +466,17 @@ class CosmicRayCleanerApp(ImageDisplay):
331
466
  # Row 2 of buttons
332
467
  self.button_frame2 = tk.Frame(self.root)
333
468
  self.button_frame2.pack(pady=5)
469
+ self.toggle_auxdata_button = tk.Button(self.button_frame2, text="[t]oggle data", command=self.toggle_auxdata)
470
+ self.toggle_auxdata_button.pack(side=tk.LEFT, padx=5)
471
+ if self.auxdata is None:
472
+ self.toggle_auxdata_button.config(state=tk.DISABLED)
473
+ else:
474
+ self.toggle_auxdata_button.config(state=tk.NORMAL)
475
+ self.image_aspect = "equal"
476
+ self.toggle_aspect_button = tk.Button(
477
+ self.button_frame2, text=f"[a]spect: {self.image_aspect}", command=self.toggle_aspect
478
+ )
479
+ self.toggle_aspect_button.pack(side=tk.LEFT, padx=5)
334
480
  self.save_button = tk.Button(self.button_frame2, text="Save cleaned FITS", command=self.save_fits_file)
335
481
  self.save_button.pack(side=tk.LEFT, padx=5)
336
482
  self.save_button.config(state=tk.DISABLED) # Initially disabled
@@ -340,6 +486,9 @@ class CosmicRayCleanerApp(ImageDisplay):
340
486
  # Row 3 of buttons
341
487
  self.button_frame3 = tk.Frame(self.root)
342
488
  self.button_frame3.pack(pady=5)
489
+ self.use_cursor = False
490
+ self.use_cursor_button = tk.Button(self.button_frame3, text="[c]ursor: OFF", command=self.set_cursor_onoff)
491
+ self.use_cursor_button.pack(side=tk.LEFT, padx=5)
343
492
  vmin, vmax = zscale(self.data)
344
493
  self.vmin_button = tk.Button(self.button_frame3, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
345
494
  self.vmin_button.pack(side=tk.LEFT, padx=5)
@@ -349,6 +498,19 @@ class CosmicRayCleanerApp(ImageDisplay):
349
498
  self.set_minmax_button.pack(side=tk.LEFT, padx=5)
350
499
  self.set_zscale_button = tk.Button(self.button_frame3, text="zscale [/]", command=self.set_zscale)
351
500
  self.set_zscale_button.pack(side=tk.LEFT, padx=5)
501
+ if self.overplot_cr_pixels:
502
+ self.overplot_cr_button = tk.Button(
503
+ self.button_frame3,
504
+ text="CR overlay: ON ",
505
+ command=self.toggle_cr_overlay,
506
+ )
507
+ else:
508
+ self.overplot_cr_button = tk.Button(
509
+ self.button_frame3,
510
+ text="CR overlay: OFF",
511
+ command=self.toggle_cr_overlay,
512
+ )
513
+ self.overplot_cr_button.pack(side=tk.LEFT, padx=5)
352
514
 
353
515
  # Figure
354
516
  self.plot_frame = tk.Frame(self.root)
@@ -378,19 +540,63 @@ class CosmicRayCleanerApp(ImageDisplay):
378
540
  xlabel = "X pixel (from 1 to NAXIS1)"
379
541
  ylabel = "Y pixel (from 1 to NAXIS2)"
380
542
  extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
543
+ self.image_aspect = "equal"
544
+ self.displaying_auxdata = False
381
545
  self.image, _, _ = imshow(
382
- self.fig,
383
- self.ax,
384
- self.data,
546
+ fig=self.fig,
547
+ ax=self.ax,
548
+ data=self.data,
385
549
  vmin=vmin,
386
550
  vmax=vmax,
387
- title=os.path.basename(self.input_fits),
551
+ title=f"data: {os.path.basename(self.input_fits)}",
388
552
  xlabel=xlabel,
389
553
  ylabel=ylabel,
390
554
  extent=extent,
555
+ aspect=self.image_aspect,
391
556
  )
392
557
  self.fig.tight_layout()
393
558
 
559
+ def set_cursor_onoff(self):
560
+ """Toggle cursor selection mode on or off."""
561
+ if not self.use_cursor:
562
+ self.use_cursor = True
563
+ self.use_cursor_button.config(text="[c]ursor: ON ")
564
+ else:
565
+ self.use_cursor = False
566
+ self.use_cursor_button.config(text="[c]ursor: OFF")
567
+
568
+ def toggle_auxdata(self):
569
+ """Toggle between main data and auxiliary data for display."""
570
+ if self.displaying_auxdata:
571
+ # Switch to main data
572
+ vmin = self.get_vmin()
573
+ vmax = self.get_vmax()
574
+ self.image.set_data(self.data)
575
+ self.image.set_clim(vmin=vmin, vmax=vmax)
576
+ self.displaying_auxdata = False
577
+ self.ax.set_title(f"data: {os.path.basename(self.input_fits)}")
578
+ else:
579
+ # Switch to auxiliary data
580
+ vmin = self.get_vmin()
581
+ vmax = self.get_vmax()
582
+ self.image.set_data(self.auxdata)
583
+ self.image.set_clim(vmin=vmin, vmax=vmax)
584
+ self.displaying_auxdata = True
585
+ self.ax.set_title(f"auxdata: {os.path.basename(self.auxfile)}")
586
+ self.canvas.draw_idle()
587
+
588
+ def toggle_aspect(self):
589
+ """Toggle the aspect ratio of the image display."""
590
+ if self.image_aspect == "equal":
591
+ self.image_aspect = "auto"
592
+ else:
593
+ self.image_aspect = "equal"
594
+ print(f"Setting image aspect to: {self.image_aspect}")
595
+ self.toggle_aspect_button.config(text=f"[a]spect: {self.image_aspect}")
596
+ self.ax.set_aspect(self.image_aspect)
597
+ self.fig.tight_layout()
598
+ self.canvas.draw_idle()
599
+
394
600
  def run_lacosmic(self):
395
601
  """Run L.A.Cosmic to detect cosmic rays."""
396
602
  self.run_lacosmic_button.config(state=tk.DISABLED)
@@ -427,17 +633,18 @@ class CosmicRayCleanerApp(ImageDisplay):
427
633
  usefulmask[usefulregion] = 1.0
428
634
  # Update parameter dictionary with new values
429
635
  self.lacosmic_params = updated_params
430
- print("Parameters updated:")
431
- for key, info in self.lacosmic_params.items():
432
- print(f" {key}: {info['value']}")
636
+ if self.verbose:
637
+ print("Parameters updated:")
638
+ for key, info in self.lacosmic_params.items():
639
+ print(f" {key}: {info['value']}")
433
640
  if self.lacosmic_params["nruns"]["value"] not in [1, 2]:
434
641
  raise ValueError("nruns must be 1 or 2")
435
642
  # Execute L.A.Cosmic with updated parameters
436
643
  print("[bold green]Executing L.A.Cosmic (run 1)...[/bold green]")
437
644
  borderpadd = updated_params["borderpadd"]["value"]
438
- data_reflection_padded = np.pad(self.data, pad_width=borderpadd, mode="reflect")
439
- cleandata_lacosmic, mask_crfound = cosmicray_lacosmic(
440
- ccd=data_reflection_padded,
645
+ cleandata_lacosmic, mask_crfound = lacosmicpad(
646
+ pad_width=borderpadd,
647
+ ccd=self.data,
441
648
  gain=self.lacosmic_params["run1_gain"]["value"],
442
649
  readnoise=self.lacosmic_params["run1_readnoise"]["value"],
443
650
  sigclip=self.lacosmic_params["run1_sigclip"]["value"],
@@ -446,16 +653,15 @@ class CosmicRayCleanerApp(ImageDisplay):
446
653
  niter=self.lacosmic_params["run1_niter"]["value"],
447
654
  verbose=self.lacosmic_params["run1_verbose"]["value"],
448
655
  )
449
- cleandata_lacosmic = cleandata_lacosmic[borderpadd:-borderpadd, borderpadd:-borderpadd]
450
- mask_crfound = mask_crfound[borderpadd:-borderpadd, borderpadd:-borderpadd]
451
656
  # Apply usefulmask to consider only selected region
452
657
  cleandata_lacosmic *= usefulmask
453
658
  mask_crfound = mask_crfound & (usefulmask.astype(bool))
454
659
  # Second execution if nruns == 2
455
660
  if self.lacosmic_params["nruns"]["value"] == 2:
456
661
  print("[bold green]Executing L.A.Cosmic (run 2)...[/bold green]")
457
- cleandata_lacosmic2, mask_crfound2 = cosmicray_lacosmic(
458
- ccd=data_reflection_padded,
662
+ cleandata_lacosmic2, mask_crfound2 = lacosmicpad(
663
+ pad_width=borderpadd,
664
+ ccd=self.data,
459
665
  gain=self.lacosmic_params["run2_gain"]["value"],
460
666
  readnoise=self.lacosmic_params["run2_readnoise"]["value"],
461
667
  sigclip=self.lacosmic_params["run2_sigclip"]["value"],
@@ -464,8 +670,6 @@ class CosmicRayCleanerApp(ImageDisplay):
464
670
  niter=self.lacosmic_params["run2_niter"]["value"],
465
671
  verbose=self.lacosmic_params["run2_verbose"]["value"],
466
672
  )
467
- cleandata_lacosmic2 = cleandata_lacosmic2[borderpadd:-borderpadd, borderpadd:-borderpadd]
468
- mask_crfound2 = mask_crfound2[borderpadd:-borderpadd, borderpadd:-borderpadd]
469
673
  # Apply usefulmask to consider only selected region
470
674
  cleandata_lacosmic2 *= usefulmask
471
675
  mask_crfound2 = mask_crfound2 & (usefulmask.astype(bool))
@@ -473,19 +677,7 @@ class CosmicRayCleanerApp(ImageDisplay):
473
677
  if np.any(mask_crfound):
474
678
  print(f"Number of cosmic ray pixels (run1).......: {np.sum(mask_crfound)}")
475
679
  print(f"Number of cosmic ray pixels (run2).......: {np.sum(mask_crfound2)}")
476
- # find features in second run
477
- structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
478
- cr_labels2, num_features2 = ndimage.label(mask_crfound2, structure=structure)
479
- # generate mask of ones at CR pixels found in first run
480
- mask_peaks = np.zeros(mask_crfound.shape, dtype=float)
481
- mask_peaks[mask_crfound] = 1.0
482
- # preserve only those CR pixels in second run that are in the first run
483
- cr_labels2_preserved = mask_peaks * cr_labels2
484
- # generate new mask with preserved CR pixels from second run
485
- mask_crfound = np.zeros_like(mask_crfound, dtype=bool)
486
- for icr in np.unique(cr_labels2_preserved):
487
- if icr > 0:
488
- mask_crfound[cr_labels2 == icr] = True
680
+ mask_crfound = merge_peak_tail_masks(mask_crfound, mask_crfound2)
489
681
  print(f"Number of cosmic ray pixels (run1 & run2): {np.sum(mask_crfound)}")
490
682
  # Use the cleandata from the second run
491
683
  cleandata_lacosmic = cleandata_lacosmic2
@@ -495,42 +687,7 @@ class CosmicRayCleanerApp(ImageDisplay):
495
687
  self.mask_crfound = np.zeros_like(self.data, dtype=bool)
496
688
  self.mask_crfound[usefulregion] = mask_crfound[usefulregion]
497
689
  # Process the mask: dilation and labeling
498
- if np.any(self.mask_crfound):
499
- num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
500
- dilation = self.lacosmic_params["dilation"]["value"]
501
- if dilation > 0:
502
- # Dilate the mask by the specified number of pixels
503
- self.mask_crfound = dilatemask(
504
- mask=self.mask_crfound, iterations=self.lacosmic_params["dilation"]["value"], connectivity=1
505
- )
506
- num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
507
- sdum = str(num_cr_pixels_after_dilation)
508
- else:
509
- sdum = str(num_cr_pixels_before_dilation)
510
- print(
511
- "Number of cosmic ray pixels detected by L.A.Cosmic: "
512
- f"{num_cr_pixels_before_dilation:>{len(sdum)}}"
513
- )
514
- if dilation > 0:
515
- print(
516
- f"Number of cosmic ray pixels after dilation........: "
517
- f"{num_cr_pixels_after_dilation:>{len(sdum)}}"
518
- )
519
- # Label connected components in the mask; note that by default,
520
- # structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
521
- # diagonal connections too, so we define a 3x3 square.
522
- structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
523
- self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
524
- print(f"Number of cosmic ray features (grouped pixels)....: {self.num_features:>{len(sdum)}}")
525
- self.apply_lacosmic_button.config(state=tk.NORMAL)
526
- self.examine_detected_cr_button.config(state=tk.NORMAL)
527
- self.update_cr_overlay()
528
- else:
529
- print("No cosmic ray pixels detected by L.A.Cosmic.")
530
- self.cr_labels = None
531
- self.num_features = 0
532
- self.apply_lacosmic_button.config(state=tk.DISABLED)
533
- self.examine_detected_cr_button.config(state=tk.DISABLED)
690
+ self.process_detected_cr(dilation=self.lacosmic_params["dilation"]["value"])
534
691
  else:
535
692
  print("Parameter editing cancelled. L.A.Cosmic detection skipped!")
536
693
  self.run_lacosmic_button.config(state=tk.NORMAL)
@@ -539,9 +696,9 @@ class CosmicRayCleanerApp(ImageDisplay):
539
696
  """Toggle the overlay of cosmic ray pixels on the image."""
540
697
  self.overplot_cr_pixels = not self.overplot_cr_pixels
541
698
  if self.overplot_cr_pixels:
542
- self.overplot_cr_button.config(text="CR overlay: On")
699
+ self.overplot_cr_button.config(text="CR overlay: ON ")
543
700
  else:
544
- self.overplot_cr_button.config(text="CR overlay: Off")
701
+ self.overplot_cr_button.config(text="CR overlay: OFF")
545
702
  self.update_cr_overlay()
546
703
 
547
704
  def update_cr_overlay(self):
@@ -617,6 +774,21 @@ class CosmicRayCleanerApp(ImageDisplay):
617
774
  # upate mask_crfound by eliminating the cleaned pixels
618
775
  self.mask_crfound[mask_crfound_region] = False
619
776
  data_has_been_modified = True
777
+ elif cleaning_method == "maskfill":
778
+ # Replace detected CR pixels with local median values
779
+ smoothed_output, _ = maskfill(
780
+ input_image=self.data,
781
+ mask=mask_crfound_region,
782
+ size=3,
783
+ operator="median",
784
+ smooth=True,
785
+ )
786
+ self.data[mask_crfound_region] = smoothed_output[mask_crfound_region]
787
+ # update mask_fixed to include the newly fixed pixels
788
+ self.mask_fixed[mask_crfound_region] = True
789
+ # upate mask_crfound by eliminating the cleaned pixels
790
+ self.mask_crfound[mask_crfound_region] = False
791
+ data_has_been_modified = True
620
792
  elif cleaning_method == "auxdata":
621
793
  if self.auxdata is None:
622
794
  print("No auxiliary data available. Cleaning skipped!")
@@ -699,9 +871,13 @@ class CosmicRayCleanerApp(ImageDisplay):
699
871
  # recalculate labels and number of features
700
872
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
701
873
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
702
- sdum = str(np.sum(self.mask_crfound))
874
+ num_cr_remaining = np.sum(self.mask_crfound)
875
+ sdum = str(num_cr_remaining)
703
876
  print(f"Remaining number of cosmic ray pixels...................: {sdum}")
704
877
  print(f"Remaining number of cosmic ray features (grouped pixels): {self.num_features:>{len(sdum)}}")
878
+ if num_cr_remaining == 0:
879
+ self.use_cursor = False
880
+ self.use_cursor_button.config(text="[c]ursor: OFF")
705
881
  # redraw image to show the changes
706
882
  self.image.set_data(self.data)
707
883
  self.canvas.draw_idle()
@@ -709,7 +885,7 @@ class CosmicRayCleanerApp(ImageDisplay):
709
885
  self.save_button.config(state=tk.NORMAL)
710
886
  if self.num_features == 0:
711
887
  self.examine_detected_cr_button.config(state=tk.DISABLED)
712
- self.apply_lacosmic_button.config(state=tk.DISABLED)
888
+ self.replace_detected_cr_button.config(state=tk.DISABLED)
713
889
  self.update_cr_overlay()
714
890
 
715
891
  def examine_detected_cr(self, first_cr_index=1, single_cr=False, ixpix=None, iypix=None):
@@ -765,9 +941,13 @@ class CosmicRayCleanerApp(ImageDisplay):
765
941
  # recalculate labels and number of features
766
942
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
767
943
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
768
- sdum = str(np.sum(self.mask_crfound))
944
+ num_remaining = np.sum(self.mask_crfound)
945
+ sdum = str(num_remaining)
769
946
  print(f"Remaining number of cosmic ray pixels...................: {sdum}")
770
947
  print(f"Remaining number of cosmic ray features (grouped pixels): {self.num_features:>{len(sdum)}}")
948
+ if num_remaining == 0:
949
+ self.use_cursor = False
950
+ self.use_cursor_button.config(text="[c]ursor: OFF")
771
951
  # redraw image to show the changes
772
952
  self.image.set_data(self.data)
773
953
  self.canvas.draw_idle()
@@ -775,7 +955,7 @@ class CosmicRayCleanerApp(ImageDisplay):
775
955
  self.save_button.config(state=tk.NORMAL)
776
956
  if self.num_features == 0:
777
957
  self.examine_detected_cr_button.config(state=tk.DISABLED)
778
- self.apply_lacosmic_button.config(state=tk.DISABLED)
958
+ self.replace_detected_cr_button.config(state=tk.DISABLED)
779
959
  self.update_cr_overlay()
780
960
 
781
961
  def stop_app(self):
@@ -792,14 +972,39 @@ class CosmicRayCleanerApp(ImageDisplay):
792
972
 
793
973
  def on_key(self, event):
794
974
  """Handle key press events."""
795
- if event.key == "q":
796
- pass # Ignore the "q" key to prevent closing the window
975
+ if event.key == "c":
976
+ self.set_cursor_onoff()
977
+ elif event.key == "a":
978
+ self.toggle_aspect()
979
+ elif event.key == "t" and self.auxdata is not None:
980
+ self.toggle_auxdata()
797
981
  elif event.key == ",":
798
982
  self.set_minmax()
799
983
  elif event.key == "/":
800
984
  self.set_zscale()
801
- else:
802
- print(f"Key pressed: {event.key}")
985
+ elif event.key == "o":
986
+ self.toolbar.zoom()
987
+ elif event.key == "h":
988
+ self.toolbar.home()
989
+ elif event.key == "p":
990
+ self.toolbar.pan()
991
+ elif event.key == "s":
992
+ self.toolbar.save_figure()
993
+ elif event.key == "?":
994
+ # Display list of keyboard shortcuts
995
+ print("[bold blue]Keyboard Shortcuts:[/bold blue]")
996
+ print("[red] c [/red]: Toggle cursor selection mode on/off")
997
+ print("[red] t [/red]: Toggle between main data and auxiliary data")
998
+ print("[red] a [/red]: Toggle image aspect ratio equal/auto")
999
+ print("[red] , [/red]: Set vmin and vmax to minmax")
1000
+ print("[red] / [/red]: Set vmin and vmax using zscale")
1001
+ print("[red] h [/red]: Go to home view \\[toolbar]")
1002
+ print("[red] o [/red]: Activate zoom mode \\[toolbar]")
1003
+ print("[red] p [/red]: Activate pan mode \\[toolbar]")
1004
+ print("[red] s [/red]: Save the current figure \\[toolbar]")
1005
+ print("[red] q [/red]: (ignored) prevent closing the window")
1006
+ elif event.key == "q":
1007
+ pass # Ignore the "q" key to prevent closing the window
803
1008
 
804
1009
  def on_click(self, event):
805
1010
  """Handle mouse click events on the image."""
@@ -814,6 +1019,10 @@ class CosmicRayCleanerApp(ImageDisplay):
814
1019
  print(f"Toolbar mode '{toolbar.mode}' active; click ignored.")
815
1020
  return
816
1021
 
1022
+ # proceed only if cursor selection mode is on
1023
+ if not self.use_cursor:
1024
+ return
1025
+
817
1026
  # ignore clicks outside the expected axes
818
1027
  # (note that the color bar is a different axes)
819
1028
  if event.inaxes == self.ax:
@@ -43,7 +43,7 @@ lacosmic_default_dict = {
43
43
  }
44
44
 
45
45
  # Default parameters for cleaning methods
46
- VALID_CLEANING_METHODS = ["x interp.", "y interp.", "surface interp.", "median", "mean", "lacosmic", "auxdata"]
46
+ VALID_CLEANING_METHODS = ["x interp.", "y interp.", "surface interp.", "median", "mean", "lacosmic", "maskfill", "auxdata"]
47
47
 
48
48
  # Maximum pixel distance to consider when finding closest CR pixel
49
49
  MAX_PIXEL_DISTANCE_TO_CR = 15
@@ -11,6 +11,7 @@
11
11
 
12
12
  from scipy import ndimage
13
13
  import numpy as np
14
+ from tqdm import tqdm
14
15
 
15
16
  from .dilatemask import dilatemask
16
17
  from .interpolation_x import interpolation_x
@@ -18,7 +19,7 @@ from .interpolation_y import interpolation_y
18
19
  from .interpolation_a import interpolation_a
19
20
 
20
21
 
21
- def cleanest(data, mask_crfound, dilation=0, interp_method=None, npoints=None, degree=None, debug=False):
22
+ def interpolate(data, mask_crfound, dilation=0, interp_method=None, npoints=None, degree=None, debug=False):
22
23
  """Interpolate pixels identified in a mask.
23
24
 
24
25
  The original data and mask are not modified. A copy of both
@@ -29,8 +30,8 @@ def cleanest(data, mask_crfound, dilation=0, interp_method=None, npoints=None, d
29
30
  data : 2D numpy.ndarray
30
31
  The image data array to be processed.
31
32
  mask_crfound : 2D numpy.ndarray of bool
32
- A boolean mask array indicating which pixels are affected by
33
- cosmic rays.
33
+ A boolean mask array indicating which pixels are flagged
34
+ and need to be interpolated (True = pixel to be fixed).
34
35
  dilation : int, optional
35
36
  The number of pixels to dilate the masked pixels before
36
37
  interpolation.
@@ -48,7 +49,7 @@ def cleanest(data, mask_crfound, dilation=0, interp_method=None, npoints=None, d
48
49
  The degree of the polynomial to fit. This parameter is
49
50
  relevant for 'x' and 'y' methods.
50
51
  debug : bool, optional
51
- If True, print debug information.
52
+ If True, print debug information and enable tqdm progress bar.
52
53
 
53
54
  Returns
54
55
  -------
@@ -86,13 +87,14 @@ def cleanest(data, mask_crfound, dilation=0, interp_method=None, npoints=None, d
86
87
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
87
88
  cr_labels, num_features = ndimage.label(updated_mask_crfound, structure=structure)
88
89
  if debug:
89
- print(f"Number of cosmic ray pixels to be cleaned: {np.sum(updated_mask_crfound)}")
90
- print(f"Number of cosmic rays (grouped pixels)...: {num_features}")
90
+ sdum = str(np.sum(updated_mask_crfound))
91
+ print(f"Number of cosmic ray pixels to be cleaned: {sdum}")
92
+ print(f"Number of cosmic rays (grouped pixels)...: {num_features:>{len(sdum)}}")
91
93
 
92
94
  # Fix cosmic rays using the specified interpolation method
93
95
  cleaned_data = data.copy()
94
96
  num_cr_cleaned = 0
95
- for cr_index in range(1, num_features + 1):
97
+ for cr_index in tqdm(range(1, num_features + 1), disable=not debug):
96
98
  if interp_method in ["x", "y"]:
97
99
  if 2 * npoints <= degree:
98
100
  raise ValueError("2*npoints must be greater than degree for polynomial interpolation.")
@@ -131,6 +133,6 @@ def cleanest(data, mask_crfound, dilation=0, interp_method=None, npoints=None, d
131
133
  raise ValueError(f"Unknown interpolation method: {interp_method}")
132
134
 
133
135
  if debug:
134
- print(f"Number of cosmic rays cleaned............: {num_cr_cleaned}")
136
+ print(f"Number of cosmic rays cleaned............: {num_cr_cleaned:>{len(sdum)}}")
135
137
 
136
138
  return cleaned_data, mask_fixed
@@ -97,6 +97,7 @@ class InterpolationEditor:
97
97
  "median": "a-median",
98
98
  "mean": "a-mean",
99
99
  "lacosmic": "lacosmic",
100
+ "maskfill": "maskfill",
100
101
  "auxdata": "auxdata",
101
102
  }
102
103
  self.check_interp_methods()
@@ -0,0 +1,57 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Execute LACosmic algorithm on a padded image."""
11
+
12
+ from ccdproc import cosmicray_lacosmic
13
+ import numpy as np
14
+
15
+
16
+ def lacosmicpad(pad_width, **kwargs):
17
+ """Execute LACosmic algorithm on a padded array.
18
+
19
+ This function pads the input image array before applying the LACosmic
20
+ cosmic ray cleaning algorithm. After processing, the padding is removed
21
+ to return an array of the original size.
22
+
23
+ The padding helps to mitigate edge effects that can occur during the
24
+ cosmic ray detection and cleaning process.
25
+
26
+ Apart from the `pad_width` parameter, all other keyword arguments
27
+ are passed directly to the `cosmicray_lacosmic` function from the
28
+ `ccdproc` package.
29
+
30
+ Parameters
31
+ ----------
32
+ pad_width : int
33
+ Width of the padding to be applied to the image before executing
34
+ the LACosmic algorithm.
35
+ **kwargs : dict
36
+ Keyword arguments to be passed to the `cosmicray_lacosmic` function.
37
+
38
+ Returns
39
+ -------
40
+ clean_array : 2D numpy.ndarray
41
+ The cleaned image array after applying the LACosmic algorithm with padding.
42
+ mask_array : 2D numpy.ndarray of bool
43
+ The mask array indicating detected cosmic rays.
44
+ """
45
+ if "ccd" not in kwargs:
46
+ raise ValueError("The 'ccd' keyword argument must be provided.")
47
+ array = kwargs.pop("ccd")
48
+ if not isinstance(array, np.ndarray):
49
+ raise TypeError("The 'ccd' keyword argument must be a numpy ndarray.")
50
+ # Pad the array
51
+ padded_array = np.pad(array, pad_width, mode="reflect")
52
+ # Apply LACosmic algorithm to the padded array
53
+ cleaned_padded_array, mask_padded_array = cosmicray_lacosmic(ccd=padded_array, **kwargs)
54
+ # Remove padding
55
+ cleaned_array = cleaned_padded_array[pad_width:-pad_width, pad_width:-pad_width]
56
+ mask_array = mask_padded_array[pad_width:-pad_width, pad_width:-pad_width]
57
+ return cleaned_array, mask_array
@@ -0,0 +1,58 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Merge peak and tail masks for cosmic ray cleaning."""
11
+
12
+ import numpy as np
13
+ from scipy import ndimage
14
+
15
+
16
+ def merge_peak_tail_masks(mask_peaks, mask_tails):
17
+ """Merge peak and tail masks for cosmic ray cleaning.
18
+
19
+ Tail pixels are preserved only if they correspond to CR features
20
+ that are also present in the peak mask.
21
+
22
+ Parameters
23
+ ----------
24
+ mask_peaks : ndarray
25
+ Boolean array indicating the pixels identified as cosmic ray peaks.
26
+ mask_tails : ndarray
27
+ Boolean array indicating the pixels identified as cosmic ray tails.
28
+
29
+ Returns
30
+ -------
31
+ merged_mask : ndarray
32
+ Boolean array indicating the merged cosmic ray mask.
33
+ """
34
+ # check that input masks are numpy arrays
35
+ if not isinstance(mask_peaks, np.ndarray) or not isinstance(mask_tails, np.ndarray):
36
+ raise TypeError("Input masks must be numpy arrays.")
37
+ # check that input masks have the same shape
38
+ if mask_peaks.shape != mask_tails.shape:
39
+ raise ValueError("Input masks must have the same shape.")
40
+ # check that input masks are boolean arrays
41
+ if mask_peaks.dtype != bool or mask_tails.dtype != bool:
42
+ raise TypeError("Input masks must be boolean arrays.")
43
+
44
+ # find structures in tail mask
45
+ structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
46
+ cr_labels_tails, num_crs_tails = ndimage.label(mask_tails, structure=structure)
47
+ # generate mask of ones at peak pixels
48
+ mask_peaks_ones = np.zeros(mask_peaks.shape, dtype=float)
49
+ mask_peaks_ones[mask_peaks] = 1.0
50
+ # preserve only those tail pixels that are flagged as peaks
51
+ cr_labels_tails_preserved = mask_peaks_ones * cr_labels_tails
52
+ # generate new mask with preserved tail pixels
53
+ merged_mask = np.zeros_like(mask_peaks, dtype=bool)
54
+ for icr in np.unique(cr_labels_tails_preserved):
55
+ if icr > 0:
56
+ merged_mask[cr_labels_tails == icr] = True
57
+
58
+ return merged_mask
@@ -116,11 +116,11 @@ class ModalProgressBar:
116
116
 
117
117
  elapsed_str = self._format_time(elapsed)
118
118
  eta_str = self._format_time(eta_seconds)
119
+ total_str = self._format_time(elapsed + eta_seconds)
119
120
  rate_str = f"{rate:.2f} CR/s" if rate >= 1 else f"{1/rate:.2f} s/CR"
120
121
 
121
122
  self.status_label.config(text=f"{self.current}/{self.total} ({percentage:.1f}%) | {rate_str}")
122
- self.time_label.config(text=f"Elapsed: {elapsed_str} | ETA: {eta_str}")
123
-
123
+ self.time_label.config(text=f"Expected Total: {total_str} | Elapsed: {elapsed_str} | ETA: {eta_str}")
124
124
  self.window.update_idletasks()
125
125
  self.window.update()
126
126
 
@@ -101,10 +101,21 @@ class ParameterEditor:
101
101
  bold_font = default_font.copy()
102
102
  bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
103
103
  subtitle_label = tk.Label(main_frame, text="L.A.Cosmic Parameters", font=bold_font)
104
- subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
104
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 10))
105
105
  row += 1
106
106
 
107
107
  # Create labels and entry fields for each parameter.
108
+ bold_font_subheader = default_font.copy()
109
+ bold_font_subheader.configure(weight="bold", size=default_font.cget("size") + 1)
110
+ label = tk.Label(main_frame, text="Parameter", font=bold_font_subheader, anchor="w", fg="gray")
111
+ label.grid(row=row, column=0, sticky="e", pady=0)
112
+ label = tk.Label(main_frame, text="Run 1", font=bold_font_subheader, anchor="w", fg="gray", width=10)
113
+ label.grid(row=row, column=1, sticky="w", padx=10, pady=0)
114
+ label = tk.Label(main_frame, text="Run 2", font=bold_font_subheader, anchor="w", fg="gray", width=10)
115
+ label.grid(row=row, column=2, sticky="w", padx=10, pady=0)
116
+ label = tk.Label(main_frame, text="Type", font=bold_font_subheader, anchor="w", fg="gray", width=10)
117
+ label.grid(row=row, column=3, sticky="w", pady=0)
118
+ row += 1
108
119
  # Note: here we are using entry_vars to trace changes in the entries
109
120
  # so that we can update the color of run2 entries if they differ from run1.
110
121
  self.entry_vars = {}
@@ -142,7 +153,7 @@ class ParameterEditor:
142
153
 
143
154
  # Subtitle for additional parameters
144
155
  subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=bold_font)
145
- subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
156
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 10))
146
157
  row += 1
147
158
 
148
159
  # Dilation label and entry
@@ -177,7 +188,7 @@ class ParameterEditor:
177
188
 
178
189
  # Subtitle for region to be examined
179
190
  subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=bold_font)
180
- subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
191
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 10))
181
192
  row += 1
182
193
 
183
194
  # Region to be examined label and entries
@@ -13,6 +13,7 @@ import tkinter as tk
13
13
  from tkinter import messagebox
14
14
  from tkinter import simpledialog
15
15
 
16
+ from maskfill import maskfill
16
17
  import matplotlib.pyplot as plt
17
18
  from matplotlib.backend_bases import key_press_handler
18
19
  from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
@@ -150,7 +151,7 @@ class ReviewCosmicRay(ImageDisplay):
150
151
  self.root.minsize(1000, 700)
151
152
  else:
152
153
  # self.root.geometry("800x700+100+100") # This does not work in Fedora
153
- self.root.minsize(800, 700)
154
+ self.root.minsize(900, 700)
154
155
  self.root.update_idletasks()
155
156
  self.root.geometry("+100+100")
156
157
  self.data = data
@@ -224,6 +225,8 @@ class ReviewCosmicRay(ImageDisplay):
224
225
  self.interp_l_button.config(state=tk.DISABLED)
225
226
  if self.cleandata_lacosmic is None:
226
227
  self.interp_l_button.config(state=tk.DISABLED)
228
+ self.interp_maskfill_button = tk.Button(self.button_frame2, text="mas[k]fill", command=self.use_maskfill)
229
+ self.interp_maskfill_button.pack(side=tk.LEFT, padx=5)
227
230
  self.interp_aux_button = tk.Button(self.button_frame2, text="[a]ux. data", command=self.use_auxdata)
228
231
  self.interp_aux_button.pack(side=tk.LEFT, padx=5)
229
232
  if self.auxdata is None:
@@ -246,7 +249,7 @@ class ReviewCosmicRay(ImageDisplay):
246
249
  if self.auxdata is not None:
247
250
  self.fig, (self.ax_aux, self.ax) = plt.subplots(ncols=2, figsize=(11, 5.5), constrained_layout=True)
248
251
  else:
249
- self.fig, self.ax = plt.subplots(figsize=(8, 5.5), constrained_layout=True)
252
+ self.fig, self.ax = plt.subplots(figsize=(9, 5.5), constrained_layout=True)
250
253
  self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
251
254
  self.canvas.get_tk_widget().pack(padx=5, pady=5)
252
255
  # The next two instructions prevent a segmentation fault when pressing "q"
@@ -376,6 +379,7 @@ class ReviewCosmicRay(ImageDisplay):
376
379
  self.interp_d_button.config(state=tk.DISABLED)
377
380
  self.interp_m_button.config(state=tk.DISABLED)
378
381
  self.interp_l_button.config(state=tk.DISABLED)
382
+ self.interp_maskfill_button.config(state=tk.DISABLED)
379
383
  self.interp_aux_button.config(state=tk.DISABLED)
380
384
 
381
385
  def interp_x(self):
@@ -461,6 +465,27 @@ class ReviewCosmicRay(ImageDisplay):
461
465
  self.set_buttons_after_cleaning_cr()
462
466
  self.update_display(cleaned=True)
463
467
 
468
+ def use_maskfill(self):
469
+ """Use maskfill cleaned data to clean a cosmic ray."""
470
+ print(f"Maskfill interpolation of cosmic ray {self.cr_index}")
471
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
472
+ mask = np.zeros(self.data.shape, dtype=bool)
473
+ for iy, ix in zip(ycr_list, xcr_list):
474
+ mask[iy, ix] = True
475
+ smoothed_output, _ = maskfill(
476
+ input_image=self.data,
477
+ mask=mask,
478
+ size=3,
479
+ operator="median",
480
+ smooth=True,
481
+ )
482
+ for iy, ix in zip(ycr_list, xcr_list):
483
+ self.data[iy, ix] = smoothed_output[iy, ix]
484
+ self.mask_fixed[iy, ix] = True
485
+ self.num_cr_cleaned += 1
486
+ self.set_buttons_after_cleaning_cr()
487
+ self.update_display(cleaned=True)
488
+
464
489
  def use_auxdata(self):
465
490
  """Use auxiliary data to clean a cosmic ray."""
466
491
  if self.auxdata is None:
@@ -505,6 +530,7 @@ class ReviewCosmicRay(ImageDisplay):
505
530
  if self.cleandata_lacosmic is not None:
506
531
  if self.last_dilation is None or self.last_dilation == 0:
507
532
  self.interp_l_button.config(state=tk.NORMAL)
533
+ self.interp_maskfill_button.config(state=tk.NORMAL)
508
534
  if self.auxdata is not None:
509
535
  self.interp_aux_button.config(state=tk.NORMAL)
510
536
  print(f"Restored all pixels of cosmic ray {self.cr_index}")
@@ -566,6 +592,9 @@ class ReviewCosmicRay(ImageDisplay):
566
592
  elif event.key == "l":
567
593
  if self.interp_l_button.cget("state") != "disabled":
568
594
  self.use_lacosmic()
595
+ elif event.key == "k":
596
+ if self.interp_maskfill_button.cget("state") != "disabled":
597
+ self.use_maskfill()
569
598
  elif event.key == "a":
570
599
  if self.interp_aux_button.cget("state") != "disabled":
571
600
  self.use_auxdata()
@@ -12,7 +12,7 @@
12
12
  import numpy as np
13
13
 
14
14
 
15
- from ..cleanest.cleanest import cleanest
15
+ from ..cleanest.interpolate import interpolate
16
16
 
17
17
 
18
18
  def test_cleanest_no_cr():
@@ -23,7 +23,7 @@ def test_cleanest_no_cr():
23
23
  mask_crfound = np.zeros_like(data, dtype=bool)
24
24
 
25
25
  for interp_method in ['x', 'y', 's', 'd', 'm']:
26
- cleaned_data, mask_fixed = cleanest(data, mask_crfound, interp_method=interp_method, npoints=2, degree=1)
26
+ cleaned_data, mask_fixed = interpolate(data, mask_crfound, interp_method=interp_method, npoints=2, degree=1)
27
27
 
28
28
  assert np.array_equal(cleaned_data, data), "Data should remain unchanged when no cosmic rays are present."
29
29
  assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged when no cosmic rays are present."
@@ -38,7 +38,7 @@ def test_cleanest_interpolation_x():
38
38
  [False, False, True, False, False],
39
39
  [False, False, True, False, False]], dtype=bool)
40
40
 
41
- cleaned_data, mask_fixed = cleanest(data, mask_crfound,
41
+ cleaned_data, mask_fixed = interpolate(data, mask_crfound,
42
42
  interp_method='x', npoints=2, degree=1)
43
43
 
44
44
  expected_data = np.array([[1, 1, 1, 1, 1],
@@ -62,8 +62,8 @@ def test_cleanest_interpolation_y():
62
62
  [False, False, False],
63
63
  [False, False, False]], dtype=bool)
64
64
 
65
- cleaned_data, mask_fixed = cleanest(data, mask_crfound,
66
- interp_method='y', npoints=2, degree=1)
65
+ cleaned_data, mask_fixed = interpolate(data, mask_crfound,
66
+ interp_method='y', npoints=2, degree=1)
67
67
 
68
68
  expected_data = np.array([[1, 2, 3],
69
69
  [1, 2, 3],
@@ -83,8 +83,8 @@ def test_cleanest_interpolation_surface():
83
83
  [False, True, False],
84
84
  [False, False, False]], dtype=bool)
85
85
 
86
- cleaned_data, mask_fixed = cleanest(data, mask_crfound,
87
- interp_method='s', npoints=1)
86
+ cleaned_data, mask_fixed = interpolate(data, mask_crfound,
87
+ interp_method='s', npoints=1)
88
88
 
89
89
  expected_data = np.array([[1, 2, 3],
90
90
  [4, 5, 6],
@@ -102,8 +102,8 @@ def test_cleanest_interpolation_median():
102
102
  [False, True, False],
103
103
  [False, False, False]], dtype=bool)
104
104
 
105
- cleaned_data, mask_fixed = cleanest(data, mask_crfound,
106
- interp_method='d', npoints=1)
105
+ cleaned_data, mask_fixed = interpolate(data, mask_crfound,
106
+ interp_method='d', npoints=1)
107
107
 
108
108
  expected_data = np.array([[1, 2, 3],
109
109
  [4, 5, 6],
@@ -121,8 +121,8 @@ def test_cleanest_interpolation_mean():
121
121
  [False, True, False],
122
122
  [False, False, False]], dtype=bool)
123
123
 
124
- cleaned_data, mask_fixed = cleanest(data, mask_crfound,
125
- interp_method='m', npoints=1)
124
+ cleaned_data, mask_fixed = interpolate(data, mask_crfound,
125
+ interp_method='m', npoints=1)
126
126
 
127
127
  expected_data = np.array([[1, 2, 3],
128
128
  [4, 5, 6],
teareduce/version.py CHANGED
@@ -9,7 +9,7 @@
9
9
  #
10
10
  """Module to define the version of the teareduce package."""
11
11
 
12
- VERSION = '0.5.5'
12
+ VERSION = '0.5.8'
13
13
 
14
14
 
15
15
  def main():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: teareduce
3
- Version: 0.5.5
3
+ Version: 0.5.8
4
4
  Summary: Utilities for astronomical data reduction
5
5
  Author-email: Nicolás Cardiel <cardiel@ucm.es>
6
6
  License: GPL-3.0-or-later
@@ -23,6 +23,7 @@ Requires-Dist: astropy
23
23
  Requires-Dist: ccdproc
24
24
  Requires-Dist: lmfit
25
25
  Requires-Dist: matplotlib
26
+ Requires-Dist: maskfill
26
27
  Requires-Dist: numpy>=1.22
27
28
  Requires-Dist: requests
28
29
  Requires-Dist: rich
@@ -15,35 +15,37 @@ teareduce/sdistortion.py,sha256=5ZsZn4vD5Sw2aoqO8-NIOH7H89Zmh7ZDkow6YbAotHU,5916
15
15
  teareduce/simulateccdexposure.py,sha256=cdbpca6GVuM3d7R1LGzlIZZvjTq_jzrlkk_Cli7aouQ,24636
16
16
  teareduce/sliceregion.py,sha256=Jdf8XvmGaY_vaY1cneTaRtSOYPxpUsJm9cXJDDMa0YM,18626
17
17
  teareduce/statsummary.py,sha256=VTNAnBV8z6suqiyB2Lhw3YjUUOjlmwUPX3enkOKRF54,5422
18
- teareduce/version.py,sha256=x8qFVHj0LWtGzf-qBtcwzyAnbALRdsIJ4c4C_GQISJc,419
18
+ teareduce/version.py,sha256=G40b1Oo_cwqV4sHca8L26xY3VpGjlKvOFDTnZdytrtg,419
19
19
  teareduce/wavecal.py,sha256=2MewWz5Y3ms41c305UtWOM_LaLNdk1mugDXCtzf-fSw,68586
20
20
  teareduce/write_array_to_fits.py,sha256=kWDrEH9coJ1yIu56oQJpWtDqJL4c8HGmssE9jle4e94,617
21
21
  teareduce/zscale.py,sha256=SDgmcDD2N5GvDn46JADCgTQJPBF_N_jSKMHoeTz9Nsw,1152
22
- teareduce/cleanest/__init__.py,sha256=3O-eptEAQareN504tLALMrJVQPGCuBx7YTfQGqMKjmc,205
23
- teareduce/cleanest/__main__.py,sha256=_wXGgwq62ete5-DlNDsKkZP0kATAVFVwTVrG9EspkU8,3298
22
+ teareduce/cleanest/__init__.py,sha256=hHFtV6uBx6HAI8LFUDli0urznTUzMotCW9J_4EKGCXc,293
23
+ teareduce/cleanest/__main__.py,sha256=cMzvNWNe2fuTGHLtT3hOc9-NAcMXDZUqHN0SSHWWDnc,5977
24
24
  teareduce/cleanest/centerchildparent.py,sha256=wHxOvNrrQ-KBLZAbtQ9bJAxYhGOzqYBF4LgdIQk7UF8,1285
25
- teareduce/cleanest/cleanest.py,sha256=hDC_3jF9ovHCX8IRAPe9eEGFWt_LeVI5rzS7atK7oWI,5025
26
- teareduce/cleanest/cosmicraycleanerapp.py,sha256=C-lGOREVXrlaqJsjEuJ66xVNx0z8axXq5f5GQh9dpMA,41226
27
- teareduce/cleanest/definitions.py,sha256=HLv41cuhUOUyEicRHea0LRTv4Wagm24miSxFt4cZN3g,2546
25
+ teareduce/cleanest/cosmicraycleanerapp.py,sha256=gugC79tdQFhT7plOWKqm5uTtjJCiWIeDpNmaCiE7aA4,50584
26
+ teareduce/cleanest/definitions.py,sha256=L7VXT0SAjv96zgZ_VhMMV1vcf-oxdwefvlmPclBA-ug,2558
28
27
  teareduce/cleanest/dilatemask.py,sha256=I5tHAv5VEO6V0Wed8Ar20uLt4F9P-tgjmLL5BAaFvgM,1276
29
28
  teareduce/cleanest/find_closest_true.py,sha256=mWdIvhipzAXDRKfePDrP7f0lP4U48cckpHiKwiB4jHI,1320
30
29
  teareduce/cleanest/imagedisplay.py,sha256=820Vn-Q0bJyHicOBsxDmfAZxuGOFepEEsm0LTxlPJjc,5848
30
+ teareduce/cleanest/interpolate.py,sha256=qlz4SHw89ahBRx1VHBMkd0IEpFkd96P6FVfsw_LoQDA,5194
31
31
  teareduce/cleanest/interpolation_a.py,sha256=zE4VIrC41vapf0Vx9qmh1oacw2qkJwcuMnV3JpSDW8Y,4007
32
32
  teareduce/cleanest/interpolation_x.py,sha256=D5hKbobT6lJk18XktP3PXhzEN12zqed6U18yfQ-reLQ,3744
33
33
  teareduce/cleanest/interpolation_y.py,sha256=O6yw5nKKlTdV6qsP1Ku6CwGhXB6o3j0_YQirXyILi8c,3759
34
- teareduce/cleanest/interpolationeditor.py,sha256=EksuNwKOM79MyGlAgxb9j-v7pIFXV8QOcyZDLVQlQjU,13221
35
- teareduce/cleanest/modalprogressbar.py,sha256=9anz3js_y_9JLjPyA7XxEsLpVwuMN_CTlnQnhf7m3uo,6355
36
- teareduce/cleanest/parametereditor.py,sha256=IdaX6_VPicwnYw3dvRA0tKoTZYb6Gp-AZB5ksFClv1o,13390
37
- teareduce/cleanest/reviewcosmicray.py,sha256=_qO9ifkAMqB4Wuvq2wIYZ8_DUniB2lWcxn9l404hXnY,28210
34
+ teareduce/cleanest/interpolationeditor.py,sha256=J5vrioLTwUSMbUeY0ezqNqm3CvLJgKFe7gvZ44Z62sc,13257
35
+ teareduce/cleanest/lacosmicpad.py,sha256=xWaxifoF_B82Gm5czuJmL7Y4Cv0W___LlvcpH633bAo,2047
36
+ teareduce/cleanest/mergemasks.py,sha256=1Vq6Wqn6DClxSAvHwy6QrJGszA8nkmoMHITyprSykHI,2072
37
+ teareduce/cleanest/modalprogressbar.py,sha256=uwd-p92PvOVJnbXd-B8DRcBZ--keKpr4ZN9PLeqm1Ws,6449
38
+ teareduce/cleanest/parametereditor.py,sha256=_0baFbkIUcPyF-teFIRMt1MpA4IzDZc7VHNh1h0qN4A,14229
39
+ teareduce/cleanest/reviewcosmicray.py,sha256=yTY9vNyYSuh_AZsIJAEtJ50mgIKhjgTe-LQob4xQx9U,29497
38
40
  teareduce/cookbook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
41
  teareduce/cookbook/get_cookbook_file.py,sha256=vde-iNii2lm1QII8GmLRsFsKNxkdsd7njCBE-8Z7io0,1088
40
42
  teareduce/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- teareduce/tests/test_cleanest.py,sha256=0uiun1Uloh3rt5ppkv1aG04KUbOJvYZ9C5BvqNizLuI,5562
43
+ teareduce/tests/test_cleanest.py,sha256=6dRqkw1RQMKsFrC8cEweMvTD6wXhiDv3P4PS57-HEqI,5598
42
44
  teareduce/tests/test_sliceregion.py,sha256=S7Zoh2eEBFIEbfsXgWBEMCf7pottjw2oLhqlZJQkAwg,3785
43
45
  teareduce/tests/test_version.py,sha256=mKLnbXyvVNc1pATq5PxR8qeoFMPAFL_GilFV6IHLOi0,172
44
- teareduce-0.5.5.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
45
- teareduce-0.5.5.dist-info/METADATA,sha256=MIifKnlyWPEQyuxUG3OaqqRnydUaE7JYhcBIB-XDu7Q,3113
46
- teareduce-0.5.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- teareduce-0.5.5.dist-info/entry_points.txt,sha256=6yBvig5jTL2ugqz5SF767AiszzrHKGRASsX1II84kqA,66
48
- teareduce-0.5.5.dist-info/top_level.txt,sha256=7OkwtX9zNRkGJ7ACgjk4ESgC74qUYcS5O2qcO0v-Si4,10
49
- teareduce-0.5.5.dist-info/RECORD,,
46
+ teareduce-0.5.8.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
47
+ teareduce-0.5.8.dist-info/METADATA,sha256=Xm0x4wiGEoHvS_3tIk7gmaLHZbN0HDuN_Iec_wdQg84,3137
48
+ teareduce-0.5.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
49
+ teareduce-0.5.8.dist-info/entry_points.txt,sha256=6yBvig5jTL2ugqz5SF767AiszzrHKGRASsX1II84kqA,66
50
+ teareduce-0.5.8.dist-info/top_level.txt,sha256=7OkwtX9zNRkGJ7ACgjk4ESgC74qUYcS5O2qcO0v-Si4,10
51
+ teareduce-0.5.8.dist-info/RECORD,,