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.
- spacr/gui.py +2 -1
- spacr/gui_elements.py +2 -7
- spacr/resources/icons/abort.png +0 -0
- spacr/resources/icons/classify.png +0 -0
- spacr/resources/icons/make_masks.png +0 -0
- spacr/resources/icons/mask.png +0 -0
- spacr/resources/icons/measure.png +0 -0
- spacr/resources/icons/recruitment.png +0 -0
- spacr/resources/icons/regression.png +0 -0
- spacr/resources/icons/run.png +0 -0
- spacr/resources/icons/umap.png +0 -0
- {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/METADATA +1 -1
- spacr-0.2.21.dist-info/RECORD +56 -0
- spacr/alpha.py +0 -807
- spacr/annotate_app.py +0 -670
- spacr/annotate_app_v2.py +0 -670
- spacr/app_make_masks_v2.py +0 -686
- spacr/classify_app.py +0 -201
- spacr/cli.py +0 -41
- spacr/foldseek.py +0 -779
- spacr/get_alfafold_structures.py +0 -72
- spacr/gui_2.py +0 -157
- spacr/gui_annotate.py +0 -145
- spacr/gui_classify_app.py +0 -201
- spacr/gui_make_masks_app.py +0 -927
- spacr/gui_make_masks_app_v2.py +0 -688
- spacr/gui_mask_app.py +0 -249
- spacr/gui_measure_app.py +0 -246
- spacr/gui_run.py +0 -58
- spacr/gui_sim_app.py +0 -0
- spacr/gui_wrappers.py +0 -149
- spacr/icons/abort.png +0 -0
- spacr/icons/abort.svg +0 -1
- spacr/icons/download.png +0 -0
- spacr/icons/download.svg +0 -1
- spacr/icons/download_for_offline_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
- spacr/icons/download_for_offline_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
- spacr/icons/logo_spacr.png +0 -0
- spacr/icons/make_masks.png +0 -0
- spacr/icons/make_masks.svg +0 -1
- spacr/icons/map_barcodes.png +0 -0
- spacr/icons/map_barcodes.svg +0 -1
- spacr/icons/mask.png +0 -0
- spacr/icons/mask.svg +0 -1
- spacr/icons/measure.png +0 -0
- spacr/icons/measure.svg +0 -1
- spacr/icons/play_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
- spacr/icons/play_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
- spacr/icons/run.png +0 -0
- spacr/icons/run.svg +0 -1
- spacr/icons/sequencing.png +0 -0
- spacr/icons/sequencing.svg +0 -1
- spacr/icons/settings.png +0 -0
- spacr/icons/settings.svg +0 -1
- spacr/icons/settings_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
- spacr/icons/settings_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
- spacr/icons/stop_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
- spacr/icons/stop_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
- spacr/icons/theater_comedy_100dp_E8EAED_FILL0_wght100_GRAD200_opsz48.png +0 -0
- spacr/icons/theater_comedy_100dp_E8EAED_FILL0_wght100_GRAD200_opsz48.svg +0 -1
- spacr/make_masks_app.py +0 -929
- spacr/make_masks_app_v2.py +0 -688
- spacr/mask_app.py +0 -249
- spacr/measure_app.py +0 -246
- spacr/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model +0 -0
- spacr/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model_settings.csv +0 -23
- spacr/models/cp/toxo_pv_lumen.CP_model +0 -0
- spacr/old_code.py +0 -358
- spacr/resources/icons/abort.svg +0 -1
- spacr/resources/icons/annotate.svg +0 -1
- spacr/resources/icons/classify.svg +0 -1
- spacr/resources/icons/download.svg +0 -1
- spacr/resources/icons/icon.psd +0 -0
- spacr/resources/icons/make_masks.svg +0 -1
- spacr/resources/icons/map_barcodes.svg +0 -1
- spacr/resources/icons/mask.svg +0 -1
- spacr/resources/icons/measure.svg +0 -1
- spacr/resources/icons/run.svg +0 -1
- spacr/resources/icons/run_2.png +0 -0
- spacr/resources/icons/run_2.svg +0 -1
- spacr/resources/icons/sequencing.svg +0 -1
- spacr/resources/icons/settings.svg +0 -1
- spacr/resources/icons/train_cellpose.svg +0 -1
- spacr/test_gui.py +0 -0
- spacr-0.2.1.dist-info/RECORD +0 -126
- /spacr/resources/icons/{cellpose.png → cellpose_all.png} +0 -0
- {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/LICENSE +0 -0
- {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/WHEEL +0 -0
- {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/entry_points.txt +0 -0
- {spacr-0.2.1.dist-info → spacr-0.2.21.dist-info}/top_level.txt +0 -0
spacr/gui_make_masks_app_v2.py
DELETED
@@ -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()
|