teareduce 0.5.4__py3-none-any.whl → 0.5.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- teareduce/cleanest/__init__.py +3 -1
- teareduce/cleanest/__main__.py +119 -11
- teareduce/cleanest/centerchildparent.py +46 -0
- teareduce/cleanest/cosmicraycleanerapp.py +517 -232
- teareduce/cleanest/definitions.py +30 -29
- teareduce/cleanest/imagedisplay.py +8 -9
- teareduce/cleanest/{cleanest.py → interpolate.py} +25 -26
- teareduce/cleanest/interpolation_a.py +5 -9
- teareduce/cleanest/interpolationeditor.py +156 -35
- teareduce/cleanest/lacosmicpad.py +57 -0
- teareduce/cleanest/mergemasks.py +58 -0
- teareduce/cleanest/modalprogressbar.py +182 -0
- teareduce/cleanest/parametereditor.py +112 -79
- teareduce/cleanest/reviewcosmicray.py +90 -62
- teareduce/tests/test_cleanest.py +11 -11
- teareduce/version.py +1 -1
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/METADATA +1 -1
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/RECORD +22 -18
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/WHEEL +0 -0
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/entry_points.txt +0 -0
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/licenses/LICENSE.txt +0 -0
- {teareduce-0.5.4.dist-info → teareduce-0.5.7.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
"""Module defining a progress bar widget for Tkinter."""
|
|
11
|
+
|
|
12
|
+
import tkinter as tk
|
|
13
|
+
from tkinter import ttk
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ModalProgressBar:
|
|
18
|
+
def __init__(
|
|
19
|
+
self, parent, iterable=None, total=None, desc="Processing", completion_msg="Processing completed successfully!"
|
|
20
|
+
):
|
|
21
|
+
self.parent = parent
|
|
22
|
+
self.iterable = iterable
|
|
23
|
+
self.total = total if total is not None else (len(iterable) if iterable is not None else 100)
|
|
24
|
+
self.current = 0
|
|
25
|
+
self.start_time = time.time()
|
|
26
|
+
self.window = None
|
|
27
|
+
self.desc = desc
|
|
28
|
+
self.completion_msg = completion_msg
|
|
29
|
+
self.continue_clicked = False
|
|
30
|
+
|
|
31
|
+
def __enter__(self):
|
|
32
|
+
# Create the modal window when entering context
|
|
33
|
+
self.window = tk.Toplevel(self.parent)
|
|
34
|
+
self.window.title("Progress")
|
|
35
|
+
|
|
36
|
+
# Make it modal
|
|
37
|
+
self.window.transient(self.parent)
|
|
38
|
+
self.window.grab_set()
|
|
39
|
+
self.window.protocol("WM_DELETE_WINDOW", lambda: None)
|
|
40
|
+
|
|
41
|
+
# Set geometry and force it
|
|
42
|
+
minwinsize_x = 400
|
|
43
|
+
minwinsize_y = 120
|
|
44
|
+
self.window.minsize(minwinsize_x, minwinsize_y)
|
|
45
|
+
self.window.update_idletasks()
|
|
46
|
+
self.window.update()
|
|
47
|
+
|
|
48
|
+
# Center on parent
|
|
49
|
+
self._center_on_parent()
|
|
50
|
+
|
|
51
|
+
# UI elements
|
|
52
|
+
default_font = tk.font.nametofont("TkDefaultFont")
|
|
53
|
+
bold_font = default_font.copy()
|
|
54
|
+
bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
|
|
55
|
+
self.desc_label = tk.Label(self.window, text=self.desc, font=bold_font)
|
|
56
|
+
self.desc_label.pack(padx=10, pady=5)
|
|
57
|
+
|
|
58
|
+
self.progress = ttk.Progressbar(self.window, length=minwinsize_x - 20, mode="determinate", maximum=self.total)
|
|
59
|
+
self.progress.pack(padx=10, pady=10)
|
|
60
|
+
|
|
61
|
+
self.status_label = tk.Label(self.window, text=f"0/{self.total} (0.0%)")
|
|
62
|
+
self.status_label.pack(padx=10, pady=2)
|
|
63
|
+
|
|
64
|
+
self.time_label = tk.Label(self.window, text="Elapsed: 0s | ETA: --")
|
|
65
|
+
self.time_label.pack(padx=10, pady=2)
|
|
66
|
+
|
|
67
|
+
# Continue button (to close the dialog after completion; hidden until done)
|
|
68
|
+
self.continue_button = tk.Button(self.window, text="Continue", command=self._on_continue)
|
|
69
|
+
self.continue_button.pack(padx=10, pady=10)
|
|
70
|
+
self.continue_button.pack_forget() # Hide initially
|
|
71
|
+
|
|
72
|
+
# Force another update
|
|
73
|
+
self.window.update_idletasks()
|
|
74
|
+
self.window.update()
|
|
75
|
+
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
79
|
+
if self.window:
|
|
80
|
+
# Show completion screen instead of closing
|
|
81
|
+
self._show_completion()
|
|
82
|
+
|
|
83
|
+
# Wait for user to click Continue
|
|
84
|
+
self.window.wait_variable(self._continue_var)
|
|
85
|
+
|
|
86
|
+
# Now close
|
|
87
|
+
self._destroy()
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def __iter__(self):
|
|
91
|
+
"""Allow iteration like tqdm"""
|
|
92
|
+
if self.iterable is None:
|
|
93
|
+
raise ValueError("No iterable provided for iteration")
|
|
94
|
+
|
|
95
|
+
for item in self.iterable:
|
|
96
|
+
yield item
|
|
97
|
+
self.update(1)
|
|
98
|
+
|
|
99
|
+
def _center_on_parent(self):
|
|
100
|
+
self.window.update_idletasks()
|
|
101
|
+
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.window.winfo_width() // 2)
|
|
102
|
+
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.window.winfo_height() // 2)
|
|
103
|
+
self.window.geometry(f"+{x}+{y}")
|
|
104
|
+
|
|
105
|
+
def update(self, n=1):
|
|
106
|
+
self.current += n
|
|
107
|
+
self.progress["value"] = self.current
|
|
108
|
+
percentage = (self.current / self.total) * 100
|
|
109
|
+
|
|
110
|
+
elapsed = time.time() - self.start_time
|
|
111
|
+
|
|
112
|
+
if self.current > 0:
|
|
113
|
+
rate = self.current / elapsed
|
|
114
|
+
remaining = self.total - self.current
|
|
115
|
+
eta_seconds = remaining / rate if rate > 0 else 0
|
|
116
|
+
|
|
117
|
+
elapsed_str = self._format_time(elapsed)
|
|
118
|
+
eta_str = self._format_time(eta_seconds)
|
|
119
|
+
total_str = self._format_time(elapsed + eta_seconds)
|
|
120
|
+
rate_str = f"{rate:.2f} CR/s" if rate >= 1 else f"{1/rate:.2f} s/CR"
|
|
121
|
+
|
|
122
|
+
self.status_label.config(text=f"{self.current}/{self.total} ({percentage:.1f}%) | {rate_str}")
|
|
123
|
+
self.time_label.config(text=f"Expected Total: {total_str} | Elapsed: {elapsed_str} | ETA: {eta_str}")
|
|
124
|
+
self.window.update_idletasks()
|
|
125
|
+
self.window.update()
|
|
126
|
+
|
|
127
|
+
def _format_time(self, seconds):
|
|
128
|
+
if seconds < 60:
|
|
129
|
+
return f"{seconds:.1f} s"
|
|
130
|
+
elif seconds < 3600:
|
|
131
|
+
minutes = int(seconds // 60)
|
|
132
|
+
secs = int(seconds % 60)
|
|
133
|
+
return f"{minutes}m {secs} s"
|
|
134
|
+
else:
|
|
135
|
+
hours = int(seconds // 3600)
|
|
136
|
+
minutes = int((seconds % 3600) // 60)
|
|
137
|
+
return f"{hours}h {minutes} m"
|
|
138
|
+
|
|
139
|
+
def _show_completion(self):
|
|
140
|
+
"""Transform the window into a completion dialog"""
|
|
141
|
+
elapsed = time.time() - self.start_time
|
|
142
|
+
elapsed_str = self._format_time(elapsed)
|
|
143
|
+
|
|
144
|
+
# Update title
|
|
145
|
+
self.window.title("Completed")
|
|
146
|
+
|
|
147
|
+
# Update description to show completion message
|
|
148
|
+
default_font = tk.font.nametofont("TkDefaultFont")
|
|
149
|
+
bold_font = default_font.copy()
|
|
150
|
+
bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
|
|
151
|
+
self.desc_label.config(text=self.completion_msg, fg="green", font=bold_font)
|
|
152
|
+
|
|
153
|
+
# Hide progress bar
|
|
154
|
+
self.progress.pack_forget()
|
|
155
|
+
|
|
156
|
+
# Update status to show final stats
|
|
157
|
+
avg_rate = self.current / elapsed if elapsed > 0 else 0
|
|
158
|
+
rate_str = f"{avg_rate:.2f} CR/s" if avg_rate >= 1 else f"{1/avg_rate:.2f} s/CR"
|
|
159
|
+
self.status_label.config(text=f"Processed {self.current} CRs | {rate_str}")
|
|
160
|
+
|
|
161
|
+
# Update time label
|
|
162
|
+
self.time_label.config(text=f"Total time: {elapsed_str}")
|
|
163
|
+
|
|
164
|
+
# Show the Continue button
|
|
165
|
+
self.continue_button.pack(padx=10, pady=15)
|
|
166
|
+
|
|
167
|
+
# Create a variable to track when Continue is clicked
|
|
168
|
+
self._continue_var = tk.BooleanVar(value=False)
|
|
169
|
+
|
|
170
|
+
# Re-enable close button to work like Continue
|
|
171
|
+
self.window.protocol("WM_DELETE_WINDOW", self._on_continue)
|
|
172
|
+
|
|
173
|
+
self.window.update()
|
|
174
|
+
self._center_on_parent()
|
|
175
|
+
|
|
176
|
+
def _on_continue(self):
|
|
177
|
+
"""Called when Continue button is clicked"""
|
|
178
|
+
self._continue_var.set(True)
|
|
179
|
+
|
|
180
|
+
def _destroy(self):
|
|
181
|
+
self.window.grab_release()
|
|
182
|
+
self.window.destroy()
|
|
@@ -13,11 +13,13 @@ import tkinter as tk
|
|
|
13
13
|
from tkinter import ttk
|
|
14
14
|
from tkinter import messagebox
|
|
15
15
|
|
|
16
|
+
from .centerchildparent import center_on_parent
|
|
16
17
|
from .definitions import lacosmic_default_dict
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class ParameterEditor:
|
|
20
21
|
"""A dialog to edit L.A.Cosmic parameters."""
|
|
22
|
+
|
|
21
23
|
def __init__(self, root, param_dict, window_title, xmin, xmax, ymin, ymax, imgshape):
|
|
22
24
|
"""Initialize the parameter editor dialog.
|
|
23
25
|
|
|
@@ -31,12 +33,16 @@ class ParameterEditor:
|
|
|
31
33
|
Title of the dialog window.
|
|
32
34
|
xmin : int
|
|
33
35
|
Minimum x-coordinate of the region to be examined.
|
|
36
|
+
From 1 to NAXIS1.
|
|
34
37
|
xmax : int
|
|
35
38
|
Maximum x-coordinate of the region to be examined.
|
|
39
|
+
From 1 to NAXIS1.
|
|
36
40
|
ymin : int
|
|
37
41
|
Minimum y-coordinate of the region to be examined.
|
|
42
|
+
From 1 to NAXIS2.
|
|
38
43
|
ymax : int
|
|
39
44
|
Maximum y-coordinate of the region to be examined.
|
|
45
|
+
From 1 to NAXIS2.
|
|
40
46
|
imgshape : tuple
|
|
41
47
|
Shape of the image (height, width).
|
|
42
48
|
|
|
@@ -70,16 +76,17 @@ class ParameterEditor:
|
|
|
70
76
|
self.root.title(window_title)
|
|
71
77
|
self.param_dict = param_dict
|
|
72
78
|
# Set default region values
|
|
73
|
-
self.param_dict[
|
|
74
|
-
self.param_dict[
|
|
75
|
-
self.param_dict[
|
|
76
|
-
self.param_dict[
|
|
79
|
+
self.param_dict["xmin"]["value"] = xmin
|
|
80
|
+
self.param_dict["xmax"]["value"] = xmax
|
|
81
|
+
self.param_dict["ymin"]["value"] = ymin
|
|
82
|
+
self.param_dict["ymax"]["value"] = ymax
|
|
77
83
|
self.imgshape = imgshape
|
|
78
|
-
self.entries = {
|
|
84
|
+
self.entries = {"run1": {}, "run2": {}} # dictionary to hold entry widgets
|
|
79
85
|
self.result_dict = None
|
|
80
86
|
|
|
81
87
|
# Create the form
|
|
82
88
|
self.create_widgets()
|
|
89
|
+
center_on_parent(child=self.root, parent=self.root.master)
|
|
83
90
|
|
|
84
91
|
def create_widgets(self):
|
|
85
92
|
"""Create the widgets for the dialog."""
|
|
@@ -90,97 +97,124 @@ class ParameterEditor:
|
|
|
90
97
|
row = 0
|
|
91
98
|
|
|
92
99
|
# Subtitle for L.A.Cosmic parameters
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
default_font = tk.font.nametofont("TkDefaultFont")
|
|
101
|
+
bold_font = default_font.copy()
|
|
102
|
+
bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
|
|
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, 10))
|
|
95
105
|
row += 1
|
|
96
106
|
|
|
97
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
|
|
98
119
|
# Note: here we are using entry_vars to trace changes in the entries
|
|
99
120
|
# so that we can update the color of run2 entries if they differ from run1.
|
|
100
121
|
self.entry_vars = {}
|
|
101
122
|
for key, info in self.param_dict.items():
|
|
102
|
-
if not key.startswith(
|
|
123
|
+
if not key.startswith("run1_"):
|
|
103
124
|
continue
|
|
104
125
|
# Parameter name label
|
|
105
|
-
label = tk.Label(main_frame, text=f"{key[5:]}:", anchor=
|
|
106
|
-
label.grid(row=row, column=0, sticky=
|
|
126
|
+
label = tk.Label(main_frame, text=f"{key[5:]}:", anchor="e", width=15)
|
|
127
|
+
label.grid(row=row, column=0, sticky="w", pady=5)
|
|
107
128
|
# Entry field for run1
|
|
108
129
|
self.entry_vars[key] = tk.StringVar()
|
|
109
|
-
self.entry_vars[key].trace_add(
|
|
130
|
+
self.entry_vars[key].trace_add("write", lambda *args: self.update_colour_param_run1_run2())
|
|
110
131
|
entry = tk.Entry(main_frame, textvariable=self.entry_vars[key], width=10)
|
|
111
|
-
entry.insert(0, str(info[
|
|
132
|
+
entry.insert(0, str(info["value"]))
|
|
112
133
|
entry.grid(row=row, column=1, padx=10, pady=5)
|
|
113
134
|
self.entries[key] = entry # dictionary to hold entry widgets
|
|
114
135
|
# Entry field for run2
|
|
115
|
-
key2 =
|
|
136
|
+
key2 = "run2_" + key[5:]
|
|
116
137
|
self.entry_vars[key2] = tk.StringVar()
|
|
117
|
-
self.entry_vars[key2].trace_add(
|
|
138
|
+
self.entry_vars[key2].trace_add("write", lambda *args: self.update_colour_param_run1_run2())
|
|
118
139
|
entry = tk.Entry(main_frame, textvariable=self.entry_vars[key2], width=10)
|
|
119
|
-
entry.insert(0, str(self.param_dict[key2][
|
|
140
|
+
entry.insert(0, str(self.param_dict[key2]["value"]))
|
|
120
141
|
entry.grid(row=row, column=2, padx=10, pady=5)
|
|
121
|
-
self.entries[
|
|
142
|
+
self.entries["run2_" + key[5:]] = entry # dictionary to hold entry widgets
|
|
122
143
|
# Type label
|
|
123
|
-
type_label = tk.Label(main_frame, text=f"({info['type'].__name__})", fg=
|
|
124
|
-
type_label.grid(row=row, column=3, sticky=
|
|
144
|
+
type_label = tk.Label(main_frame, text=f"({info['type'].__name__})", fg="gray", anchor="w", width=10)
|
|
145
|
+
type_label.grid(row=row, column=3, sticky="w", pady=5)
|
|
125
146
|
row += 1
|
|
126
147
|
# self.update_colour_param_run1_run2()
|
|
127
148
|
|
|
128
149
|
# Separator
|
|
129
|
-
separator1 = ttk.Separator(main_frame, orient=
|
|
130
|
-
separator1.grid(row=row, column=0, columnspan=4, sticky=
|
|
150
|
+
separator1 = ttk.Separator(main_frame, orient="horizontal")
|
|
151
|
+
separator1.grid(row=row, column=0, columnspan=4, sticky="ew", pady=(10, 10))
|
|
131
152
|
row += 1
|
|
132
153
|
|
|
133
154
|
# Subtitle for additional parameters
|
|
134
|
-
subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=
|
|
135
|
-
subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0,
|
|
155
|
+
subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=bold_font)
|
|
156
|
+
subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 10))
|
|
136
157
|
row += 1
|
|
137
158
|
|
|
138
159
|
# Dilation label and entry
|
|
139
|
-
label = tk.Label(main_frame, text="Dilation:", anchor=
|
|
140
|
-
label.grid(row=row, column=0, sticky=
|
|
160
|
+
label = tk.Label(main_frame, text="Dilation:", anchor="e", width=15)
|
|
161
|
+
label.grid(row=row, column=0, sticky="w", pady=5)
|
|
162
|
+
entry = tk.Entry(main_frame, width=10)
|
|
163
|
+
entry.insert(0, str(self.param_dict["dilation"]["value"]))
|
|
164
|
+
entry.grid(row=row, column=1, padx=10, pady=5)
|
|
165
|
+
self.entries["dilation"] = entry
|
|
166
|
+
type_label = tk.Label(
|
|
167
|
+
main_frame, text=f"({self.param_dict['dilation']['type'].__name__})", fg="gray", anchor="w", width=10
|
|
168
|
+
)
|
|
169
|
+
type_label.grid(row=row, column=2, sticky="w", pady=5)
|
|
170
|
+
row += 1
|
|
171
|
+
|
|
172
|
+
label = tk.Label(main_frame, text="Border Padding:", anchor="e", width=15)
|
|
173
|
+
label.grid(row=row, column=0, sticky="w", pady=5)
|
|
141
174
|
entry = tk.Entry(main_frame, width=10)
|
|
142
|
-
entry.insert(0, str(self.param_dict[
|
|
175
|
+
entry.insert(0, str(self.param_dict["borderpadd"]["value"]))
|
|
143
176
|
entry.grid(row=row, column=1, padx=10, pady=5)
|
|
144
|
-
self.entries[
|
|
145
|
-
type_label = tk.Label(
|
|
146
|
-
|
|
147
|
-
|
|
177
|
+
self.entries["borderpadd"] = entry
|
|
178
|
+
type_label = tk.Label(
|
|
179
|
+
main_frame, text=f"({self.param_dict['borderpadd']['type'].__name__})", fg="gray", anchor="w", width=10
|
|
180
|
+
)
|
|
181
|
+
type_label.grid(row=row, column=2, sticky="w", pady=5)
|
|
148
182
|
row += 1
|
|
149
183
|
|
|
150
184
|
# Separator
|
|
151
|
-
separator2 = ttk.Separator(main_frame, orient=
|
|
152
|
-
separator2.grid(row=row, column=0, columnspan=4, sticky=
|
|
185
|
+
separator2 = ttk.Separator(main_frame, orient="horizontal")
|
|
186
|
+
separator2.grid(row=row, column=0, columnspan=4, sticky="ew", pady=(10, 10))
|
|
153
187
|
row += 1
|
|
154
188
|
|
|
155
189
|
# Subtitle for region to be examined
|
|
156
|
-
subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=
|
|
157
|
-
subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0,
|
|
190
|
+
subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=bold_font)
|
|
191
|
+
subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 10))
|
|
158
192
|
row += 1
|
|
159
193
|
|
|
160
194
|
# Region to be examined label and entries
|
|
161
195
|
for key, info in self.param_dict.items():
|
|
162
|
-
if key.lower() in [
|
|
196
|
+
if key.lower() in ["xmin", "xmax", "ymin", "ymax"]:
|
|
163
197
|
# Parameter name label
|
|
164
|
-
label = tk.Label(main_frame, text=f"{key}:", anchor=
|
|
165
|
-
label.grid(row=row, column=0, sticky=
|
|
198
|
+
label = tk.Label(main_frame, text=f"{key}:", anchor="e", width=15)
|
|
199
|
+
label.grid(row=row, column=0, sticky="w", pady=5)
|
|
166
200
|
# Entry field
|
|
167
201
|
entry = tk.Entry(main_frame, width=10)
|
|
168
|
-
entry.insert(0, str(info[
|
|
202
|
+
entry.insert(0, str(info["value"]))
|
|
169
203
|
entry.grid(row=row, column=1, padx=10, pady=5)
|
|
170
204
|
self.entries[key] = entry # dictionary to hold entry widgets
|
|
171
205
|
# Type label
|
|
172
206
|
dumtext = f"({info['type'].__name__})"
|
|
173
|
-
if key.lower() in [
|
|
207
|
+
if key.lower() in ["xmin", "xmax"]:
|
|
174
208
|
dumtext += f" --> [1, {self.imgshape[1]}]"
|
|
175
209
|
else:
|
|
176
210
|
dumtext += f" --> [1, {self.imgshape[0]}]"
|
|
177
|
-
type_label = tk.Label(main_frame, text=dumtext, fg=
|
|
178
|
-
type_label.grid(row=row, column=2, sticky=
|
|
211
|
+
type_label = tk.Label(main_frame, text=dumtext, fg="gray", anchor="w", width=15)
|
|
212
|
+
type_label.grid(row=row, column=2, sticky="w", pady=5)
|
|
179
213
|
row += 1
|
|
180
214
|
|
|
181
215
|
# Separator
|
|
182
|
-
separator3 = ttk.Separator(main_frame, orient=
|
|
183
|
-
separator3.grid(row=row, column=0, columnspan=4, sticky=
|
|
216
|
+
separator3 = ttk.Separator(main_frame, orient="horizontal")
|
|
217
|
+
separator3.grid(row=row, column=0, columnspan=4, sticky="ew", pady=(10, 10))
|
|
184
218
|
row += 1
|
|
185
219
|
|
|
186
220
|
# Button frame
|
|
@@ -189,15 +223,15 @@ class ParameterEditor:
|
|
|
189
223
|
|
|
190
224
|
# OK button
|
|
191
225
|
ok_button = tk.Button(button_frame, text="OK", width=5, command=self.on_ok)
|
|
192
|
-
ok_button.pack(side=
|
|
226
|
+
ok_button.pack(side="left", padx=5)
|
|
193
227
|
|
|
194
228
|
# Cancel button
|
|
195
229
|
cancel_button = tk.Button(button_frame, text="Cancel", width=5, command=self.on_cancel)
|
|
196
|
-
cancel_button.pack(side=
|
|
230
|
+
cancel_button.pack(side="left", padx=5)
|
|
197
231
|
|
|
198
232
|
# Reset button
|
|
199
233
|
reset_button = tk.Button(button_frame, text="Reset", width=5, command=self.on_reset)
|
|
200
|
-
reset_button.pack(side=
|
|
234
|
+
reset_button.pack(side="left", padx=5)
|
|
201
235
|
|
|
202
236
|
def on_ok(self):
|
|
203
237
|
"""Validate and save the updated values"""
|
|
@@ -205,61 +239,60 @@ class ParameterEditor:
|
|
|
205
239
|
updated_dict = {}
|
|
206
240
|
|
|
207
241
|
for key, info in self.param_dict.items():
|
|
208
|
-
if key ==
|
|
242
|
+
if key == "nruns":
|
|
209
243
|
continue
|
|
210
244
|
entry_value = self.entries[key].get()
|
|
211
|
-
value_type = info[
|
|
245
|
+
value_type = info["type"]
|
|
212
246
|
|
|
213
247
|
# Convert string to appropriate type
|
|
214
248
|
if value_type == bool:
|
|
215
249
|
# Handle boolean conversion
|
|
216
|
-
if entry_value.lower() in [
|
|
250
|
+
if entry_value.lower() in ["true", "1", "yes"]:
|
|
217
251
|
converted_value = True
|
|
218
|
-
elif entry_value.lower() in [
|
|
252
|
+
elif entry_value.lower() in ["false", "0", "no"]:
|
|
219
253
|
converted_value = False
|
|
220
254
|
else:
|
|
221
255
|
raise ValueError(f"Invalid boolean value for {key}")
|
|
222
256
|
else:
|
|
223
257
|
converted_value = value_type(entry_value)
|
|
224
|
-
if
|
|
258
|
+
if "positive" in info and info["positive"] and converted_value < 0:
|
|
225
259
|
raise ValueError(f"Value for {key} must be positive")
|
|
226
260
|
|
|
227
|
-
updated_dict[key] = {
|
|
228
|
-
'value': converted_value,
|
|
229
|
-
'type': value_type
|
|
230
|
-
}
|
|
261
|
+
updated_dict[key] = {"value": converted_value, "type": value_type}
|
|
231
262
|
|
|
232
263
|
# Check whether any run1 and run2 parameters differ
|
|
233
264
|
nruns = 1
|
|
234
265
|
for key in self.param_dict.keys():
|
|
235
|
-
if key.startswith(
|
|
266
|
+
if key.startswith("run1_"):
|
|
236
267
|
parname = key[5:]
|
|
237
|
-
key2 =
|
|
238
|
-
if updated_dict[key][
|
|
268
|
+
key2 = "run2_" + parname
|
|
269
|
+
if updated_dict[key]["value"] != updated_dict[key2]["value"]:
|
|
239
270
|
nruns = 2
|
|
240
|
-
print(
|
|
241
|
-
|
|
271
|
+
print(
|
|
272
|
+
f"Parameter '{parname}' differs between run1 and run2: "
|
|
273
|
+
f"{updated_dict[key]['value']} (run1) vs {updated_dict[key2]['value']} (run2)"
|
|
274
|
+
)
|
|
242
275
|
|
|
243
276
|
# Additional validation for region limits
|
|
244
277
|
try:
|
|
245
|
-
if updated_dict[
|
|
278
|
+
if updated_dict["xmax"]["value"] <= updated_dict["xmin"]["value"]:
|
|
246
279
|
raise ValueError("xmax must be greater than xmin")
|
|
247
|
-
if updated_dict[
|
|
280
|
+
if updated_dict["ymax"]["value"] <= updated_dict["ymin"]["value"]:
|
|
248
281
|
raise ValueError("ymax must be greater than ymin")
|
|
249
282
|
self.result_dict = updated_dict
|
|
250
|
-
self.result_dict[
|
|
283
|
+
self.result_dict["nruns"] = {"value": nruns, "type": int, "positive": True}
|
|
251
284
|
if nruns not in [1, 2]:
|
|
252
285
|
raise ValueError("nruns must be 1 or 2")
|
|
253
286
|
self.root.destroy()
|
|
254
287
|
except ValueError as e:
|
|
255
|
-
messagebox.showerror(
|
|
256
|
-
|
|
257
|
-
|
|
288
|
+
messagebox.showerror(
|
|
289
|
+
"Invalid Inputs", "Error in region limits:\n" f"{str(e)}\n\nPlease check your inputs."
|
|
290
|
+
)
|
|
258
291
|
|
|
259
292
|
except ValueError as e:
|
|
260
|
-
messagebox.showerror(
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
messagebox.showerror(
|
|
294
|
+
"Invalid Inputs", f"Error converting value for {key}:\n{str(e)}\n\n" "Please check your inputs."
|
|
295
|
+
)
|
|
263
296
|
|
|
264
297
|
def on_cancel(self):
|
|
265
298
|
"""Close without saving"""
|
|
@@ -269,15 +302,15 @@ class ParameterEditor:
|
|
|
269
302
|
def on_reset(self):
|
|
270
303
|
"""Reset all fields to original values"""
|
|
271
304
|
self.param_dict = lacosmic_default_dict.copy()
|
|
272
|
-
self.param_dict[
|
|
273
|
-
self.param_dict[
|
|
274
|
-
self.param_dict[
|
|
275
|
-
self.param_dict[
|
|
305
|
+
self.param_dict["xmin"]["value"] = 1
|
|
306
|
+
self.param_dict["xmax"]["value"] = self.imgshape[1]
|
|
307
|
+
self.param_dict["ymin"]["value"] = 1
|
|
308
|
+
self.param_dict["ymax"]["value"] = self.imgshape[0]
|
|
276
309
|
for key, info in self.param_dict.items():
|
|
277
|
-
if key ==
|
|
310
|
+
if key == "nruns":
|
|
278
311
|
continue
|
|
279
312
|
self.entries[key].delete(0, tk.END)
|
|
280
|
-
self.entries[key].insert(0, str(info[
|
|
313
|
+
self.entries[key].insert(0, str(info["value"]))
|
|
281
314
|
|
|
282
315
|
def get_result(self):
|
|
283
316
|
"""Return the updated dictionary"""
|
|
@@ -287,10 +320,10 @@ class ParameterEditor:
|
|
|
287
320
|
"""Update the foreground color of run1 and run2 entries."""
|
|
288
321
|
# Highlight run2 parameter if different from run1
|
|
289
322
|
for key in self.param_dict.keys():
|
|
290
|
-
if key.startswith(
|
|
323
|
+
if key.startswith("run1_"):
|
|
291
324
|
parname = key[5:]
|
|
292
|
-
if key in self.entries and
|
|
293
|
-
if self.entries[key].get() != self.entries[
|
|
294
|
-
self.entries[
|
|
325
|
+
if key in self.entries and "run2_" + parname in self.entries:
|
|
326
|
+
if self.entries[key].get() != self.entries["run2_" + parname].get():
|
|
327
|
+
self.entries["run2_" + parname].config(fg="red")
|
|
295
328
|
else:
|
|
296
|
-
self.entries[
|
|
329
|
+
self.entries["run2_" + parname].config(fg="black")
|