teareduce 0.5.4__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
@@ -137,10 +169,17 @@ class CosmicRayCleanerApp(ImageDisplay):
137
169
  """
138
170
  self.root = root
139
171
  # self.root.geometry("800x800+50+0") # This does not work in Fedora
140
- self.root.minsize(800, 800)
172
+ self.width = width
173
+ self.height = height
174
+ self.root.minsize(self.width, self.height)
141
175
  self.root.update_idletasks()
142
- self.root.geometry("+50+0")
143
176
  self.root.title("Cosmic Ray Cleaner")
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
+ )
144
183
  self.lacosmic_params = lacosmic_default_dict.copy()
145
184
  self.input_fits = input_fits
146
185
  self.extension = extension
@@ -178,21 +217,21 @@ class CosmicRayCleanerApp(ImageDisplay):
178
217
  The loaded data is stored in `self.data` and `self.auxdata` attributes.
179
218
  """
180
219
  try:
181
- with fits.open(self.input_fits, mode='readonly') as hdul:
220
+ with fits.open(self.input_fits, mode="readonly") as hdul:
182
221
  self.data = hdul[self.extension].data
183
- if 'CRMASK' in hdul:
184
- self.mask_fixed = hdul['CRMASK'].data.astype(bool)
222
+ if "CRMASK" in hdul:
223
+ self.mask_fixed = hdul["CRMASK"].data.astype(bool)
185
224
  else:
186
225
  self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
187
226
  except Exception as e:
188
227
  print(f"Error loading FITS file: {e}")
189
228
  self.mask_crfound = np.zeros(self.data.shape, dtype=bool)
190
229
  naxis2, naxis1 = self.data.shape
191
- self.region = SliceRegion2D(f'[1:{naxis1}, 1:{naxis2}]', mode='fits').python
230
+ self.region = SliceRegion2D(f"[1:{naxis1}, 1:{naxis2}]", mode="fits").python
192
231
  # Read auxiliary file if provided
193
232
  if self.auxfile is not None:
194
233
  try:
195
- with fits.open(self.auxfile, mode='readonly') as hdul_aux:
234
+ with fits.open(self.auxfile, mode="readonly") as hdul_aux:
196
235
  self.auxdata = hdul_aux[self.extension_auxfile].data
197
236
  if self.auxdata.shape != self.data.shape:
198
237
  print(f"data shape...: {self.data.shape}")
@@ -229,15 +268,15 @@ class CosmicRayCleanerApp(ImageDisplay):
229
268
  title="Save cleaned FITS file",
230
269
  defaultextension=".fits",
231
270
  filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
232
- initialfile=suggested_name
271
+ initialfile=suggested_name,
233
272
  )
234
273
  try:
235
- with fits.open(self.input_fits, mode='readonly') as hdul:
274
+ with fits.open(self.input_fits, mode="readonly") as hdul:
236
275
  hdul[self.extension].data = self.data
237
- if 'CRMASK' in hdul:
238
- hdul['CRMASK'].data = self.mask_fixed.astype(np.uint8)
276
+ if "CRMASK" in hdul:
277
+ hdul["CRMASK"].data = self.mask_fixed.astype(np.uint8)
239
278
  else:
240
- 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")
241
280
  hdul.append(crmask_hdu)
242
281
  hdul.writeto(output_fits, overwrite=True)
243
282
  print(f"Cleaned data saved to {output_fits}")
@@ -270,18 +309,22 @@ class CosmicRayCleanerApp(ImageDisplay):
270
309
  self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
271
310
  self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
272
311
  if self.overplot_cr_pixels:
273
- self.overplot_cr_button = tk.Button(self.button_frame1, text="CR overlay: On",
274
- command=self.toggle_cr_overlay)
312
+ self.overplot_cr_button = tk.Button(
313
+ self.button_frame1, text="CR overlay: On", command=self.toggle_cr_overlay
314
+ )
275
315
  else:
276
- self.overplot_cr_button = tk.Button(self.button_frame1, text="CR overlay: Off",
277
- 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
+ )
278
319
  self.overplot_cr_button.pack(side=tk.LEFT, padx=5)
