spacr 0.1.0__py3-none-any.whl → 0.1.6__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.
- spacr/__init__.py +19 -12
- spacr/annotate_app.py +258 -99
- spacr/annotate_app_v2.py +163 -4
- spacr/app_annotate.py +538 -0
- spacr/app_classify.py +8 -0
- spacr/app_make_masks.py +925 -0
- spacr/app_make_masks_v2.py +686 -0
- spacr/app_mask.py +8 -0
- spacr/app_measure.py +8 -0
- spacr/app_sequencing.py +8 -0
- spacr/app_umap.py +8 -0
- spacr/classify_app.py +201 -0
- spacr/core.py +8 -6
- spacr/deep_spacr.py +3 -1
- spacr/gui.py +50 -31
- spacr/gui_2.py +106 -36
- spacr/gui_annotate.py +145 -0
- spacr/gui_classify_app.py +22 -8
- spacr/gui_core.py +608 -0
- spacr/gui_elements.py +322 -0
- spacr/gui_make_masks_app.py +927 -0
- spacr/gui_make_masks_app_v2.py +688 -0
- spacr/gui_mask_app.py +42 -15
- spacr/gui_measure_app.py +46 -21
- spacr/gui_run.py +58 -0
- spacr/gui_utils.py +78 -967
- spacr/gui_wrappers.py +137 -0
- spacr/make_masks_app.py +929 -0
- spacr/make_masks_app_v2.py +688 -0
- spacr/mask_app.py +239 -915
- spacr/measure.py +24 -3
- spacr/measure_app.py +246 -0
- spacr/sequencing.py +1 -17
- spacr/settings.py +441 -6
- spacr/sim_app.py +0 -0
- spacr/utils.py +60 -7
- {spacr-0.1.0.dist-info → spacr-0.1.6.dist-info}/METADATA +13 -22
- spacr-0.1.6.dist-info/RECORD +60 -0
- spacr-0.1.6.dist-info/entry_points.txt +8 -0
- spacr-0.1.0.dist-info/RECORD +0 -40
- spacr-0.1.0.dist-info/entry_points.txt +0 -9
- {spacr-0.1.0.dist-info → spacr-0.1.6.dist-info}/LICENSE +0 -0
- {spacr-0.1.0.dist-info → spacr-0.1.6.dist-info}/WHEEL +0 -0
- {spacr-0.1.0.dist-info → spacr-0.1.6.dist-info}/top_level.txt +0 -0
spacr/mask_app.py
CHANGED
@@ -1,925 +1,249 @@
|
|
1
|
-
import
|
2
|
-
|
3
|
-
import
|
4
|
-
import imageio.v2 as imageio
|
5
|
-
from collections import deque
|
6
|
-
from PIL import Image, ImageTk
|
7
|
-
from skimage.draw import polygon, line
|
8
|
-
from skimage.transform import resize
|
9
|
-
from scipy.ndimage import binary_fill_holes, label
|
1
|
+
#import customtkinter as ctk
|
2
|
+
|
3
|
+
import sys, ctypes, matplotlib
|
10
4
|
import tkinter as tk
|
11
|
-
from tkinter import ttk
|
12
|
-
from
|
5
|
+
from tkinter import ttk, scrolledtext
|
6
|
+
from matplotlib.figure import Figure
|
7
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
8
|
+
matplotlib.use('Agg')
|
9
|
+
from tkinter import filedialog
|
10
|
+
from multiprocessing import Process, Queue, Value
|
11
|
+
import traceback
|
12
|
+
|
13
|
+
try:
|
14
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(True)
|
15
|
+
except AttributeError:
|
16
|
+
pass
|
13
17
|
|
14
18
|
from .logger import log_function_call
|
15
|
-
|
16
|
-
from .gui_utils import
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
def update_original_mask_from_zoom(self):
|
48
|
-
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
49
|
-
zoomed_mask_resized = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
50
|
-
self.mask[y0:y1, x0:x1] = zoomed_mask_resized
|
51
|
-
|
52
|
-
def update_original_mask(self, zoomed_mask, x0, x1, y0, y1):
|
53
|
-
actual_mask_region = self.mask[y0:y1, x0:x1]
|
54
|
-
target_shape = actual_mask_region.shape
|
55
|
-
resized_mask = resize(zoomed_mask, target_shape, order=0, preserve_range=True).astype(np.uint8)
|
56
|
-
if resized_mask.shape != actual_mask_region.shape:
|
57
|
-
raise ValueError(f"Shape mismatch: resized_mask {resized_mask.shape}, actual_mask_region {actual_mask_region.shape}")
|
58
|
-
self.mask[y0:y1, x0:x1] = np.maximum(actual_mask_region, resized_mask)
|
59
|
-
self.mask = self.mask.copy()
|
60
|
-
self.mask[y0:y1, x0:x1] = np.maximum(self.mask[y0:y1, x0:x1], resized_mask)
|
61
|
-
self.mask = self.mask.copy()
|
62
|
-
|
63
|
-
def get_scaling_factors(self, img_width, img_height, canvas_width, canvas_height):
|
64
|
-
x_scale = img_width / canvas_width
|
65
|
-
y_scale = img_height / canvas_height
|
66
|
-
return x_scale, y_scale
|
67
|
-
|
68
|
-
def canvas_to_image(self, x_canvas, y_canvas):
|
69
|
-
x_scale, y_scale = self.get_scaling_factors(
|
70
|
-
self.image.shape[1], self.image.shape[0],
|
71
|
-
self.canvas_width, self.canvas_height
|
72
|
-
)
|
73
|
-
x_image = int(x_canvas * x_scale)
|
74
|
-
y_image = int(y_canvas * y_scale)
|
75
|
-
return x_image, y_image
|
76
|
-
|
77
|
-
def apply_zoom_on_enter(self, event):
|
78
|
-
if self.zoom_active and self.zoom_rectangle_start is not None:
|
79
|
-
self.set_zoom_rectangle_end(event)
|
80
|
-
|
81
|
-
def normalize_image(self, image, lower_quantile, upper_quantile):
|
82
|
-
lower_bound = np.percentile(image, lower_quantile)
|
83
|
-
upper_bound = np.percentile(image, upper_quantile)
|
84
|
-
normalized = np.clip(image, lower_bound, upper_bound)
|
85
|
-
normalized = (normalized - lower_bound) / (upper_bound - lower_bound)
|
86
|
-
max_value = np.iinfo(image.dtype).max
|
87
|
-
normalized = (normalized * max_value).astype(image.dtype)
|
88
|
-
return normalized
|
89
|
-
|
90
|
-
def resize_arrays(self, img, mask):
|
91
|
-
original_dtype = img.dtype
|
92
|
-
scaled_height = int(img.shape[0] * self.scale_factor)
|
93
|
-
scaled_width = int(img.shape[1] * self.scale_factor)
|
94
|
-
scaled_img = resize(img, (scaled_height, scaled_width), anti_aliasing=True, preserve_range=True)
|
95
|
-
scaled_mask = resize(mask, (scaled_height, scaled_width), order=0, anti_aliasing=False, preserve_range=True)
|
96
|
-
stretched_img = resize(scaled_img, (self.canvas_height, self.canvas_width), anti_aliasing=True, preserve_range=True)
|
97
|
-
stretched_mask = resize(scaled_mask, (self.canvas_height, self.canvas_width), order=0, anti_aliasing=False, preserve_range=True)
|
98
|
-
return stretched_img.astype(original_dtype), stretched_mask.astype(original_dtype)
|
99
|
-
|
100
|
-
####################################################################################################
|
101
|
-
#Initiate canvas elements#
|
102
|
-
####################################################################################################
|
103
|
-
|
104
|
-
def load_first_image(self):
|
105
|
-
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
106
|
-
self.original_size = self.image.shape
|
107
|
-
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
108
|
-
self.display_image()
|
109
|
-
|
110
|
-
def setup_canvas(self):
|
111
|
-
self.canvas = tk.Canvas(self.root, width=self.canvas_width, height=self.canvas_height)
|
112
|
-
self.canvas.pack()
|
113
|
-
self.canvas.bind("<Motion>", self.update_mouse_info)
|
114
|
-
|
115
|
-
def initialize_flags(self):
|
116
|
-
self.zoom_rectangle_start = None
|
117
|
-
self.zoom_rectangle_end = None
|
118
|
-
self.zoom_rectangle_id = None
|
119
|
-
self.zoom_x0 = None
|
120
|
-
self.zoom_y0 = None
|
121
|
-
self.zoom_x1 = None
|
122
|
-
self.zoom_y1 = None
|
123
|
-
self.zoom_mask = None
|
124
|
-
self.zoom_image = None
|
125
|
-
self.zoom_image_orig = None
|
126
|
-
self.zoom_scale = 1
|
127
|
-
self.drawing = False
|
128
|
-
self.zoom_active = False
|
129
|
-
self.magic_wand_active = False
|
130
|
-
self.brush_active = False
|
131
|
-
self.dividing_line_active = False
|
132
|
-
self.dividing_line_coords = []
|
133
|
-
self.current_dividing_line = None
|
134
|
-
self.lower_quantile = tk.StringVar(value="1.0")
|
135
|
-
self.upper_quantile = tk.StringVar(value="99.9")
|
136
|
-
self.magic_wand_tolerance = tk.StringVar(value="1000")
|
137
|
-
|
138
|
-
def update_mouse_info(self, event):
|
139
|
-
x, y = event.x, event.y
|
140
|
-
intensity = "N/A"
|
141
|
-
mask_value = "N/A"
|
142
|
-
pixel_count = "N/A"
|
143
|
-
if self.zoom_active:
|
144
|
-
if 0 <= x < self.canvas_width and 0 <= y < self.canvas_height:
|
145
|
-
intensity = self.zoom_image_orig[y, x] if self.zoom_image_orig is not None else "N/A"
|
146
|
-
mask_value = self.zoom_mask[y, x] if self.zoom_mask is not None else "N/A"
|
147
|
-
else:
|
148
|
-
if 0 <= x < self.image.shape[1] and 0 <= y < self.image.shape[0]:
|
149
|
-
intensity = self.image[y, x]
|
150
|
-
mask_value = self.mask[y, x]
|
151
|
-
if mask_value != "N/A" and mask_value != 0:
|
152
|
-
pixel_count = np.sum(self.mask == mask_value)
|
153
|
-
self.intensity_label.config(text=f"Intensity: {intensity}")
|
154
|
-
self.mask_value_label.config(text=f"Mask: {mask_value}, Area: {pixel_count}")
|
155
|
-
self.mask_value_label.config(text=f"Mask: {mask_value}")
|
156
|
-
if mask_value != "N/A" and mask_value != 0:
|
157
|
-
self.pixel_count_label.config(text=f"Area: {pixel_count}")
|
158
|
-
else:
|
159
|
-
self.pixel_count_label.config(text="Area: N/A")
|
160
|
-
|
161
|
-
def setup_navigation_toolbar(self):
|
162
|
-
navigation_toolbar = tk.Frame(self.root)
|
163
|
-
navigation_toolbar.pack(side='top', fill='x')
|
164
|
-
prev_btn = tk.Button(navigation_toolbar, text="Previous", command=self.previous_image)
|
165
|
-
prev_btn.pack(side='left')
|
166
|
-
next_btn = tk.Button(navigation_toolbar, text="Next", command=self.next_image)
|
167
|
-
next_btn.pack(side='left')
|
168
|
-
save_btn = tk.Button(navigation_toolbar, text="Save", command=self.save_mask)
|
169
|
-
save_btn.pack(side='left')
|
170
|
-
self.intensity_label = tk.Label(navigation_toolbar, text="Image: N/A")
|
171
|
-
self.intensity_label.pack(side='right')
|
172
|
-
self.mask_value_label = tk.Label(navigation_toolbar, text="Mask: N/A")
|
173
|
-
self.mask_value_label.pack(side='right')
|
174
|
-
self.pixel_count_label = tk.Label(navigation_toolbar, text="Area: N/A")
|
175
|
-
self.pixel_count_label.pack(side='right')
|
176
|
-
|
177
|
-
def setup_mode_toolbar(self):
|
178
|
-
self.mode_toolbar = tk.Frame(self.root)
|
179
|
-
self.mode_toolbar.pack(side='top', fill='x')
|
180
|
-
self.draw_btn = tk.Button(self.mode_toolbar, text="Draw", command=self.toggle_draw_mode)
|
181
|
-
self.draw_btn.pack(side='left')
|
182
|
-
self.magic_wand_btn = tk.Button(self.mode_toolbar, text="Magic Wand", command=self.toggle_magic_wand_mode)
|
183
|
-
self.magic_wand_btn.pack(side='left')
|
184
|
-
tk.Label(self.mode_toolbar, text="Tolerance:").pack(side='left')
|
185
|
-
self.tolerance_entry = tk.Entry(self.mode_toolbar, textvariable=self.magic_wand_tolerance)
|
186
|
-
self.tolerance_entry.pack(side='left')
|
187
|
-
tk.Label(self.mode_toolbar, text="Max Pixels:").pack(side='left')
|
188
|
-
self.max_pixels_entry = tk.Entry(self.mode_toolbar)
|
189
|
-
self.max_pixels_entry.insert(0, "1000")
|
190
|
-
self.max_pixels_entry.pack(side='left')
|
191
|
-
self.erase_btn = tk.Button(self.mode_toolbar, text="Erase", command=self.toggle_erase_mode)
|
192
|
-
self.erase_btn.pack(side='left')
|
193
|
-
self.brush_btn = tk.Button(self.mode_toolbar, text="Brush", command=self.toggle_brush_mode)
|
194
|
-
self.brush_btn.pack(side='left')
|
195
|
-
self.brush_size_entry = tk.Entry(self.mode_toolbar)
|
196
|
-
self.brush_size_entry.insert(0, "10")
|
197
|
-
self.brush_size_entry.pack(side='left')
|
198
|
-
tk.Label(self.mode_toolbar, text="Brush Size:").pack(side='left')
|
199
|
-
self.dividing_line_btn = tk.Button(self.mode_toolbar, text="Dividing Line", command=self.toggle_dividing_line_mode)
|
200
|
-
self.dividing_line_btn.pack(side='left')
|
201
|
-
|
202
|
-
def setup_function_toolbar(self):
|
203
|
-
self.function_toolbar = tk.Frame(self.root)
|
204
|
-
self.function_toolbar.pack(side='top', fill='x')
|
205
|
-
self.fill_btn = tk.Button(self.function_toolbar, text="Fill", command=self.fill_objects)
|
206
|
-
self.fill_btn.pack(side='left')
|
207
|
-
self.relabel_btn = tk.Button(self.function_toolbar, text="Relabel", command=self.relabel_objects)
|
208
|
-
self.relabel_btn.pack(side='left')
|
209
|
-
self.clear_btn = tk.Button(self.function_toolbar, text="Clear", command=self.clear_objects)
|
210
|
-
self.clear_btn.pack(side='left')
|
211
|
-
self.invert_btn = tk.Button(self.function_toolbar, text="Invert", command=self.invert_mask)
|
212
|
-
self.invert_btn.pack(side='left')
|
213
|
-
remove_small_btn = tk.Button(self.function_toolbar, text="Remove Small", command=self.remove_small_objects)
|
214
|
-
remove_small_btn.pack(side='left')
|
215
|
-
tk.Label(self.function_toolbar, text="Min Area:").pack(side='left')
|
216
|
-
self.min_area_entry = tk.Entry(self.function_toolbar)
|
217
|
-
self.min_area_entry.insert(0, "100") # Default minimum area
|
218
|
-
self.min_area_entry.pack(side='left')
|
219
|
-
|
220
|
-
def setup_zoom_toolbar(self):
|
221
|
-
self.zoom_toolbar = tk.Frame(self.root)
|
222
|
-
self.zoom_toolbar.pack(side='top', fill='x')
|
223
|
-
self.zoom_btn = tk.Button(self.zoom_toolbar, text="Zoom", command=self.toggle_zoom_mode)
|
224
|
-
self.zoom_btn.pack(side='left')
|
225
|
-
self.normalize_btn = tk.Button(self.zoom_toolbar, text="Apply Normalization", command=self.apply_normalization)
|
226
|
-
self.normalize_btn.pack(side='left')
|
227
|
-
self.lower_entry = tk.Entry(self.zoom_toolbar, textvariable=self.lower_quantile)
|
228
|
-
self.lower_entry.pack(side='left')
|
229
|
-
tk.Label(self.zoom_toolbar, text="Lower Percentile:").pack(side='left')
|
230
|
-
self.upper_entry = tk.Entry(self.zoom_toolbar, textvariable=self.upper_quantile)
|
231
|
-
self.upper_entry.pack(side='left')
|
232
|
-
tk.Label(self.zoom_toolbar, text="Upper Percentile:").pack(side='left')
|
233
|
-
|
234
|
-
def load_image_and_mask(self, index):
|
235
|
-
image_path = os.path.join(self.folder_path, self.image_filenames[index])
|
236
|
-
image = imageio.imread(image_path)
|
237
|
-
mask_path = os.path.join(self.masks_folder, self.image_filenames[index])
|
238
|
-
if os.path.exists(mask_path):
|
239
|
-
print(f'loading mask:{mask_path} for image: {image_path}')
|
240
|
-
mask = imageio.imread(mask_path)
|
241
|
-
if mask.dtype != np.uint8:
|
242
|
-
mask = (mask / np.max(mask) * 255).astype(np.uint8)
|
243
|
-
else:
|
244
|
-
mask = np.zeros(image.shape[:2], dtype=np.uint8)
|
245
|
-
print(f'loaded new mask for image: {image_path}')
|
246
|
-
return image, mask
|
247
|
-
|
248
|
-
####################################################################################################
|
249
|
-
# Image Display functions#
|
250
|
-
####################################################################################################
|
251
|
-
def display_image(self):
|
252
|
-
if self.zoom_rectangle_id is not None:
|
253
|
-
self.canvas.delete(self.zoom_rectangle_id)
|
254
|
-
self.zoom_rectangle_id = None
|
255
|
-
lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
|
256
|
-
upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
|
257
|
-
normalized = self.normalize_image(self.image, lower_quantile, upper_quantile)
|
258
|
-
combined = self.overlay_mask_on_image(normalized, self.mask)
|
259
|
-
self.tk_image = ImageTk.PhotoImage(image=Image.fromarray(combined))
|
260
|
-
self.canvas.create_image(0, 0, anchor='nw', image=self.tk_image)
|
261
|
-
|
262
|
-
def display_zoomed_image(self):
|
263
|
-
if self.zoom_rectangle_start and self.zoom_rectangle_end:
|
264
|
-
# Convert canvas coordinates to image coordinates
|
265
|
-
x0, y0 = self.canvas_to_image(*self.zoom_rectangle_start)
|
266
|
-
x1, y1 = self.canvas_to_image(*self.zoom_rectangle_end)
|
267
|
-
x0, x1 = min(x0, x1), max(x0, x1)
|
268
|
-
y0, y1 = min(y0, y1), max(y0, y1)
|
269
|
-
self.zoom_x0 = x0
|
270
|
-
self.zoom_y0 = y0
|
271
|
-
self.zoom_x1 = x1
|
272
|
-
self.zoom_y1 = y1
|
273
|
-
# Normalize the entire image
|
274
|
-
lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
|
275
|
-
upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
|
276
|
-
normalized_image = self.normalize_image(self.image, lower_quantile, upper_quantile)
|
277
|
-
# Extract the zoomed portion of the normalized image and mask
|
278
|
-
self.zoom_image = normalized_image[y0:y1, x0:x1]
|
279
|
-
self.zoom_image_orig = self.image[y0:y1, x0:x1]
|
280
|
-
self.zoom_mask = self.mask[y0:y1, x0:x1]
|
281
|
-
original_mask_area = self.mask.shape[0] * self.mask.shape[1]
|
282
|
-
zoom_mask_area = self.zoom_mask.shape[0] * self.zoom_mask.shape[1]
|
283
|
-
if original_mask_area > 0:
|
284
|
-
self.zoom_scale = original_mask_area/zoom_mask_area
|
285
|
-
# Resize the zoomed image and mask to fit the canvas
|
286
|
-
canvas_height = self.canvas.winfo_height()
|
287
|
-
canvas_width = self.canvas.winfo_width()
|
288
|
-
|
289
|
-
if self.zoom_image.size > 0 and canvas_height > 0 and canvas_width > 0:
|
290
|
-
self.zoom_image = resize(self.zoom_image, (canvas_height, canvas_width), preserve_range=True).astype(self.zoom_image.dtype)
|
291
|
-
self.zoom_image_orig = resize(self.zoom_image_orig, (canvas_height, canvas_width), preserve_range=True).astype(self.zoom_image_orig.dtype)
|
292
|
-
#self.zoom_mask = resize(self.zoom_mask, (canvas_height, canvas_width), preserve_range=True).astype(np.uint8)
|
293
|
-
self.zoom_mask = resize(self.zoom_mask, (canvas_height, canvas_width), order=0, preserve_range=True).astype(np.uint8)
|
294
|
-
combined = self.overlay_mask_on_image(self.zoom_image, self.zoom_mask)
|
295
|
-
self.tk_image = ImageTk.PhotoImage(image=Image.fromarray(combined))
|
296
|
-
self.canvas.create_image(0, 0, anchor='nw', image=self.tk_image)
|
297
|
-
|
298
|
-
def overlay_mask_on_image(self, image, mask, alpha=0.5):
|
299
|
-
if len(image.shape) == 2:
|
300
|
-
image = np.stack((image,) * 3, axis=-1)
|
301
|
-
mask = mask.astype(np.int32)
|
302
|
-
max_label = np.max(mask)
|
303
|
-
np.random.seed(0)
|
304
|
-
colors = np.random.randint(0, 255, size=(max_label + 1, 3), dtype=np.uint8)
|
305
|
-
colors[0] = [0, 0, 0] # background color
|
306
|
-
colored_mask = colors[mask]
|
307
|
-
image_8bit = (image / 256).astype(np.uint8)
|
308
|
-
# Blend the mask and the image with transparency
|
309
|
-
combined_image = np.where(mask[..., None] > 0,
|
310
|
-
np.clip(image_8bit * (1 - alpha) + colored_mask * alpha, 0, 255),
|
311
|
-
image_8bit)
|
312
|
-
# Convert the final image back to uint8
|
313
|
-
combined_image = combined_image.astype(np.uint8)
|
314
|
-
return combined_image
|
315
|
-
|
316
|
-
####################################################################################################
|
317
|
-
# Navigation functions#
|
318
|
-
####################################################################################################
|
319
|
-
|
320
|
-
def previous_image(self):
|
321
|
-
if self.current_image_index > 0:
|
322
|
-
self.current_image_index -= 1
|
323
|
-
self.initialize_flags()
|
324
|
-
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
325
|
-
self.original_size = self.image.shape
|
326
|
-
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
327
|
-
self.display_image()
|
328
|
-
|
329
|
-
def next_image(self):
|
330
|
-
if self.current_image_index < len(self.image_filenames) - 1:
|
331
|
-
self.current_image_index += 1
|
332
|
-
self.initialize_flags()
|
333
|
-
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
334
|
-
self.original_size = self.image.shape
|
335
|
-
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
336
|
-
self.display_image()
|
337
|
-
|
338
|
-
def save_mask(self):
|
339
|
-
if self.current_image_index < len(self.image_filenames):
|
340
|
-
original_size = self.original_size
|
341
|
-
if self.mask.shape != original_size:
|
342
|
-
resized_mask = resize(self.mask, original_size, order=0, preserve_range=True).astype(np.uint16)
|
343
|
-
else:
|
344
|
-
resized_mask = self.mask
|
345
|
-
resized_mask, _ = label(resized_mask > 0)
|
346
|
-
save_folder = os.path.join(self.folder_path, 'masks')
|
347
|
-
if not os.path.exists(save_folder):
|
348
|
-
os.makedirs(save_folder)
|
349
|
-
image_filename = os.path.splitext(self.image_filenames[self.current_image_index])[0] + '.tif'
|
350
|
-
save_path = os.path.join(save_folder, image_filename)
|
351
|
-
|
352
|
-
print(f"Saving mask to: {save_path}") # Debug print
|
353
|
-
imageio.imwrite(save_path, resized_mask)
|
354
|
-
|
355
|
-
####################################################################################################
|
356
|
-
# Zoom Functions #
|
357
|
-
####################################################################################################
|
358
|
-
def set_zoom_rectangle_start(self, event):
|
359
|
-
if self.zoom_active:
|
360
|
-
self.zoom_rectangle_start = (event.x, event.y)
|
361
|
-
|
362
|
-
def set_zoom_rectangle_end(self, event):
|
363
|
-
if self.zoom_active:
|
364
|
-
self.zoom_rectangle_end = (event.x, event.y)
|
365
|
-
if self.zoom_rectangle_id is not None:
|
366
|
-
self.canvas.delete(self.zoom_rectangle_id)
|
367
|
-
self.zoom_rectangle_id = None
|
368
|
-
self.display_zoomed_image()
|
369
|
-
self.canvas.unbind("<Motion>")
|
370
|
-
self.canvas.unbind("<Button-1>")
|
371
|
-
self.canvas.unbind("<Button-3>")
|
372
|
-
self.canvas.bind("<Motion>", self.update_mouse_info)
|
373
|
-
|
374
|
-
def update_zoom_box(self, event):
|
375
|
-
if self.zoom_active and self.zoom_rectangle_start is not None:
|
376
|
-
if self.zoom_rectangle_id is not None:
|
377
|
-
self.canvas.delete(self.zoom_rectangle_id)
|
378
|
-
# Assuming event.x and event.y are already in image coordinates
|
379
|
-
self.zoom_rectangle_end = (event.x, event.y)
|
380
|
-
x0, y0 = self.zoom_rectangle_start
|
381
|
-
x1, y1 = self.zoom_rectangle_end
|
382
|
-
self.zoom_rectangle_id = self.canvas.create_rectangle(x0, y0, x1, y1, outline="red", width=2)
|
383
|
-
|
384
|
-
####################################################################################################
|
385
|
-
# Mode activation#
|
386
|
-
####################################################################################################
|
387
|
-
|
388
|
-
def toggle_zoom_mode(self):
|
389
|
-
if not self.zoom_active:
|
390
|
-
self.brush_btn.config(text="Brush")
|
391
|
-
self.canvas.unbind("<B1-Motion>")
|
392
|
-
self.canvas.unbind("<B3-Motion>")
|
393
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
394
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
395
|
-
self.zoom_active = True
|
396
|
-
self.drawing = False
|
397
|
-
self.magic_wand_active = False
|
398
|
-
self.erase_active = False
|
399
|
-
self.brush_active = False
|
400
|
-
self.dividing_line_active = False
|
401
|
-
self.draw_btn.config(text="Draw")
|
402
|
-
self.erase_btn.config(text="Erase")
|
403
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
404
|
-
self.zoom_btn.config(text="Zoom ON")
|
405
|
-
self.dividing_line_btn.config(text="Dividing Line")
|
406
|
-
self.canvas.unbind("<Button-1>")
|
407
|
-
self.canvas.unbind("<Button-3>")
|
408
|
-
self.canvas.unbind("<Motion>")
|
409
|
-
self.canvas.bind("<Button-1>", self.set_zoom_rectangle_start)
|
410
|
-
self.canvas.bind("<Button-3>", self.set_zoom_rectangle_end)
|
411
|
-
self.canvas.bind("<Motion>", self.update_zoom_box)
|
19
|
+
from .gui_utils import ScrollableFrame, StdoutRedirector, ToggleSwitch, CustomButton, ToolTip
|
20
|
+
from .gui_utils import clear_canvas, main_thread_update_function, set_dark_style, generate_fields, process_stdout_stderr, set_default_font, style_text_boxes
|
21
|
+
from .gui_utils import mask_variables, check_mask_gui_settings, preprocess_generate_masks_wrapper, read_settings_from_csv, update_settings_from_csv, create_menu_bar
|
22
|
+
|
23
|
+
thread_control = {"run_thread": None, "stop_requested": False}
|
24
|
+
|
25
|
+
def toggle_test_mode():
|
26
|
+
global vars_dict
|
27
|
+
current_state = vars_dict['test_mode'][2].get()
|
28
|
+
new_state = not current_state
|
29
|
+
vars_dict['test_mode'][2].set(new_state)
|
30
|
+
if new_state:
|
31
|
+
test_mode_button.config(bg="blue")
|
32
|
+
else:
|
33
|
+
test_mode_button.config(bg="gray")
|
34
|
+
|
35
|
+
def toggle_advanced_settings():
|
36
|
+
global vars_dict
|
37
|
+
|
38
|
+
timelapse_settings = ['timelapse', 'timelapse_memory', 'timelapse_remove_transient', 'timelapse_mode', 'timelapse_objects', 'timelapse_displacement', 'timelapse_frame_limits', 'fps']
|
39
|
+
misc_settings = ['examples_to_plot', 'all_to_mip', 'pick_slice', 'skip_mode']
|
40
|
+
opperational_settings = ['preprocess', 'masks', 'randomize', 'batch_size', 'custom_regex', 'merge', 'normalize_plots', 'workers', 'plot', 'remove_background', 'lower_quantile']
|
41
|
+
|
42
|
+
advanced_settings = timelapse_settings+misc_settings+opperational_settings
|
43
|
+
|
44
|
+
# Toggle visibility of advanced settings
|
45
|
+
for setting in advanced_settings:
|
46
|
+
label, widget, var = vars_dict[setting]
|
47
|
+
if advanced_var.get() is False:
|
48
|
+
label.grid_remove() # Hide the label
|
49
|
+
widget.grid_remove() # Hide the widget
|
412
50
|
else:
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
def toggle_dividing_line_mode(self):
|
468
|
-
self.dividing_line_active = not self.dividing_line_active
|
469
|
-
if self.dividing_line_active:
|
470
|
-
self.drawing = False
|
471
|
-
self.magic_wand_active = False
|
472
|
-
self.erase_active = False
|
473
|
-
self.brush_active = False
|
474
|
-
self.draw_btn.config(text="Draw")
|
475
|
-
self.erase_btn.config(text="Erase")
|
476
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
477
|
-
self.brush_btn.config(text="Brush")
|
478
|
-
self.dividing_line_btn.config(text="Dividing Line ON")
|
479
|
-
self.canvas.unbind("<Button-1>")
|
480
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
481
|
-
self.canvas.unbind("<Motion>")
|
482
|
-
self.canvas.bind("<Button-1>", self.start_dividing_line)
|
483
|
-
self.canvas.bind("<ButtonRelease-1>", self.finish_dividing_line)
|
484
|
-
self.canvas.bind("<Motion>", self.update_dividing_line_preview)
|
485
|
-
else:
|
486
|
-
print("Dividing Line Mode: OFF")
|
487
|
-
self.dividing_line_active = False
|
488
|
-
self.dividing_line_btn.config(text="Dividing Line")
|
489
|
-
self.canvas.unbind("<Button-1>")
|
490
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
491
|
-
self.canvas.unbind("<Motion>")
|
492
|
-
self.display_image()
|
493
|
-
|
494
|
-
def start_dividing_line(self, event):
|
495
|
-
if self.dividing_line_active:
|
496
|
-
self.dividing_line_coords = [(event.x, event.y)]
|
497
|
-
self.current_dividing_line = self.canvas.create_line(event.x, event.y, event.x, event.y, fill="red", width=2)
|
498
|
-
|
499
|
-
def finish_dividing_line(self, event):
|
500
|
-
if self.dividing_line_active:
|
501
|
-
self.dividing_line_coords.append((event.x, event.y))
|
502
|
-
if self.zoom_active:
|
503
|
-
self.dividing_line_coords = [self.canvas_to_image(x, y) for x, y in self.dividing_line_coords]
|
504
|
-
self.apply_dividing_line()
|
505
|
-
self.canvas.delete(self.current_dividing_line)
|
506
|
-
self.current_dividing_line = None
|
507
|
-
|
508
|
-
def update_dividing_line_preview(self, event):
|
509
|
-
if self.dividing_line_active and self.dividing_line_coords:
|
510
|
-
x, y = event.x, event.y
|
511
|
-
if self.zoom_active:
|
512
|
-
x, y = self.canvas_to_image(x, y)
|
513
|
-
self.dividing_line_coords.append((x, y))
|
514
|
-
canvas_coords = [(self.image_to_canvas(*pt) if self.zoom_active else pt) for pt in self.dividing_line_coords]
|
515
|
-
flat_canvas_coords = [coord for pt in canvas_coords for coord in pt]
|
516
|
-
self.canvas.coords(self.current_dividing_line, *flat_canvas_coords)
|
517
|
-
|
518
|
-
def apply_dividing_line(self):
|
519
|
-
if self.dividing_line_coords:
|
520
|
-
coords = self.dividing_line_coords
|
521
|
-
if self.zoom_active:
|
522
|
-
coords = [self.canvas_to_image(x, y) for x, y in coords]
|
523
|
-
|
524
|
-
rr, cc = [], []
|
525
|
-
for (x0, y0), (x1, y1) in zip(coords[:-1], coords[1:]):
|
526
|
-
line_rr, line_cc = line(y0, x0, y1, x1)
|
527
|
-
rr.extend(line_rr)
|
528
|
-
cc.extend(line_cc)
|
529
|
-
rr, cc = np.array(rr), np.array(cc)
|
530
|
-
|
531
|
-
mask_copy = self.mask.copy()
|
532
|
-
|
533
|
-
if self.zoom_active:
|
534
|
-
# Update the zoomed mask
|
535
|
-
self.zoom_mask[rr, cc] = 0
|
536
|
-
# Reflect changes to the original mask
|
537
|
-
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
538
|
-
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
539
|
-
self.mask[y0:y1, x0:x1] = zoomed_mask_resized_back
|
540
|
-
else:
|
541
|
-
# Directly update the original mask
|
542
|
-
mask_copy[rr, cc] = 0
|
543
|
-
self.mask = mask_copy
|
544
|
-
|
545
|
-
labeled_mask, num_labels = label(self.mask > 0)
|
546
|
-
self.mask = labeled_mask
|
547
|
-
self.update_display()
|
548
|
-
|
549
|
-
self.dividing_line_coords = []
|
550
|
-
self.canvas.unbind("<Button-1>")
|
551
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
552
|
-
self.canvas.unbind("<Motion>")
|
553
|
-
self.dividing_line_active = False
|
554
|
-
self.dividing_line_btn.config(text="Dividing Line")
|
555
|
-
|
556
|
-
def toggle_draw_mode(self):
|
557
|
-
self.drawing = not self.drawing
|
558
|
-
if self.drawing:
|
559
|
-
self.brush_btn.config(text="Brush")
|
560
|
-
self.canvas.unbind("<B1-Motion>")
|
561
|
-
self.canvas.unbind("<B3-Motion>")
|
562
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
563
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
564
|
-
self.magic_wand_active = False
|
565
|
-
self.erase_active = False
|
566
|
-
self.brush_active = False
|
567
|
-
self.draw_btn.config(text="Draw ON")
|
568
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
569
|
-
self.erase_btn.config(text="Erase")
|
570
|
-
self.draw_coordinates = []
|
571
|
-
self.canvas.unbind("<Button-1>")
|
572
|
-
self.canvas.unbind("<Motion>")
|
573
|
-
self.canvas.bind("<B1-Motion>", self.draw)
|
574
|
-
self.canvas.bind("<ButtonRelease-1>", self.finish_drawing)
|
575
|
-
else:
|
576
|
-
self.drawing = False
|
577
|
-
self.draw_btn.config(text="Draw")
|
578
|
-
self.canvas.unbind("<B1-Motion>")
|
579
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
580
|
-
|
581
|
-
def toggle_magic_wand_mode(self):
|
582
|
-
self.magic_wand_active = not self.magic_wand_active
|
583
|
-
if self.magic_wand_active:
|
584
|
-
self.brush_btn.config(text="Brush")
|
585
|
-
self.canvas.unbind("<B1-Motion>")
|
586
|
-
self.canvas.unbind("<B3-Motion>")
|
587
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
588
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
589
|
-
self.drawing = False
|
590
|
-
self.erase_active = False
|
591
|
-
self.brush_active = False
|
592
|
-
self.draw_btn.config(text="Draw")
|
593
|
-
self.erase_btn.config(text="Erase")
|
594
|
-
self.magic_wand_btn.config(text="Magic Wand ON")
|
595
|
-
self.canvas.bind("<Button-1>", self.use_magic_wand)
|
596
|
-
self.canvas.bind("<Button-3>", self.use_magic_wand)
|
597
|
-
else:
|
598
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
599
|
-
self.canvas.unbind("<Button-1>")
|
600
|
-
self.canvas.unbind("<Button-3>")
|
601
|
-
|
602
|
-
def toggle_erase_mode(self):
|
603
|
-
self.erase_active = not self.erase_active
|
604
|
-
if self.erase_active:
|
605
|
-
self.brush_btn.config(text="Brush")
|
606
|
-
self.canvas.unbind("<B1-Motion>")
|
607
|
-
self.canvas.unbind("<B3-Motion>")
|
608
|
-
self.canvas.unbind("<ButtonRelease-1>")
|
609
|
-
self.canvas.unbind("<ButtonRelease-3>")
|
610
|
-
self.erase_btn.config(text="Erase ON")
|
611
|
-
self.canvas.bind("<Button-1>", self.erase_object)
|
612
|
-
self.drawing = False
|
613
|
-
self.magic_wand_active = False
|
614
|
-
self.brush_active = False
|
615
|
-
self.draw_btn.config(text="Draw")
|
616
|
-
self.magic_wand_btn.config(text="Magic Wand")
|
617
|
-
else:
|
618
|
-
self.erase_active = False
|
619
|
-
self.erase_btn.config(text="Erase")
|
620
|
-
self.canvas.unbind("<Button-1>")
|
621
|
-
|
622
|
-
####################################################################################################
|
623
|
-
# Mode functions#
|
624
|
-
####################################################################################################
|
625
|
-
|
626
|
-
def apply_brush_release(self, event):
|
627
|
-
if hasattr(self, 'brush_path'):
|
628
|
-
for x, y, brush_size in self.brush_path:
|
629
|
-
img_x, img_y = (x, y) if self.zoom_active else self.canvas_to_image(x, y)
|
630
|
-
x0 = max(img_x - brush_size // 2, 0)
|
631
|
-
y0 = max(img_y - brush_size // 2, 0)
|
632
|
-
x1 = min(img_x + brush_size // 2, self.zoom_mask.shape[1] if self.zoom_active else self.mask.shape[1])
|
633
|
-
y1 = min(img_y + brush_size // 2, self.zoom_mask.shape[0] if self.zoom_active else self.mask.shape[0])
|
634
|
-
if self.zoom_active:
|
635
|
-
self.zoom_mask[y0:y1, x0:x1] = 255
|
636
|
-
self.update_original_mask_from_zoom()
|
637
|
-
else:
|
638
|
-
self.mask[y0:y1, x0:x1] = 255
|
639
|
-
del self.brush_path
|
640
|
-
self.canvas.delete("temp_line")
|
641
|
-
self.update_display()
|
642
|
-
|
643
|
-
def erase_brush_release(self, event):
|
644
|
-
if hasattr(self, 'erase_path'):
|
645
|
-
for x, y, brush_size in self.erase_path:
|
646
|
-
img_x, img_y = (x, y) if self.zoom_active else self.canvas_to_image(x, y)
|
647
|
-
x0 = max(img_x - brush_size // 2, 0)
|
648
|
-
y0 = max(img_y - brush_size // 2, 0)
|
649
|
-
x1 = min(img_x + brush_size // 2, self.zoom_mask.shape[1] if self.zoom_active else self.mask.shape[1])
|
650
|
-
y1 = min(img_y + brush_size // 2, self.zoom_mask.shape[0] if self.zoom_active else self.mask.shape[0])
|
651
|
-
if self.zoom_active:
|
652
|
-
self.zoom_mask[y0:y1, x0:x1] = 0
|
653
|
-
self.update_original_mask_from_zoom()
|
654
|
-
else:
|
655
|
-
self.mask[y0:y1, x0:x1] = 0
|
656
|
-
del self.erase_path
|
657
|
-
self.canvas.delete("temp_line")
|
658
|
-
self.update_display()
|
659
|
-
|
660
|
-
def apply_brush(self, event):
|
661
|
-
brush_size = int(self.brush_size_entry.get())
|
662
|
-
x, y = event.x, event.y
|
663
|
-
if not hasattr(self, 'brush_path'):
|
664
|
-
self.brush_path = []
|
665
|
-
self.last_brush_coord = (x, y)
|
666
|
-
if self.last_brush_coord:
|
667
|
-
last_x, last_y = self.last_brush_coord
|
668
|
-
rr, cc = line(last_y, last_x, y, x)
|
669
|
-
for ry, rx in zip(rr, cc):
|
670
|
-
self.brush_path.append((rx, ry, brush_size))
|
671
|
-
|
672
|
-
self.canvas.create_line(self.last_brush_coord[0], self.last_brush_coord[1], x, y, width=brush_size, fill="blue", tag="temp_line")
|
673
|
-
self.last_brush_coord = (x, y)
|
674
|
-
|
675
|
-
def erase_brush(self, event):
|
676
|
-
brush_size = int(self.brush_size_entry.get())
|
677
|
-
x, y = event.x, event.y
|
678
|
-
if not hasattr(self, 'erase_path'):
|
679
|
-
self.erase_path = []
|
680
|
-
self.last_erase_coord = (x, y)
|
681
|
-
if self.last_erase_coord:
|
682
|
-
last_x, last_y = self.last_erase_coord
|
683
|
-
rr, cc = line(last_y, last_x, y, x)
|
684
|
-
for ry, rx in zip(rr, cc):
|
685
|
-
self.erase_path.append((rx, ry, brush_size))
|
686
|
-
|
687
|
-
self.canvas.create_line(self.last_erase_coord[0], self.last_erase_coord[1], x, y, width=brush_size, fill="white", tag="temp_line")
|
688
|
-
self.last_erase_coord = (x, y)
|
689
|
-
|
690
|
-
def erase_object(self, event):
|
691
|
-
x, y = event.x, event.y
|
692
|
-
if self.zoom_active:
|
693
|
-
canvas_x, canvas_y = x, y
|
694
|
-
zoomed_x = int(canvas_x * (self.zoom_image.shape[1] / self.canvas_width))
|
695
|
-
zoomed_y = int(canvas_y * (self.zoom_image.shape[0] / self.canvas_height))
|
696
|
-
orig_x = int(zoomed_x * ((self.zoom_x1 - self.zoom_x0) / self.canvas_width) + self.zoom_x0)
|
697
|
-
orig_y = int(zoomed_y * ((self.zoom_y1 - self.zoom_y0) / self.canvas_height) + self.zoom_y0)
|
698
|
-
if orig_x < 0 or orig_y < 0 or orig_x >= self.image.shape[1] or orig_y >= self.image.shape[0]:
|
699
|
-
print("Point is out of bounds in the original image.")
|
700
|
-
return
|
701
|
-
else:
|
702
|
-
orig_x, orig_y = x, y
|
703
|
-
label_to_remove = self.mask[orig_y, orig_x]
|
704
|
-
if label_to_remove > 0:
|
705
|
-
self.mask[self.mask == label_to_remove] = 0
|
706
|
-
self.update_display()
|
707
|
-
|
708
|
-
def use_magic_wand(self, event):
|
709
|
-
x, y = event.x, event.y
|
710
|
-
tolerance = int(self.magic_wand_tolerance.get())
|
711
|
-
maximum = int(self.max_pixels_entry.get())
|
712
|
-
action = 'add' if event.num == 1 else 'erase'
|
713
|
-
if self.zoom_active:
|
714
|
-
self.magic_wand_zoomed((x, y), tolerance, action)
|
715
|
-
else:
|
716
|
-
self.magic_wand_normal((x, y), tolerance, action)
|
717
|
-
|
718
|
-
def apply_magic_wand(self, image, mask, seed_point, tolerance, maximum, action='add'):
|
719
|
-
x, y = seed_point
|
720
|
-
initial_value = image[y, x].astype(np.float32)
|
721
|
-
visited = np.zeros_like(image, dtype=bool)
|
722
|
-
queue = deque([(x, y)])
|
723
|
-
added_pixels = 0
|
724
|
-
|
725
|
-
while queue and added_pixels < maximum:
|
726
|
-
cx, cy = queue.popleft()
|
727
|
-
if visited[cy, cx]:
|
728
|
-
continue
|
729
|
-
visited[cy, cx] = True
|
730
|
-
current_value = image[cy, cx].astype(np.float32)
|
731
|
-
|
732
|
-
if np.linalg.norm(abs(current_value - initial_value)) <= tolerance:
|
733
|
-
if mask[cy, cx] == 0:
|
734
|
-
added_pixels += 1
|
735
|
-
mask[cy, cx] = 255 if action == 'add' else 0
|
736
|
-
|
737
|
-
if added_pixels >= maximum:
|
738
|
-
break
|
739
|
-
|
740
|
-
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
741
|
-
nx, ny = cx + dx, cy + dy
|
742
|
-
if 0 <= nx < image.shape[1] and 0 <= ny < image.shape[0] and not visited[ny, nx]:
|
743
|
-
queue.append((nx, ny))
|
744
|
-
return mask
|
745
|
-
|
746
|
-
def magic_wand_normal(self, seed_point, tolerance, action):
|
747
|
-
try:
|
748
|
-
maximum = int(self.max_pixels_entry.get())
|
749
|
-
except ValueError:
|
750
|
-
print("Invalid maximum value; using default of 1000")
|
751
|
-
maximum = 1000
|
752
|
-
self.mask = self.apply_magic_wand(self.image, self.mask, seed_point, tolerance, maximum, action)
|
753
|
-
self.display_image()
|
754
|
-
|
755
|
-
def magic_wand_zoomed(self, seed_point, tolerance, action):
|
756
|
-
if self.zoom_image_orig is None or self.zoom_mask is None:
|
757
|
-
print("Zoomed image or mask not initialized")
|
758
|
-
return
|
759
|
-
try:
|
760
|
-
maximum = int(self.max_pixels_entry.get())
|
761
|
-
maximum = maximum * self.zoom_scale
|
762
|
-
except ValueError:
|
763
|
-
print("Invalid maximum value; using default of 1000")
|
764
|
-
maximum = 1000
|
765
|
-
|
766
|
-
canvas_x, canvas_y = seed_point
|
767
|
-
if canvas_x < 0 or canvas_y < 0 or canvas_x >= self.zoom_image_orig.shape[1] or canvas_y >= self.zoom_image_orig.shape[0]:
|
768
|
-
print("Selected point is out of bounds in the zoomed image.")
|
769
|
-
return
|
770
|
-
|
771
|
-
self.zoom_mask = self.apply_magic_wand(self.zoom_image_orig, self.zoom_mask, (canvas_x, canvas_y), tolerance, maximum, action)
|
772
|
-
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
773
|
-
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
774
|
-
if action == 'erase':
|
775
|
-
self.mask[y0:y1, x0:x1] = np.where(zoomed_mask_resized_back == 0, 0, self.mask[y0:y1, x0:x1])
|
776
|
-
else:
|
777
|
-
self.mask[y0:y1, x0:x1] = np.where(zoomed_mask_resized_back > 0, zoomed_mask_resized_back, self.mask[y0:y1, x0:x1])
|
778
|
-
self.update_display()
|
779
|
-
|
780
|
-
def draw(self, event):
|
781
|
-
if self.drawing:
|
782
|
-
x, y = event.x, event.y
|
783
|
-
if self.draw_coordinates:
|
784
|
-
last_x, last_y = self.draw_coordinates[-1]
|
785
|
-
self.current_line = self.canvas.create_line(last_x, last_y, x, y, fill="yellow", width=3)
|
786
|
-
self.draw_coordinates.append((x, y))
|
787
|
-
|
788
|
-
def draw_on_zoomed_mask(self, draw_coordinates):
|
789
|
-
canvas_height = self.canvas.winfo_height()
|
790
|
-
canvas_width = self.canvas.winfo_width()
|
791
|
-
zoomed_mask = np.zeros((canvas_height, canvas_width), dtype=np.uint8)
|
792
|
-
rr, cc = polygon(np.array(draw_coordinates)[:, 1], np.array(draw_coordinates)[:, 0], shape=zoomed_mask.shape)
|
793
|
-
zoomed_mask[rr, cc] = 255
|
794
|
-
return zoomed_mask
|
795
|
-
|
796
|
-
def finish_drawing(self, event):
|
797
|
-
if len(self.draw_coordinates) > 2:
|
798
|
-
self.draw_coordinates.append(self.draw_coordinates[0])
|
799
|
-
if self.zoom_active:
|
800
|
-
x0, x1, y0, y1 = self.zoom_x0, self.zoom_x1, self.zoom_y0, self.zoom_y1
|
801
|
-
zoomed_mask = self.draw_on_zoomed_mask(self.draw_coordinates)
|
802
|
-
self.update_original_mask(zoomed_mask, x0, x1, y0, y1)
|
803
|
-
else:
|
804
|
-
rr, cc = polygon(np.array(self.draw_coordinates)[:, 1], np.array(self.draw_coordinates)[:, 0], shape=self.mask.shape)
|
805
|
-
self.mask[rr, cc] = np.maximum(self.mask[rr, cc], 255)
|
806
|
-
self.mask = self.mask.copy()
|
807
|
-
self.canvas.delete(self.current_line)
|
808
|
-
self.draw_coordinates.clear()
|
809
|
-
self.update_display()
|
810
|
-
|
811
|
-
def finish_drawing_if_active(self, event):
|
812
|
-
if self.drawing and len(self.draw_coordinates) > 2:
|
813
|
-
self.finish_drawing(event)
|
814
|
-
|
815
|
-
####################################################################################################
|
816
|
-
# Single function butons#
|
817
|
-
####################################################################################################
|
818
|
-
|
819
|
-
def apply_normalization(self):
|
820
|
-
self.lower_quantile.set(self.lower_entry.get())
|
821
|
-
self.upper_quantile.set(self.upper_entry.get())
|
822
|
-
self.update_display()
|
823
|
-
|
824
|
-
def fill_objects(self):
|
825
|
-
binary_mask = self.mask > 0
|
826
|
-
filled_mask = binary_fill_holes(binary_mask)
|
827
|
-
self.mask = filled_mask.astype(np.uint8) * 255
|
828
|
-
labeled_mask, _ = label(filled_mask)
|
829
|
-
self.mask = labeled_mask
|
830
|
-
self.update_display()
|
831
|
-
|
832
|
-
def relabel_objects(self):
|
833
|
-
mask = self.mask
|
834
|
-
labeled_mask, num_labels = label(mask > 0)
|
835
|
-
self.mask = labeled_mask
|
836
|
-
self.update_display()
|
837
|
-
|
838
|
-
def clear_objects(self):
|
839
|
-
self.mask = np.zeros_like(self.mask)
|
840
|
-
self.update_display()
|
841
|
-
|
842
|
-
def invert_mask(self):
|
843
|
-
self.mask = np.where(self.mask > 0, 0, 1)
|
844
|
-
self.relabel_objects()
|
845
|
-
self.update_display()
|
846
|
-
|
847
|
-
def remove_small_objects(self):
|
848
|
-
try:
|
849
|
-
min_area = int(self.min_area_entry.get())
|
850
|
-
except ValueError:
|
851
|
-
print("Invalid minimum area value; using default of 100")
|
852
|
-
min_area = 100
|
853
|
-
|
854
|
-
labeled_mask, num_labels = label(self.mask > 0)
|
855
|
-
for i in range(1, num_labels + 1): # Skip background
|
856
|
-
if np.sum(labeled_mask == i) < min_area:
|
857
|
-
self.mask[labeled_mask == i] = 0 # Remove small objects
|
858
|
-
self.update_display()
|
859
|
-
|
860
|
-
##@log_function_call
|
861
|
-
def initiate_mask_app_root(width, height):
|
862
|
-
theme = 'breeze'
|
863
|
-
root = ThemedTk(theme=theme)
|
864
|
-
style = ttk.Style(root)
|
51
|
+
label.grid() # Show the label
|
52
|
+
widget.grid() # Show the widget
|
53
|
+
|
54
|
+
#@log_function_call
|
55
|
+
def initiate_abort():
|
56
|
+
global thread_control
|
57
|
+
if thread_control.get("stop_requested") is not None:
|
58
|
+
thread_control["stop_requested"].value = 1
|
59
|
+
|
60
|
+
if thread_control.get("run_thread") is not None:
|
61
|
+
thread_control["run_thread"].join(timeout=5)
|
62
|
+
if thread_control["run_thread"].is_alive():
|
63
|
+
thread_control["run_thread"].terminate()
|
64
|
+
thread_control["run_thread"] = None
|
65
|
+
|
66
|
+
#@log_function_call
|
67
|
+
def run_mask_gui(q, fig_queue, stop_requested):
|
68
|
+
global vars_dict
|
69
|
+
process_stdout_stderr(q)
|
70
|
+
try:
|
71
|
+
settings = check_mask_gui_settings(vars_dict)
|
72
|
+
preprocess_generate_masks_wrapper(settings, q, fig_queue)
|
73
|
+
except Exception as e:
|
74
|
+
q.put(f"Error during processing: {e}")
|
75
|
+
traceback.print_exc()
|
76
|
+
finally:
|
77
|
+
stop_requested.value = 1
|
78
|
+
|
79
|
+
#@log_function_call
|
80
|
+
def start_process(q, fig_queue):
|
81
|
+
global thread_control
|
82
|
+
if thread_control.get("run_thread") is not None:
|
83
|
+
initiate_abort()
|
84
|
+
|
85
|
+
stop_requested = Value('i', 0) # multiprocessing shared value for inter-process communication
|
86
|
+
thread_control["stop_requested"] = stop_requested
|
87
|
+
thread_control["run_thread"] = Process(target=run_mask_gui, args=(q, fig_queue, stop_requested))
|
88
|
+
thread_control["run_thread"].start()
|
89
|
+
|
90
|
+
def import_settings(scrollable_frame):
|
91
|
+
global vars_dict
|
92
|
+
|
93
|
+
csv_file_path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
|
94
|
+
csv_settings = read_settings_from_csv(csv_file_path)
|
95
|
+
variables = mask_variables()
|
96
|
+
new_settings = update_settings_from_csv(variables, csv_settings)
|
97
|
+
vars_dict = generate_fields(new_settings, scrollable_frame)
|
98
|
+
|
99
|
+
#@log_function_call
|
100
|
+
def initiate_mask_root(parent_frame):
|
101
|
+
global vars_dict, q, canvas, fig_queue, canvas_widget, thread_control, advanced_var, scrollable_frame
|
102
|
+
|
103
|
+
style = ttk.Style(parent_frame)
|
865
104
|
set_dark_style(style)
|
866
|
-
|
867
105
|
style_text_boxes(style)
|
868
|
-
set_default_font(
|
869
|
-
|
870
|
-
|
871
|
-
|
106
|
+
set_default_font(parent_frame, font_name="Helvetica", size=8)
|
107
|
+
parent_frame.configure(bg='black')
|
108
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
109
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
110
|
+
|
111
|
+
fig_queue = Queue()
|
112
|
+
|
113
|
+
# Initialize after_tasks if not already done
|
114
|
+
if not hasattr(parent_frame, 'after_tasks'):
|
115
|
+
parent_frame.after_tasks = []
|
872
116
|
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
117
|
+
def _process_fig_queue():
|
118
|
+
global canvas
|
119
|
+
try:
|
120
|
+
while not fig_queue.empty():
|
121
|
+
clear_canvas(canvas)
|
122
|
+
fig = fig_queue.get_nowait()
|
123
|
+
for ax in fig.get_axes():
|
124
|
+
ax.set_xticks([]) # Remove x-axis ticks
|
125
|
+
ax.set_yticks([]) # Remove y-axis ticks
|
126
|
+
ax.xaxis.set_visible(False) # Hide the x-axis
|
127
|
+
ax.yaxis.set_visible(False) # Hide the y-axis
|
128
|
+
fig.tight_layout()
|
129
|
+
fig.set_facecolor('black')
|
130
|
+
canvas.figure = fig
|
131
|
+
fig_width, fig_height = canvas_widget.winfo_width(), canvas_widget.winfo_height()
|
132
|
+
fig.set_size_inches(fig_width / fig.dpi, fig_height / fig.dpi, forward=True)
|
133
|
+
canvas.draw_idle()
|
134
|
+
except Exception as e:
|
135
|
+
traceback.print_exc()
|
136
|
+
finally:
|
137
|
+
after_id = canvas_widget.after(100, _process_fig_queue)
|
138
|
+
parent_frame.after_tasks.append(after_id)
|
139
|
+
|
140
|
+
def _process_console_queue():
|
141
|
+
while not q.empty():
|
142
|
+
message = q.get_nowait()
|
143
|
+
console_output.insert(tk.END, message)
|
144
|
+
console_output.see(tk.END)
|
145
|
+
after_id = console_output.after(100, _process_console_queue)
|
146
|
+
parent_frame.after_tasks.append(after_id)
|
147
|
+
|
148
|
+
# Clear previous content if any
|
149
|
+
for widget in parent_frame.winfo_children():
|
150
|
+
widget.destroy()
|
151
|
+
|
152
|
+
vertical_container = tk.PanedWindow(parent_frame, orient=tk.HORIZONTAL)
|
153
|
+
vertical_container.grid(row=0, column=0, sticky=tk.NSEW)
|
154
|
+
parent_frame.grid_rowconfigure(0, weight=1)
|
155
|
+
parent_frame.grid_columnconfigure(0, weight=1)
|
156
|
+
|
157
|
+
# Settings Section
|
158
|
+
settings_frame = tk.Frame(vertical_container, bg='black')
|
159
|
+
vertical_container.add(settings_frame, stretch="always")
|
160
|
+
settings_label = ttk.Label(settings_frame, text="Settings", style="Custom.TLabel")
|
161
|
+
settings_label.grid(row=0, column=0, pady=10, padx=10)
|
162
|
+
scrollable_frame = ScrollableFrame(settings_frame, width=600)
|
163
|
+
scrollable_frame.grid(row=1, column=0, sticky="nsew")
|
164
|
+
settings_frame.grid_rowconfigure(1, weight=1)
|
165
|
+
settings_frame.grid_columnconfigure(0, weight=1)
|
166
|
+
|
167
|
+
# Create advanced settings checkbox
|
168
|
+
advanced_var = tk.BooleanVar(value=False)
|
169
|
+
advanced_Toggle = ToggleSwitch(scrollable_frame.scrollable_frame, text="Advanced Settings", variable=advanced_var, command=toggle_advanced_settings)
|
170
|
+
advanced_Toggle.grid(row=48, column=0, pady=10, padx=10)
|
171
|
+
variables = mask_variables()
|
172
|
+
vars_dict = generate_fields(variables, scrollable_frame)
|
173
|
+
toggle_advanced_settings()
|
174
|
+
vars_dict['Test mode'] = (None, None, tk.BooleanVar(value=False))
|
175
|
+
|
176
|
+
# Button section
|
177
|
+
test_mode_button = CustomButton(scrollable_frame.scrollable_frame, text="Test Mode", command=toggle_test_mode, font=('Helvetica', 10))
|
178
|
+
#CustomButton(buttons_frame, text=app_name, command=lambda app_name=app_name: self.load_app(app_name, app_func), font=('Helvetica', 12))
|
179
|
+
|
180
|
+
test_mode_button.grid(row=47, column=1, pady=10, padx=10)
|
181
|
+
import_btn = CustomButton(scrollable_frame.scrollable_frame, text="Import", command=lambda: import_settings(scrollable_frame), font=('Helvetica', 10))
|
182
|
+
import_btn.grid(row=47, column=0, pady=10, padx=10)
|
183
|
+
run_button = CustomButton(scrollable_frame.scrollable_frame, text="Run", command=lambda: start_process(q, fig_queue))
|
184
|
+
run_button.grid(row=45, column=0, pady=10, padx=10)
|
185
|
+
abort_button = CustomButton(scrollable_frame.scrollable_frame, text="Abort", command=initiate_abort, font=('Helvetica', 10))
|
186
|
+
abort_button.grid(row=45, column=1, pady=10, padx=10)
|
187
|
+
progress_label = ttk.Label(scrollable_frame.scrollable_frame, text="Processing: 0%", background="black", foreground="white")
|
188
|
+
progress_label.grid(row=50, column=0, columnspan=2, sticky="ew", pady=(5, 0), padx=10)
|
189
|
+
|
190
|
+
# Plot Canvas Section
|
191
|
+
plot_frame = tk.PanedWindow(vertical_container, orient=tk.VERTICAL)
|
192
|
+
vertical_container.add(plot_frame, stretch="always")
|
193
|
+
figure = Figure(figsize=(30, 4), dpi=100, facecolor='black')
|
194
|
+
plot = figure.add_subplot(111)
|
195
|
+
plot.plot([], []) # This creates an empty plot.
|
196
|
+
plot.axis('off')
|
197
|
+
canvas = FigureCanvasTkAgg(figure, master=plot_frame)
|
198
|
+
canvas.get_tk_widget().configure(cursor='arrow', background='black', highlightthickness=0)
|
199
|
+
canvas_widget = canvas.get_tk_widget()
|
200
|
+
plot_frame.add(canvas_widget, stretch="always")
|
201
|
+
canvas.draw()
|
202
|
+
canvas.figure = figure
|
203
|
+
|
204
|
+
# Console Section
|
205
|
+
console_frame = tk.Frame(vertical_container, bg='black')
|
206
|
+
vertical_container.add(console_frame, stretch="always")
|
207
|
+
console_label = ttk.Label(console_frame, text="Console", background="black", foreground="white")
|
208
|
+
console_label.grid(row=0, column=0, pady=10, padx=10)
|
209
|
+
console_output = scrolledtext.ScrolledText(console_frame, height=10, bg='black', fg='white', insertbackground='white')
|
210
|
+
console_output.grid(row=1, column=0, sticky="nsew")
|
211
|
+
console_frame.grid_rowconfigure(1, weight=1)
|
212
|
+
console_frame.grid_columnconfigure(0, weight=1)
|
213
|
+
|
214
|
+
q = Queue()
|
215
|
+
sys.stdout = StdoutRedirector(console_output)
|
216
|
+
sys.stderr = StdoutRedirector(console_output)
|
217
|
+
|
218
|
+
_process_console_queue()
|
219
|
+
_process_fig_queue()
|
220
|
+
|
221
|
+
after_id = parent_frame.after(100, lambda: main_thread_update_function(parent_frame, q, fig_queue, canvas_widget, progress_label))
|
222
|
+
parent_frame.after_tasks.append(after_id)
|
223
|
+
|
224
|
+
return parent_frame, vars_dict
|
225
|
+
|
226
|
+
def gui_mask():
|
227
|
+
root = tk.Tk()
|
228
|
+
width = root.winfo_screenwidth()
|
229
|
+
height = root.winfo_screenheight()
|
230
|
+
root.geometry(f"{width}x{height}")
|
231
|
+
root.title("SpaCr: generate masks")
|
232
|
+
|
233
|
+
# Clear previous content if any
|
234
|
+
if hasattr(root, 'content_frame'):
|
235
|
+
for widget in root.content_frame.winfo_children():
|
236
|
+
widget.destroy()
|
237
|
+
root.content_frame.grid_forget()
|
238
|
+
else:
|
239
|
+
root.content_frame = tk.Frame(root)
|
240
|
+
root.content_frame.grid(row=1, column=0, sticky="nsew")
|
241
|
+
root.grid_rowconfigure(1, weight=1)
|
242
|
+
root.grid_columnconfigure(0, weight=1)
|
243
|
+
|
244
|
+
initiate_mask_root(root.content_frame)
|
245
|
+
create_menu_bar(root)
|
922
246
|
root.mainloop()
|
923
247
|
|
924
248
|
if __name__ == "__main__":
|
925
|
-
|
249
|
+
gui_mask()
|