teareduce 0.5.3__py3-none-any.whl → 0.5.5__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,6 +11,7 @@
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
15
16
  import sys
16
17
 
@@ -23,12 +24,16 @@ from scipy import ndimage
23
24
  import numpy as np
24
25
  import os
25
26
  from rich import print
26
- from tqdm import tqdm
27
27
 
28
+ from .centerchildparent import center_on_parent
28
29
  from .definitions import lacosmic_default_dict
29
30
  from .definitions import DEFAULT_NPOINTS_INTERP
30
31
  from .definitions import DEFAULT_DEGREE_INTERP
31
32
  from .definitions import MAX_PIXEL_DISTANCE_TO_CR
33
+ from .definitions import DEFAULT_TK_WINDOW_SIZE_X
34
+ from .definitions import DEFAULT_TK_WINDOW_SIZE_Y
35
+ from .definitions import DEFAULT_FONT_FAMILY
36
+ from .definitions import DEFAULT_FONT_SIZE
32
37
  from .dilatemask import dilatemask
33
38
  from .find_closest_true import find_closest_true
34
39
  from .interpolation_a import interpolation_a
@@ -38,19 +43,32 @@ from .interpolationeditor import InterpolationEditor
38
43
  from .imagedisplay import ImageDisplay
39
44
  from .parametereditor import ParameterEditor
40
45
  from .reviewcosmicray import ReviewCosmicRay
46
+ from .modalprogressbar import ModalProgressBar
41
47
 
42
48
  from ..imshow import imshow
43
49
  from ..sliceregion import SliceRegion2D
44
50
  from ..zscale import zscale
45
51
 
46
52
  import matplotlib
53
+
47
54
  matplotlib.use("TkAgg")
48
55
 
49
56
 
50
57
  class CosmicRayCleanerApp(ImageDisplay):
51
58
  """Main application class for cosmic ray cleaning."""
52
59
 
53
- def __init__(self, root, input_fits, extension=0, auxfile=None, extension_auxfile=0):
60
+ def __init__(
61
+ self,
62
+ root,
63
+ input_fits,
64
+ extension=0,
65
+ auxfile=None,
66
+ extension_auxfile=0,
67
+ fontfamily=DEFAULT_FONT_FAMILY,
68
+ fontsize=DEFAULT_FONT_SIZE,
69
+ width=DEFAULT_TK_WINDOW_SIZE_X,
70
+ height=DEFAULT_TK_WINDOW_SIZE_Y,
71
+ ):
54
72
  """
55
73
  Initialize the application.
56
74
 
@@ -66,6 +84,10 @@ class CosmicRayCleanerApp(ImageDisplay):
66
84
  Path to an auxiliary FITS file (default is None).
67
85
  extension_auxfile : int, optional
68
86
  FITS extension for auxiliary file (default is 0).
87
+ fontfamily : str, optional
88
+ Font family for the GUI (default is "Helvetica").
89
+ fontsize : int, optional
90
+ Font size for the GUI (default is 14).
69
91
 
70
92
  Methods
71
93
  -------
@@ -96,6 +118,12 @@ class CosmicRayCleanerApp(ImageDisplay):
96
118
  ----------
97
119
  root : tk.Tk
98
120
  The main Tkinter window.
121
+ fontfamily : str
122
+ Font family for the GUI.
123
+ fontsize : int
124
+ Font size for the GUI.
125
+ default_font : tkfont.Font
126
+ The default font used in the GUI.
99
127
  lacosmic_params : dict
100
128
  Dictionary of L.A.Cosmic parameters.
101
129
  input_fits : str
@@ -116,12 +144,16 @@ class CosmicRayCleanerApp(ImageDisplay):
116
144
  Boolean mask of detected cosmic ray pixels.
117
145
  last_xmin : int
118
146
  Last used minimum x-coordinate for region selection.
147
+ From 1 to NAXIS1.
119
148
  last_xmax : int
120
149
  Last used maximum x-coordinate for region selection.
150
+ From 1 to NAXIS1.
121
151
  last_ymin : int
122
152
  Last used minimum y-coordinate for region selection.
153
+ From 1 to NAXIS2.
123
154
  last_ymax : int
124
155
  Last used maximum y-coordinate for region selection.
156
+ From 1 to NAXIS2.
125
157
  last_npoints : int
126
158
  Last used number of points for interpolation.
127
159
  last_degree : int
@@ -136,8 +168,18 @@ class CosmicRayCleanerApp(ImageDisplay):
136
168
  Flag to indicate if the review window is active.
137
169
  """
138
170
  self.root = root
171
+ # self.root.geometry("800x800+50+0") # This does not work in Fedora
172
+ self.width = width
173
+ self.height = height
174
+ self.root.minsize(self.width, self.height)
175
+ self.root.update_idletasks()
139
176
  self.root.title("Cosmic Ray Cleaner")