279
- self.apply_lacosmic_button = tk.Button(self.button_frame1, text="Replace all detected CRs",
280
- command=self.apply_lacosmic)
320
+ self.apply_lacosmic_button = tk.Button(
321
+ self.button_frame1, text="Replace detected CRs", command=self.apply_lacosmic
322
+ )
281
323
  self.apply_lacosmic_button.pack(side=tk.LEFT, padx=5)
282
324
  self.apply_lacosmic_button.config(state=tk.DISABLED) # Initially disabled
283
- self.examine_detected_cr_button = tk.Button(self.button_frame1, text="Examine detected CRs",
284
- command=lambda: self.examine_detected_cr(1))
325
+ self.examine_detected_cr_button = tk.Button(
326
+ self.button_frame1, text="Examine detected CRs", command=lambda: self.examine_detected_cr(1)
327
+ )
285
328
  self.examine_detected_cr_button.pack(side=tk.LEFT, padx=5)
286
329
  self.examine_detected_cr_button.config(state=tk.DISABLED) # Initially disabled
287
330
 
@@ -308,16 +351,22 @@ class CosmicRayCleanerApp(ImageDisplay):
308
351
  self.set_zscale_button.pack(side=tk.LEFT, padx=5)
309
352
 
310
353
  # Figure
311
- self.fig, self.ax = plt.subplots(figsize=(7, 5.5))
312
- self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
313
- self.canvas.get_tk_widget().pack(padx=5, pady=5)
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)
314
365
  # The next two instructions prevent a segmentation fault when pressing "q"
315
366
  self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
316
367
  self.canvas.mpl_connect("key_press_event", self.on_key)
317
368
  self.canvas.mpl_connect("button_press_event", self.on_click)
318
369
  canvas_widget = self.canvas.get_tk_widget()
319
- # canvas_widget.pack(fill=tk.BOTH, expand=True) # This does not work in Fedora
320
- canvas_widget.pack(expand=True)
321
370
 
322
371
  # Matplotlib toolbar
323
372
  self.toolbar_frame = tk.Frame(self.root)
@@ -326,13 +375,20 @@ class CosmicRayCleanerApp(ImageDisplay):
326
375
  self.toolbar.update()
327
376
 
328
377
  # update the image display
329
- xlabel = 'X pixel (from 1 to NAXIS1)'
330
- ylabel = 'Y pixel (from 1 to NAXIS2)'
378
+ xlabel = "X pixel (from 1 to NAXIS1)"
379
+ ylabel = "Y pixel (from 1 to NAXIS2)"
331
380
  extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
332
- self.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
333
- title=os.path.basename(self.input_fits),
334
- xlabel=xlabel, ylabel=ylabel,
335
- extent=extent)
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
+ )
336
392
  self.fig.tight_layout()
337
393
 
338
394
  def run_lacosmic(self):
@@ -340,15 +396,16 @@ class CosmicRayCleanerApp(ImageDisplay):
340
396
  self.run_lacosmic_button.config(state=tk.DISABLED)
341
397
  # Define parameters for L.A.Cosmic from default dictionary
342
398
  editor_window = tk.Toplevel(self.root)
399
+ center_on_parent(child=editor_window, parent=self.root)
343
400
  editor = ParameterEditor(
344
401
  root=editor_window,
345
402
  param_dict=self.lacosmic_params,
346
- window_title='Cosmic Ray Mask Generation Parameters',
403
+ window_title="Cosmic Ray Mask Generation Parameters",
347
404
  xmin=self.last_xmin,
348
405
  xmax=self.last_xmax,
349
406
  ymin=self.last_ymin,
350
407
  ymax=self.last_ymax,
351
- imgshape=self.data.shape
408
+ imgshape=self.data.shape,
352
409
  )
353
410
  # Make it modal (blocks interaction with main window)
354
411
  editor_window.transient(self.root)
@@ -359,12 +416,13 @@ class CosmicRayCleanerApp(ImageDisplay):
359
416
  updated_params = editor.get_result()
360
417
  if updated_params is not None:
361
418
  # Update last used region values
362
- self.last_xmin = updated_params['xmin']['value']
363
- self.last_xmax = updated_params['xmax']['value']
364
- self.last_ymin = updated_params['ymin']['value']
365
- self.last_ymax = updated_params['ymax']['value']
366
- usefulregion = SliceRegion2D(f"[{self.last_xmin}:{self.last_xmax},{self.last_ymin}:{self.last_ymax}]",
367
- mode="fits").python
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
368
426
  usefulmask = np.zeros_like(self.data)
