spacr 0.2.1__py3-none-any.whl → 0.2.21__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.
Files changed (90) hide show
  1. spacr/gui.py +2 -1
  2. spacr/gui_elements.py +2 -7
  3. spacr/resources/icons/abort.png +0 -0
  4. spacr/resources/icons/classify.png +0 -0
  5. spacr/resources/icons/make_masks.png +0 -0
  6. spacr/resources/icons/mask.png +0 -0
  7. spacr/resources/icons/measure.png +0 -0
  8. spacr/resources/icons/recruitment.png +0 -0
  9. spacr/resources/icons/regression.png +0 -0
  10. spacr/resources/icons/run.png +0 -0
  11. spacr/resources/icons/umap.png +0 -0
  12. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/METADATA +1 -1
  13. spacr-0.2.21.dist-info/RECORD +56 -0
  14. spacr/alpha.py +0 -807
  15. spacr/annotate_app.py +0 -670
  16. spacr/annotate_app_v2.py +0 -670
  17. spacr/app_make_masks_v2.py +0 -686
  18. spacr/classify_app.py +0 -201
  19. spacr/cli.py +0 -41
  20. spacr/foldseek.py +0 -779
  21. spacr/get_alfafold_structures.py +0 -72
  22. spacr/gui_2.py +0 -157
  23. spacr/gui_annotate.py +0 -145
  24. spacr/gui_classify_app.py +0 -201
  25. spacr/gui_make_masks_app.py +0 -927
  26. spacr/gui_make_masks_app_v2.py +0 -688
  27. spacr/gui_mask_app.py +0 -249
  28. spacr/gui_measure_app.py +0 -246
  29. spacr/gui_run.py +0 -58
  30. spacr/gui_sim_app.py +0 -0
  31. spacr/gui_wrappers.py +0 -149
  32. spacr/icons/abort.png +0 -0
  33. spacr/icons/abort.svg +0 -1
  34. spacr/icons/download.png +0 -0
  35. spacr/icons/download.svg +0 -1
  36. spacr/icons/download_for_offline_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
  37. spacr/icons/download_for_offline_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
  38. spacr/icons/logo_spacr.png +0 -0
  39. spacr/icons/make_masks.png +0 -0
  40. spacr/icons/make_masks.svg +0 -1
  41. spacr/icons/map_barcodes.png +0 -0
  42. spacr/icons/map_barcodes.svg +0 -1
  43. spacr/icons/mask.png +0 -0
  44. spacr/icons/mask.svg +0 -1
  45. spacr/icons/measure.png +0 -0
  46. spacr/icons/measure.svg +0 -1
  47. spacr/icons/play_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
  48. spacr/icons/play_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
  49. spacr/icons/run.png +0 -0
  50. spacr/icons/run.svg +0 -1
  51. spacr/icons/sequencing.png +0 -0
  52. spacr/icons/sequencing.svg +0 -1
  53. spacr/icons/settings.png +0 -0
  54. spacr/icons/settings.svg +0 -1
  55. spacr/icons/settings_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
  56. spacr/icons/settings_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
  57. spacr/icons/stop_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
  58. spacr/icons/stop_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
  59. spacr/icons/theater_comedy_100dp_E8EAED_FILL0_wght100_GRAD200_opsz48.png +0 -0
  60. spacr/icons/theater_comedy_100dp_E8EAED_FILL0_wght100_GRAD200_opsz48.svg +0 -1
  61. spacr/make_masks_app.py +0 -929
  62. spacr/make_masks_app_v2.py +0 -688
  63. spacr/mask_app.py +0 -249
  64. spacr/measure_app.py +0 -246
  65. spacr/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model +0 -0
  66. spacr/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model_settings.csv +0 -23
  67. spacr/models/cp/toxo_pv_lumen.CP_model +0 -0
  68. spacr/old_code.py +0 -358
  69. spacr/resources/icons/abort.svg +0 -1
  70. spacr/resources/icons/annotate.svg +0 -1
  71. spacr/resources/icons/classify.svg +0 -1
  72. spacr/resources/icons/download.svg +0 -1
  73. spacr/resources/icons/icon.psd +0 -0
  74. spacr/resources/icons/make_masks.svg +0 -1
  75. spacr/resources/icons/map_barcodes.svg +0 -1
  76. spacr/resources/icons/mask.svg +0 -1
  77. spacr/resources/icons/measure.svg +0 -1
  78. spacr/resources/icons/run.svg +0 -1
  79. spacr/resources/icons/run_2.png +0 -0
  80. spacr/resources/icons/run_2.svg +0 -1
  81. spacr/resources/icons/sequencing.svg +0 -1
  82. spacr/resources/icons/settings.svg +0 -1
  83. spacr/resources/icons/train_cellpose.svg +0 -1
  84. spacr/test_gui.py +0 -0
  85. spacr-0.2.1.dist-info/RECORD +0 -126
  86. /spacr/resources/icons/{cellpose.png → cellpose_all.png} +0 -0
  87. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/LICENSE +0 -0
  88. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/WHEEL +0 -0
  89. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/entry_points.txt +0 -0
  90. {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/top_level.txt +0 -0
@@ -1,688 +0,0 @@
1
- import os
2
- from qtpy import QtCore
3
- from tkinter import ttk
4
- import numpy as np
5
- import tkinter as tk
6
- import imageio.v2 as imageio
7
- from collections import deque
8
- from PIL import Image, ImageTk
9
- from skimage.draw import polygon, line
10
- from skimage.transform import resize
11
- from scipy.ndimage import binary_fill_holes, label
12
- from ttkthemes import ThemedTk
13
- from pyqtgraph import GraphicsLayoutWidget, ViewBox, ImageItem, mkQApp
14
-
15
- from .logger import log_function_call
16
- from .gui_utils import ScrollableFrame, CustomButton, set_dark_style, set_default_font, create_dark_mode, style_text_boxes, create_menu_bar
17
-
18
- class ModifyMasks:
19
- def __init__(self, root, folder_path, scale_factor):
20
- self.root = root
21
- self.folder_path = folder_path
22
- self.scale_factor = scale_factor
23
- self.image_filenames = sorted([f for f in os.listdir(folder_path) if f.endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff'))])
24
- self.masks_folder = os.path.join(folder_path, 'masks')
25
- self.current_image_index = 0
26
- self.initialize_flags()
27
- self.canvas_width = 1000
28
- self.canvas_height = 1000
29
- self.root.configure(bg='black')
30
- self.setup_navigation_toolbar()
31
- self.setup_mode_toolbar()
32
- self.setup_function_toolbar()
33
- self.setup_canvas()
34
- self.load_first_image()
35
-
36
- def update_display(self):
37
- self.display_image()
38
-
39
- def update_original_mask(self, zoomed_mask, x0, x1, y0, y1):
40
- actual_mask_region = self.mask[y0:y1, x0:x1]
41
- target_shape = actual_mask_region.shape
42
- resized_mask = resize(zoomed_mask, target_shape, order=0, preserve_range=True).astype(np.uint8)
43
- if resized_mask.shape != actual_mask_region.shape:
44
- raise ValueError(f"Shape mismatch: resized_mask {resized_mask.shape}, actual_mask_region {actual_mask_region.shape}")
45
- self.mask[y0:y1, x0:x1] = np.maximum(actual_mask_region, resized_mask)
46
- self.mask = self.mask.copy()
47
- self.mask[y0:y1, x0:x1] = np.maximum(self.mask[y0:y1, x0:x1], resized_mask)
48
- self.mask = self.mask.copy()
49
-
50
- def get_scaling_factors(self, img_width, img_height, canvas_width, canvas_height):
51
- x_scale = img_width / canvas_width
52
- y_scale = img_height / canvas_height
53
- return x_scale, y_scale
54
-
55
- def canvas_to_image(self, x_canvas, y_canvas):
56
- x_scale, y_scale = self.get_scaling_factors(
57
- self.image.shape[1], self.image.shape[0],
58
- self.canvas_width, self.canvas_height
59
- )
60
- x_image = int(x_canvas * x_scale)
61
- y_image = int(y_canvas * y_scale)
62
- return x_image, y_image
63
-
64
- def normalize_image(self, image, lower_quantile, upper_quantile):
65
- lower_bound = np.percentile(image, lower_quantile)
66
- upper_bound = np.percentile(image, upper_quantile)
67
- normalized = np.clip(image, lower_bound, upper_bound)
68
- normalized = (normalized - lower_bound) / (upper_bound - lower_bound)
69
- max_value = np.iinfo(image.dtype).max
70
- normalized = (normalized * max_value).astype(image.dtype)
71
- return normalized
72
-
73
- def resize_arrays(self, img, mask):
74
- original_dtype = img.dtype
75
- scaled_height = int(img.shape[0] * self.scale_factor)
76
- scaled_width = int(img.shape[1] * self.scale_factor)
77
- scaled_img = resize(img, (scaled_height, scaled_width), anti_aliasing=True, preserve_range=True)
78
- scaled_mask = resize(mask, (scaled_height, scaled_width), order=0, anti_aliasing=False, preserve_range=True)
79
- stretched_img = resize(scaled_img, (self.canvas_height, self.canvas_width), anti_aliasing=True, preserve_range=True)
80
- stretched_mask = resize(scaled_mask, (self.canvas_height, self.canvas_width), order=0, anti_aliasing=False, preserve_range=True)
81
- return stretched_img.astype(original_dtype), stretched_mask.astype(original_dtype)
82
-
83
- def load_first_image(self):
84
- self.image, self.mask = self.load_image_and_mask(self.current_image_index)
85
- self.original_size = self.image.shape
86
- self.image, self.mask = self.resize_arrays(self.image, self.mask)
87
- self.display_image()
88
-
89
- def setup_canvas(self):
90
- self.app = mkQApp()
91
- self.win = GraphicsLayoutWidget(show=True)
92
- self.win.setWindowTitle('Image Canvas')
93
-
94
- self.view = ViewBox()
95
- self.view.setAspectLocked(True)
96
- self.win.addItem(self.view)
97
-
98
- self.img = ImageItem()
99
- self.view.addItem(self.img)
100
-
101
- self.view.sigMouseDragged.connect(self.mouse_dragged)
102
- self.view.sigMouseClicked.connect(self.mouse_clicked)
103
- self.view.sigMouseWheel.connect(self.mouse_wheel)
104
-
105
- def initialize_flags(self):
106
- self.drawing = False
107
- self.magic_wand_active = False
108
- self.brush_active = False
109
- self.dividing_line_active = False
110
- self.dividing_line_coords = []
111
- self.current_dividing_line = None
112
- self.lower_quantile = tk.StringVar(value="1.0")
113
- self.upper_quantile = tk.StringVar(value="99.9")
114
- self.magic_wand_tolerance = tk.StringVar(value="1000")
115
-
116
- def update_mouse_info(self, event):
117
- x, y = event.x, event.y
118
- intensity = "N/A"
119
- mask_value = "N/A"
120
- pixel_count = "N/A"
121
- if 0 <= x < self.image.shape[1] and 0 <= y < self.image.shape[0]:
122
- intensity = self.image[y, x]
123
- mask_value = self.mask[y, x]
124
- if mask_value != "N/A" and mask_value != 0:
125
- pixel_count = np.sum(self.mask == mask_value)
126
- self.intensity_label.config(text=f"Intensity: {intensity}")
127
- self.mask_value_label.config(text=f"Mask: {mask_value}, Area: {pixel_count}")
128
- self.mask_value_label.config(text=f"Mask: {mask_value}")
129
- if mask_value != "N/A" and mask_value != 0:
130
- self.pixel_count_label.config(text=f"Area: {pixel_count}")
131
- else:
132
- self.pixel_count_label.config(text="Area: N/A")
133
-
134
- def setup_navigation_toolbar(self):
135
- navigation_toolbar = tk.Frame(self.root, bg='black')
136
- navigation_toolbar.pack(side='top', fill='x')
137
- prev_btn = tk.Button(navigation_toolbar, text="Previous", command=self.previous_image, bg='black', fg='white')
138
- prev_btn.pack(side='left')
139
- next_btn = tk.Button(navigation_toolbar, text="Next", command=self.next_image, bg='black', fg='white')
140
- next_btn.pack(side='left')
141
- save_btn = tk.Button(navigation_toolbar, text="Save", command=self.save_mask, bg='black', fg='white')
142
- save_btn.pack(side='left')
143
- self.intensity_label = tk.Label(navigation_toolbar, text="Image: N/A", bg='black', fg='white')
144
- self.intensity_label.pack(side='right')
145
- self.mask_value_label = tk.Label(navigation_toolbar, text="Mask: N/A", bg='black', fg='white')
146
- self.mask_value_label.pack(side='right')
147
- self.pixel_count_label = tk.Label(navigation_toolbar, text="Area: N/A", bg='black', fg='white')
148
- self.pixel_count_label.pack(side='right')
149
-
150
- def setup_mode_toolbar(self):
151
- self.mode_toolbar = tk.Frame(self.root, bg='black')
152
- self.mode_toolbar.pack(side='top', fill='x')
153
- self.draw_btn = tk.Button(self.mode_toolbar, text="Draw", command=self.toggle_draw_mode, bg='black', fg='white')
154
- self.draw_btn.pack(side='left')
155
- self.magic_wand_btn = tk.Button(self.mode_toolbar, text="Magic Wand", command=self.toggle_magic_wand_mode, bg='black', fg='white')
156
- self.magic_wand_btn.pack(side='left')
157
- tk.Label(self.mode_toolbar, text="Tolerance:", bg='black', fg='white').pack(side='left')
158
- self.tolerance_entry = tk.Entry(self.mode_toolbar, textvariable=self.magic_wand_tolerance, bg='black', fg='white')
159
- self.tolerance_entry.pack(side='left')
160
- tk.Label(self.mode_toolbar, text="Max Pixels:", bg='black', fg='white').pack(side='left')
161
- self.max_pixels_entry = tk.Entry(self.mode_toolbar, bg='black', fg='white')
162
- self.max_pixels_entry.insert(0, "1000")
163
- self.max_pixels_entry.pack(side='left')
164
- self.erase_btn = tk.Button(self.mode_toolbar, text="Erase", command=self.toggle_erase_mode, bg='black', fg='white')
165
- self.erase_btn.pack(side='left')
166
- self.brush_btn = tk.Button(self.mode_toolbar, text="Brush", command=self.toggle_brush_mode, bg='black', fg='white')
167
- self.brush_btn.pack(side='left')
168
- self.brush_size_entry = tk.Entry(self.mode_toolbar, bg='black', fg='white')
169
- self.brush_size_entry.insert(0, "10")
170
- self.brush_size_entry.pack(side='left')
171
- tk.Label(self.mode_toolbar, text="Brush Size:", bg='black', fg='white').pack(side='left')
172
- self.dividing_line_btn = tk.Button(self.mode_toolbar, text="Dividing Line", command=self.toggle_dividing_line_mode, bg='black', fg='white')
173
- self.dividing_line_btn.pack(side='left')
174
-
175
- def setup_function_toolbar(self):
176
- self.function_toolbar = tk.Frame(self.root, bg='black')
177
- self.function_toolbar.pack(side='top', fill='x')
178
- self.fill_btn = tk.Button(self.function_toolbar, text="Fill", command=self.fill_objects, bg='black', fg='white')
179
- self.fill_btn.pack(side='left')
180
- self.relabel_btn = tk.Button(self.function_toolbar, text="Relabel", command=self.relabel_objects, bg='black', fg='white')
181
- self.relabel_btn.pack(side='left')
182
- self.clear_btn = tk.Button(self.function_toolbar, text="Clear", command=self.clear_objects, bg='black', fg='white')
183
- self.clear_btn.pack(side='left')
184
- self.invert_btn = tk.Button(self.function_toolbar, text="Invert", command=self.invert_mask, bg='black', fg='white')
185
- self.invert_btn.pack(side='left')
186
- remove_small_btn = tk.Button(self.function_toolbar, text="Remove Small", command=self.remove_small_objects, bg='black', fg='white')
187
- remove_small_btn.pack(side='left')
188
- tk.Label(self.function_toolbar, text="Min Area:", bg='black', fg='white').pack(side='left')
189
- self.min_area_entry = tk.Entry(self.function_toolbar, bg='black', fg='white')
190
- self.min_area_entry.insert(0, "100") # Default minimum area
191
- self.min_area_entry.pack(side='left')
192
-
193
- def load_image_and_mask(self, index):
194
- image_path = os.path.join(self.folder_path, self.image_filenames[index])
195
- image = imageio.imread(image_path)
196
- mask_path = os.path.join(self.masks_folder, self.image_filenames[index])
197
- if os.path.exists(mask_path):
198
- print(f'loading mask:{mask_path} for image: {image_path}')
199
- mask = imageio.imread(mask_path)
200
- if mask.dtype != np.uint8:
201
- mask = (mask / np.max(mask) * 255).astype(np.uint8)
202
- else:
203
- mask = np.zeros(image.shape[:2], dtype=np.uint8)
204
- print(f'loaded new mask for image: {image_path}')
205
- return image, mask
206
-
207
- def display_image(self):
208
- lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
209
- upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
210
- normalized = self.normalize_image(self.image, lower_quantile, upper_quantile)
211
- combined = self.overlay_mask_on_image(normalized, self.mask)
212
- self.img.setImage(combined)
213
-
214
- def overlay_mask_on_image(self, image, mask, alpha=0.5):
215
- if len(image.shape) == 2:
216
- image = np.stack((image,) * 3, axis=-1)
217
- mask = mask.astype(np.int32)
218
- max_label = np.max(mask)
219
- np.random.seed(0)
220
- colors = np.random.randint(0, 255, size=(max_label + 1, 3), dtype=np.uint8)
221
- colors[0] = [0, 0, 0] # background color
222
- colored_mask = colors[mask]
223
- image_8bit = (image / 256).astype(np.uint8)
224
- combined_image = np.where(mask[..., None] > 0,
225
- np.clip(image_8bit * (1 - alpha) + colored_mask * alpha, 0, 255),
226
- image_8bit)
227
- combined_image = combined_image.astype(np.uint8)
228
- return combined_image
229
-
230
- def previous_image(self):
231
- if self.current_image_index > 0:
232
- self.current_image_index -= 1
233
- self.initialize_flags()
234
- self.image, self.mask = self.load_image_and_mask(self.current_image_index)
235
- self.original_size = self.image.shape
236
- self.image, self.mask = self.resize_arrays(self.image, self.mask)
237
- self.display_image()
238
-
239
- def next_image(self):
240
- if self.current_image_index < len(self.image_filenames) - 1:
241
- self.current_image_index += 1
242
- self.initialize_flags()
243
- self.image, self.mask = self.load_image_and_mask(self.current_image_index)
244
- self.original_size = self.image.shape
245
- self.image, self.mask = self.resize_arrays(self.image, self.mask)
246
- self.display_image()
247
-
248
- def save_mask(self):
249
- if self.current_image_index < len(self.image_filenames):
250
- original_size = self.original_size
251
- if self.mask.shape != original_size:
252
- resized_mask = resize(self.mask, original_size, order=0, preserve_range=True).astype(np.uint16)
253
- else:
254
- resized_mask = self.mask
255
- resized_mask, _ = label(resized_mask > 0)
256
- save_folder = os.path.join(self.folder_path, 'masks')
257
- if not os.path.exists(save_folder):
258
- os.makedirs(save_folder)
259
- image_filename = os.path.splitext(self.image_filenames[self.current_image_index])[0] + '.tif'
260
- save_path = os.path.join(save_folder, image_filename)
261
-
262
- print(f"Saving mask to: {save_path}")
263
- imageio.imwrite(save_path, resized_mask)
264
-
265
- def mouse_dragged(self, event):
266
- if event.button() == QtCore.Qt.LeftButton:
267
- self.view.translateBy(event.delta())
268
- event.accept()
269
-
270
- def mouse_clicked(self, event):
271
- if event.button() == QtCore.Qt.RightButton:
272
- self.view.resetTransform()
273
- event.accept()
274
-
275
- def mouse_wheel(self, event):
276
- delta = event.delta()
277
- if delta > 0:
278
- self.view.scaleBy((1.1, 1.1))
279
- else:
280
- self.view.scaleBy((0.9, 0.9))
281
- event.accept()
282
-
283
- def toggle_brush_mode(self):
284
- self.brush_active = not self.brush_active
285
- if self.brush_active:
286
- self.drawing = False
287
- self.magic_wand_active = False
288
- self.erase_active = False
289
- self.brush_btn.config(text="Brush ON")
290
- self.draw_btn.config(text="Draw")
291
- self.erase_btn.config(text="Erase")
292
- self.magic_wand_btn.config(text="Magic Wand")
293
- self.canvas.unbind("<Button-1>")
294
- self.canvas.unbind("<Button-3>")
295
- self.canvas.unbind("<Motion>")
296
- self.canvas.bind("<B1-Motion>", self.apply_brush)
297
- self.canvas.bind("<B3-Motion>", self.erase_brush)
298
- self.canvas.bind("<ButtonRelease-1>", self.apply_brush_release)
299
- self.canvas.bind("<ButtonRelease-3>", self.erase_brush_release)
300
- else:
301
- self.brush_active = False
302
- self.brush_btn.config(text="Brush")
303
- self.canvas.unbind("<B1-Motion>")
304
- self.canvas.unbind("<B3-Motion>")
305
- self.canvas.unbind("<ButtonRelease-1>")
306
- self.canvas.unbind("<ButtonRelease-3>")
307
-
308
- def image_to_canvas(self, x_image, y_image):
309
- x_scale, y_scale = self.get_scaling_factors(
310
- self.image.shape[1], self.image.shape[0],
311
- self.canvas_width, self.canvas_height
312
- )
313
- x_canvas = int(x_image / x_scale)
314
- y_canvas = int(y_image / y_scale)
315
- return x_canvas, y_canvas
316
-
317
- def toggle_dividing_line_mode(self):
318
- self.dividing_line_active = not self.dividing_line_active
319
- if self.dividing_line_active:
320
- self.drawing = False
321
- self.magic_wand_active = False
322
- self.erase_active = False
323
- self.brush_active = False
324
- self.draw_btn.config(text="Draw")
325
- self.erase_btn.config(text="Erase")
326
- self.magic_wand_btn.config(text="Magic Wand")
327
- self.brush_btn.config(text="Brush")
328
- self.dividing_line_btn.config(text="Dividing Line ON")
329
- self.canvas.unbind("<Button-1>")
330
- self.canvas.unbind("<ButtonRelease-1>")
331
- self.canvas.unbind("<Motion>")
332
- self.canvas.bind("<Button-1>", self.start_dividing_line)
333
- self.canvas.bind("<ButtonRelease-1>", self.finish_dividing_line)
334
- self.canvas.bind("<Motion>", self.update_dividing_line_preview)
335
- else:
336
- self.dividing_line_active = False
337
- self.dividing_line_btn.config(text="Dividing Line")
338
- self.canvas.unbind("<Button-1>")
339
- self.canvas.unbind("<ButtonRelease-1>")
340
- self.canvas.unbind("<Motion>")
341
- self.display_image()
342
-
343
- def start_dividing_line(self, event):
344
- if self.dividing_line_active:
345
- self.dividing_line_coords = [(event.x, event.y)]
346
- self.current_dividing_line = self.canvas.create_line(event.x, event.y, event.x, event.y, fill="red", width=2)
347
-
348
- def finish_dividing_line(self, event):
349
- if self.dividing_line_active:
350
- self.dividing_line_coords.append((event.x, event.y))
351
- self.apply_dividing_line()
352
- self.canvas.delete(self.current_dividing_line)
353
- self.current_dividing_line = None
354
-
355
- def update_dividing_line_preview(self, event):
356
- if self.dividing_line_active and self.dividing_line_coords:
357
- x, y = event.x, event.y
358
- self.dividing_line_coords.append((x, y))
359
- canvas_coords = [self.image_to_canvas(*pt) for pt in self.dividing_line_coords]
360
- flat_canvas_coords = [coord for pt in canvas_coords for coord in pt]
361
- self.canvas.coords(self.current_dividing_line, *flat_canvas_coords)
362
-
363
- def apply_dividing_line(self):
364
- if self.dividing_line_coords:
365
- coords = self.dividing_line_coords
366
- rr, cc = [], []
367
- for (x0, y0), (x1, y1) in zip(coords[:-1], coords[1:]):
368
- line_rr, line_cc = line(y0, x0, y1, x1)
369
- rr.extend(line_rr)
370
- cc.extend(line_cc)
371
- rr, cc = np.array(rr), np.array(cc)
372
-
373
- mask_copy = self.mask.copy()
374
- mask_copy[rr, cc] = 0
375
- self.mask = mask_copy
376
-
377
- labeled_mask, num_labels = label(self.mask > 0)
378
- self.mask = labeled_mask
379
- self.update_display()
380
-
381
- self.dividing_line_coords = []
382
- self.canvas.unbind("<Button-1>")
383
- self.canvas.unbind("<ButtonRelease-1>")
384
- self.canvas.unbind("<Motion>")
385
- self.dividing_line_active = False
386
- self.dividing_line_btn.config(text="Dividing Line")
387
-
388
- def toggle_draw_mode(self):
389
- self.drawing = not self.drawing
390
- if self.drawing:
391
- self.brush_btn.config(text="Brush")
392
- self.canvas.unbind("<B1-Motion>")
393
- self.canvas.unbind("<B3-Motion>")
394
- self.canvas.unbind("<ButtonRelease-1>")
395
- self.canvas.unbind("<ButtonRelease-3>")
396
- self.magic_wand_active = False
397
- self.erase_active = False
398
- self.brush_active = False
399
- self.draw_btn.config(text="Draw ON")
400
- self.magic_wand_btn.config(text="Magic Wand")
401
- self.erase_btn.config(text="Erase")
402
- self.draw_coordinates = []
403
- self.canvas.unbind("<Button-1>")
404
- self.canvas.unbind("<Motion>")
405
- self.canvas.bind("<B1-Motion>", self.draw)
406
- self.canvas.bind("<ButtonRelease-1>", self.finish_drawing)
407
- else:
408
- self.drawing = False
409
- self.draw_btn.config(text="Draw")
410
- self.canvas.unbind("<B1-Motion>")
411
- self.canvas.unbind("<ButtonRelease-1>")
412
-
413
- def toggle_magic_wand_mode(self):
414
- self.magic_wand_active = not self.magic_wand_active
415
- if self.magic_wand_active:
416
- self.brush_btn.config(text="Brush")
417
- self.canvas.unbind("<B1-Motion>")
418
- self.canvas.unbind("<B3-Motion>")
419
- self.canvas.unbind("<ButtonRelease-1>")
420
- self.canvas.unbind("<ButtonRelease-3>")
421
- self.drawing = False
422
- self.erase_active = False
423
- self.brush_active = False
424
- self.draw_btn.config(text="Draw")
425
- self.erase_btn.config(text="Erase")
426
- self.magic_wand_btn.config(text="Magic Wand ON")
427
- self.canvas.bind("<Button-1>", self.use_magic_wand)
428
- self.canvas.bind("<Button-3>", self.use_magic_wand)
429
- else:
430
- self.magic_wand_btn.config(text="Magic Wand")
431
- self.canvas.unbind("<Button-1>")
432
- self.canvas.unbind("<Button-3>")
433
-
434
- def toggle_erase_mode(self):
435
- self.erase_active = not self.erase_active
436
- if self.erase_active:
437
- self.brush_btn.config(text="Brush")
438
- self.canvas.unbind("<B1-Motion>")
439
- self.canvas.unbind("<B3-Motion>")
440
- self.canvas.unbind("<ButtonRelease-1>")
441
- self.canvas.unbind("<ButtonRelease-3>")
442
- self.erase_btn.config(text="Erase ON")
443
- self.canvas.bind("<Button-1>", self.erase_object)
444
- self.drawing = False
445
- self.magic_wand_active = False
446
- self.brush_active = False
447
- self.draw_btn.config(text="Draw")
448
- self.magic_wand_btn.config(text="Magic Wand")
449
- else:
450
- self.erase_active = False
451
- self.erase_btn.config(text="Erase")
452
- self.canvas.unbind("<Button-1>")
453
-
454
- def apply_brush_release(self, event):
455
- if hasattr(self, 'brush_path'):
456
- for x, y, brush_size in self.brush_path:
457
- img_x, img_y = (x, y)
458
- x0 = max(img_x - brush_size // 2, 0)
459
- y0 = max(img_y - brush_size // 2, 0)
460
- x1 = min(img_x + brush_size // 2, self.mask.shape[1])
461
- y1 = min(img_y + brush_size // 2, self.mask.shape[0])
462
- self.mask[y0:y1, x0:x1] = 255
463
- del self.brush_path
464
- self.canvas.delete("temp_line")
465
- self.update_display()
466
-
467
- def erase_brush_release(self, event):
468
- if hasattr(self, 'erase_path'):
469
- for x, y, brush_size in self.erase_path:
470
- img_x, img_y = (x, y)
471
- x0 = max(img_x - brush_size // 2, 0)
472
- y0 = max(img_y - brush_size // 2, 0)
473
- x1 = min(img_x + brush_size // 2, self.mask.shape[1])
474
- y1 = min(img_y + brush_size // 2, self.mask.shape[0])
475
- self.mask[y0:y1, x0:x1] = 0
476
- del self.erase_path
477
- self.canvas.delete("temp_line")
478
- self.update_display()
479
-
480
- def apply_brush(self, event):
481
- brush_size = int(self.brush_size_entry.get())
482
- x, y = event.x, event.y
483
- if not hasattr(self, 'brush_path'):
484
- self.brush_path = []
485
- self.last_brush_coord = (x, y)
486
- if self.last_brush_coord:
487
- last_x, last_y = self.last_brush_coord
488
- rr, cc = line(last_y, last_x, y, x)
489
- for ry, rx in zip(rr, cc):
490
- self.brush_path.append((rx, ry, brush_size))
491
-
492
- self.canvas.create_line(self.last_brush_coord[0], self.last_brush_coord[1], x, y, width=brush_size, fill="blue", tag="temp_line")
493
- self.last_brush_coord = (x, y)
494
-
495
- def erase_brush(self, event):
496
- brush_size = int(self.brush_size_entry.get())
497
- x, y = event.x, event.y
498
- if not hasattr(self, 'erase_path'):
499
- self.erase_path = []
500
- self.last_erase_coord = (x, y)
501
- if self.last_erase_coord:
502
- last_x, last_y = self.last_erase_coord
503
- rr, cc = line(last_y, last_x, y, x)
504
- for ry, rx in zip(rr, cc):
505
- self.erase_path.append((rx, ry, brush_size))
506
-
507
- self.canvas.create_line(self.last_erase_coord[0], self.last_erase_coord[1], x, y, width=brush_size, fill="white", tag="temp_line")
508
- self.last_erase_coord = (x, y)
509
-
510
- def erase_object(self, event):
511
- x, y = event.x, event.y
512
- orig_x, orig_y = x, y
513
- label_to_remove = self.mask[orig_y, orig_x]
514
- if label_to_remove > 0:
515
- self.mask[self.mask == label_to_remove] = 0
516
- self.update_display()
517
-
518
- def use_magic_wand(self, event):
519
- x, y = event.x, event.y
520
- tolerance = int(self.magic_wand_tolerance.get())
521
- maximum = int(self.max_pixels_entry.get())
522
- action = 'add' if event.num == 1 else 'erase'
523
- self.magic_wand_normal((x, y), tolerance, action)
524
-
525
- def apply_magic_wand(self, image, mask, seed_point, tolerance, maximum, action='add'):
526
- x, y = seed_point
527
- initial_value = image[y, x].astype(np.float32)
528
- visited = np.zeros_like(image, dtype=bool)
529
- queue = deque([(x, y)])
530
- added_pixels = 0
531
-
532
- while queue and added_pixels < maximum:
533
- cx, cy = queue.popleft()
534
- if visited[cy, cx]:
535
- continue
536
- visited[cy, cx] = True
537
- current_value = image[cy, cx].astype(np.float32)
538
-
539
- if np.linalg.norm(abs(current_value - initial_value)) <= tolerance:
540
- if mask[cy, cx] == 0:
541
- added_pixels += 1
542
- mask[cy, cx] = 255 if action == 'add' else 0
543
-
544
- if added_pixels >= maximum:
545
- break
546
-
547
- for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
548
- nx, ny = cx + dx, cy + dy
549
- if 0 <= nx < image.shape[1] and 0 <= ny < image.shape[0] and not visited[ny, nx]:
550
- queue.append((nx, ny))
551
- return mask
552
-
553
- def magic_wand_normal(self, seed_point, tolerance, action):
554
- try:
555
- maximum = int(self.max_pixels_entry.get())
556
- except ValueError:
557
- print("Invalid maximum value; using default of 1000")
558
- maximum = 1000
559
- self.mask = self.apply_magic_wand(self.image, self.mask, seed_point, tolerance, maximum, action)
560
- self.display_image()
561
-
562
- def draw(self, event):
563
- if self.drawing:
564
- x, y = event.x, event.y
565
- if self.draw_coordinates:
566
- last_x, last_y = self.draw_coordinates[-1]
567
- self.current_line = self.canvas.create_line(last_x, last_y, x, y, fill="yellow", width=3)
568
- self.draw_coordinates.append((x, y))
569
-
570
- def draw_on_zoomed_mask(self, draw_coordinates):
571
- canvas_height = self.canvas.winfo_height()
572
- canvas_width = self.canvas.winfo_width()
573
- zoomed_mask = np.zeros((canvas_height, canvas_width), dtype=np.uint8)
574
- rr, cc = polygon(np.array(draw_coordinates)[:, 1], np.array(draw_coordinates)[:, 0], shape=zoomed_mask.shape)
575
- zoomed_mask[rr, cc] = 255
576
- return zoomed_mask
577
-
578
- def finish_drawing(self, event):
579
- if len(self.draw_coordinates) > 2:
580
- self.draw_coordinates.append(self.draw_coordinates[0])
581
- rr, cc = polygon(np.array(self.draw_coordinates)[:, 1], np.array(self.draw_coordinates)[:, 0], shape=self.mask.shape)
582
- self.mask[rr, cc] = np.maximum(self.mask[rr, cc], 255)
583
- self.mask = self.mask.copy()
584
- self.canvas.delete(self.current_line)
585
- self.draw_coordinates.clear()
586
- self.update_display()
587
-
588
- def finish_drawing_if_active(self, event):
589
- if self.drawing and len(self.draw_coordinates) > 2:
590
- self.finish_drawing(event)
591
-
592
- def apply_normalization(self):
593
- self.lower_quantile.set(self.lower_entry.get())
594
- self.upper_quantile.set(self.upper_entry.get())
595
- self.update_display()
596
-
597
- def fill_objects(self):
598
- binary_mask = self.mask > 0
599
- filled_mask = binary_fill_holes(binary_mask)
600
- self.mask = filled_mask.astype(np.uint8) * 255
601
- labeled_mask, _ = label(filled_mask)
602
- self.mask = labeled_mask
603
- self.update_display()
604
-
605
- def relabel_objects(self):
606
- mask = self.mask
607
- labeled_mask, num_labels = label(mask > 0)
608
- self.mask = labeled_mask
609
- self.update_display()
610
-
611
- def clear_objects(self):
612
- self.mask = np.zeros_like(self.mask)
613
- self.update_display()
614
-
615
- def invert_mask(self):
616
- self.mask = np.where(self.mask > 0, 0, 1)
617
- self.relabel_objects()
618
- self.update_display()
619
-
620
- def remove_small_objects(self):
621
- try:
622
- min_area = int(self.min_area_entry.get())
623
- except ValueError:
624
- print("Invalid minimum area value; using default of 100")
625
- min_area = 100
626
-
627
- labeled_mask, num_labels = label(self.mask > 0)
628
- for i in range(1, num_labels + 1):
629
- if np.sum(labeled_mask == i) < min_area:
630
- self.mask[labeled_mask == i] = 0
631
- self.update_display()
632
-
633
- def initiate_mask_app_root(width, height):
634
- theme = 'breeze'
635
- root = ThemedTk(theme=theme)
636
- style = ttk.Style(root)
637
- set_dark_style(style)
638
-
639
- style_text_boxes(style)
640
- set_default_font(root, font_name="Arial", size=8)
641
- root.geometry(f"{width}x{height}")
642
- root.title("Mask App")
643
- create_menu_bar(root)
644
-
645
- container = tk.PanedWindow(root, orient=tk.HORIZONTAL)
646
- container.pack(fill=tk.BOTH, expand=True)
647
-
648
- scrollable_frame = ScrollableFrame(container, bg='#333333')
649
- container.add(scrollable_frame, stretch="always")
650
-
651
- vars_dict = {
652
- 'folder_path': ttk.Entry(scrollable_frame.scrollable_frame),
653
- 'scale_factor': ttk.Entry(scrollable_frame.scrollable_frame)
654
- }
655
-
656
- row = 0
657
- for name, entry in vars_dict.items():
658
- ttk.Label(scrollable_frame.scrollable_frame, text=f"{name.replace('_', ' ').capitalize()}:").grid(row=row, column=0)
659
- entry.grid(row=row, column=1)
660
- row += 1
661
-
662
- def run_app():
663
- folder_path = vars_dict['folder_path'].get()
664
- scale_factor = float(vars_dict['scale_factor'].get())
665
-
666
- root.quit()
667
- root.destroy()
668
-
669
- new_root = ThemedTk(theme=theme)
670
- new_root.geometry(f"{1000}x{1000}")
671
- new_root.title("Mask Application")
672
-
673
- app_instance = ModifyMasks(new_root, folder_path, scale_factor)
674
- new_root.mainloop()
675
-
676
- create_dark_mode(root, style, console_output=None)
677
-
678
- run_button = CustomButton(scrollable_frame.scrollable_frame, text="Run", command=run_app)
679
- run_button.grid(row=row, column=0, columnspan=2, pady=10, padx=10)
680
-
681
- return root
682
-
683
- def gui_make_masks():
684
- root = initiate_mask_app_root(330, 150)
685
- root.mainloop()
686
-
687
- if __name__ == "__main__":
688
- gui_make_masks()