140
- self.root.geometry("800x800+50+0")
177
+ self.fontfamily = fontfamily
178
+ self.fontsize = fontsize
179
+ self.default_font = tkfont.nametofont("TkDefaultFont")
180
+ self.default_font.configure(
181
+ family=fontfamily, size=fontsize, weight="normal", slant="roman", underline=0, overstrike=0
182
+ )
141
183
  self.lacosmic_params = lacosmic_default_dict.copy()
142
184
  self.input_fits = input_fits
143
185
  self.extension = extension
@@ -175,21 +217,21 @@ class CosmicRayCleanerApp(ImageDisplay):
175
217
  The loaded data is stored in `self.data` and `self.auxdata` attributes.
176
218
  """
177
219
  try:
178
- with fits.open(self.input_fits, mode='readonly') as hdul:
220
+ with fits.open(self.input_fits, mode="readonly") as hdul:
179
221
  self.data = hdul[self.extension].data
180
- if 'CRMASK' in hdul:
181
- self.mask_fixed = hdul['CRMASK'].data.astype(bool)
222
+ if "CRMASK" in hdul:
223
+ self.mask_fixed = hdul["CRMASK"].data.astype(bool)
182
224
  else:
183
225
  self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
184
226
  except Exception as e:
185
227
  print(f"Error loading FITS file: {e}")
186
228
  self.mask_crfound = np.zeros(self.data.shape, dtype=bool)
187
229
  naxis2, naxis1 = self.data.shape
188
- self.region = SliceRegion2D(f'[1:{naxis1}, 1:{naxis2}]', mode='fits').python
230
+ self.region = SliceRegion2D(f"[1:{naxis1}, 1:{naxis2}]", mode="fits").python
189
231
  # Read auxiliary file if provided
190
232
  if self.auxfile is not None:
191
233
  try:
192
- with fits.open(self.auxfile, mode='readonly') as hdul_aux:
234
+ with fits.open(self.auxfile, mode="readonly") as hdul_aux:
193
235
  self.auxdata = hdul_aux[self.extension_auxfile].data
194
236
  if self.auxdata.shape != self.data.shape:
195
237
  print(f"data shape...: {self.data.shape}")
@@ -226,15 +268,15 @@ class CosmicRayCleanerApp(ImageDisplay):
226
268
  title="Save cleaned FITS file",
227
269
  defaultextension=".fits",
228
270
  filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
229
- initialfile=suggested_name
271
+ initialfile=suggested_name,
230
272
  )
231
273
  try:
232
- with fits.open(self.input_fits, mode='readonly') as hdul:
274
+ with fits.open(self.input_fits, mode="readonly") as hdul:
233
275
  hdul[self.extension].data = self.data
234
- if 'CRMASK' in hdul:
235
- hdul['CRMASK'].data = self.mask_fixed.astype(np.uint8)
276
+ if "CRMASK" in hdul:
277
+ hdul["CRMASK"].data = self.mask_fixed.astype(np.uint8)
236
278
  else:
237
- crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name='CRMASK')
279
+ crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name="CRMASK")
238
280
  hdul.append(crmask_hdu)
239
281
  hdul.writeto(output_fits, overwrite=True)
240
282
  print(f"Cleaned data saved to {output_fits}")
@@ -267,18 +309,22 @@ class CosmicRayCleanerApp(ImageDisplay):
267
309
  self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
268
310
  self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
269
311
  if self.overplot_cr_pixels:
270
- self.overplot_cr_button = tk.Button(self.button_frame1, text="CR overlay: On",
271
- command=self.toggle_cr_overlay)
312
+ self.overplot_cr_button = tk.Button(
313
+ self.button_frame1, text="CR overlay: On", command=self.toggle_cr_overlay
314
+ )
272
315
  else:
273
- self.overplot_cr_button = tk.Button(self.button_frame1, text="CR overlay: Off",
274
- command=self.toggle_cr_overlay)
316
+ self.overplot_cr_button = tk.Button(
317
+ self.button_frame1, text="CR overlay: Off", command=self.toggle_cr_overlay
318
+ )
275
319
  self.overplot_cr_button.pack(side=tk.LEFT, padx=5)
276
- self.apply_lacosmic_button = tk.Button(self.button_frame1, text="Replace all detected CRs",
277
- command=self.apply_lacosmic)
320
+ self.apply_lacosmic_button = tk.Button(
321
+ self.button_frame1, text="Replace detected CRs", command=self.apply_lacosmic
322
+ )
278
323
  self.apply_lacosmic_button.pack(side=tk.LEFT, padx=5)
279
324
  self.apply_lacosmic_button.config(state=tk.DISABLED) # Initially disabled
280
- self.examine_detected_cr_button = tk.Button(self.button_frame1, text="Examine detected CRs",
281
- command=lambda: self.examine_detected_cr(1))
325
+ self.examine_detected_cr_button = tk.Button(
326
+ self.button_frame1, text="Examine detected CRs", command=lambda: self.examine_detected_cr(1)
327
+ )
282
328
  self.examine_detected_cr_button.pack(side=tk.LEFT, padx=5)
283
329
  self.examine_detected_cr_button.config(state=tk.DISABLED) # Initially disabled
284
330
 
@@ -305,15 +351,22 @@ class CosmicRayCleanerApp(ImageDisplay):
305
351
  self.set_zscale_button.pack(side=tk.LEFT, padx=5)
306
352
 
307
353
  # Figure
308
- self.fig, self.ax = plt.subplots(figsize=(7, 5.5))
309
- self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
310
- self.canvas.get_tk_widget().pack(padx=5, pady=5)
354
+ self.plot_frame = tk.Frame(self.root)
355
+ self.plot_frame.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
356
+ fig_dpi = 100
357
+ image_ratio = 480 / 640 # Default image ratio
358
+ fig_width_inches = self.width / fig_dpi
359
+ fig_height_inches = self.height * image_ratio / fig_dpi
360
+ self.fig, self.ax = plt.subplots(figsize=(fig_width_inches, fig_height_inches), dpi=fig_dpi)
361
+ self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
362
+ canvas_widget = self.canvas.get_tk_widget()
363
+ canvas_widget.config(width=self.width, height=self.height * image_ratio)
364
+ canvas_widget.pack(expand=True)
311
365
  # The next two instructions prevent a segmentation fault when pressing "q"
312
366
  self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
313
367
  self.canvas.mpl_connect("key_press_event", self.on_key)
314
368
  self.canvas.mpl_connect("button_press_event", self.on_click)
315
369
  canvas_widget = self.canvas.get_tk_widget()
316
- canvas_widget.pack(fill=tk.BOTH, expand=True)
317
370
 
318
371
  # Matplotlib toolbar
319
372
  self.toolbar_frame = tk.Frame(self.root)
@@ -322,13 +375,20 @@ class CosmicRayCleanerApp(ImageDisplay):
322
375
  self.toolbar.update()
323
376
 
324
377
  # update the image display
325
- xlabel = 'X pixel (from 1 to NAXIS1)'
326
- ylabel = 'Y pixel (from 1 to NAXIS2)'
378
+ xlabel = "X pixel (from 1 to NAXIS1)"
379
+ ylabel = "Y pixel (from 1 to NAXIS2)"
327
380
  extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
328
- self.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
329
- title=os.path.basename(self.input_fits),
330
- xlabel=xlabel, ylabel=ylabel,
331
- extent=extent)
381
+ self.image, _, _ = imshow(
382
+ self.fig,
383
+ self.ax,
384
+ self.data,
385
+ vmin=vmin,
386
+ vmax=vmax,
387
+ title=os.path.basename(self.input_fits),
388
+ xlabel=xlabel,
389
+ ylabel=ylabel,
390
+ extent=extent,
391
+ )
332
392
  self.fig.tight_layout()
333
393
 
334
394
  def run_lacosmic(self):
@@ -336,15 +396,16 @@ class CosmicRayCleanerApp(ImageDisplay):
336
396
  self.run_lacosmic_button.config(state=tk.DISABLED)
337
397
  # Define parameters for L.A.Cosmic from default dictionary
338
398
  editor_window = tk.Toplevel(self.root)
399
+ center_on_parent(child=editor_window, parent=self.root)
339
400
  editor = ParameterEditor(
340
401
  root=editor_window,
341
402
  param_dict=self.lacosmic_params,
342
- window_title='Cosmic Ray Mask Generation Parameters',
403
+ window_title="Cosmic Ray Mask Generation Parameters",
343
404
  xmin=self.last_xmin,
344
405
  xmax=self.last_xmax,
345
406
  ymin=self.last_ymin,
346
407
  ymax=self.last_ymax,
347
- imgshape=self.data.shape
408
+ imgshape=self.data.shape,
348
409
  )
349
410
  # Make it modal (blocks interaction with main window)
350
411
  editor_window.transient(self.root)
@@ -355,12 +416,13 @@ class CosmicRayCleanerApp(ImageDisplay):
355
416
  updated_params = editor.get_result()
356
417
  if updated_params is not None:
357
418
  # Update last used region values
358
- self.last_xmin = updated_params['xmin']['value']
359
- self.last_xmax = updated_params['xmax']['value']
360
- self.last_ymin = updated_params['ymin']['value']
361
- self.last_ymax = updated_params['ymax']['value']
362
- usefulregion = SliceRegion2D(f"[{self.last_xmin}:{self.last_xmax},{self.last_ymin}:{self.last_ymax}]",
363
- mode="fits").python
419
+ self.last_xmin = updated_params["xmin"]["value"]
420
+ self.last_xmax = updated_params["xmax"]["value"]
421
+ self.last_ymin = updated_params["ymin"]["value"]
422
+ self.last_ymax = updated_params["ymax"]["value"]
423
+ usefulregion = SliceRegion2D(
424
+ f"[{self.last_xmin}:{self.last_xmax},{self.last_ymin}:{self.last_ymax}]", mode="fits"
425
+ ).python
364
426
  usefulmask = np.zeros_like(self.data)
365
427
  usefulmask[usefulregion] = 1.0
366
428
  # Update parameter dictionary with new values
@@ -368,34 +430,42 @@ class CosmicRayCleanerApp(ImageDisplay):
368
430
  print("Parameters updated:")
369
431
  for key, info in self.lacosmic_params.items():
370
432
  print(f" {key}: {info['value']}")
371
- if self.lacosmic_params['nruns']['value'] not in [1, 2]:
433
+ if self.lacosmic_params["nruns"]["value"] not in [1, 2]:
372
434
  raise ValueError("nruns must be 1 or 2")
373
435
  # Execute L.A.Cosmic with updated parameters
436
+ print("[bold green]Executing L.A.Cosmic (run 1)...[/bold green]")
437
+ borderpadd = updated_params["borderpadd"]["value"]
438
+ data_reflection_padded = np.pad(self.data, pad_width=borderpadd, mode="reflect")
374
439
  cleandata_lacosmic, mask_crfound = cosmicray_lacosmic(
375
- self.data,
376
- gain=self.lacosmic_params['run1_gain']['value'],
377
- readnoise=self.lacosmic_params['run1_readnoise']['value'],
378
- sigclip=self.lacosmic_params['run1_sigclip']['value'],
379
- sigfrac=self.lacosmic_params['run1_sigfrac']['value'],
380
- objlim=self.lacosmic_params['run1_objlim']['value'],
381
- niter=self.lacosmic_params['run1_niter']['value'],
382
- verbose=self.lacosmic_params['run1_verbose']['value']
440
+ ccd=data_reflection_padded,
441
+ gain=self.lacosmic_params["run1_gain"]["value"],
442
+ readnoise=self.lacosmic_params["run1_readnoise"]["value"],
443
+ sigclip=self.lacosmic_params["run1_sigclip"]["value"],
444
+ sigfrac=self.lacosmic_params["run1_sigfrac"]["value"],
445
+ objlim=self.lacosmic_params["run1_objlim"]["value"],
446
+ niter=self.lacosmic_params["run1_niter"]["value"],
447
+ verbose=self.lacosmic_params["run1_verbose"]["value"],
383
448
  )
449
+ cleandata_lacosmic = cleandata_lacosmic[borderpadd:-borderpadd, borderpadd:-borderpadd]
450
+ mask_crfound = mask_crfound[borderpadd:-borderpadd, borderpadd:-borderpadd]
384
451
  # Apply usefulmask to consider only selected region
385
452
  cleandata_lacosmic *= usefulmask
386
453
  mask_crfound = mask_crfound & (usefulmask.astype(bool))
387
454
  # Second execution if nruns == 2
388
- if self.lacosmic_params['nruns']['value'] == 2:
455
+ if self.lacosmic_params["nruns"]["value"] == 2:
456
+ print("[bold green]Executing L.A.Cosmic (run 2)...[/bold green]")
389
457
  cleandata_lacosmic2, mask_crfound2 = cosmicray_lacosmic(
390
- self.data,
391
- gain=self.lacosmic_params['run2_gain']['value'],
392
- readnoise=self.lacosmic_params['run2_readnoise']['value'],
393
- sigclip=self.lacosmic_params['run2_sigclip']['value'],
394
- sigfrac=self.lacosmic_params['run2_sigfrac']['value'],
395
- objlim=self.lacosmic_params['run2_objlim']['value'],
396
- niter=self.lacosmic_params['run2_niter']['value'],
397
- verbose=self.lacosmic_params['run2_verbose']['value']
458
+ ccd=data_reflection_padded,
459
+ gain=self.lacosmic_params["run2_gain"]["value"],
460
+ readnoise=self.lacosmic_params["run2_readnoise"]["value"],
461
+ sigclip=self.lacosmic_params["run2_sigclip"]["value"],
462
+ sigfrac=self.lacosmic_params["run2_sigfrac"]["value"],
463
+ objlim=self.lacosmic_params["run2_objlim"]["value"],
464
+ niter=self.lacosmic_params["run2_niter"]["value"],
465
+ verbose=self.lacosmic_params["run2_verbose"]["value"],
398
466
  )
467
+ cleandata_lacosmic2 = cleandata_lacosmic2[borderpadd:-borderpadd, borderpadd:-borderpadd]
468
+ mask_crfound2 = mask_crfound2[borderpadd:-borderpadd, borderpadd:-borderpadd]
399
469
  # Apply usefulmask to consider only selected region
400
470
  cleandata_lacosmic2 *= usefulmask
401
471
  mask_crfound2 = mask_crfound2 & (usefulmask.astype(bool))
@@ -416,7 +486,7 @@ class CosmicRayCleanerApp(ImageDisplay):
416
486
  for icr in np.unique(cr_labels2_preserved):
417
487
  if icr > 0:
418
488
  mask_crfound[cr_labels2 == icr] = True
419
- print(f'Number of cosmic ray pixels (run1 & run2): {np.sum(mask_crfound)}')
489
+ print(f"Number of cosmic ray pixels (run1 & run2): {np.sum(mask_crfound)}")
420
490
  # Use the cleandata from the second run
421
491
  cleandata_lacosmic = cleandata_lacosmic2
422
492
  # Select the image region to process
@@ -427,29 +497,31 @@ class CosmicRayCleanerApp(ImageDisplay):
427
497
  # Process the mask: dilation and labeling
428
498
  if np.any(self.mask_crfound):
429
499
  num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
430
- dilation = self.lacosmic_params['dilation']['value']
500
+ dilation = self.lacosmic_params["dilation"]["value"]
431
501
  if dilation > 0:
432
502
  # Dilate the mask by the specified number of pixels
433
503
  self.mask_crfound = dilatemask(
434
- mask=self.mask_crfound,
435
- iterations=self.lacosmic_params['dilation']['value'],
436
- connectivity=1
504
+ mask=self.mask_crfound, iterations=self.lacosmic_params["dilation"]["value"], connectivity=1
437
505
  )
438
506
  num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
439
507
  sdum = str(num_cr_pixels_after_dilation)
440
508
  else:
441
509
  sdum = str(num_cr_pixels_before_dilation)
442
- print("Number of cosmic ray pixels detected by L.A.Cosmic: "
443
- f"{num_cr_pixels_before_dilation:{len(sdum)}}")
510
+ print(
511
+ "Number of cosmic ray pixels detected by L.A.Cosmic: "
512
+ f"{num_cr_pixels_before_dilation:>{len(sdum)}}"
513
+ )
444
514
  if dilation > 0:
445
- print(f"Number of cosmic ray pixels after dilation........: "
446
- f"{num_cr_pixels_after_dilation:{len(sdum)}}")
515
+ print(
516
+ f"Number of cosmic ray pixels after dilation........: "
517
+ f"{num_cr_pixels_after_dilation:>{len(sdum)}}"
518
+ )
447
519
  # Label connected components in the mask; note that by default,
448
520
  # structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
449
521
  # diagonal connections too, so we define a 3x3 square.
450
522
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
451
523
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
452
- print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features:>{len(sdum)}}")
524
+ print(f"Number of cosmic ray features (grouped pixels)....: {self.num_features:>{len(sdum)}}")
453
525
  self.apply_lacosmic_button.config(state=tk.NORMAL)
454
526
  self.examine_detected_cr_button.config(state=tk.NORMAL)
455
527
  self.update_cr_overlay()
@@ -476,16 +548,16 @@ class CosmicRayCleanerApp(ImageDisplay):
476
548
  """Update the overlay of cosmic ray pixels on the image."""
477
549
  if self.overplot_cr_pixels:
478
550
  # Remove previous CR pixel overlay (if any)
479
- if hasattr(self, 'scatter_cr'):
551
+ if hasattr(self, "scatter_cr"):
480
552
  self.scatter_cr.remove()
481
553
  del self.scatter_cr
482
554
  # Overlay CR pixels in red
483
555
  if np.any(self.mask_crfound):
484
556
  y_indices, x_indices = np.where(self.mask_crfound)
485
- self.scatter_cr = self.ax.scatter(x_indices + 1, y_indices + 1, s=1, c='red', marker='o')
557
+ self.scatter_cr = self.ax.scatter(x_indices + 1, y_indices + 1, s=1, c="red", marker="o")
486
558
  else:
487
559
  # Remove CR pixel overlay
488
- if hasattr(self, 'scatter_cr'):
560
+ if hasattr(self, "scatter_cr"):
489
561
  self.scatter_cr.remove()
490
562
  del self.scatter_cr
491
563
  self.canvas.draw_idle()
@@ -494,21 +566,25 @@ class CosmicRayCleanerApp(ImageDisplay):
494
566
  """Apply the selected cleaning method to the detected cosmic rays."""
495
567
  if np.any(self.mask_crfound):
496
568
  # recalculate labels and number of features
497
- structure = [[1, 1, 1],
498
- [1, 1, 1],
499
- [1, 1, 1]]
569
+ structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
500
570
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
501
571
  sdum = str(np.sum(self.mask_crfound))
502
572
  print(f"Number of cosmic ray pixels detected by L.A.Cosmic...........: {sdum}")
503
573
  print(f"Number of cosmic rays (grouped pixels) detected by L.A.Cosmic: {self.num_features:>{len(sdum)}}")
504
574
  # Define parameters for L.A.Cosmic from default dictionary
505
575
  editor_window = tk.Toplevel(self.root)
576
+ center_on_parent(child=editor_window, parent=self.root)
506
577
  editor = InterpolationEditor(
507
578
  root=editor_window,
508
- last_dilation=self.lacosmic_params['dilation']['value'],
579
+ last_dilation=self.lacosmic_params["dilation"]["value"],
509
580
  last_npoints=self.last_npoints,
510
581
  last_degree=self.last_degree,
511
582
  auxdata=self.auxdata,
583
+ xmin=self.last_xmin,
584
+ xmax=self.last_xmax,
585
+ ymin=self.last_ymin,
586
+ ymax=self.last_ymax,
587
+ imgshape=self.data.shape,
512
588
  )
513
589
  # Make it modal (blocks interaction with main window)
514
590
  editor_window.transient(self.root)
@@ -517,98 +593,119 @@ class CosmicRayCleanerApp(ImageDisplay):
517
593
  self.root.wait_window(editor_window)
518
594
  # Get the result after window closes
519
595
  cleaning_method = editor.cleaning_method
520
- num_cr_cleaned = 0
521
596
  if cleaning_method is None:
522
597
  print("Interpolation method selection cancelled. No cleaning applied!")
523
598
  return
524
599
  self.last_npoints = editor.npoints
525
600
  self.last_degree = editor.degree
526
- if cleaning_method == 'lacosmic':
527
- # Replace all detected CR pixels with L.A.Cosmic values
528
- self.data[self.mask_crfound] = self.cleandata_lacosmic[self.mask_crfound]
529
- # update mask_fixed to include the newly fixed pixels
530
- self.mask_fixed[self.mask_crfound] = True
531
- # upate mask_crfound by eliminating the cleaned pixels
532
- self.mask_crfound[self.mask_crfound] = False
533
- num_cr_cleaned = self.num_features
534
- elif cleaning_method == 'auxdata':
535
- if self.auxdata is None:
536
- print("No auxiliary data available. Cleaning skipped!")
537
- return
538
- # Replace all detected CR pixels with auxiliary data values
539
- self.data[self.mask_crfound] = self.auxdata[self.mask_crfound]
540
- # update mask_fixed to include the newly fixed pixels
541
- self.mask_fixed[self.mask_crfound] = True
542
- # upate mask_crfound by eliminating the cleaned pixels
543
- self.mask_crfound[self.mask_crfound] = False
544
- num_cr_cleaned = self.num_features
601
+ cleaning_region = SliceRegion2D(
602
+ f"[{editor.xmin}:{editor.xmax},{editor.ymin}:{editor.ymax}]", mode="fits"
603
+ ).python
604
+ print(
605
+ "Applying cleaning method to region "
606
+ f"x=[{editor.xmin},{editor.xmax}], y=[{editor.ymin},{editor.ymax}]"
607
+ )
608
+ mask_crfound_region = np.zeros_like(self.mask_crfound, dtype=bool)
609
+ mask_crfound_region[cleaning_region] = self.mask_crfound[cleaning_region]
610
+ data_has_been_modified = False
611
+ if np.any(mask_crfound_region):
612
+ if cleaning_method == "lacosmic":
613
+ # Replace detected CR pixels with L.A.Cosmic values
614
+ self.data[mask_crfound_region] = self.cleandata_lacosmic[mask_crfound_region]
615
+ # update mask_fixed to include the newly fixed pixels
616
+ self.mask_fixed[mask_crfound_region] = True
617
+ # upate mask_crfound by eliminating the cleaned pixels
618
+ self.mask_crfound[mask_crfound_region] = False
619
+ data_has_been_modified = True
620
+ elif cleaning_method == "auxdata":
621
+ if self.auxdata is None:
622
+ print("No auxiliary data available. Cleaning skipped!")
623
+ return
624
+ # Replace detected CR pixels with auxiliary data values
625
+ self.data[mask_crfound_region] = self.auxdata[mask_crfound_region]
626
+ # update mask_fixed to include the newly fixed pixels
627
+ self.mask_fixed[mask_crfound_region] = True
628
+ # upate mask_crfound by eliminating the cleaned pixels
629
+ self.mask_crfound[mask_crfound_region] = False
630
+ data_has_been_modified = True
631
+ else:
632
+ # Determine features to process within the selected region
633
+ features_in_region = np.unique(self.cr_labels[mask_crfound_region])
634
+ with ModalProgressBar(
635
+ parent=self.root, iterable=range(1, self.num_features + 1), desc="Cleaning cosmic rays"
636
+ ) as pbar:
637
+ for i in pbar:
638
+ if i in features_in_region:
639
+ tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
640
+ if cleaning_method == "x":
641
+ interpolation_performed, _, _ = interpolation_x(
642
+ data=self.data,
643
+ mask_fixed=tmp_mask_fixed,
644
+ cr_labels=self.cr_labels,
645
+ cr_index=i,
646
+ npoints=editor.npoints,
647
+ degree=editor.degree,
648
+ )
649
+ elif cleaning_method == "y":
650
+ interpolation_performed, _, _ = interpolation_y(
651
+ data=self.data,
652
+ mask_fixed=tmp_mask_fixed,
653
+ cr_labels=self.cr_labels,
654
+ cr_index=i,
655
+ npoints=editor.npoints,
656
+ degree=editor.degree,
657
+ )
658
+ elif cleaning_method == "a-plane":
659
+ interpolation_performed, _, _ = interpolation_a(
660
+ data=self.data,
661
+ mask_fixed=tmp_mask_fixed,
662
+ cr_labels=self.cr_labels,
663
+ cr_index=i,
664
+ npoints=editor.npoints,
665
+ method="surface",
666
+ )
667
+ elif cleaning_method == "a-median":
668
+ interpolation_performed, _, _ = interpolation_a(
669
+ data=self.data,
670
+ mask_fixed=tmp_mask_fixed,
671
+ cr_labels=self.cr_labels,
672
+ cr_index=i,
673
+ npoints=editor.npoints,
674
+ method="median",
675
+ )
676
+ elif cleaning_method == "a-mean":
677
+ interpolation_performed, _, _ = interpolation_a(
678
+ data=self.data,
679
+ mask_fixed=tmp_mask_fixed,
680
+ cr_labels=self.cr_labels,
681
+ cr_index=i,
682
+ npoints=editor.npoints,
683
+ method="mean",
684
+ )
685
+ else:
686
+ raise ValueError(f"Unknown cleaning method: {cleaning_method}")
687
+ if interpolation_performed:
688
+ # update mask_fixed to include the newly fixed pixels
689
+ self.mask_fixed[tmp_mask_fixed] = True
690
+ # upate mask_crfound by eliminating the cleaned pixels
691
+ self.mask_crfound[tmp_mask_fixed] = False
692
+ # mark that data has been modified
693
+ data_has_been_modified = True
694
+ # If any pixels were cleaned, print message
695
+ if data_has_been_modified:
696
+ print("Cosmic ray cleaning applied.")
545
697
  else:
546
- for i in tqdm(range(1, self.num_features + 1)):
547
- tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
548
- if cleaning_method == 'x':
549
- interpolation_performed, _, _ = interpolation_x(
550
- data=self.data,
551
- mask_fixed=tmp_mask_fixed,
552
- cr_labels=self.cr_labels,
553
- cr_index=i,
554
- npoints=editor.npoints,
555
- degree=editor.degree
556
- )
557
- elif cleaning_method == 'y':
558
- interpolation_performed, _, _ = interpolation_y(
559
- data=self.data,
560
- mask_fixed=tmp_mask_fixed,
561
- cr_labels=self.cr_labels,
562
- cr_index=i,
563
- npoints=editor.npoints,
564
- degree=editor.degree
565
- )
566
- elif cleaning_method == 'a-plane':
567
- interpolation_performed, _, _ = interpolation_a(
568
- data=self.data,
569
- mask_fixed=tmp_mask_fixed,
570
- cr_labels=self.cr_labels,
571
- cr_index=i,
572
- npoints=editor.npoints,
573
- method='surface'
574
- )
575
- elif cleaning_method == 'a-median':
576
- interpolation_performed, _, _ = interpolation_a(
577
- data=self.data,
578
- mask_fixed=tmp_mask_fixed,
579
- cr_labels=self.cr_labels,
580
- cr_index=i,
581
- npoints=editor.npoints,
582
- method='median'
583
- )
584
- elif cleaning_method == 'a-mean':
585
- interpolation_performed, _, _ = interpolation_a(
586
- data=self.data,
587
- mask_fixed=tmp_mask_fixed,
588
- cr_labels=self.cr_labels,
589
- cr_index=i,
590
- npoints=editor.npoints,
591
- method='mean'
592
- )
593
- else:
594
- raise ValueError(f"Unknown cleaning method: {cleaning_method}")
595
- if interpolation_performed:
596
- num_cr_cleaned += 1
597
- # update mask_fixed to include the newly fixed pixels
598
- self.mask_fixed[tmp_mask_fixed] = True
599
- # upate mask_crfound by eliminating the cleaned pixels
600
- self.mask_crfound[tmp_mask_fixed] = False
601
- print(f"Number of cosmic rays identified and cleaned: {num_cr_cleaned}")
698
+ print("No cosmic ray pixels cleaned.")
602
699
  # recalculate labels and number of features
603
700
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
604
701
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
605
702
  sdum = str(np.sum(self.mask_crfound))
606
- print(f"Remaining number of cosmic ray pixels...........: {sdum}")
607
- print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
703
+ print(f"Remaining number of cosmic ray pixels...................: {sdum}")
704
+ print(f"Remaining number of cosmic ray features (grouped pixels): {self.num_features:>{len(sdum)}}")
608
705
  # redraw image to show the changes
609
706
  self.image.set_data(self.data)
610
707
  self.canvas.draw_idle()
611
- if num_cr_cleaned > 0:
708
+ if data_has_been_modified:
612
709
  self.save_button.config(state=tk.NORMAL)
613
710
  if self.num_features == 0:
614
711
  self.examine_detected_cr_button.config(state=tk.DISABLED)
@@ -619,6 +716,7 @@ class CosmicRayCleanerApp(ImageDisplay):
619
716
  """Open a window to examine and possibly clean detected cosmic rays."""
620
717
  self.working_in_review_window = True
621
718
  review_window = tk.Toplevel(self.root)
719
+ center_on_parent(child=review_window, parent=self.root)
622
720
  if ixpix is not None and iypix is not None:
623
721
  # select single pixel based on provided coordinates
624
722
  tmp_cr_labels = np.zeros_like(self.data, dtype=int)
@@ -632,9 +730,9 @@ class CosmicRayCleanerApp(ImageDisplay):
632
730
  num_features=1,
633
731
  first_cr_index=1,
634
732
  single_cr=True,
635
- last_dilation=self.lacosmic_params['dilation']['value'],
733
+ last_dilation=self.lacosmic_params["dilation"]["value"],
636
734
  last_npoints=self.last_npoints,
637
- last_degree=self.last_degree
735
+ last_degree=self.last_degree,
638
736
  )
639
737
  else:
640
738
  review = ReviewCosmicRay(
@@ -646,9 +744,9 @@ class CosmicRayCleanerApp(ImageDisplay):
646
744
  num_features=self.num_features,
647
745
  first_cr_index=first_cr_index,
648
746
  single_cr=single_cr,
649
- last_dilation=self.lacosmic_params['dilation']['value'],
747
+ last_dilation=self.lacosmic_params["dilation"]["value"],
650
748
  last_npoints=self.last_npoints,
651
- last_degree=self.last_degree
749
+ last_degree=self.last_degree,
652
750
  )
653
751
  # Make it modal (blocks interaction with main window)
654
752
  review_window.transient(self.root)
@@ -668,8 +766,8 @@ class CosmicRayCleanerApp(ImageDisplay):
668
766
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
669
767
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
670
768
  sdum = str(np.sum(self.mask_crfound))
671
- print(f"Remaining number of cosmic ray pixels...........: {sdum}")
672
- print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
769
+ print(f"Remaining number of cosmic ray pixels...................: {sdum}")
770
+ print(f"Remaining number of cosmic ray features (grouped pixels): {self.num_features:>{len(sdum)}}")
673
771
  # redraw image to show the changes
674
772
  self.image.set_data(self.data)
675
773
  self.canvas.draw_idle()
@@ -683,12 +781,10 @@ class CosmicRayCleanerApp(ImageDisplay):
683
781
  def stop_app(self):
684
782
  """Stop the application, prompting to save if there are unsaved changes."""
685
783
  proceed_with_stop = True
686
- if self.save_button['state'] == tk.NORMAL:
784
+ if self.save_button["state"] == tk.NORMAL:
687
785
  print("Warning: There are unsaved changes!")
688
786
  proceed_with_stop = messagebox.askyesno(
689
- "Unsaved Changes",
690
- "You have unsaved changes.\nDo you really want to quit?",
691
- default=messagebox.NO
787
+ "Unsaved Changes", "You have unsaved changes.\nDo you really want to quit?", default=messagebox.NO
692
788
  )
693
789
  if proceed_with_stop:
694
790
  self.root.quit()
@@ -696,11 +792,11 @@ class CosmicRayCleanerApp(ImageDisplay):
696
792
 
697
793
  def on_key(self, event):
698
794
  """Handle key press events."""
699
- if event.key == 'q':
795
+ if event.key == "q":
700
796
  pass # Ignore the "q" key to prevent closing the window
701
- elif event.key == ',':
797
+ elif event.key == ",":
702
798
  self.set_minmax()
703
- elif event.key == '/':
799
+ elif event.key == "/":
704
800
  self.set_zscale()
705
801
  else:
706
802
  print(f"Key pressed: {event.key}")
@@ -749,8 +845,8 @@ class CosmicRayCleanerApp(ImageDisplay):
749
845
  imin = (iy - 1) - semiwidth if (iy - 1) - semiwidth >= 0 else 0
750
846
  imax = (iy - 1) + semiwidth if (iy - 1) + semiwidth < self.data.shape[0] else self.data.shape[0] - 1
751
847
  ijmax = np.unravel_index(
752
- np.argmax(self.data[imin:imax+1, jmin:jmax+1]),
753
- self.data[imin:imax+1, jmin:jmax+1].shape
848
+ np.argmax(self.data[imin : imax + 1, jmin : jmax + 1]),
849
+ self.data[imin : imax + 1, jmin : jmax + 1].shape,
754
850
  )
755
851
  ixpix = ijmax[1] + jmin + 1
756
852
  iypix = ijmax[0] + imin + 1