369
427
  usefulmask[usefulregion] = 1.0
370
428
  # Update parameter dictionary with new values
@@ -372,34 +430,42 @@ class CosmicRayCleanerApp(ImageDisplay):
372
430
  print("Parameters updated:")
373
431
  for key, info in self.lacosmic_params.items():
374
432
  print(f" {key}: {info['value']}")
375
- if self.lacosmic_params['nruns']['value'] not in [1, 2]:
433
+ if self.lacosmic_params["nruns"]["value"] not in [1, 2]:
376
434
  raise ValueError("nruns must be 1 or 2")
377
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")
378
439
  cleandata_lacosmic, mask_crfound = cosmicray_lacosmic(
379
- self.data,
380
- gain=self.lacosmic_params['run1_gain']['value'],
381
- readnoise=self.lacosmic_params['run1_readnoise']['value'],
382
- sigclip=self.lacosmic_params['run1_sigclip']['value'],
383
- sigfrac=self.lacosmic_params['run1_sigfrac']['value'],
384
- objlim=self.lacosmic_params['run1_objlim']['value'],
385
- niter=self.lacosmic_params['run1_niter']['value'],
386
- verbose=self.lacosmic_params['run1_verbose']['value']
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"],
387
448
  )
449
+ cleandata_lacosmic = cleandata_lacosmic[borderpadd:-borderpadd, borderpadd:-borderpadd]
450
+ mask_crfound = mask_crfound[borderpadd:-borderpadd, borderpadd:-borderpadd]
388
451
  # Apply usefulmask to consider only selected region
389
452
  cleandata_lacosmic *= usefulmask
390
453
  mask_crfound = mask_crfound & (usefulmask.astype(bool))
391
454
  # Second execution if nruns == 2
392
- 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]")
393
457
  cleandata_lacosmic2, mask_crfound2 = cosmicray_lacosmic(
394
- self.data,
395
- gain=self.lacosmic_params['run2_gain']['value'],
396
- readnoise=self.lacosmic_params['run2_readnoise']['value'],
397
- sigclip=self.lacosmic_params['run2_sigclip']['value'],
398
- sigfrac=self.lacosmic_params['run2_sigfrac']['value'],
399
- objlim=self.lacosmic_params['run2_objlim']['value'],
400
- niter=self.lacosmic_params['run2_niter']['value'],
401
- verbose=self.lacosmic_params['run2_verbose']['value']
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"],
402
466
  )
467
+ cleandata_lacosmic2 = cleandata_lacosmic2[borderpadd:-borderpadd, borderpadd:-borderpadd]
468
+ mask_crfound2 = mask_crfound2[borderpadd:-borderpadd, borderpadd:-borderpadd]
403
469
  # Apply usefulmask to consider only selected region
404
470
  cleandata_lacosmic2 *= usefulmask
405
471
  mask_crfound2 = mask_crfound2 & (usefulmask.astype(bool))
@@ -420,7 +486,7 @@ class CosmicRayCleanerApp(ImageDisplay):
420
486
  for icr in np.unique(cr_labels2_preserved):
421
487
  if icr > 0:
422
488
  mask_crfound[cr_labels2 == icr] = True
423
- 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)}")
424
490
  # Use the cleandata from the second run
425
491
  cleandata_lacosmic = cleandata_lacosmic2
426
492
  # Select the image region to process
@@ -431,29 +497,31 @@ class CosmicRayCleanerApp(ImageDisplay):
431
497
  # Process the mask: dilation and labeling
432
498
  if np.any(self.mask_crfound):
433
499
  num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
434
- dilation = self.lacosmic_params['dilation']['value']
500
+ dilation = self.lacosmic_params["dilation"]["value"]
435
501
  if dilation > 0:
436
502
  # Dilate the mask by the specified number of pixels
437
503
  self.mask_crfound = dilatemask(
438
- mask=self.mask_crfound,
439
- iterations=self.lacosmic_params['dilation']['value'],
440
- connectivity=1
504
+ mask=self.mask_crfound, iterations=self.lacosmic_params["dilation"]["value"], connectivity=1
441
505
  )
442
506
  num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
