spacr 0.1.64__py3-none-any.whl → 0.1.76__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/app_annotate.py +38 -524
- spacr/app_make_masks.py +30 -904
- spacr/core.py +21 -21
- spacr/deep_spacr.py +6 -6
- spacr/gui.py +4 -20
- spacr/gui_core.py +133 -144
- spacr/gui_elements.py +1179 -12
- spacr/gui_utils.py +197 -10
- spacr/gui_wrappers.py +27 -15
- spacr/measure.py +4 -4
- spacr/settings.py +341 -260
- spacr/utils.py +12 -15
- {spacr-0.1.64.dist-info → spacr-0.1.76.dist-info}/METADATA +1 -1
- {spacr-0.1.64.dist-info → spacr-0.1.76.dist-info}/RECORD +18 -18
- {spacr-0.1.64.dist-info → spacr-0.1.76.dist-info}/LICENSE +0 -0
- {spacr-0.1.64.dist-info → spacr-0.1.76.dist-info}/WHEEL +0 -0
- {spacr-0.1.64.dist-info → spacr-0.1.76.dist-info}/entry_points.txt +0 -0
- {spacr-0.1.64.dist-info → spacr-0.1.76.dist-info}/top_level.txt +0 -0
spacr/gui_elements.py
CHANGED
@@ -1,6 +1,19 @@
|
|
1
|
+
import os, threading, time, sqlite3
|
1
2
|
import tkinter as tk
|
2
3
|
from tkinter import ttk
|
3
4
|
import tkinter.font as tkFont
|
5
|
+
from queue import Queue
|
6
|
+
from tkinter import Label
|
7
|
+
import numpy as np
|
8
|
+
from PIL import Image, ImageOps, ImageTk
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
10
|
+
from skimage.exposure import rescale_intensity
|
11
|
+
from IPython.display import display, HTML
|
12
|
+
import imageio.v2 as imageio
|
13
|
+
from collections import deque
|
14
|
+
from skimage.draw import polygon, line
|
15
|
+
from skimage.transform import resize
|
16
|
+
from scipy.ndimage import binary_fill_holes, label
|
4
17
|
|
5
18
|
class spacrDropdownMenu(tk.OptionMenu):
|
6
19
|
def __init__(self, parent, variable, options, command=None, **kwargs):
|
@@ -261,19 +274,1172 @@ class spacrToolTip:
|
|
261
274
|
self.tooltip_window.destroy()
|
262
275
|
self.tooltip_window = None
|
263
276
|
|
277
|
+
class modify_masks:
|
278
|
+
|
279
|
+
def __init__(self, root, folder_path, scale_factor):
|
280
|
+
self.root = root
|
281
|
+
self.folder_path = folder_path
|
282
|
+
self.scale_factor = scale_factor
|
283
|
+
self.image_filenames = sorted([f for f in os.listdir(folder_path) if f.endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff'))])
|
284
|
+
self.masks_folder = os.path.join(folder_path, 'masks')
|
285
|
+
self.current_image_index = 0
|
286
|
+
self.initialize_flags()
|
287
|
+
self.canvas_width = self.root.winfo_screenheight() -100
|
288
|
+
self.canvas_height = self.root.winfo_screenheight() -100
|
289
|
+
self.root.configure(bg='black')
|
290
|
+
self.setup_navigation_toolbar()
|
291
|
+
self.setup_mode_toolbar()
|
292
|
+
self.setup_function_toolbar()
|
293
|
+
self.setup_zoom_toolbar()
|
294
|
+
self.setup_canvas()
|
295
|
+
self.load_first_image()
|
296
|
+
|
297
|
+
####################################################################################################
|
298
|
+
# Helper functions#
|
299
|
+
####################################################################################################
|
300
|
+
|
301
|
+
def update_display(self):
|
302
|
+
if self.zoom_active:
|
303
|
+
self.display_zoomed_image()
|
304
|
+
else:
|
305
|
+
self.display_image()
|
306
|
+
|
307
|
+
def update_original_mask_from_zoom(self):
|
308
|
+
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
309
|
+
zoomed_mask_resized = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
310
|
+
self.mask[y0:y1, x0:x1] = zoomed_mask_resized
|
311
|
+
|
312
|
+
def update_original_mask(self, zoomed_mask, x0, x1, y0, y1):
|
313
|
+
actual_mask_region = self.mask[y0:y1, x0:x1]
|
314
|
+
target_shape = actual_mask_region.shape
|
315
|
+
resized_mask = resize(zoomed_mask, target_shape, order=0, preserve_range=True).astype(np.uint8)
|
316
|
+
if resized_mask.shape != actual_mask_region.shape:
|
317
|
+
raise ValueError(f"Shape mismatch: resized_mask {resized_mask.shape}, actual_mask_region {actual_mask_region.shape}")
|
318
|
+
self.mask[y0:y1, x0:x1] = np.maximum(actual_mask_region, resized_mask)
|
319
|
+
self.mask = self.mask.copy()
|
320
|
+
self.mask[y0:y1, x0:x1] = np.maximum(self.mask[y0:y1, x0:x1], resized_mask)
|
321
|
+
self.mask = self.mask.copy()
|
322
|
+
|
323
|
+
def get_scaling_factors(self, img_width, img_height, canvas_width, canvas_height):
|
324
|
+
x_scale = img_width / canvas_width
|
325
|
+
y_scale = img_height / canvas_height
|
326
|
+
return x_scale, y_scale
|
327
|
+
|
328
|
+
def canvas_to_image(self, x_canvas, y_canvas):
|
329
|
+
x_scale, y_scale = self.get_scaling_factors(
|
330
|
+
self.image.shape[1], self.image.shape[0],
|
331
|
+
self.canvas_width, self.canvas_height
|
332
|
+
)
|
333
|
+
x_image = int(x_canvas * x_scale)
|
334
|
+
y_image = int(y_canvas * y_scale)
|
335
|
+
return x_image, y_image
|
336
|
+
|
337
|
+
def apply_zoom_on_enter(self, event):
|
338
|
+
if self.zoom_active and self.zoom_rectangle_start is not None:
|
339
|
+
self.set_zoom_rectangle_end(event)
|
340
|
+
|
341
|
+
def normalize_image(self, image, lower_quantile, upper_quantile):
|
342
|
+
lower_bound = np.percentile(image, lower_quantile)
|
343
|
+
upper_bound = np.percentile(image, upper_quantile)
|
344
|
+
normalized = np.clip(image, lower_bound, upper_bound)
|
345
|
+
normalized = (normalized - lower_bound) / (upper_bound - lower_bound)
|
346
|
+
max_value = np.iinfo(image.dtype).max
|
347
|
+
normalized = (normalized * max_value).astype(image.dtype)
|
348
|
+
return normalized
|
349
|
+
|
350
|
+
def resize_arrays(self, img, mask):
|
351
|
+
original_dtype = img.dtype
|
352
|
+
scaled_height = int(img.shape[0] * self.scale_factor)
|
353
|
+
scaled_width = int(img.shape[1] * self.scale_factor)
|
354
|
+
scaled_img = resize(img, (scaled_height, scaled_width), anti_aliasing=True, preserve_range=True)
|
355
|
+
scaled_mask = resize(mask, (scaled_height, scaled_width), order=0, anti_aliasing=False, preserve_range=True)
|
356
|
+
stretched_img = resize(scaled_img, (self.canvas_height, self.canvas_width), anti_aliasing=True, preserve_range=True)
|
357
|
+
stretched_mask = resize(scaled_mask, (self.canvas_height, self.canvas_width), order=0, anti_aliasing=False, preserve_range=True)
|
358
|
+
return stretched_img.astype(original_dtype), stretched_mask.astype(original_dtype)
|
359
|
+
|
360
|
+
####################################################################################################
|
361
|
+
#Initiate canvas elements#
|
362
|
+
####################################################################################################
|
363
|
+
|
364
|
+
def load_first_image(self):
|
365
|
+
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
366
|
+
self.original_size = self.image.shape
|
367
|
+
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
368
|
+
self.display_image()
|
369
|
+
|
370
|
+
def setup_canvas(self):
|
371
|
+
self.canvas = tk.Canvas(self.root, width=self.canvas_width, height=self.canvas_height, bg='black')
|
372
|
+
self.canvas.pack()
|
373
|
+
self.canvas.bind("<Motion>", self.update_mouse_info)
|
374
|
+
|
375
|
+
def initialize_flags(self):
|
376
|
+
self.zoom_rectangle_start = None
|
377
|
+
self.zoom_rectangle_end = None
|
378
|
+
self.zoom_rectangle_id = None
|
379
|
+
self.zoom_x0 = None
|
380
|
+
self.zoom_y0 = None
|
381
|
+
self.zoom_x1 = None
|
382
|
+
self.zoom_y1 = None
|
383
|
+
self.zoom_mask = None
|
384
|
+
self.zoom_image = None
|
385
|
+
self.zoom_image_orig = None
|
386
|
+
self.zoom_scale = 1
|
387
|
+
self.drawing = False
|
388
|
+
self.zoom_active = False
|
389
|
+
self.magic_wand_active = False
|
390
|
+
self.brush_active = False
|
391
|
+
self.dividing_line_active = False
|
392
|
+
self.dividing_line_coords = []
|
393
|
+
self.current_dividing_line = None
|
394
|
+
self.lower_quantile = tk.StringVar(value="1.0")
|
395
|
+
self.upper_quantile = tk.StringVar(value="99.9")
|
396
|
+
self.magic_wand_tolerance = tk.StringVar(value="1000")
|
397
|
+
|
398
|
+
def update_mouse_info(self, event):
|
399
|
+
x, y = event.x, event.y
|
400
|
+
intensity = "N/A"
|
401
|
+
mask_value = "N/A"
|
402
|
+
pixel_count = "N/A"
|
403
|
+
if self.zoom_active:
|
404
|
+
if 0 <= x < self.canvas_width and 0 <= y < self.canvas_height:
|
405
|
+
intensity = self.zoom_image_orig[y, x] if self.zoom_image_orig is not None else "N/A"
|
406
|
+
mask_value = self.zoom_mask[y, x] if self.zoom_mask is not None else "N/A"
|
407
|
+
else:
|
408
|
+
if 0 <= x < self.image.shape[1] and 0 <= y < self.image.shape[0]:
|
409
|
+
intensity = self.image[y, x]
|
410
|
+
mask_value = self.mask[y, x]
|
411
|
+
if mask_value != "N/A" and mask_value != 0:
|
412
|
+
pixel_count = np.sum(self.mask == mask_value)
|
413
|
+
self.intensity_label.config(text=f"Intensity: {intensity}")
|
414
|
+
self.mask_value_label.config(text=f"Mask: {mask_value}, Area: {pixel_count}")
|
415
|
+
self.mask_value_label.config(text=f"Mask: {mask_value}")
|
416
|
+
if mask_value != "N/A" and mask_value != 0:
|
417
|
+
self.pixel_count_label.config(text=f"Area: {pixel_count}")
|
418
|
+
else:
|
419
|
+
self.pixel_count_label.config(text="Area: N/A")
|
420
|
+
|
421
|
+
def setup_navigation_toolbar(self):
|
422
|
+
navigation_toolbar = tk.Frame(self.root, bg='black')
|
423
|
+
navigation_toolbar.pack(side='top', fill='x')
|
424
|
+
prev_btn = tk.Button(navigation_toolbar, text="Previous", command=self.previous_image, bg='black', fg='white')
|
425
|
+
prev_btn.pack(side='left')
|
426
|
+
next_btn = tk.Button(navigation_toolbar, text="Next", command=self.next_image, bg='black', fg='white')
|
427
|
+
next_btn.pack(side='left')
|
428
|
+
save_btn = tk.Button(navigation_toolbar, text="Save", command=self.save_mask, bg='black', fg='white')
|
429
|
+
save_btn.pack(side='left')
|
430
|
+
self.intensity_label = tk.Label(navigation_toolbar, text="Image: N/A", bg='black', fg='white')
|
431
|
+
self.intensity_label.pack(side='right')
|
432
|
+
self.mask_value_label = tk.Label(navigation_toolbar, text="Mask: N/A", bg='black', fg='white')
|
433
|
+
self.mask_value_label.pack(side='right')
|
434
|
+
self.pixel_count_label = tk.Label(navigation_toolbar, text="Area: N/A", bg='black', fg='white')
|
435
|
+
self.pixel_count_label.pack(side='right')
|
436
|
+
|
437
|
+
def setup_mode_toolbar(self):
|
438
|
+
self.mode_toolbar = tk.Frame(self.root, bg='black')
|
439
|
+
self.mode_toolbar.pack(side='top', fill='x')
|
440
|
+
self.draw_btn = tk.Button(self.mode_toolbar, text="Draw", command=self.toggle_draw_mode, bg='black', fg='white')
|
441
|
+
self.draw_btn.pack(side='left')
|
442
|
+
self.magic_wand_btn = tk.Button(self.mode_toolbar, text="Magic Wand", command=self.toggle_magic_wand_mode, bg='black', fg='white')
|
443
|
+
self.magic_wand_btn.pack(side='left')
|
444
|
+
tk.Label(self.mode_toolbar, text="Tolerance:", bg='black', fg='white').pack(side='left')
|
445
|
+
self.tolerance_entry = tk.Entry(self.mode_toolbar, textvariable=self.magic_wand_tolerance, bg='black', fg='white')
|
446
|
+
self.tolerance_entry.pack(side='left')
|
447
|
+
tk.Label(self.mode_toolbar, text="Max Pixels:", bg='black', fg='white').pack(side='left')
|
448
|
+
self.max_pixels_entry = tk.Entry(self.mode_toolbar, bg='black', fg='white')
|
449
|
+
self.max_pixels_entry.insert(0, "1000")
|
450
|
+
self.max_pixels_entry.pack(side='left')
|
451
|
+
self.erase_btn = tk.Button(self.mode_toolbar, text="Erase", command=self.toggle_erase_mode, bg='black', fg='white')
|
452
|
+
self.erase_btn.pack(side='left')
|
453
|
+
self.brush_btn = tk.Button(self.mode_toolbar, text="Brush", command=self.toggle_brush_mode, bg='black', fg='white')
|
454
|
+
self.brush_btn.pack(side='left')
|
455
|
+
self.brush_size_entry = tk.Entry(self.mode_toolbar, bg='black', fg='white')
|
456
|
+
self.brush_size_entry.insert(0, "10")
|
457
|
+
self.brush_size_entry.pack(side='left')
|
458
|
+
tk.Label(self.mode_toolbar, text="Brush Size:", bg='black', fg='white').pack(side='left')
|
459
|
+
self.dividing_line_btn = tk.Button(self.mode_toolbar, text="Dividing Line", command=self.toggle_dividing_line_mode, bg='black', fg='white')
|
460
|
+
self.dividing_line_btn.pack(side='left')
|
461
|
+
|
462
|
+
def setup_function_toolbar(self):
|
463
|
+
self.function_toolbar = tk.Frame(self.root, bg='black')
|
464
|
+
self.function_toolbar.pack(side='top', fill='x')
|
465
|
+
self.fill_btn = tk.Button(self.function_toolbar, text="Fill", command=self.fill_objects, bg='black', fg='white')
|
466
|
+
self.fill_btn.pack(side='left')
|
467
|
+
self.relabel_btn = tk.Button(self.function_toolbar, text="Relabel", command=self.relabel_objects, bg='black', fg='white')
|
468
|
+
self.relabel_btn.pack(side='left')
|
469
|
+
self.clear_btn = tk.Button(self.function_toolbar, text="Clear", command=self.clear_objects, bg='black', fg='white')
|
470
|
+
self.clear_btn.pack(side='left')
|
471
|
+
self.invert_btn = tk.Button(self.function_toolbar, text="Invert", command=self.invert_mask, bg='black', fg='white')
|
472
|
+
self.invert_btn.pack(side='left')
|
473
|
+
remove_small_btn = tk.Button(self.function_toolbar, text="Remove Small", command=self.remove_small_objects, bg='black', fg='white')
|
474
|
+
remove_small_btn.pack(side='left')
|
475
|
+
tk.Label(self.function_toolbar, text="Min Area:", bg='black', fg='white').pack(side='left')
|
476
|
+
self.min_area_entry = tk.Entry(self.function_toolbar, bg='black', fg='white')
|
477
|
+
self.min_area_entry.insert(0, "100") # Default minimum area
|
478
|
+
self.min_area_entry.pack(side='left')
|
479
|
+
|
480
|
+
def setup_zoom_toolbar(self):
|
481
|
+
self.zoom_toolbar = tk.Frame(self.root, bg='black')
|
482
|
+
self.zoom_toolbar.pack(side='top', fill='x')
|
483
|
+
self.zoom_btn = tk.Button(self.zoom_toolbar, text="Zoom", command=self.toggle_zoom_mode, bg='black', fg='white')
|
484
|
+
self.zoom_btn.pack(side='left')
|
485
|
+
self.normalize_btn = tk.Button(self.zoom_toolbar, text="Apply Normalization", command=self.apply_normalization, bg='black', fg='white')
|
486
|
+
self.normalize_btn.pack(side='left')
|
487
|
+
tk.Label(self.zoom_toolbar, text="Lower Percentile:", bg='black', fg='white').pack(side='left')
|
488
|
+
self.lower_entry = tk.Entry(self.zoom_toolbar, textvariable=self.lower_quantile, bg='black', fg='white')
|
489
|
+
self.lower_entry.pack(side='left')
|
490
|
+
|
491
|
+
tk.Label(self.zoom_toolbar, text="Upper Percentile:", bg='black', fg='white').pack(side='left')
|
492
|
+
self.upper_entry = tk.Entry(self.zoom_toolbar, textvariable=self.upper_quantile, bg='black', fg='white')
|
493
|
+
self.upper_entry.pack(side='left')
|
494
|
+
|
495
|
+
|
496
|
+
def load_image_and_mask(self, index):
|
497
|
+
image_path = os.path.join(self.folder_path, self.image_filenames[index])
|
498
|
+
image = imageio.imread(image_path)
|
499
|
+
mask_path = os.path.join(self.masks_folder, self.image_filenames[index])
|
500
|
+
if os.path.exists(mask_path):
|
501
|
+
print(f'loading mask:{mask_path} for image: {image_path}')
|
502
|
+
mask = imageio.imread(mask_path)
|
503
|
+
if mask.dtype != np.uint8:
|
504
|
+
mask = (mask / np.max(mask) * 255).astype(np.uint8)
|
505
|
+
else:
|
506
|
+
mask = np.zeros(image.shape[:2], dtype=np.uint8)
|
507
|
+
print(f'loaded new mask for image: {image_path}')
|
508
|
+
return image, mask
|
509
|
+
|
510
|
+
####################################################################################################
|
511
|
+
# Image Display functions#
|
512
|
+
####################################################################################################
|
513
|
+
def display_image(self):
|
514
|
+
if self.zoom_rectangle_id is not None:
|
515
|
+
self.canvas.delete(self.zoom_rectangle_id)
|
516
|
+
self.zoom_rectangle_id = None
|
517
|
+
lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
|
518
|
+
upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
|
519
|
+
normalized = self.normalize_image(self.image, lower_quantile, upper_quantile)
|
520
|
+
combined = self.overlay_mask_on_image(normalized, self.mask)
|
521
|
+
self.tk_image = ImageTk.PhotoImage(image=Image.fromarray(combined))
|
522
|
+
self.canvas.create_image(0, 0, anchor='nw', image=self.tk_image)
|
523
|
+
|
524
|
+
def display_zoomed_image(self):
|
525
|
+
if self.zoom_rectangle_start and self.zoom_rectangle_end:
|
526
|
+
# Convert canvas coordinates to image coordinates
|
527
|
+
x0, y0 = self.canvas_to_image(*self.zoom_rectangle_start)
|
528
|
+
x1, y1 = self.canvas_to_image(*self.zoom_rectangle_end)
|
529
|
+
x0, x1 = min(x0, x1), max(x0, x1)
|
530
|
+
y0, y1 = min(y0, y1), max(y0, y1)
|
531
|
+
self.zoom_x0 = x0
|
532
|
+
self.zoom_y0 = y0
|
533
|
+
self.zoom_x1 = x1
|
534
|
+
self.zoom_y1 = y1
|
535
|
+
# Normalize the entire image
|
536
|
+
lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
|
537
|
+
upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
|
538
|
+
normalized_image = self.normalize_image(self.image, lower_quantile, upper_quantile)
|
539
|
+
# Extract the zoomed portion of the normalized image and mask
|
540
|
+
self.zoom_image = normalized_image[y0:y1, x0:x1]
|
541
|
+
self.zoom_image_orig = self.image[y0:y1, x0:x1]
|
542
|
+
self.zoom_mask = self.mask[y0:y1, x0:x1]
|
543
|
+
original_mask_area = self.mask.shape[0] * self.mask.shape[1]
|
544
|
+
zoom_mask_area = self.zoom_mask.shape[0] * self.zoom_mask.shape[1]
|
545
|
+
if original_mask_area > 0:
|
546
|
+
self.zoom_scale = original_mask_area/zoom_mask_area
|
547
|
+
# Resize the zoomed image and mask to fit the canvas
|
548
|
+
canvas_height = self.canvas.winfo_height()
|
549
|
+
canvas_width = self.canvas.winfo_width()
|
550
|
+
|
551
|
+
if self.zoom_image.size > 0 and canvas_height > 0 and canvas_width > 0:
|
552
|
+
self.zoom_image = resize(self.zoom_image, (canvas_height, canvas_width), preserve_range=True).astype(self.zoom_image.dtype)
|
553
|
+
self.zoom_image_orig = resize(self.zoom_image_orig, (canvas_height, canvas_width), preserve_range=True).astype(self.zoom_image_orig.dtype)
|
554
|
+
#self.zoom_mask = resize(self.zoom_mask, (canvas_height, canvas_width), preserve_range=True).astype(np.uint8)
|
555
|
+
self.zoom_mask = resize(self.zoom_mask, (canvas_height, canvas_width), order=0, preserve_range=True).astype(np.uint8)
|
556
|
+
combined = self.overlay_mask_on_image(self.zoom_image, self.zoom_mask)
|
557
|
+
self.tk_image = ImageTk.PhotoImage(image=Image.fromarray(combined))
|
558
|
+
self.canvas.create_image(0, 0, anchor='nw', image=self.tk_image)
|
559
|
+
|
560
|
+
def overlay_mask_on_image(self, image, mask, alpha=0.5):
|
561
|
+
if len(image.shape) == 2:
|
562
|
+
image = np.stack((image,) * 3, axis=-1)
|
563
|
+
mask = mask.astype(np.int32)
|
564
|
+
max_label = np.max(mask)
|
565
|
+
np.random.seed(0)
|
566
|
+
colors = np.random.randint(0, 255, size=(max_label + 1, 3), dtype=np.uint8)
|
567
|
+
colors[0] = [0, 0, 0] # background color
|
568
|
+
colored_mask = colors[mask]
|
569
|
+
image_8bit = (image / 256).astype(np.uint8)
|
570
|
+
# Blend the mask and the image with transparency
|
571
|
+
combined_image = np.where(mask[..., None] > 0,
|
572
|
+
np.clip(image_8bit * (1 - alpha) + colored_mask * alpha, 0, 255),
|
573
|
+
image_8bit)
|
574
|
+
# Convert the final image back to uint8
|
575
|
+
combined_image = combined_image.astype(np.uint8)
|
576
|
+
return combined_image
|
577
|
+
|
578
|
+
####################################################################################################
|
579
|
+
# Navigation functions#
|
580
|
+
####################################################################################################
|
581
|
+
|
582
|
+
def previous_image(self):
|
583
|
+
if self.current_image_index > 0:
|
584
|
+
self.current_image_index -= 1
|
585
|
+
self.initialize_flags()
|
586
|
+
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
587
|
+
self.original_size = self.image.shape
|
588
|
+
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
589
|
+
self.display_image()
|
590
|
+
|
591
|
+
def next_image(self):
|
592
|
+
if self.current_image_index < len(self.image_filenames) - 1:
|
593
|
+
self.current_image_index += 1
|
594
|
+
self.initialize_flags()
|
595
|
+
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
|
596
|
+
self.original_size = self.image.shape
|
597
|
+
self.image, self.mask = self.resize_arrays(self.image, self.mask)
|
598
|
+
self.display_image()
|
599
|
+
|
600
|
+
def save_mask(self):
|
601
|
+
if self.current_image_index < len(self.image_filenames):
|
602
|
+
original_size = self.original_size
|
603
|
+
if self.mask.shape != original_size:
|
604
|
+
resized_mask = resize(self.mask, original_size, order=0, preserve_range=True).astype(np.uint16)
|
605
|
+
else:
|
606
|
+
resized_mask = self.mask
|
607
|
+
resized_mask, _ = label(resized_mask > 0)
|
608
|
+
save_folder = os.path.join(self.folder_path, 'masks')
|
609
|
+
if not os.path.exists(save_folder):
|
610
|
+
os.makedirs(save_folder)
|
611
|
+
image_filename = os.path.splitext(self.image_filenames[self.current_image_index])[0] + '.tif'
|
612
|
+
save_path = os.path.join(save_folder, image_filename)
|
613
|
+
|
614
|
+
print(f"Saving mask to: {save_path}") # Debug print
|
615
|
+
imageio.imwrite(save_path, resized_mask)
|
616
|
+
|
617
|
+
####################################################################################################
|
618
|
+
# Zoom Functions #
|
619
|
+
####################################################################################################
|
620
|
+
def set_zoom_rectangle_start(self, event):
|
621
|
+
if self.zoom_active:
|
622
|
+
self.zoom_rectangle_start = (event.x, event.y)
|
623
|
+
|
624
|
+
def set_zoom_rectangle_end(self, event):
|
625
|
+
if self.zoom_active:
|
626
|
+
self.zoom_rectangle_end = (event.x, event.y)
|
627
|
+
if self.zoom_rectangle_id is not None:
|
628
|
+
self.canvas.delete(self.zoom_rectangle_id)
|
629
|
+
self.zoom_rectangle_id = None
|
630
|
+
self.display_zoomed_image()
|
631
|
+
self.canvas.unbind("<Motion>")
|
632
|
+
self.canvas.unbind("<Button-1>")
|
633
|
+
self.canvas.unbind("<Button-3>")
|
634
|
+
self.canvas.bind("<Motion>", self.update_mouse_info)
|
635
|
+
|
636
|
+
def update_zoom_box(self, event):
|
637
|
+
if self.zoom_active and self.zoom_rectangle_start is not None:
|
638
|
+
if self.zoom_rectangle_id is not None:
|
639
|
+
self.canvas.delete(self.zoom_rectangle_id)
|
640
|
+
# Assuming event.x and event.y are already in image coordinates
|
641
|
+
self.zoom_rectangle_end = (event.x, event.y)
|
642
|
+
x0, y0 = self.zoom_rectangle_start
|
643
|
+
x1, y1 = self.zoom_rectangle_end
|
644
|
+
self.zoom_rectangle_id = self.canvas.create_rectangle(x0, y0, x1, y1, outline="red", width=2)
|
645
|
+
|
646
|
+
####################################################################################################
|
647
|
+
# Mode activation#
|
648
|
+
####################################################################################################
|
649
|
+
|
650
|
+
def toggle_zoom_mode(self):
|
651
|
+
if not self.zoom_active:
|
652
|
+
self.brush_btn.config(text="Brush")
|
653
|
+
self.canvas.unbind("<B1-Motion>")
|
654
|
+
self.canvas.unbind("<B3-Motion>")
|
655
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
656
|
+
self.canvas.unbind("<ButtonRelease-3>")
|
657
|
+
self.zoom_active = True
|
658
|
+
self.drawing = False
|
659
|
+
self.magic_wand_active = False
|
660
|
+
self.erase_active = False
|
661
|
+
self.brush_active = False
|
662
|
+
self.dividing_line_active = False
|
663
|
+
self.draw_btn.config(text="Draw")
|
664
|
+
self.erase_btn.config(text="Erase")
|
665
|
+
self.magic_wand_btn.config(text="Magic Wand")
|
666
|
+
self.zoom_btn.config(text="Zoom ON")
|
667
|
+
self.dividing_line_btn.config(text="Dividing Line")
|
668
|
+
self.canvas.unbind("<Button-1>")
|
669
|
+
self.canvas.unbind("<Button-3>")
|
670
|
+
self.canvas.unbind("<Motion>")
|
671
|
+
self.canvas.bind("<Button-1>", self.set_zoom_rectangle_start)
|
672
|
+
self.canvas.bind("<Button-3>", self.set_zoom_rectangle_end)
|
673
|
+
self.canvas.bind("<Motion>", self.update_zoom_box)
|
674
|
+
else:
|
675
|
+
self.zoom_active = False
|
676
|
+
self.zoom_btn.config(text="Zoom")
|
677
|
+
self.canvas.unbind("<Button-1>")
|
678
|
+
self.canvas.unbind("<Button-3>")
|
679
|
+
self.canvas.unbind("<Motion>")
|
680
|
+
self.zoom_rectangle_start = self.zoom_rectangle_end = None
|
681
|
+
self.zoom_rectangle_id = None
|
682
|
+
self.display_image()
|
683
|
+
self.canvas.bind("<Motion>", self.update_mouse_info)
|
684
|
+
self.zoom_rectangle_start = None
|
685
|
+
self.zoom_rectangle_end = None
|
686
|
+
self.zoom_rectangle_id = None
|
687
|
+
self.zoom_x0 = None
|
688
|
+
self.zoom_y0 = None
|
689
|
+
self.zoom_x1 = None
|
690
|
+
self.zoom_y1 = None
|
691
|
+
self.zoom_mask = None
|
692
|
+
self.zoom_image = None
|
693
|
+
self.zoom_image_orig = None
|
694
|
+
|
695
|
+
def toggle_brush_mode(self):
|
696
|
+
self.brush_active = not self.brush_active
|
697
|
+
if self.brush_active:
|
698
|
+
self.drawing = False
|
699
|
+
self.magic_wand_active = False
|
700
|
+
self.erase_active = False
|
701
|
+
self.brush_btn.config(text="Brush ON")
|
702
|
+
self.draw_btn.config(text="Draw")
|
703
|
+
self.erase_btn.config(text="Erase")
|
704
|
+
self.magic_wand_btn.config(text="Magic Wand")
|
705
|
+
self.canvas.unbind("<Button-1>")
|
706
|
+
self.canvas.unbind("<Button-3>")
|
707
|
+
self.canvas.unbind("<Motion>")
|
708
|
+
self.canvas.bind("<B1-Motion>", self.apply_brush) # Left click and drag to apply brush
|
709
|
+
self.canvas.bind("<B3-Motion>", self.erase_brush) # Right click and drag to erase with brush
|
710
|
+
self.canvas.bind("<ButtonRelease-1>", self.apply_brush_release) # Left button release
|
711
|
+
self.canvas.bind("<ButtonRelease-3>", self.erase_brush_release) # Right button release
|
712
|
+
else:
|
713
|
+
self.brush_active = False
|
714
|
+
self.brush_btn.config(text="Brush")
|
715
|
+
self.canvas.unbind("<B1-Motion>")
|
716
|
+
self.canvas.unbind("<B3-Motion>")
|
717
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
718
|
+
self.canvas.unbind("<ButtonRelease-3>")
|
719
|
+
|
720
|
+
def image_to_canvas(self, x_image, y_image):
|
721
|
+
x_scale, y_scale = self.get_scaling_factors(
|
722
|
+
self.image.shape[1], self.image.shape[0],
|
723
|
+
self.canvas_width, self.canvas_height
|
724
|
+
)
|
725
|
+
x_canvas = int(x_image / x_scale)
|
726
|
+
y_canvas = int(y_image / y_scale)
|
727
|
+
return x_canvas, y_canvas
|
728
|
+
|
729
|
+
def toggle_dividing_line_mode(self):
|
730
|
+
self.dividing_line_active = not self.dividing_line_active
|
731
|
+
if self.dividing_line_active:
|
732
|
+
self.drawing = False
|
733
|
+
self.magic_wand_active = False
|
734
|
+
self.erase_active = False
|
735
|
+
self.brush_active = False
|
736
|
+
self.draw_btn.config(text="Draw")
|
737
|
+
self.erase_btn.config(text="Erase")
|
738
|
+
self.magic_wand_btn.config(text="Magic Wand")
|
739
|
+
self.brush_btn.config(text="Brush")
|
740
|
+
self.dividing_line_btn.config(text="Dividing Line ON")
|
741
|
+
self.canvas.unbind("<Button-1>")
|
742
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
743
|
+
self.canvas.unbind("<Motion>")
|
744
|
+
self.canvas.bind("<Button-1>", self.start_dividing_line)
|
745
|
+
self.canvas.bind("<ButtonRelease-1>", self.finish_dividing_line)
|
746
|
+
self.canvas.bind("<Motion>", self.update_dividing_line_preview)
|
747
|
+
else:
|
748
|
+
print("Dividing Line Mode: OFF")
|
749
|
+
self.dividing_line_active = False
|
750
|
+
self.dividing_line_btn.config(text="Dividing Line")
|
751
|
+
self.canvas.unbind("<Button-1>")
|
752
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
753
|
+
self.canvas.unbind("<Motion>")
|
754
|
+
self.display_image()
|
755
|
+
|
756
|
+
def start_dividing_line(self, event):
|
757
|
+
if self.dividing_line_active:
|
758
|
+
self.dividing_line_coords = [(event.x, event.y)]
|
759
|
+
self.current_dividing_line = self.canvas.create_line(event.x, event.y, event.x, event.y, fill="red", width=2)
|
760
|
+
|
761
|
+
def finish_dividing_line(self, event):
|
762
|
+
if self.dividing_line_active:
|
763
|
+
self.dividing_line_coords.append((event.x, event.y))
|
764
|
+
if self.zoom_active:
|
765
|
+
self.dividing_line_coords = [self.canvas_to_image(x, y) for x, y in self.dividing_line_coords]
|
766
|
+
self.apply_dividing_line()
|
767
|
+
self.canvas.delete(self.current_dividing_line)
|
768
|
+
self.current_dividing_line = None
|
769
|
+
|
770
|
+
def update_dividing_line_preview(self, event):
|
771
|
+
if self.dividing_line_active and self.dividing_line_coords:
|
772
|
+
x, y = event.x, event.y
|
773
|
+
if self.zoom_active:
|
774
|
+
x, y = self.canvas_to_image(x, y)
|
775
|
+
self.dividing_line_coords.append((x, y))
|
776
|
+
canvas_coords = [(self.image_to_canvas(*pt) if self.zoom_active else pt) for pt in self.dividing_line_coords]
|
777
|
+
flat_canvas_coords = [coord for pt in canvas_coords for coord in pt]
|
778
|
+
self.canvas.coords(self.current_dividing_line, *flat_canvas_coords)
|
779
|
+
|
780
|
+
def apply_dividing_line(self):
|
781
|
+
if self.dividing_line_coords:
|
782
|
+
coords = self.dividing_line_coords
|
783
|
+
if self.zoom_active:
|
784
|
+
coords = [self.canvas_to_image(x, y) for x, y in coords]
|
785
|
+
|
786
|
+
rr, cc = [], []
|
787
|
+
for (x0, y0), (x1, y1) in zip(coords[:-1], coords[1:]):
|
788
|
+
line_rr, line_cc = line(y0, x0, y1, x1)
|
789
|
+
rr.extend(line_rr)
|
790
|
+
cc.extend(line_cc)
|
791
|
+
rr, cc = np.array(rr), np.array(cc)
|
792
|
+
|
793
|
+
mask_copy = self.mask.copy()
|
794
|
+
|
795
|
+
if self.zoom_active:
|
796
|
+
# Update the zoomed mask
|
797
|
+
self.zoom_mask[rr, cc] = 0
|
798
|
+
# Reflect changes to the original mask
|
799
|
+
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
800
|
+
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
801
|
+
self.mask[y0:y1, x0:x1] = zoomed_mask_resized_back
|
802
|
+
else:
|
803
|
+
# Directly update the original mask
|
804
|
+
mask_copy[rr, cc] = 0
|
805
|
+
self.mask = mask_copy
|
806
|
+
|
807
|
+
labeled_mask, num_labels = label(self.mask > 0)
|
808
|
+
self.mask = labeled_mask
|
809
|
+
self.update_display()
|
810
|
+
|
811
|
+
self.dividing_line_coords = []
|
812
|
+
self.canvas.unbind("<Button-1>")
|
813
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
814
|
+
self.canvas.unbind("<Motion>")
|
815
|
+
self.dividing_line_active = False
|
816
|
+
self.dividing_line_btn.config(text="Dividing Line")
|
817
|
+
|
818
|
+
def toggle_draw_mode(self):
|
819
|
+
self.drawing = not self.drawing
|
820
|
+
if self.drawing:
|
821
|
+
self.brush_btn.config(text="Brush")
|
822
|
+
self.canvas.unbind("<B1-Motion>")
|
823
|
+
self.canvas.unbind("<B3-Motion>")
|
824
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
825
|
+
self.canvas.unbind("<ButtonRelease-3>")
|
826
|
+
self.magic_wand_active = False
|
827
|
+
self.erase_active = False
|
828
|
+
self.brush_active = False
|
829
|
+
self.draw_btn.config(text="Draw ON")
|
830
|
+
self.magic_wand_btn.config(text="Magic Wand")
|
831
|
+
self.erase_btn.config(text="Erase")
|
832
|
+
self.draw_coordinates = []
|
833
|
+
self.canvas.unbind("<Button-1>")
|
834
|
+
self.canvas.unbind("<Motion>")
|
835
|
+
self.canvas.bind("<B1-Motion>", self.draw)
|
836
|
+
self.canvas.bind("<ButtonRelease-1>", self.finish_drawing)
|
837
|
+
else:
|
838
|
+
self.drawing = False
|
839
|
+
self.draw_btn.config(text="Draw")
|
840
|
+
self.canvas.unbind("<B1-Motion>")
|
841
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
842
|
+
|
843
|
+
def toggle_magic_wand_mode(self):
|
844
|
+
self.magic_wand_active = not self.magic_wand_active
|
845
|
+
if self.magic_wand_active:
|
846
|
+
self.brush_btn.config(text="Brush")
|
847
|
+
self.canvas.unbind("<B1-Motion>")
|
848
|
+
self.canvas.unbind("<B3-Motion>")
|
849
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
850
|
+
self.canvas.unbind("<ButtonRelease-3>")
|
851
|
+
self.drawing = False
|
852
|
+
self.erase_active = False
|
853
|
+
self.brush_active = False
|
854
|
+
self.draw_btn.config(text="Draw")
|
855
|
+
self.erase_btn.config(text="Erase")
|
856
|
+
self.magic_wand_btn.config(text="Magic Wand ON")
|
857
|
+
self.canvas.bind("<Button-1>", self.use_magic_wand)
|
858
|
+
self.canvas.bind("<Button-3>", self.use_magic_wand)
|
859
|
+
else:
|
860
|
+
self.magic_wand_btn.config(text="Magic Wand")
|
861
|
+
self.canvas.unbind("<Button-1>")
|
862
|
+
self.canvas.unbind("<Button-3>")
|
863
|
+
|
864
|
+
def toggle_erase_mode(self):
|
865
|
+
self.erase_active = not self.erase_active
|
866
|
+
if self.erase_active:
|
867
|
+
self.brush_btn.config(text="Brush")
|
868
|
+
self.canvas.unbind("<B1-Motion>")
|
869
|
+
self.canvas.unbind("<B3-Motion>")
|
870
|
+
self.canvas.unbind("<ButtonRelease-1>")
|
871
|
+
self.canvas.unbind("<ButtonRelease-3>")
|
872
|
+
self.erase_btn.config(text="Erase ON")
|
873
|
+
self.canvas.bind("<Button-1>", self.erase_object)
|
874
|
+
self.drawing = False
|
875
|
+
self.magic_wand_active = False
|
876
|
+
self.brush_active = False
|
877
|
+
self.draw_btn.config(text="Draw")
|
878
|
+
self.magic_wand_btn.config(text="Magic Wand")
|
879
|
+
else:
|
880
|
+
self.erase_active = False
|
881
|
+
self.erase_btn.config(text="Erase")
|
882
|
+
self.canvas.unbind("<Button-1>")
|
883
|
+
|
884
|
+
####################################################################################################
|
885
|
+
# Mode functions#
|
886
|
+
####################################################################################################
|
887
|
+
|
888
|
+
def apply_brush_release(self, event):
|
889
|
+
if hasattr(self, 'brush_path'):
|
890
|
+
for x, y, brush_size in self.brush_path:
|
891
|
+
img_x, img_y = (x, y) if self.zoom_active else self.canvas_to_image(x, y)
|
892
|
+
x0 = max(img_x - brush_size // 2, 0)
|
893
|
+
y0 = max(img_y - brush_size // 2, 0)
|
894
|
+
x1 = min(img_x + brush_size // 2, self.zoom_mask.shape[1] if self.zoom_active else self.mask.shape[1])
|
895
|
+
y1 = min(img_y + brush_size // 2, self.zoom_mask.shape[0] if self.zoom_active else self.mask.shape[0])
|
896
|
+
if self.zoom_active:
|
897
|
+
self.zoom_mask[y0:y1, x0:x1] = 255
|
898
|
+
self.update_original_mask_from_zoom()
|
899
|
+
else:
|
900
|
+
self.mask[y0:y1, x0:x1] = 255
|
901
|
+
del self.brush_path
|
902
|
+
self.canvas.delete("temp_line")
|
903
|
+
self.update_display()
|
904
|
+
|
905
|
+
def erase_brush_release(self, event):
|
906
|
+
if hasattr(self, 'erase_path'):
|
907
|
+
for x, y, brush_size in self.erase_path:
|
908
|
+
img_x, img_y = (x, y) if self.zoom_active else self.canvas_to_image(x, y)
|
909
|
+
x0 = max(img_x - brush_size // 2, 0)
|
910
|
+
y0 = max(img_y - brush_size // 2, 0)
|
911
|
+
x1 = min(img_x + brush_size // 2, self.zoom_mask.shape[1] if self.zoom_active else self.mask.shape[1])
|
912
|
+
y1 = min(img_y + brush_size // 2, self.zoom_mask.shape[0] if self.zoom_active else self.mask.shape[0])
|
913
|
+
if self.zoom_active:
|
914
|
+
self.zoom_mask[y0:y1, x0:x1] = 0
|
915
|
+
self.update_original_mask_from_zoom()
|
916
|
+
else:
|
917
|
+
self.mask[y0:y1, x0:x1] = 0
|
918
|
+
del self.erase_path
|
919
|
+
self.canvas.delete("temp_line")
|
920
|
+
self.update_display()
|
921
|
+
|
922
|
+
def apply_brush(self, event):
|
923
|
+
brush_size = int(self.brush_size_entry.get())
|
924
|
+
x, y = event.x, event.y
|
925
|
+
if not hasattr(self, 'brush_path'):
|
926
|
+
self.brush_path = []
|
927
|
+
self.last_brush_coord = (x, y)
|
928
|
+
if self.last_brush_coord:
|
929
|
+
last_x, last_y = self.last_brush_coord
|
930
|
+
rr, cc = line(last_y, last_x, y, x)
|
931
|
+
for ry, rx in zip(rr, cc):
|
932
|
+
self.brush_path.append((rx, ry, brush_size))
|
933
|
+
|
934
|
+
self.canvas.create_line(self.last_brush_coord[0], self.last_brush_coord[1], x, y, width=brush_size, fill="blue", tag="temp_line")
|
935
|
+
self.last_brush_coord = (x, y)
|
936
|
+
|
937
|
+
def erase_brush(self, event):
|
938
|
+
brush_size = int(self.brush_size_entry.get())
|
939
|
+
x, y = event.x, event.y
|
940
|
+
if not hasattr(self, 'erase_path'):
|
941
|
+
self.erase_path = []
|
942
|
+
self.last_erase_coord = (x, y)
|
943
|
+
if self.last_erase_coord:
|
944
|
+
last_x, last_y = self.last_erase_coord
|
945
|
+
rr, cc = line(last_y, last_x, y, x)
|
946
|
+
for ry, rx in zip(rr, cc):
|
947
|
+
self.erase_path.append((rx, ry, brush_size))
|
948
|
+
|
949
|
+
self.canvas.create_line(self.last_erase_coord[0], self.last_erase_coord[1], x, y, width=brush_size, fill="white", tag="temp_line")
|
950
|
+
self.last_erase_coord = (x, y)
|
951
|
+
|
952
|
+
def erase_object(self, event):
|
953
|
+
x, y = event.x, event.y
|
954
|
+
if self.zoom_active:
|
955
|
+
canvas_x, canvas_y = x, y
|
956
|
+
zoomed_x = int(canvas_x * (self.zoom_image.shape[1] / self.canvas_width))
|
957
|
+
zoomed_y = int(canvas_y * (self.zoom_image.shape[0] / self.canvas_height))
|
958
|
+
orig_x = int(zoomed_x * ((self.zoom_x1 - self.zoom_x0) / self.canvas_width) + self.zoom_x0)
|
959
|
+
orig_y = int(zoomed_y * ((self.zoom_y1 - self.zoom_y0) / self.canvas_height) + self.zoom_y0)
|
960
|
+
if orig_x < 0 or orig_y < 0 or orig_x >= self.image.shape[1] or orig_y >= self.image.shape[0]:
|
961
|
+
print("Point is out of bounds in the original image.")
|
962
|
+
return
|
963
|
+
else:
|
964
|
+
orig_x, orig_y = x, y
|
965
|
+
label_to_remove = self.mask[orig_y, orig_x]
|
966
|
+
if label_to_remove > 0:
|
967
|
+
self.mask[self.mask == label_to_remove] = 0
|
968
|
+
self.update_display()
|
969
|
+
|
970
|
+
def use_magic_wand(self, event):
|
971
|
+
x, y = event.x, event.y
|
972
|
+
tolerance = int(self.magic_wand_tolerance.get())
|
973
|
+
maximum = int(self.max_pixels_entry.get())
|
974
|
+
action = 'add' if event.num == 1 else 'erase'
|
975
|
+
if self.zoom_active:
|
976
|
+
self.magic_wand_zoomed((x, y), tolerance, action)
|
977
|
+
else:
|
978
|
+
self.magic_wand_normal((x, y), tolerance, action)
|
979
|
+
|
980
|
+
def apply_magic_wand(self, image, mask, seed_point, tolerance, maximum, action='add'):
|
981
|
+
x, y = seed_point
|
982
|
+
initial_value = image[y, x].astype(np.float32)
|
983
|
+
visited = np.zeros_like(image, dtype=bool)
|
984
|
+
queue = deque([(x, y)])
|
985
|
+
added_pixels = 0
|
986
|
+
|
987
|
+
while queue and added_pixels < maximum:
|
988
|
+
cx, cy = queue.popleft()
|
989
|
+
if visited[cy, cx]:
|
990
|
+
continue
|
991
|
+
visited[cy, cx] = True
|
992
|
+
current_value = image[cy, cx].astype(np.float32)
|
993
|
+
|
994
|
+
if np.linalg.norm(abs(current_value - initial_value)) <= tolerance:
|
995
|
+
if mask[cy, cx] == 0:
|
996
|
+
added_pixels += 1
|
997
|
+
mask[cy, cx] = 255 if action == 'add' else 0
|
998
|
+
|
999
|
+
if added_pixels >= maximum:
|
1000
|
+
break
|
1001
|
+
|
1002
|
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
1003
|
+
nx, ny = cx + dx, cy + dy
|
1004
|
+
if 0 <= nx < image.shape[1] and 0 <= ny < image.shape[0] and not visited[ny, nx]:
|
1005
|
+
queue.append((nx, ny))
|
1006
|
+
return mask
|
1007
|
+
|
1008
|
+
def magic_wand_normal(self, seed_point, tolerance, action):
|
1009
|
+
try:
|
1010
|
+
maximum = int(self.max_pixels_entry.get())
|
1011
|
+
except ValueError:
|
1012
|
+
print("Invalid maximum value; using default of 1000")
|
1013
|
+
maximum = 1000
|
1014
|
+
self.mask = self.apply_magic_wand(self.image, self.mask, seed_point, tolerance, maximum, action)
|
1015
|
+
self.display_image()
|
1016
|
+
|
1017
|
+
def magic_wand_zoomed(self, seed_point, tolerance, action):
|
1018
|
+
if self.zoom_image_orig is None or self.zoom_mask is None:
|
1019
|
+
print("Zoomed image or mask not initialized")
|
1020
|
+
return
|
1021
|
+
try:
|
1022
|
+
maximum = int(self.max_pixels_entry.get())
|
1023
|
+
maximum = maximum * self.zoom_scale
|
1024
|
+
except ValueError:
|
1025
|
+
print("Invalid maximum value; using default of 1000")
|
1026
|
+
maximum = 1000
|
1027
|
+
|
1028
|
+
canvas_x, canvas_y = seed_point
|
1029
|
+
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]:
|
1030
|
+
print("Selected point is out of bounds in the zoomed image.")
|
1031
|
+
return
|
1032
|
+
|
1033
|
+
self.zoom_mask = self.apply_magic_wand(self.zoom_image_orig, self.zoom_mask, (canvas_x, canvas_y), tolerance, maximum, action)
|
1034
|
+
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
|
1035
|
+
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
|
1036
|
+
if action == 'erase':
|
1037
|
+
self.mask[y0:y1, x0:x1] = np.where(zoomed_mask_resized_back == 0, 0, self.mask[y0:y1, x0:x1])
|
1038
|
+
else:
|
1039
|
+
self.mask[y0:y1, x0:x1] = np.where(zoomed_mask_resized_back > 0, zoomed_mask_resized_back, self.mask[y0:y1, x0:x1])
|
1040
|
+
self.update_display()
|
1041
|
+
|
1042
|
+
def draw(self, event):
|
1043
|
+
if self.drawing:
|
1044
|
+
x, y = event.x, event.y
|
1045
|
+
if self.draw_coordinates:
|
1046
|
+
last_x, last_y = self.draw_coordinates[-1]
|
1047
|
+
self.current_line = self.canvas.create_line(last_x, last_y, x, y, fill="yellow", width=3)
|
1048
|
+
self.draw_coordinates.append((x, y))
|
1049
|
+
|
1050
|
+
def draw_on_zoomed_mask(self, draw_coordinates):
|
1051
|
+
canvas_height = self.canvas.winfo_height()
|
1052
|
+
canvas_width = self.canvas.winfo_width()
|
1053
|
+
zoomed_mask = np.zeros((canvas_height, canvas_width), dtype=np.uint8)
|
1054
|
+
rr, cc = polygon(np.array(draw_coordinates)[:, 1], np.array(draw_coordinates)[:, 0], shape=zoomed_mask.shape)
|
1055
|
+
zoomed_mask[rr, cc] = 255
|
1056
|
+
return zoomed_mask
|
1057
|
+
|
1058
|
+
def finish_drawing(self, event):
|
1059
|
+
if len(self.draw_coordinates) > 2:
|
1060
|
+
self.draw_coordinates.append(self.draw_coordinates[0])
|
1061
|
+
if self.zoom_active:
|
1062
|
+
x0, x1, y0, y1 = self.zoom_x0, self.zoom_x1, self.zoom_y0, self.zoom_y1
|
1063
|
+
zoomed_mask = self.draw_on_zoomed_mask(self.draw_coordinates)
|
1064
|
+
self.update_original_mask(zoomed_mask, x0, x1, y0, y1)
|
1065
|
+
else:
|
1066
|
+
rr, cc = polygon(np.array(self.draw_coordinates)[:, 1], np.array(self.draw_coordinates)[:, 0], shape=self.mask.shape)
|
1067
|
+
self.mask[rr, cc] = np.maximum(self.mask[rr, cc], 255)
|
1068
|
+
self.mask = self.mask.copy()
|
1069
|
+
self.canvas.delete(self.current_line)
|
1070
|
+
self.draw_coordinates.clear()
|
1071
|
+
self.update_display()
|
1072
|
+
|
1073
|
+
def finish_drawing_if_active(self, event):
|
1074
|
+
if self.drawing and len(self.draw_coordinates) > 2:
|
1075
|
+
self.finish_drawing(event)
|
1076
|
+
|
1077
|
+
####################################################################################################
|
1078
|
+
# Single function butons#
|
1079
|
+
####################################################################################################
|
1080
|
+
|
1081
|
+
def apply_normalization(self):
|
1082
|
+
self.lower_quantile.set(self.lower_entry.get())
|
1083
|
+
self.upper_quantile.set(self.upper_entry.get())
|
1084
|
+
self.update_display()
|
1085
|
+
|
1086
|
+
def fill_objects(self):
|
1087
|
+
binary_mask = self.mask > 0
|
1088
|
+
filled_mask = binary_fill_holes(binary_mask)
|
1089
|
+
self.mask = filled_mask.astype(np.uint8) * 255
|
1090
|
+
labeled_mask, _ = label(filled_mask)
|
1091
|
+
self.mask = labeled_mask
|
1092
|
+
self.update_display()
|
1093
|
+
|
1094
|
+
def relabel_objects(self):
|
1095
|
+
mask = self.mask
|
1096
|
+
labeled_mask, num_labels = label(mask > 0)
|
1097
|
+
self.mask = labeled_mask
|
1098
|
+
self.update_display()
|
1099
|
+
|
1100
|
+
def clear_objects(self):
|
1101
|
+
self.mask = np.zeros_like(self.mask)
|
1102
|
+
self.update_display()
|
1103
|
+
|
1104
|
+
def invert_mask(self):
|
1105
|
+
self.mask = np.where(self.mask > 0, 0, 1)
|
1106
|
+
self.relabel_objects()
|
1107
|
+
self.update_display()
|
1108
|
+
|
1109
|
+
def remove_small_objects(self):
|
1110
|
+
try:
|
1111
|
+
min_area = int(self.min_area_entry.get())
|
1112
|
+
except ValueError:
|
1113
|
+
print("Invalid minimum area value; using default of 100")
|
1114
|
+
min_area = 100
|
1115
|
+
|
1116
|
+
labeled_mask, num_labels = label(self.mask > 0)
|
1117
|
+
for i in range(1, num_labels + 1): # Skip background
|
1118
|
+
if np.sum(labeled_mask == i) < min_area:
|
1119
|
+
self.mask[labeled_mask == i] = 0 # Remove small objects
|
1120
|
+
self.update_display()
|
1121
|
+
|
1122
|
+
class ImageApp:
|
1123
|
+
def __init__(self, root, db_path, src, image_type=None, channels=None, grid_rows=None, grid_cols=None, image_size=(200, 200), annotation_column='annotate', normalize=False, percentiles=(1,99), measurement=None, threshold=None):
|
1124
|
+
self.root = root
|
1125
|
+
self.db_path = db_path
|
1126
|
+
self.src = src
|
1127
|
+
self.index = 0
|
1128
|
+
self.grid_rows = grid_rows
|
1129
|
+
self.grid_cols = grid_cols
|
1130
|
+
self.image_size = image_size
|
1131
|
+
self.annotation_column = annotation_column
|
1132
|
+
self.image_type = image_type
|
1133
|
+
self.channels = channels
|
1134
|
+
self.normalize = normalize
|
1135
|
+
self.percentiles = percentiles
|
1136
|
+
self.images = {}
|
1137
|
+
self.pending_updates = {}
|
1138
|
+
self.labels = []
|
1139
|
+
self.adjusted_to_original_paths = {}
|
1140
|
+
self.terminate = False
|
1141
|
+
self.update_queue = Queue()
|
1142
|
+
self.status_label = Label(self.root, text="", font=("Arial", 12))
|
1143
|
+
self.status_label.grid(row=self.grid_rows + 1, column=0, columnspan=self.grid_cols)
|
1144
|
+
self.measurement = measurement
|
1145
|
+
self.threshold = threshold
|
1146
|
+
|
1147
|
+
self.filtered_paths_annotations = []
|
1148
|
+
self.prefilter_paths_annotations()
|
1149
|
+
|
1150
|
+
self.db_update_thread = threading.Thread(target=self.update_database_worker)
|
1151
|
+
self.db_update_thread.start()
|
1152
|
+
|
1153
|
+
for i in range(grid_rows * grid_cols):
|
1154
|
+
label = Label(root)
|
1155
|
+
label.grid(row=i // grid_cols, column=i % grid_cols)
|
1156
|
+
self.labels.append(label)
|
1157
|
+
|
1158
|
+
def prefilter_paths_annotations(self):
|
1159
|
+
from .io import _read_and_join_tables
|
1160
|
+
from .utils import is_list_of_lists
|
1161
|
+
|
1162
|
+
if self.measurement and self.threshold is not None:
|
1163
|
+
df = _read_and_join_tables(self.db_path)
|
1164
|
+
df[self.annotation_column] = None
|
1165
|
+
before = len(df)
|
1166
|
+
|
1167
|
+
if is_list_of_lists(self.measurement):
|
1168
|
+
if isinstance(self.threshold, list) or is_list_of_lists(self.threshold):
|
1169
|
+
if len(self.measurement) == len(self.threshold):
|
1170
|
+
for idx, var in enumerate(self.measurement):
|
1171
|
+
df = df[df[var[idx]] > self.threshold[idx]]
|
1172
|
+
after = len(df)
|
1173
|
+
elif len(self.measurement) == len(self.threshold)*2:
|
1174
|
+
th_idx = 0
|
1175
|
+
for idx, var in enumerate(self.measurement):
|
1176
|
+
if idx % 2 != 0:
|
1177
|
+
th_idx += 1
|
1178
|
+
thd = self.threshold
|
1179
|
+
if isinstance(thd, list):
|
1180
|
+
thd = thd[0]
|
1181
|
+
df[f'threshold_measurement_{idx}'] = df[self.measurement[idx]]/df[self.measurement[idx+1]]
|
1182
|
+
print(f"mean threshold_measurement_{idx}: {np.mean(df['threshold_measurement'])}")
|
1183
|
+
print(f"median threshold measurement: {np.median(df[self.measurement])}")
|
1184
|
+
df = df[df[f'threshold_measurement_{idx}'] > thd]
|
1185
|
+
after = len(df)
|
1186
|
+
elif isinstance(self.measurement, list):
|
1187
|
+
df['threshold_measurement'] = df[self.measurement[0]]/df[self.measurement[1]]
|
1188
|
+
print(f"mean threshold measurement: {np.mean(df['threshold_measurement'])}")
|
1189
|
+
print(f"median threshold measurement: {np.median(df[self.measurement])}")
|
1190
|
+
df = df[df['threshold_measurement'] > self.threshold]
|
1191
|
+
after = len(df)
|
1192
|
+
self.measurement = 'threshold_measurement'
|
1193
|
+
print(f'Removed: {before-after} rows, retained {after}')
|
1194
|
+
else:
|
1195
|
+
print(f"mean threshold measurement: {np.mean(df[self.measurement])}")
|
1196
|
+
print(f"median threshold measurement: {np.median(df[self.measurement])}")
|
1197
|
+
before = len(df)
|
1198
|
+
if isinstance(self.threshold, str):
|
1199
|
+
if self.threshold == 'q1':
|
1200
|
+
self.threshold = df[self.measurement].quantile(0.1)
|
1201
|
+
if self.threshold == 'q2':
|
1202
|
+
self.threshold = df[self.measurement].quantile(0.2)
|
1203
|
+
if self.threshold == 'q3':
|
1204
|
+
self.threshold = df[self.measurement].quantile(0.3)
|
1205
|
+
if self.threshold == 'q4':
|
1206
|
+
self.threshold = df[self.measurement].quantile(0.4)
|
1207
|
+
if self.threshold == 'q5':
|
1208
|
+
self.threshold = df[self.measurement].quantile(0.5)
|
1209
|
+
if self.threshold == 'q6':
|
1210
|
+
self.threshold = df[self.measurement].quantile(0.6)
|
1211
|
+
if self.threshold == 'q7':
|
1212
|
+
self.threshold = df[self.measurement].quantile(0.7)
|
1213
|
+
if self.threshold == 'q8':
|
1214
|
+
self.threshold = df[self.measurement].quantile(0.8)
|
1215
|
+
if self.threshold == 'q9':
|
1216
|
+
self.threshold = df[self.measurement].quantile(0.9)
|
1217
|
+
print(f"threshold: {self.threshold}")
|
1218
|
+
|
1219
|
+
df = df[df[self.measurement] > self.threshold]
|
1220
|
+
after = len(df)
|
1221
|
+
print(f'Removed: {before-after} rows, retained {after}')
|
1222
|
+
|
1223
|
+
df = df.dropna(subset=['png_path'])
|
1224
|
+
if self.image_type:
|
1225
|
+
before = len(df)
|
1226
|
+
if isinstance(self.image_type, list):
|
1227
|
+
for tpe in self.image_type:
|
1228
|
+
df = df[df['png_path'].str.contains(tpe)]
|
1229
|
+
else:
|
1230
|
+
df = df[df['png_path'].str.contains(self.image_type)]
|
1231
|
+
after = len(df)
|
1232
|
+
print(f'image_type: Removed: {before-after} rows, retained {after}')
|
1233
|
+
|
1234
|
+
self.filtered_paths_annotations = df[['png_path', self.annotation_column]].values.tolist()
|
1235
|
+
else:
|
1236
|
+
conn = sqlite3.connect(self.db_path)
|
1237
|
+
c = conn.cursor()
|
1238
|
+
if self.image_type:
|
1239
|
+
c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list WHERE png_path LIKE ?", (f"%{self.image_type}%",))
|
1240
|
+
else:
|
1241
|
+
c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list")
|
1242
|
+
self.filtered_paths_annotations = c.fetchall()
|
1243
|
+
conn.close()
|
1244
|
+
|
1245
|
+
def load_images(self):
|
1246
|
+
for label in self.labels:
|
1247
|
+
label.config(image='')
|
1248
|
+
|
1249
|
+
self.images = {}
|
1250
|
+
paths_annotations = self.filtered_paths_annotations[self.index:self.index + self.grid_rows * self.grid_cols]
|
1251
|
+
|
1252
|
+
adjusted_paths = []
|
1253
|
+
for path, annotation in paths_annotations:
|
1254
|
+
if not path.startswith(self.src):
|
1255
|
+
parts = path.split('/data/')
|
1256
|
+
if len(parts) > 1:
|
1257
|
+
new_path = os.path.join(self.src, 'data', parts[1])
|
1258
|
+
self.adjusted_to_original_paths[new_path] = path
|
1259
|
+
adjusted_paths.append((new_path, annotation))
|
1260
|
+
else:
|
1261
|
+
adjusted_paths.append((path, annotation))
|
1262
|
+
else:
|
1263
|
+
adjusted_paths.append((path, annotation))
|
1264
|
+
|
1265
|
+
with ThreadPoolExecutor() as executor:
|
1266
|
+
loaded_images = list(executor.map(self.load_single_image, adjusted_paths))
|
1267
|
+
|
1268
|
+
for i, (img, annotation) in enumerate(loaded_images):
|
1269
|
+
if annotation:
|
1270
|
+
border_color = 'teal' if annotation == 1 else 'red'
|
1271
|
+
img = self.add_colored_border(img, border_width=5, border_color=border_color)
|
1272
|
+
|
1273
|
+
photo = ImageTk.PhotoImage(img)
|
1274
|
+
label = self.labels[i]
|
1275
|
+
self.images[label] = photo
|
1276
|
+
label.config(image=photo)
|
1277
|
+
|
1278
|
+
path = adjusted_paths[i][0]
|
1279
|
+
label.bind('<Button-1>', self.get_on_image_click(path, label, img))
|
1280
|
+
label.bind('<Button-3>', self.get_on_image_click(path, label, img))
|
1281
|
+
|
1282
|
+
self.root.update()
|
1283
|
+
|
1284
|
+
def load_single_image(self, path_annotation_tuple):
|
1285
|
+
path, annotation = path_annotation_tuple
|
1286
|
+
img = Image.open(path)
|
1287
|
+
img = self.normalize_image(img, self.normalize, self.percentiles)
|
1288
|
+
img = img.convert('RGB')
|
1289
|
+
img = self.filter_channels(img)
|
1290
|
+
img = img.resize(self.image_size)
|
1291
|
+
return img, annotation
|
1292
|
+
|
1293
|
+
@staticmethod
|
1294
|
+
def normalize_image(img, normalize=False, percentiles=(1, 99)):
|
1295
|
+
img_array = np.array(img)
|
1296
|
+
|
1297
|
+
if normalize:
|
1298
|
+
if img_array.ndim == 2: # Grayscale image
|
1299
|
+
p2, p98 = np.percentile(img_array, percentiles)
|
1300
|
+
img_array = rescale_intensity(img_array, in_range=(p2, p98), out_range=(0, 255))
|
1301
|
+
else: # Color image or multi-channel image
|
1302
|
+
for channel in range(img_array.shape[2]):
|
1303
|
+
p2, p98 = np.percentile(img_array[:, :, channel], percentiles)
|
1304
|
+
img_array[:, :, channel] = rescale_intensity(img_array[:, :, channel], in_range=(p2, p98), out_range=(0, 255))
|
1305
|
+
|
1306
|
+
img_array = np.clip(img_array, 0, 255).astype('uint8')
|
1307
|
+
|
1308
|
+
return Image.fromarray(img_array)
|
1309
|
+
|
1310
|
+
def add_colored_border(self, img, border_width, border_color):
|
1311
|
+
top_border = Image.new('RGB', (img.width, border_width), color=border_color)
|
1312
|
+
bottom_border = Image.new('RGB', (img.width, border_width), color=border_color)
|
1313
|
+
left_border = Image.new('RGB', (border_width, img.height), color=border_color)
|
1314
|
+
right_border = Image.new('RGB', (border_width, img.height), color=border_color)
|
1315
|
+
|
1316
|
+
bordered_img = Image.new('RGB', (img.width + 2 * border_width, img.height + 2 * border_width), color='white')
|
1317
|
+
bordered_img.paste(top_border, (border_width, 0))
|
1318
|
+
bordered_img.paste(bottom_border, (border_width, img.height + border_width))
|
1319
|
+
bordered_img.paste(left_border, (0, border_width))
|
1320
|
+
bordered_img.paste(right_border, (img.width + border_width, border_width))
|
1321
|
+
bordered_img.paste(img, (border_width, border_width))
|
1322
|
+
|
1323
|
+
return bordered_img
|
1324
|
+
|
1325
|
+
def filter_channels(self, img):
|
1326
|
+
r, g, b = img.split()
|
1327
|
+
if self.channels:
|
1328
|
+
if 'r' not in self.channels:
|
1329
|
+
r = r.point(lambda _: 0)
|
1330
|
+
if 'g' not in self.channels:
|
1331
|
+
g = g.point(lambda _: 0)
|
1332
|
+
if 'b' not in self.channels:
|
1333
|
+
b = b.point(lambda _: 0)
|
1334
|
+
|
1335
|
+
if len(self.channels) == 1:
|
1336
|
+
channel_img = r if 'r' in self.channels else (g if 'g' in self.channels else b)
|
1337
|
+
return ImageOps.grayscale(channel_img)
|
1338
|
+
|
1339
|
+
return Image.merge("RGB", (r, g, b))
|
1340
|
+
|
1341
|
+
def get_on_image_click(self, path, label, img):
|
1342
|
+
def on_image_click(event):
|
1343
|
+
new_annotation = 1 if event.num == 1 else (2 if event.num == 3 else None)
|
1344
|
+
|
1345
|
+
original_path = self.adjusted_to_original_paths.get(path, path)
|
1346
|
+
|
1347
|
+
if original_path in self.pending_updates and self.pending_updates[original_path] == new_annotation:
|
1348
|
+
self.pending_updates[original_path] = None
|
1349
|
+
new_annotation = None
|
1350
|
+
else:
|
1351
|
+
self.pending_updates[original_path] = new_annotation
|
1352
|
+
|
1353
|
+
print(f"Image {os.path.split(path)[1]} annotated: {new_annotation}")
|
1354
|
+
|
1355
|
+
img_ = img.crop((5, 5, img.width-5, img.height-5))
|
1356
|
+
border_fill = 'teal' if new_annotation == 1 else ('red' if new_annotation == 2 else None)
|
1357
|
+
img_ = ImageOps.expand(img_, border=5, fill=border_fill) if border_fill else img_
|
1358
|
+
|
1359
|
+
photo = ImageTk.PhotoImage(img_)
|
1360
|
+
self.images[label] = photo
|
1361
|
+
label.config(image=photo)
|
1362
|
+
self.root.update()
|
1363
|
+
|
1364
|
+
return on_image_click
|
1365
|
+
|
1366
|
+
@staticmethod
|
1367
|
+
def update_html(text):
|
1368
|
+
display(HTML(f"""
|
1369
|
+
<script>
|
1370
|
+
document.getElementById('unique_id').innerHTML = '{text}';
|
1371
|
+
</script>
|
1372
|
+
"""))
|
1373
|
+
|
1374
|
+
def update_database_worker(self):
|
1375
|
+
conn = sqlite3.connect(self.db_path)
|
1376
|
+
c = conn.cursor()
|
1377
|
+
|
1378
|
+
display(HTML("<div id='unique_id'>Initial Text</div>"))
|
1379
|
+
|
1380
|
+
while True:
|
1381
|
+
if self.terminate:
|
1382
|
+
conn.close()
|
1383
|
+
break
|
1384
|
+
|
1385
|
+
if not self.update_queue.empty():
|
1386
|
+
ImageApp.update_html("Do not exit, Updating database...")
|
1387
|
+
self.status_label.config(text='Do not exit, Updating database...')
|
1388
|
+
|
1389
|
+
pending_updates = self.update_queue.get()
|
1390
|
+
for path, new_annotation in pending_updates.items():
|
1391
|
+
if new_annotation is None:
|
1392
|
+
c.execute(f'UPDATE png_list SET {self.annotation_column} = NULL WHERE png_path = ?', (path,))
|
1393
|
+
else:
|
1394
|
+
c.execute(f'UPDATE png_list SET {self.annotation_column} = ? WHERE png_path = ?', (new_annotation, path))
|
1395
|
+
conn.commit()
|
1396
|
+
|
1397
|
+
ImageApp.update_html('')
|
1398
|
+
self.status_label.config(text='')
|
1399
|
+
self.root.update()
|
1400
|
+
time.sleep(0.1)
|
1401
|
+
|
1402
|
+
def update_gui_text(self, text):
|
1403
|
+
self.status_label.config(text=text)
|
1404
|
+
self.root.update()
|
1405
|
+
|
1406
|
+
def next_page(self):
|
1407
|
+
if self.pending_updates:
|
1408
|
+
self.update_queue.put(self.pending_updates.copy())
|
1409
|
+
self.pending_updates.clear()
|
1410
|
+
self.index += self.grid_rows * self.grid_cols
|
1411
|
+
self.load_images()
|
1412
|
+
|
1413
|
+
def previous_page(self):
|
1414
|
+
if self.pending_updates:
|
1415
|
+
self.update_queue.put(self.pending_updates.copy())
|
1416
|
+
self.pending_updates.clear()
|
1417
|
+
self.index -= self.grid_rows * self.grid_cols
|
1418
|
+
if self.index < 0:
|
1419
|
+
self.index = 0
|
1420
|
+
self.load_images()
|
1421
|
+
|
1422
|
+
def shutdown(self):
|
1423
|
+
self.terminate = True
|
1424
|
+
self.update_queue.put(self.pending_updates.copy())
|
1425
|
+
self.pending_updates.clear()
|
1426
|
+
self.db_update_thread.join()
|
1427
|
+
self.root.quit()
|
1428
|
+
self.root.destroy()
|
1429
|
+
print(f'Quit application')
|
1430
|
+
|
264
1431
|
def create_menu_bar(root):
|
265
|
-
from .app_annotate import initiate_annotation_app_root
|
266
|
-
from .app_make_masks import initiate_mask_app_root
|
267
1432
|
from .gui_utils import load_app
|
1433
|
+
from .gui_core import initiate_root
|
268
1434
|
|
269
1435
|
gui_apps = {
|
270
|
-
"Mask": 'mask',
|
271
|
-
"Measure": 'measure',
|
272
|
-
"Annotate":
|
273
|
-
"Make Masks":
|
274
|
-
"Classify": 'classify',
|
275
|
-
"Sequencing": 'sequencing',
|
276
|
-
"Umap": 'umap'
|
1436
|
+
"Mask": (lambda frame: initiate_root(frame, 'mask'), "Generate cellpose masks for cells, nuclei and pathogen images."),
|
1437
|
+
"Measure": (lambda frame: initiate_root(frame, 'measure'), "Measure single object intensity and morphological feature. Crop and save single object image"),
|
1438
|
+
"Annotate": (lambda frame: initiate_root(frame, 'annotate'), "Annotation single object images on a grid. Annotations are saved to database."),
|
1439
|
+
"Make Masks": (lambda frame: initiate_root(frame, 'make_masks'), "Adjust pre-existing Cellpose models to your specific dataset for improved performance"),
|
1440
|
+
"Classify": (lambda frame: initiate_root(frame, 'classify'), "Train Torch Convolutional Neural Networks (CNNs) or Transformers to classify single object images."),
|
1441
|
+
"Sequencing": (lambda frame: initiate_root(frame, 'sequencing'), "Analyze sequencing data."),
|
1442
|
+
"Umap": (lambda frame: initiate_root(frame, 'umap'), "Generate UMAP embeddings with datapoints represented as images.")
|
277
1443
|
}
|
278
1444
|
|
279
1445
|
def load_app_wrapper(app_name, app_func):
|
@@ -285,7 +1451,8 @@ def create_menu_bar(root):
|
|
285
1451
|
app_menu = tk.Menu(menu_bar, tearoff=0, bg="#008080", fg="white")
|
286
1452
|
menu_bar.add_cascade(label="SpaCr Applications", menu=app_menu)
|
287
1453
|
# Add options to the "SpaCr Applications" menu
|
288
|
-
for app_name,
|
1454
|
+
for app_name, app_data in gui_apps.items():
|
1455
|
+
app_func, app_desc = app_data
|
289
1456
|
app_menu.add_command(label=app_name, command=lambda app_name=app_name, app_func=app_func: load_app_wrapper(app_name, app_func))
|
290
1457
|
# Add a separator and an exit option
|
291
1458
|
app_menu.add_separator()
|
@@ -296,8 +1463,8 @@ def create_menu_bar(root):
|
|
296
1463
|
def set_dark_style(style):
|
297
1464
|
font_style = tkFont.Font(family="Helvetica", size=24)
|
298
1465
|
style.configure('TEntry', padding='5 5 5 5', borderwidth=1, relief='solid', fieldbackground='black', foreground='#ffffff', font=font_style)
|
299
|
-
style.configure('TCombobox', fieldbackground='black', background='black', foreground='#ffffff', font=font_style)
|
300
|
-
style.map('TCombobox', fieldbackground=[('readonly', 'black')], foreground=[('readonly', '#ffffff')])
|
1466
|
+
style.configure('TCombobox', fieldbackground='black', background='black', foreground='#ffffff', selectbackground='black', selectforeground='#ffffff', font=font_style)
|
1467
|
+
style.map('TCombobox', fieldbackground=[('readonly', 'black')], foreground=[('readonly', '#ffffff')], selectbackground=[('readonly', 'black')], selectforeground=[('readonly', '#ffffff')])
|
301
1468
|
style.configure('Custom.TButton', background='black', foreground='white', bordercolor='white', focusthickness=3, focuscolor='white', font=('Helvetica', 12))
|
302
1469
|
style.map('Custom.TButton', background=[('active', 'teal'), ('!active', 'black')], foreground=[('active', 'white'), ('!active', 'white')], bordercolor=[('active', 'white'), ('!active', 'white')])
|
303
1470
|
style.configure('Custom.TLabel', padding='5 5 5 5', borderwidth=1, relief='flat', background='black', foreground='#ffffff', font=font_style)
|