443
507
  sdum = str(num_cr_pixels_after_dilation)
444
508
  else:
445
509
  sdum = str(num_cr_pixels_before_dilation)
446
- print("Number of cosmic ray pixels detected by L.A.Cosmic: "
447
- f"{num_cr_pixels_before_dilation:{len(sdum)}}")
510
+ print(
511
+ "Number of cosmic ray pixels detected by L.A.Cosmic: "
512
+ f"{num_cr_pixels_before_dilation:>{len(sdum)}}"
513
+ )
448
514
  if dilation > 0:
449
- print(f"Number of cosmic ray pixels after dilation........: "
450
- 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
+ )
451
519
  # Label connected components in the mask; note that by default,
452
520
  # structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
453
521
  # diagonal connections too, so we define a 3x3 square.
454
522
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
455
523
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
456
- print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features:>{len(sdum)}}")
524
+ print(f"Number of cosmic ray features (grouped pixels)....: {self.num_features:>{len(sdum)}}")
457
525
  self.apply_lacosmic_button.config(state=tk.NORMAL)
458
526
  self.examine_detected_cr_button.config(state=tk.NORMAL)
459
527
  self.update_cr_overlay()
@@ -480,16 +548,16 @@ class CosmicRayCleanerApp(ImageDisplay):
480
548
  """Update the overlay of cosmic ray pixels on the image."""
481
549
  if self.overplot_cr_pixels:
482
550
  # Remove previous CR pixel overlay (if any)
483
- if hasattr(self, 'scatter_cr'):
551
+ if hasattr(self, "scatter_cr"):
484
552
  self.scatter_cr.remove()
485
553
  del self.scatter_cr
486
554
  # Overlay CR pixels in red
487
555
  if np.any(self.mask_crfound):
488
556
  y_indices, x_indices = np.where(self.mask_crfound)
489
- self.scatter_cr = self.ax.scatter(x_indices + 1, y_indices + 1, s=1, c='red', marker='o')
557
+ self.scatter_cr = self.ax.scatter(x_indices + 1, y_indices + 1, s=1, c="red", marker="o")
490
558
  else:
491
559
  # Remove CR pixel overlay
492
- if hasattr(self, 'scatter_cr'):
560
+ if hasattr(self, "scatter_cr"):
493
561
  self.scatter_cr.remove()
494
562
  del self.scatter_cr
495
563
  self.canvas.draw_idle()
@@ -498,21 +566,25 @@ class CosmicRayCleanerApp(ImageDisplay):
498
566
  """Apply the selected cleaning method to the detected cosmic rays."""
499
567
  if np.any(self.mask_crfound):
500
568
  # recalculate labels and number of features
501
- structure = [[1, 1, 1],
502
- [1, 1, 1],
503
- [1, 1, 1]]
569
+ structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
504
570
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
505
571
  sdum = str(np.sum(self.mask_crfound))
506
572
  print(f"Number of cosmic ray pixels detected by L.A.Cosmic...........: {sdum}")
507
573
  print(f"Number of cosmic rays (grouped pixels) detected by L.A.Cosmic: {self.num_features:>{len(sdum)}}")
508
574
  # Define parameters for L.A.Cosmic from default dictionary
509
575
  editor_window = tk.Toplevel(self.root)
576
+ center_on_parent(child=editor_window, parent=self.root)
510
577
  editor = InterpolationEditor(
511
578
  root=editor_window,
512
- last_dilation=self.lacosmic_params['dilation']['value'],
579
+ last_dilation=self.lacosmic_params["dilation"]["value"],
513
580
  last_npoints=self.last_npoints,
514
581
  last_degree=self.last_degree,
515
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,
516
588
  )
517
589
  # Make it modal (blocks interaction with main window)
518
590
  editor_window.transient(self.root)
@@ -521,98 +593,119 @@ class CosmicRayCleanerApp(ImageDisplay):
521
593
  self.root.wait_window(editor_window)
522
594
  # Get the result after window closes
523
595
  cleaning_method = editor.cleaning_method
524
- num_cr_cleaned = 0
525
596
  if cleaning_method is None:
526
597
  print("Interpolation method selection cancelled. No cleaning applied!")
527
598
  return
528
599
  self.last_npoints = editor.npoints
529
600
  self.last_degree = editor.degree
530
- if cleaning_method == 'lacosmic':
531
- # Replace all detected CR pixels with L.A.Cosmic values
532
- self.data[self.mask_crfound] = self.cleandata_lacosmic[self.mask_crfound]
533
- # update mask_fixed to include the newly fixed pixels
534
- self.mask_fixed[self.mask_crfound] = True
535
- # upate mask_crfound by eliminating the cleaned pixels
536
- self.mask_crfound[self.mask_crfound] = False
537
- num_cr_cleaned = self.num_features
538
- elif cleaning_method == 'auxdata':
539
- if self.auxdata is None:
540
- print("No auxiliary data available. Cleaning skipped!")
541
- return
542
- # Replace all detected CR pixels with auxiliary data values
543
- self.data[self.mask_crfound] = self.auxdata[self.mask_crfound]
544
- # update mask_fixed to include the newly fixed pixels
545
- self.mask_fixed[self.mask_crfound] = True
546
- # upate mask_crfound by eliminating the cleaned pixels
547
- self.mask_crfound[self.mask_crfound] = False
548
- num_cr_cleaned = self.num_features
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.")
549
697
  else:
550
- for i in tqdm(range(1, self.num_features + 1)):
551
- tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
552
- if cleaning_method == 'x':
553
- interpolation_performed, _, _ = interpolation_x(
554
- data=self.data,
555
- mask_fixed=tmp_mask_fixed,
556
- cr_labels=self.cr_labels,
557
- cr_index=i,
558
- npoints=editor.npoints,
559
- degree=editor.degree
560
- )
561
- elif cleaning_method == 'y':
562
- interpolation_performed, _, _ = interpolation_y(
563
- data=self.data,
564
- mask_fixed=tmp_mask_fixed,
565
- cr_labels=self.cr_labels,
566
- cr_index=i,
567
- npoints=editor.npoints,
568
- degree=editor.degree
569
- )
570
- elif cleaning_method == 'a-plane':
571
- interpolation_performed, _, _ = interpolation_a(
572
- data=self.data,
573
- mask_fixed=tmp_mask_fixed,
574
- cr_labels=self.cr_labels,
575
- cr_index=i,
576
- npoints=editor.npoints,
577
- method='surface'
578
- )
579
- elif cleaning_method == 'a-median':
580
- interpolation_performed, _, _ = interpolation_a(
581
- data=self.data,
582
- mask_fixed=tmp_mask_fixed,
583
- cr_labels=self.cr_labels,
584
- cr_index=i,
585
- npoints=editor.npoints,
586
- method='median'
587
- )
588
- elif cleaning_method == 'a-mean':
589
- interpolation_performed, _, _ = interpolation_a(
590
- data=self.data,
591
- mask_fixed=tmp_mask_fixed,
592
- cr_labels=self.cr_labels,
593
- cr_index=i,
594
- npoints=editor.npoints,
595
- method='mean'
596
- )
597
- else:
598
- raise ValueError(f"Unknown cleaning method: {cleaning_method}")
599
- if interpolation_performed:
600
- num_cr_cleaned += 1
601
- # update mask_fixed to include the newly fixed pixels
602
- self.mask_fixed[tmp_mask_fixed] = True
603
- # upate mask_crfound by eliminating the cleaned pixels
604
- self.mask_crfound[tmp_mask_fixed] = False
605
- print(f"Number of cosmic rays identified and cleaned: {num_cr_cleaned}")
698
+ print("No cosmic ray pixels cleaned.")
606
699
  # recalculate labels and number of features
607
700
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
608
701
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
609
702
  sdum = str(np.sum(self.mask_crfound))
610
- print(f"Remaining number of cosmic ray pixels...........: {sdum}")
611
- print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
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)}}")
612
705
  # redraw image to show the changes
613
706
  self.image.set_data(self.data)
614
707
  self.canvas.draw_idle()
615
- if num_cr_cleaned > 0:
708
+ if data_has_been_modified:
616
709
  self.save_button.config(state=tk.NORMAL)
617
710
  if self.num_features == 0:
618
711
  self.examine_detected_cr_button.config(state=tk.DISABLED)
@@ -623,6 +716,7 @@ class CosmicRayCleanerApp(ImageDisplay):
623
716
  """Open a window to examine and possibly clean detected cosmic rays."""
624
717
  self.working_in_review_window = True
625
718
  review_window = tk.Toplevel(self.root)
719
+ center_on_parent(child=review_window, parent=self.root)
626
720
  if ixpix is not None and iypix is not None:
627
721
  # select single pixel based on provided coordinates
628
722
  tmp_cr_labels = np.zeros_like(self.data, dtype=int)
@@ -636,9 +730,9 @@ class CosmicRayCleanerApp(ImageDisplay):
636
730
  num_features=1,
637
731
  first_cr_index=1,
638
732
  single_cr=True,
639
- last_dilation=self.lacosmic_params['dilation']['value'],
733
+ last_dilation=self.lacosmic_params["dilation"]["value"],
640
734
  last_npoints=self.last_npoints,
641
- last_degree=self.last_degree
735
+ last_degree=self.last_degree,
642
736
  )
643
737
  else:
644
738
  review = ReviewCosmicRay(
@@ -650,9 +744,9 @@ class CosmicRayCleanerApp(ImageDisplay):
650
744
  num_features=self.num_features,
651
745
  first_cr_index=first_cr_index,
652
746
  single_cr=single_cr,
653
- last_dilation=self.lacosmic_params['dilation']['value'],
747
+ last_dilation=self.lacosmic_params["dilation"]["value"],
654
748
  last_npoints=self.last_npoints,
655
- last_degree=self.last_degree
749
+ last_degree=self.last_degree,
656
750
  )
657
751
  # Make it modal (blocks interaction with main window)
658
752
  review_window.transient(self.root)
@@ -672,8 +766,8 @@ class CosmicRayCleanerApp(ImageDisplay):
672
766
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
673
767
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
674
768
  sdum = str(np.sum(self.mask_crfound))
675
- print(f"Remaining number of cosmic ray pixels...........: {sdum}")
676
- print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
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)}}")
677
771
  # redraw image to show the changes
678
772
  self.image.set_data(self.data)
679
773
  self.canvas.draw_idle()
@@ -687,12 +781,10 @@ class CosmicRayCleanerApp(ImageDisplay):
687
781
  def stop_app(self):
688
782
  """Stop the application, prompting to save if there are unsaved changes."""
689
783
  proceed_with_stop = True
690
- if self.save_button['state'] == tk.NORMAL:
784
+ if self.save_button["state"] == tk.NORMAL:
691
785
  print("Warning: There are unsaved changes!")
692
786
  proceed_with_stop = messagebox.askyesno(
693
- "Unsaved Changes",
694
- "You have unsaved changes.\nDo you really want to quit?",
695
- default=messagebox.NO
787
+ "Unsaved Changes", "You have unsaved changes.\nDo you really want to quit?", default=messagebox.NO
696
788
  )
697
789
  if proceed_with_stop:
698
790
  self.root.quit()
@@ -700,11 +792,11 @@ class CosmicRayCleanerApp(ImageDisplay):
700
792
 
701
793
  def on_key(self, event):
702
794
  """Handle key press events."""
703
- if event.key == 'q':
795
+ if event.key == "q":
704
796
  pass # Ignore the "q" key to prevent closing the window
705
- elif event.key == ',':
797
+ elif event.key == ",":
706
798
  self.set_minmax()
707
- elif event.key == '/':
799
+ elif event.key == "/":
708
800
  self.set_zscale()
709
801
  else:
710
802
  print(f"Key pressed: {event.key}")
@@ -753,8 +845,8 @@ class CosmicRayCleanerApp(ImageDisplay):
753
845
  imin = (iy - 1) - semiwidth if (iy - 1) - semiwidth >= 0 else 0
754
846
  imax = (iy - 1) + semiwidth if (iy - 1) + semiwidth < self.data.shape[0] else self.data.shape[0] - 1
755
847
  ijmax = np.unravel_index(
756
- np.argmax(self.data[imin:imax+1, jmin:jmax+1]),
757
- self.data[imin:imax+1, jmin:jmax+1].shape
848
+ np.argmax(self.data[imin : imax + 1, jmin : jmax + 1]),
849
+ self.data[imin : imax + 1, jmin : jmax + 1].shape,
758
850
  )
759
851
  ixpix = ijmax[1] + jmin + 1
760
852
  iypix = ijmax[0] + imin + 1