nettracer3d 0.4.2__py3-none-any.whl → 0.4.4__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.
- nettracer3d/morphology.py +2 -0
- nettracer3d/nettracer.py +139 -2
- nettracer3d/nettracer_gui.py +1007 -107
- nettracer3d/proximity.py +3 -1
- nettracer3d/segmenter.py +290 -0
- nettracer3d/smart_dilate.py +44 -3
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.4.dist-info}/METADATA +3 -9
- nettracer3d-0.4.4.dist-info/RECORD +21 -0
- nettracer3d-0.4.2.dist-info/RECORD +0 -20
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.4.dist-info}/LICENSE +0 -0
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.4.dist-info}/WHEEL +0 -0
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.4.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.4.2.dist-info → nettracer3d-0.4.4.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -16,13 +16,15 @@ from nettracer3d import nettracer as n3d
|
|
|
16
16
|
from nettracer3d import smart_dilate as sdl
|
|
17
17
|
from nettracer3d import proximity as pxt
|
|
18
18
|
from matplotlib.colors import LinearSegmentedColormap
|
|
19
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
19
20
|
import pandas as pd
|
|
20
|
-
from PyQt6.QtGui import (QFont, QCursor, QColor)
|
|
21
|
+
from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QPainter, QPen)
|
|
21
22
|
import tifffile
|
|
22
23
|
import copy
|
|
23
24
|
import multiprocessing as mp
|
|
24
25
|
from concurrent.futures import ThreadPoolExecutor
|
|
25
26
|
from functools import partial
|
|
27
|
+
from nettracer3d import segmenter
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
class ImageViewerWindow(QMainWindow):
|
|
@@ -105,11 +107,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
105
107
|
self.zoom_mode = False
|
|
106
108
|
self.original_xlim = None
|
|
107
109
|
self.original_ylim = None
|
|
110
|
+
self.zoom_changed = False
|
|
108
111
|
|
|
109
112
|
# Pan mode state
|
|
110
113
|
self.pan_mode = False
|
|
111
114
|
self.panning = False
|
|
112
115
|
self.pan_start = None
|
|
116
|
+
|
|
117
|
+
#For ML segmenting mode
|
|
118
|
+
self.brush_mode = False
|
|
119
|
+
self.painting = False
|
|
120
|
+
self.foreground = True
|
|
121
|
+
self.machine_window = None
|
|
122
|
+
self.brush_size = 1 # Start with 1 pixel
|
|
123
|
+
self.min_brush_size = 1
|
|
124
|
+
self.max_brush_size = 10
|
|
113
125
|
|
|
114
126
|
# Store brightness/contrast values for each channel
|
|
115
127
|
self.channel_brightness = [{
|
|
@@ -176,6 +188,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
176
188
|
buttons_layout.addWidget(self.pan_button)
|
|
177
189
|
|
|
178
190
|
control_layout.addWidget(buttons_widget)
|
|
191
|
+
|
|
192
|
+
self.preview = False #Whether in preview mode or not
|
|
193
|
+
self.targs = None #Targets for preview mode
|
|
179
194
|
|
|
180
195
|
# Create channel buttons
|
|
181
196
|
self.channel_buttons = []
|
|
@@ -236,6 +251,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
236
251
|
self.ax = self.figure.add_subplot(111)
|
|
237
252
|
left_layout.addWidget(self.canvas)
|
|
238
253
|
|
|
254
|
+
self.canvas.mpl_connect('scroll_event', self.on_mpl_scroll)
|
|
255
|
+
|
|
239
256
|
|
|
240
257
|
left_layout.addWidget(control_panel)
|
|
241
258
|
|
|
@@ -344,6 +361,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
344
361
|
self.canvas.mpl_connect('button_press_event', self.on_mouse_press)
|
|
345
362
|
self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
|
|
346
363
|
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
|
|
364
|
+
|
|
347
365
|
#self.canvas.mpl_connect('button_press_event', self.on_mouse_click)
|
|
348
366
|
|
|
349
367
|
# Initialize measurement points tracking
|
|
@@ -396,7 +414,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
396
414
|
self.slice_slider.setValue(new_value)
|
|
397
415
|
|
|
398
416
|
|
|
399
|
-
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None):
|
|
417
|
+
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
|
|
400
418
|
"""
|
|
401
419
|
Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
|
|
402
420
|
|
|
@@ -410,6 +428,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
410
428
|
mask = np.isin(chunk_data, indices_to_check)
|
|
411
429
|
return mask * 255
|
|
412
430
|
|
|
431
|
+
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
432
|
+
"""Process a single chunk of the array to create highlight mask"""
|
|
433
|
+
|
|
434
|
+
mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
|
|
435
|
+
return mask * 255
|
|
436
|
+
|
|
413
437
|
if node_indices is not None:
|
|
414
438
|
if 0 in node_indices:
|
|
415
439
|
node_indices.remove(0)
|
|
@@ -453,7 +477,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
453
477
|
num_cores = mp.cpu_count()
|
|
454
478
|
|
|
455
479
|
# Calculate chunk size along y-axis
|
|
456
|
-
chunk_size = full_shape[
|
|
480
|
+
chunk_size = full_shape[1] // num_cores
|
|
457
481
|
if chunk_size < 1:
|
|
458
482
|
chunk_size = 1
|
|
459
483
|
|
|
@@ -463,25 +487,32 @@ class ImageViewerWindow(QMainWindow):
|
|
|
463
487
|
|
|
464
488
|
# Create chunks
|
|
465
489
|
chunks = []
|
|
466
|
-
for i in range(0, array_shape[
|
|
467
|
-
end = min(i + chunk_size, array_shape[
|
|
468
|
-
chunks.append(channel_data[i:end])
|
|
490
|
+
for i in range(0, array_shape[1], chunk_size):
|
|
491
|
+
end = min(i + chunk_size, array_shape[1])
|
|
492
|
+
chunks.append(channel_data[:, i:end, :])
|
|
469
493
|
|
|
470
494
|
# Process chunks in parallel using ThreadPoolExecutor
|
|
471
|
-
|
|
495
|
+
if not bounds:
|
|
496
|
+
process_func = partial(process_chunk, indices_to_check=indices)
|
|
497
|
+
else:
|
|
498
|
+
if len(indices) == 1:
|
|
499
|
+
indices.insert(0, 0)
|
|
500
|
+
process_func = partial(process_chunk_bounds, indices_to_check=indices)
|
|
501
|
+
|
|
472
502
|
|
|
473
503
|
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
474
504
|
chunk_results = list(executor.map(process_func, chunks))
|
|
475
505
|
|
|
476
506
|
# Reassemble the chunks
|
|
477
|
-
return np.
|
|
507
|
+
return np.concatenate(chunk_results, axis=1)
|
|
478
508
|
|
|
479
509
|
# Process nodes and edges in parallel using multiprocessing
|
|
480
|
-
with ThreadPoolExecutor(max_workers=
|
|
510
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
481
511
|
future_nodes = executor.submit(process_channel, self.channel_data[0], node_indices, full_shape)
|
|
482
512
|
future_edges = executor.submit(process_channel, self.channel_data[1], edge_indices, full_shape)
|
|
483
513
|
future_overlay1 = executor.submit(process_channel, self.channel_data[2], overlay1_indices, full_shape)
|
|
484
514
|
future_overlay2 = executor.submit(process_channel, self.channel_data[3], overlay2_indices, full_shape)
|
|
515
|
+
|
|
485
516
|
|
|
486
517
|
# Get results
|
|
487
518
|
node_overlay = future_nodes.result()
|
|
@@ -502,6 +533,84 @@ class ImageViewerWindow(QMainWindow):
|
|
|
502
533
|
# Update display
|
|
503
534
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
504
535
|
|
|
536
|
+
def create_highlight_overlay_slice(self, indices, bounds = False):
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
540
|
+
"""Process a single chunk of the array to create highlight mask"""
|
|
541
|
+
mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
|
|
542
|
+
return mask * 255
|
|
543
|
+
|
|
544
|
+
def process_chunk(chunk_data, indices_to_check):
|
|
545
|
+
"""Process a single chunk of the array to create highlight mask"""
|
|
546
|
+
|
|
547
|
+
mask = np.isin(chunk_data, indices_to_check)
|
|
548
|
+
return mask * 255
|
|
549
|
+
|
|
550
|
+
array = self.channel_data[self.active_channel]
|
|
551
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
552
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
553
|
+
|
|
554
|
+
current_slice = array[self.current_slice, :, :]
|
|
555
|
+
full_shape = array.shape
|
|
556
|
+
slice_shape = current_slice.shape
|
|
557
|
+
|
|
558
|
+
if self.highlight_overlay is None:
|
|
559
|
+
|
|
560
|
+
self.highlight_overlay = np.zeros(full_shape, dtype=np.uint8)
|
|
561
|
+
|
|
562
|
+
# Get number of CPU cores
|
|
563
|
+
num_cores = mp.cpu_count()
|
|
564
|
+
|
|
565
|
+
# Calculate chunk size along y-axis
|
|
566
|
+
chunk_size = slice_shape[0] // num_cores
|
|
567
|
+
if chunk_size < 1:
|
|
568
|
+
chunk_size = 1
|
|
569
|
+
|
|
570
|
+
def process_channel(channel_data, indices, array_shape):
|
|
571
|
+
if channel_data is None or not indices:
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
# Create chunks
|
|
575
|
+
chunks = []
|
|
576
|
+
for i in range(0, array_shape[0], chunk_size):
|
|
577
|
+
end = min(i + chunk_size, array_shape[0])
|
|
578
|
+
chunks.append(channel_data[i:end])
|
|
579
|
+
|
|
580
|
+
# Process chunks in parallel using ThreadPoolExecutor
|
|
581
|
+
if not bounds:
|
|
582
|
+
process_func = partial(process_chunk, indices_to_check=indices)
|
|
583
|
+
else:
|
|
584
|
+
if len(indices) == 1:
|
|
585
|
+
indices.insert(0, 0)
|
|
586
|
+
process_func = partial(process_chunk_bounds, indices_to_check=indices)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
590
|
+
chunk_results = list(executor.map(process_func, chunks))
|
|
591
|
+
|
|
592
|
+
# Reassemble the chunks
|
|
593
|
+
return np.vstack(chunk_results)
|
|
594
|
+
|
|
595
|
+
# Process nodes and edges in parallel using multiprocessing
|
|
596
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
597
|
+
future_highlight = executor.submit(process_channel, current_slice, indices, slice_shape)
|
|
598
|
+
|
|
599
|
+
# Get results
|
|
600
|
+
overlay = future_highlight.result()
|
|
601
|
+
|
|
602
|
+
try:
|
|
603
|
+
|
|
604
|
+
self.highlight_overlay[self.current_slice, :, :] = overlay
|
|
605
|
+
except:
|
|
606
|
+
pass
|
|
607
|
+
|
|
608
|
+
# Update display
|
|
609
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim), called = True)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
|
|
505
614
|
|
|
506
615
|
|
|
507
616
|
|
|
@@ -1328,9 +1437,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1328
1437
|
if self.zoom_mode:
|
|
1329
1438
|
self.pan_button.setChecked(False)
|
|
1330
1439
|
self.pan_mode = False
|
|
1440
|
+
self.brush_mode = False
|
|
1441
|
+
if self.machine_window is not None:
|
|
1442
|
+
self.machine_window.silence_button()
|
|
1331
1443
|
self.canvas.setCursor(Qt.CursorShape.CrossCursor)
|
|
1332
1444
|
else:
|
|
1333
|
-
self.
|
|
1445
|
+
if self.machine_window is None:
|
|
1446
|
+
self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
|
|
1447
|
+
else:
|
|
1448
|
+
self.machine_window.toggle_brush_button()
|
|
1449
|
+
|
|
1334
1450
|
|
|
1335
1451
|
def toggle_pan_mode(self):
|
|
1336
1452
|
"""Toggle pan mode on/off."""
|
|
@@ -1338,14 +1454,132 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1338
1454
|
if self.pan_mode:
|
|
1339
1455
|
self.zoom_button.setChecked(False)
|
|
1340
1456
|
self.zoom_mode = False
|
|
1457
|
+
self.brush_mode = False
|
|
1458
|
+
if self.machine_window is not None:
|
|
1459
|
+
self.machine_window.silence_button()
|
|
1341
1460
|
self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
|
|
1342
1461
|
else:
|
|
1343
|
-
self.
|
|
1462
|
+
if self.machine_window is None:
|
|
1463
|
+
self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
|
|
1464
|
+
else:
|
|
1465
|
+
self.machine_window.toggle_brush_button()
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def on_mpl_scroll(self, event):
|
|
1470
|
+
"""Handle matplotlib canvas scroll events"""
|
|
1471
|
+
#Wheel events
|
|
1472
|
+
if self.brush_mode and event.inaxes == self.ax:
|
|
1473
|
+
# Check if Ctrl is pressed
|
|
1474
|
+
if event.guiEvent.modifiers() & Qt.ShiftModifier:
|
|
1475
|
+
pass
|
|
1476
|
+
|
|
1477
|
+
elif event.guiEvent.modifiers() & Qt.ControlModifier:
|
|
1478
|
+
# Brush size adjustment code...
|
|
1479
|
+
step = 1 if event.button == 'up' else -1
|
|
1480
|
+
new_size = self.brush_size + step
|
|
1481
|
+
|
|
1482
|
+
if new_size < self.min_brush_size:
|
|
1483
|
+
new_size = self.min_brush_size
|
|
1484
|
+
elif new_size > self.max_brush_size:
|
|
1485
|
+
new_size = self.max_brush_size
|
|
1486
|
+
|
|
1487
|
+
self.brush_size = new_size
|
|
1488
|
+
self.update_brush_cursor()
|
|
1489
|
+
event.guiEvent.accept()
|
|
1490
|
+
return
|
|
1491
|
+
|
|
1492
|
+
# General scrolling code outside the brush mode condition
|
|
1493
|
+
step = 1 if event.button == 'up' else -1
|
|
1494
|
+
|
|
1495
|
+
if event.guiEvent.modifiers() & Qt.ShiftModifier:
|
|
1496
|
+
if event.guiEvent.modifiers() & Qt.ControlModifier:
|
|
1497
|
+
step = step * 3
|
|
1498
|
+
if (self.current_slice + step) < 0 or (self.current_slice + step) > self.slice_slider.maximum():
|
|
1499
|
+
return
|
|
1500
|
+
|
|
1501
|
+
self.current_slice = self.current_slice + step
|
|
1502
|
+
self.slice_slider.setValue(self.current_slice + step)
|
|
1503
|
+
|
|
1504
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
1505
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
1506
|
+
|
|
1507
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
1508
|
+
|
|
1509
|
+
def keyPressEvent(self, event):
|
|
1510
|
+
if event.key() == Qt.Key_Z:
|
|
1511
|
+
self.zoom_button.click()
|
|
1512
|
+
if self.machine_window is not None:
|
|
1513
|
+
if event.key() == Qt.Key_A:
|
|
1514
|
+
self.machine_window.switch_foreground()
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def update_brush_cursor(self):
|
|
1518
|
+
"""Update the cursor to show brush size"""
|
|
1519
|
+
if not self.brush_mode:
|
|
1520
|
+
return
|
|
1521
|
+
|
|
1522
|
+
# Create a pixmap for the cursor
|
|
1523
|
+
size = self.brush_size * 2 + 2 # Add padding for border
|
|
1524
|
+
pixmap = QPixmap(size, size)
|
|
1525
|
+
pixmap.fill(Qt.transparent)
|
|
1526
|
+
|
|
1527
|
+
# Create painter for the pixmap
|
|
1528
|
+
painter = QPainter(pixmap)
|
|
1529
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
1530
|
+
|
|
1531
|
+
# Draw circle
|
|
1532
|
+
pen = QPen(Qt.white)
|
|
1533
|
+
pen.setWidth(1)
|
|
1534
|
+
painter.setPen(pen)
|
|
1535
|
+
painter.setBrush(Qt.transparent)
|
|
1536
|
+
painter.drawEllipse(1, 1, size-2, size-2)
|
|
1537
|
+
|
|
1538
|
+
# Create cursor from pixmap
|
|
1539
|
+
cursor = QCursor(pixmap)
|
|
1540
|
+
self.canvas.setCursor(cursor)
|
|
1541
|
+
|
|
1542
|
+
painter.end()
|
|
1543
|
+
|
|
1544
|
+
def get_line_points(self, x0, y0, x1, y1):
|
|
1545
|
+
"""Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm"""
|
|
1546
|
+
points = []
|
|
1547
|
+
dx = abs(x1 - x0)
|
|
1548
|
+
dy = abs(y1 - y0)
|
|
1549
|
+
x, y = x0, y0
|
|
1550
|
+
sx = 1 if x0 < x1 else -1
|
|
1551
|
+
sy = 1 if y0 < y1 else -1
|
|
1552
|
+
|
|
1553
|
+
if dx > dy:
|
|
1554
|
+
err = dx / 2.0
|
|
1555
|
+
while x != x1:
|
|
1556
|
+
points.append((x, y))
|
|
1557
|
+
err -= dy
|
|
1558
|
+
if err < 0:
|
|
1559
|
+
y += sy
|
|
1560
|
+
err += dx
|
|
1561
|
+
x += sx
|
|
1562
|
+
else:
|
|
1563
|
+
err = dy / 2.0
|
|
1564
|
+
while y != y1:
|
|
1565
|
+
points.append((x, y))
|
|
1566
|
+
err -= dx
|
|
1567
|
+
if err < 0:
|
|
1568
|
+
x += sx
|
|
1569
|
+
err += dy
|
|
1570
|
+
y += sy
|
|
1571
|
+
|
|
1572
|
+
points.append((x, y))
|
|
1573
|
+
return points
|
|
1344
1574
|
|
|
1345
1575
|
def on_mouse_press(self, event):
|
|
1346
1576
|
"""Handle mouse press events."""
|
|
1347
1577
|
if event.inaxes != self.ax:
|
|
1348
1578
|
return
|
|
1579
|
+
|
|
1580
|
+
if event.button == 2:
|
|
1581
|
+
self.pan_button.click()
|
|
1582
|
+
return
|
|
1349
1583
|
|
|
1350
1584
|
if self.zoom_mode:
|
|
1351
1585
|
# Handle zoom mode press
|
|
@@ -1364,6 +1598,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1364
1598
|
|
|
1365
1599
|
self.ax.set_xlim([xdata - x_range, xdata + x_range])
|
|
1366
1600
|
self.ax.set_ylim([ydata - y_range, ydata + y_range])
|
|
1601
|
+
|
|
1602
|
+
self.zoom_changed = True # Flag that zoom has changed
|
|
1603
|
+
|
|
1604
|
+
if not hasattr(self, 'zoom_changed'):
|
|
1605
|
+
self.zoom_changed = False
|
|
1367
1606
|
|
|
1368
1607
|
elif event.button == 3: # Right click - zoom out
|
|
1369
1608
|
x_range = (current_xlim[1] - current_xlim[0])
|
|
@@ -1381,6 +1620,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1381
1620
|
else:
|
|
1382
1621
|
self.ax.set_xlim(new_xlim)
|
|
1383
1622
|
self.ax.set_ylim(new_ylim)
|
|
1623
|
+
|
|
1624
|
+
self.zoom_changed = False # Flag that zoom has changed
|
|
1625
|
+
|
|
1626
|
+
if not hasattr(self, 'zoom_changed'):
|
|
1627
|
+
self.zoom_changed = False
|
|
1384
1628
|
|
|
1385
1629
|
self.canvas.draw()
|
|
1386
1630
|
|
|
@@ -1388,6 +1632,50 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1388
1632
|
self.panning = True
|
|
1389
1633
|
self.pan_start = (event.xdata, event.ydata)
|
|
1390
1634
|
self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
1635
|
+
|
|
1636
|
+
elif self.brush_mode:
|
|
1637
|
+
if event.inaxes != self.ax:
|
|
1638
|
+
return
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
if event.button == 1 or event.button == 3:
|
|
1642
|
+
|
|
1643
|
+
if event.button == 3:
|
|
1644
|
+
self.erase = True
|
|
1645
|
+
else:
|
|
1646
|
+
self.erase = False
|
|
1647
|
+
|
|
1648
|
+
self.painting = True
|
|
1649
|
+
x, y = int(event.xdata), int(event.ydata)
|
|
1650
|
+
self.last_paint_pos = (x, y)
|
|
1651
|
+
|
|
1652
|
+
if self.foreground:
|
|
1653
|
+
channel = 2
|
|
1654
|
+
else:
|
|
1655
|
+
channel = 3
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
# Paint at initial position
|
|
1659
|
+
self.paint_at_position(x, y, self.erase, channel)
|
|
1660
|
+
|
|
1661
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
1662
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
self.canvas.draw()
|
|
1666
|
+
#self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
1667
|
+
self.restore_channels = []
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
for i in range(4):
|
|
1671
|
+
if i == channel:
|
|
1672
|
+
self.channel_visible[i] = True
|
|
1673
|
+
elif self.channel_data[i] is not None and self.channel_visible[i] == True:
|
|
1674
|
+
self.channel_visible[i] = False
|
|
1675
|
+
self.restore_channels.append(i)
|
|
1676
|
+
self.update_display(preserve_zoom = (current_xlim, current_ylim), begin_paint = True)
|
|
1677
|
+
self.update_display_slice(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
1678
|
+
|
|
1391
1679
|
|
|
1392
1680
|
elif event.button == 3: # Right click (for context menu)
|
|
1393
1681
|
self.create_context_menu(event)
|
|
@@ -1397,12 +1685,32 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1397
1685
|
self.selection_start = (event.xdata, event.ydata)
|
|
1398
1686
|
self.selecting = False # Will be set to True if the mouse moves while button is held
|
|
1399
1687
|
|
|
1688
|
+
def paint_at_position(self, center_x, center_y, erase = False, channel = 2):
|
|
1689
|
+
"""Paint pixels within brush radius at given position"""
|
|
1690
|
+
if self.channel_data[channel] is None:
|
|
1691
|
+
return
|
|
1692
|
+
|
|
1693
|
+
if erase:
|
|
1694
|
+
val = 0
|
|
1695
|
+
else:
|
|
1696
|
+
val = 255
|
|
1697
|
+
|
|
1698
|
+
height, width = self.channel_data[channel][self.current_slice].shape
|
|
1699
|
+
radius = self.brush_size // 2
|
|
1700
|
+
|
|
1701
|
+
# Calculate brush area
|
|
1702
|
+
for y in range(max(0, center_y - radius), min(height, center_y + radius + 1)):
|
|
1703
|
+
for x in range(max(0, center_x - radius), min(width, center_x + radius + 1)):
|
|
1704
|
+
# Check if point is within circular brush area
|
|
1705
|
+
if (x - center_x) ** 2 + (y - center_y) ** 2 <= radius ** 2:
|
|
1706
|
+
self.channel_data[channel][self.current_slice][y, x] = val
|
|
1707
|
+
|
|
1400
1708
|
def on_mouse_move(self, event):
|
|
1401
1709
|
"""Handle mouse movement events."""
|
|
1402
1710
|
if event.inaxes != self.ax:
|
|
1403
1711
|
return
|
|
1404
1712
|
|
|
1405
|
-
if self.selection_start and not self.selecting and not self.pan_mode and not self.zoom_mode:
|
|
1713
|
+
if self.selection_start and not self.selecting and not self.pan_mode and not self.zoom_mode and not self.brush_mode:
|
|
1406
1714
|
# If mouse has moved more than a tiny amount while button is held, start selection
|
|
1407
1715
|
if (abs(event.xdata - self.selection_start[0]) > 1 or
|
|
1408
1716
|
abs(event.ydata - self.selection_start[1]) > 1):
|
|
@@ -1424,6 +1732,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1424
1732
|
self.canvas.draw()
|
|
1425
1733
|
|
|
1426
1734
|
elif self.panning and self.pan_start is not None:
|
|
1735
|
+
|
|
1427
1736
|
# Calculate the movement
|
|
1428
1737
|
dx = event.xdata - self.pan_start[0]
|
|
1429
1738
|
dy = event.ydata - self.pan_start[1]
|
|
@@ -1459,6 +1768,39 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1459
1768
|
# Update pan start position
|
|
1460
1769
|
self.pan_start = (event.xdata, event.ydata)
|
|
1461
1770
|
|
|
1771
|
+
elif self.painting and self.brush_mode:
|
|
1772
|
+
if event.inaxes != self.ax:
|
|
1773
|
+
return
|
|
1774
|
+
|
|
1775
|
+
x, y = int(event.xdata), int(event.ydata)
|
|
1776
|
+
|
|
1777
|
+
if self.foreground:
|
|
1778
|
+
channel = 2
|
|
1779
|
+
else:
|
|
1780
|
+
channel = 3
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
if self.channel_data[2] is not None:
|
|
1784
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
1785
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
1786
|
+
height, width = self.channel_data[2][self.current_slice].shape
|
|
1787
|
+
|
|
1788
|
+
if hasattr(self, 'last_paint_pos'):
|
|
1789
|
+
last_x, last_y = self.last_paint_pos
|
|
1790
|
+
points = self.get_line_points(last_x, last_y, x, y)
|
|
1791
|
+
|
|
1792
|
+
# Paint at each point along the line
|
|
1793
|
+
for px, py in points:
|
|
1794
|
+
if 0 <= px < width and 0 <= py < height:
|
|
1795
|
+
self.paint_at_position(px, py, self.erase, channel)
|
|
1796
|
+
|
|
1797
|
+
self.last_paint_pos = (x, y)
|
|
1798
|
+
|
|
1799
|
+
self.canvas.draw()
|
|
1800
|
+
#self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
1801
|
+
self.update_display_slice(channel, preserve_zoom=(current_xlim, current_ylim))
|
|
1802
|
+
|
|
1803
|
+
|
|
1462
1804
|
def on_mouse_release(self, event):
|
|
1463
1805
|
"""Handle mouse release events."""
|
|
1464
1806
|
if self.pan_mode:
|
|
@@ -1520,15 +1862,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1520
1862
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
1521
1863
|
# Handle as a normal click
|
|
1522
1864
|
self.on_mouse_click(event)
|
|
1865
|
+
|
|
1523
1866
|
|
|
1524
1867
|
# Clean up
|
|
1525
1868
|
self.selection_start = None
|
|
1526
1869
|
self.selecting = False
|
|
1870
|
+
|
|
1871
|
+
|
|
1527
1872
|
if self.selection_rect is not None:
|
|
1528
1873
|
self.selection_rect.remove()
|
|
1529
1874
|
self.selection_rect = None
|
|
1530
1875
|
self.canvas.draw()
|
|
1531
1876
|
|
|
1877
|
+
if self.brush_mode:
|
|
1878
|
+
self.painting = False
|
|
1879
|
+
for i in self.restore_channels:
|
|
1880
|
+
self.channel_visible[i] = True
|
|
1881
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
1882
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
1883
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
1884
|
+
|
|
1885
|
+
|
|
1532
1886
|
def highlight_value_in_tables(self, clicked_value):
|
|
1533
1887
|
"""Helper method to find and highlight a value in both tables."""
|
|
1534
1888
|
|
|
@@ -1622,6 +1976,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1622
1976
|
|
|
1623
1977
|
self.ax.set_xlim([xdata - x_range, xdata + x_range])
|
|
1624
1978
|
self.ax.set_ylim([ydata - y_range, ydata + y_range])
|
|
1979
|
+
|
|
1980
|
+
self.zoom_changed = True # Flag that zoom has changed
|
|
1981
|
+
|
|
1982
|
+
if not hasattr(self, 'zoom_changed'):
|
|
1983
|
+
self.zoom_changed = False
|
|
1625
1984
|
|
|
1626
1985
|
elif event.button == 3: # Right click - zoom out
|
|
1627
1986
|
x_range = (current_xlim[1] - current_xlim[0])
|
|
@@ -1639,6 +1998,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1639
1998
|
else:
|
|
1640
1999
|
self.ax.set_xlim(new_xlim)
|
|
1641
2000
|
self.ax.set_ylim(new_ylim)
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
self.zoom_changed = False # Flag that zoom has changed
|
|
2004
|
+
|
|
2005
|
+
|
|
1642
2006
|
|
|
1643
2007
|
self.canvas.draw()
|
|
1644
2008
|
|
|
@@ -1652,7 +2016,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1652
2016
|
x_idx = int(round(event.xdata))
|
|
1653
2017
|
y_idx = int(round(event.ydata))
|
|
1654
2018
|
# Check if Ctrl key is pressed (using matplotlib's key_press system)
|
|
1655
|
-
ctrl_pressed = 'ctrl' in event.modifiers
|
|
2019
|
+
ctrl_pressed = 'ctrl' in event.modifiers
|
|
1656
2020
|
if self.channel_data[self.active_channel][self.current_slice, y_idx, x_idx] != 0:
|
|
1657
2021
|
clicked_value = self.channel_data[self.active_channel][self.current_slice, y_idx, x_idx]
|
|
1658
2022
|
else:
|
|
@@ -2055,8 +2419,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2055
2419
|
|
|
2056
2420
|
def show_thresh_dialog(self):
|
|
2057
2421
|
"""Show threshold dialog"""
|
|
2058
|
-
|
|
2059
|
-
|
|
2422
|
+
dialog = ThresholdDialog(self)
|
|
2423
|
+
dialog.exec()
|
|
2424
|
+
|
|
2060
2425
|
|
|
2061
2426
|
def show_mask_dialog(self):
|
|
2062
2427
|
"""Show the mask dialog"""
|
|
@@ -2265,13 +2630,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2265
2630
|
|
|
2266
2631
|
|
|
2267
2632
|
except Exception as e:
|
|
2268
|
-
import traceback
|
|
2269
|
-
print(traceback.format_exc())
|
|
2270
2633
|
print(f"An error has occured: {e}")
|
|
2271
2634
|
|
|
2272
2635
|
except Exception as e:
|
|
2273
|
-
|
|
2274
|
-
print(traceback.format_exc())
|
|
2636
|
+
|
|
2275
2637
|
QMessageBox.critical(
|
|
2276
2638
|
self,
|
|
2277
2639
|
"Error Loading",
|
|
@@ -2513,12 +2875,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2513
2875
|
"""Shows a dialog asking user to confirm if image is 2D RGB"""
|
|
2514
2876
|
msg = QMessageBox()
|
|
2515
2877
|
msg.setIcon(QMessageBox.Icon.Question)
|
|
2516
|
-
msg.setText("Image Format
|
|
2878
|
+
msg.setText("Image Format Alert")
|
|
2517
2879
|
msg.setInformativeText("Is this a 2D color (RGB/CMYK) image?")
|
|
2518
2880
|
msg.setWindowTitle("Confirm Image Format")
|
|
2519
2881
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
2520
2882
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
2521
2883
|
|
|
2884
|
+
def confirm_resize_dialog(self):
|
|
2885
|
+
"""Shows a dialog asking user to resize image"""
|
|
2886
|
+
msg = QMessageBox()
|
|
2887
|
+
msg.setIcon(QMessageBox.Icon.Question)
|
|
2888
|
+
msg.setText("Image Format Alert")
|
|
2889
|
+
msg.setInformativeText(f"This image is a different shape than the ones loaded into the viewer window. Trying to run processes with images of different sizes has a high probability of crashing the program.\nPress yes to resize the new image to the other images. Press no to load it anyway.")
|
|
2890
|
+
msg.setWindowTitle("Resize")
|
|
2891
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
2892
|
+
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
2893
|
+
|
|
2522
2894
|
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
|
|
2523
2895
|
"""Load a channel and enable active channel selection if needed."""
|
|
2524
2896
|
|
|
@@ -2532,6 +2904,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2532
2904
|
"TIFF Files (*.tif *.tiff)"
|
|
2533
2905
|
)
|
|
2534
2906
|
self.channel_data[channel_index] = tifffile.imread(filename)
|
|
2907
|
+
|
|
2535
2908
|
if len(self.channel_data[channel_index].shape) == 2: # handle 2d data
|
|
2536
2909
|
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
2537
2910
|
|
|
@@ -2547,6 +2920,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2547
2920
|
if len(self.channel_data[channel_index].shape) == 4 and (channel_index == 0 or channel_index == 1):
|
|
2548
2921
|
self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index])
|
|
2549
2922
|
|
|
2923
|
+
reset_resize = False
|
|
2924
|
+
|
|
2925
|
+
for i in range(4): #Try to ensure users don't load in different sized arrays
|
|
2926
|
+
if self.channel_data[i] is None or i == channel_index or data:
|
|
2927
|
+
if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
|
|
2928
|
+
if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
|
|
2929
|
+
self.resizing = True
|
|
2930
|
+
reset_resize = True
|
|
2931
|
+
self.highlight_overlay = None
|
|
2932
|
+
continue
|
|
2933
|
+
else:
|
|
2934
|
+
old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
|
|
2935
|
+
if old_shape != self.channel_data[channel_index].shape[:3]:
|
|
2936
|
+
if self.confirm_resize_dialog():
|
|
2937
|
+
self.channel_data[channel_index] = n3d.upsample_with_padding(self.channel_data[channel_index], original_shape = old_shape)
|
|
2938
|
+
break
|
|
2939
|
+
|
|
2550
2940
|
|
|
2551
2941
|
if channel_index == 0:
|
|
2552
2942
|
my_network.nodes = self.channel_data[channel_index]
|
|
@@ -2601,11 +2991,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2601
2991
|
self.volume_dict[channel_index] = None #reset volumes
|
|
2602
2992
|
|
|
2603
2993
|
if assign_shape: #keep original shape tracked to undo resampling.
|
|
2604
|
-
self.original_shape
|
|
2994
|
+
if self.original_shape is None:
|
|
2995
|
+
self.original_shape = self.channel_data[channel_index].shape
|
|
2996
|
+
elif self.original_shape[0] < self.channel_data[channel_index].shape[0] or self.original_shape[1] < self.channel_data[channel_index].shape[1] or self.original_shape[2] < self.channel_data[channel_index].shape[2]:
|
|
2997
|
+
self.original_shape = self.channel_data[channel_index].shape
|
|
2605
2998
|
if len(self.original_shape) == 4:
|
|
2606
2999
|
self.original_shape = (self.original_shape[0], self.original_shape[1], self.original_shape[2])
|
|
2607
3000
|
|
|
2608
|
-
self.update_display()
|
|
3001
|
+
self.update_display(reset_resize = reset_resize)
|
|
2609
3002
|
|
|
2610
3003
|
|
|
2611
3004
|
|
|
@@ -2829,13 +3222,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2829
3222
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2830
3223
|
# Convert slider values (0-100) to data values (0-1)
|
|
2831
3224
|
min_val, max_val = values
|
|
2832
|
-
self.channel_brightness[channel_index]['min'] = min_val / 255
|
|
3225
|
+
self.channel_brightness[channel_index]['min'] = min_val / 255 #Accomodate 32 bit data?
|
|
2833
3226
|
self.channel_brightness[channel_index]['max'] = max_val / 255
|
|
2834
3227
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
3228
|
+
|
|
3229
|
+
|
|
3230
|
+
|
|
2835
3231
|
|
|
2836
|
-
def update_display(self, preserve_zoom=None, dims = None):
|
|
3232
|
+
def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False, begin_paint = False):
|
|
2837
3233
|
"""Update the display with currently visible channels and highlight overlay."""
|
|
2838
3234
|
|
|
3235
|
+
if begin_paint:
|
|
3236
|
+
# Store/update the static background with current zoom level
|
|
3237
|
+
self.static_background = self.canvas.copy_from_bbox(self.ax.bbox)
|
|
3238
|
+
|
|
3239
|
+
|
|
2839
3240
|
self.figure.clear()
|
|
2840
3241
|
|
|
2841
3242
|
# Get active channels and their dimensions
|
|
@@ -2927,6 +3328,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2927
3328
|
vmax=1,
|
|
2928
3329
|
extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
|
|
2929
3330
|
|
|
3331
|
+
if self.preview and not called:
|
|
3332
|
+
self.create_highlight_overlay_slice(self.targs, bounds = self.bounds)
|
|
3333
|
+
|
|
2930
3334
|
# Add highlight overlay if it exists
|
|
2931
3335
|
if self.highlight_overlay is not None:
|
|
2932
3336
|
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
@@ -2939,6 +3343,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2939
3343
|
alpha=0.5)
|
|
2940
3344
|
|
|
2941
3345
|
|
|
3346
|
+
|
|
2942
3347
|
|
|
2943
3348
|
# Style the axes
|
|
2944
3349
|
self.ax.set_xlabel('X')
|
|
@@ -2987,9 +3392,42 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2987
3392
|
if current_xlim is not None and current_ylim is not None:
|
|
2988
3393
|
self.ax.set_xlim(current_xlim)
|
|
2989
3394
|
self.ax.set_ylim(current_ylim)
|
|
3395
|
+
if reset_resize:
|
|
3396
|
+
self.resizing = False
|
|
2990
3397
|
|
|
2991
3398
|
self.canvas.draw()
|
|
2992
3399
|
|
|
3400
|
+
def update_display_slice(self, channel, preserve_zoom=None):
|
|
3401
|
+
"""Ultra minimal update that only changes the paint channel's data"""
|
|
3402
|
+
if not self.channel_visible[channel]:
|
|
3403
|
+
return
|
|
3404
|
+
|
|
3405
|
+
if preserve_zoom:
|
|
3406
|
+
current_xlim, current_ylim = preserve_zoom
|
|
3407
|
+
if current_xlim is not None and current_ylim is not None:
|
|
3408
|
+
self.ax.set_xlim(current_xlim)
|
|
3409
|
+
self.ax.set_ylim(current_ylim)
|
|
3410
|
+
|
|
3411
|
+
|
|
3412
|
+
# Find the existing image for channel (paint channel)
|
|
3413
|
+
channel_image = None
|
|
3414
|
+
for img in self.ax.images:
|
|
3415
|
+
if img.cmap.name == f'custom_{channel}':
|
|
3416
|
+
channel_image = img
|
|
3417
|
+
break
|
|
3418
|
+
|
|
3419
|
+
if channel_image is not None:
|
|
3420
|
+
# Update the data of the existing image
|
|
3421
|
+
channel_image.set_array(self.channel_data[channel][self.current_slice])
|
|
3422
|
+
|
|
3423
|
+
# Restore the static background (all other channels) at current zoom level
|
|
3424
|
+
self.canvas.restore_region(self.static_background)
|
|
3425
|
+
# Draw just our paint channel
|
|
3426
|
+
self.ax.draw_artist(channel_image)
|
|
3427
|
+
# Blit everything
|
|
3428
|
+
self.canvas.blit(self.ax.bbox)
|
|
3429
|
+
self.canvas.flush_events()
|
|
3430
|
+
|
|
2993
3431
|
def show_netshow_dialog(self):
|
|
2994
3432
|
dialog = NetShowDialog(self)
|
|
2995
3433
|
dialog.exec()
|
|
@@ -4162,17 +4600,18 @@ class WhiteDialog(QDialog):
|
|
|
4162
4600
|
def white_overlay(self):
|
|
4163
4601
|
|
|
4164
4602
|
try:
|
|
4165
|
-
|
|
4166
|
-
try:
|
|
4603
|
+
if isinstance(my_network.nodes, np.ndarray) :
|
|
4167
4604
|
overlay = np.ones_like(my_network.nodes).astype(np.uint8) * 255
|
|
4168
|
-
|
|
4605
|
+
elif isinstance(my_network.edges, np.ndarray):
|
|
4169
4606
|
overlay = np.ones_like(my_network.edges).astype(np.uint8) * 255
|
|
4170
|
-
|
|
4171
|
-
my_network.
|
|
4607
|
+
elif isinstance(my_network.network_overlay, np.ndarray):
|
|
4608
|
+
overlay = np.ones_like(my_network.network_overlay).astype(np.uint8) * 255
|
|
4172
4609
|
|
|
4173
|
-
|
|
4610
|
+
my_network.id_overlay = overlay
|
|
4174
4611
|
|
|
4175
|
-
|
|
4612
|
+
self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
|
|
4613
|
+
|
|
4614
|
+
self.accept()
|
|
4176
4615
|
|
|
4177
4616
|
except Exception as e:
|
|
4178
4617
|
print(f"Error making white background: {e}")
|
|
@@ -4274,7 +4713,7 @@ class NetShowDialog(QDialog):
|
|
|
4274
4713
|
self.geo_layout = QPushButton("geo_layout")
|
|
4275
4714
|
self.geo_layout.setCheckable(True)
|
|
4276
4715
|
self.geo_layout.setChecked(False)
|
|
4277
|
-
layout.addRow("Use
|
|
4716
|
+
layout.addRow("Use Geographic Layout:", self.geo_layout)
|
|
4278
4717
|
|
|
4279
4718
|
# Add mode selection dropdown
|
|
4280
4719
|
self.mode_selector = QComboBox()
|
|
@@ -5246,101 +5685,504 @@ class LabelDialog(QDialog):
|
|
|
5246
5685
|
f"Error running label: {str(e)}"
|
|
5247
5686
|
)
|
|
5248
5687
|
|
|
5249
|
-
class
|
|
5688
|
+
class ThresholdDialog(QDialog):
|
|
5250
5689
|
def __init__(self, parent=None):
|
|
5251
5690
|
super().__init__(parent)
|
|
5252
|
-
self.setWindowTitle("Threshold
|
|
5253
|
-
|
|
5254
|
-
# Create central widget and layout
|
|
5255
|
-
central_widget = QWidget()
|
|
5256
|
-
self.setCentralWidget(central_widget)
|
|
5257
|
-
layout = QFormLayout(central_widget)
|
|
5258
|
-
|
|
5259
|
-
self.min = QLineEdit("")
|
|
5260
|
-
layout.addRow("Minimum Value to retain:", self.min)
|
|
5691
|
+
self.setWindowTitle("Choose Threshold Mode")
|
|
5692
|
+
self.setModal(True)
|
|
5261
5693
|
|
|
5262
|
-
|
|
5263
|
-
self.max = QLineEdit("")
|
|
5264
|
-
layout.addRow("Maximum Value to retain:", self.max)
|
|
5694
|
+
layout = QFormLayout(self)
|
|
5265
5695
|
|
|
5266
5696
|
# Add mode selection dropdown
|
|
5267
5697
|
self.mode_selector = QComboBox()
|
|
5268
|
-
self.mode_selector.addItems(["Using
|
|
5698
|
+
self.mode_selector.addItems(["Using Label/Brightness", "Using Volumes"])
|
|
5269
5699
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
5270
5700
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
5271
5701
|
|
|
5272
5702
|
# Add Run button
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
layout.addRow(
|
|
5703
|
+
run_button = QPushButton("Select")
|
|
5704
|
+
run_button.clicked.connect(self.thresh_mode)
|
|
5705
|
+
layout.addRow(run_button)
|
|
5706
|
+
|
|
5707
|
+
# Add ML button
|
|
5708
|
+
ML = QPushButton("Machine Learning")
|
|
5709
|
+
ML.clicked.connect(self.start_ml)
|
|
5710
|
+
layout.addRow(ML)
|
|
5711
|
+
|
|
5712
|
+
|
|
5713
|
+
def thresh_mode(self):
|
|
5714
|
+
|
|
5715
|
+
try:
|
|
5716
|
+
|
|
5717
|
+
accepted_mode = self.mode_selector.currentIndex()
|
|
5718
|
+
|
|
5719
|
+
if accepted_mode == 1:
|
|
5720
|
+
if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
|
|
5721
|
+
self.parent().show_label_dialog()
|
|
5722
|
+
|
|
5723
|
+
if self.parent().volume_dict[self.parent().active_channel] is None:
|
|
5724
|
+
self.parent().volumes()
|
|
5725
|
+
|
|
5726
|
+
thresh_window = ThresholdWindow(self.parent(), accepted_mode)
|
|
5727
|
+
thresh_window.show() # Non-modal window
|
|
5728
|
+
self.highlight_overlay = None
|
|
5729
|
+
self.accept()
|
|
5730
|
+
except:
|
|
5731
|
+
pass
|
|
5732
|
+
|
|
5733
|
+
def start_ml(self):
|
|
5734
|
+
|
|
5735
|
+
|
|
5736
|
+
if self.parent().channel_data[2] is not None or self.parent().channel_data[3] is not None or self.parent().highlight_overlay is not None:
|
|
5737
|
+
if self.confirm_machine_dialog():
|
|
5738
|
+
pass
|
|
5739
|
+
else:
|
|
5740
|
+
return
|
|
5741
|
+
elif self.parent().channel_data[0] is None and self.parent().channel_data[1] is None:
|
|
5742
|
+
QMessageBox.critical(
|
|
5743
|
+
self,
|
|
5744
|
+
"Alert",
|
|
5745
|
+
"Requires the channel for segmentation to be loaded into either the nodes or edges channels"
|
|
5746
|
+
)
|
|
5747
|
+
return
|
|
5748
|
+
|
|
5749
|
+
|
|
5750
|
+
self.parent().machine_window = MachineWindow(self.parent())
|
|
5751
|
+
self.parent().machine_window.show() # Non-modal window
|
|
5752
|
+
self.accept()
|
|
5753
|
+
|
|
5754
|
+
def confirm_machine_dialog(self):
|
|
5755
|
+
"""Shows a dialog asking user to confirm if they want to start the segmenter"""
|
|
5756
|
+
msg = QMessageBox()
|
|
5757
|
+
msg.setIcon(QMessageBox.Icon.Question)
|
|
5758
|
+
msg.setText("Alert")
|
|
5759
|
+
msg.setInformativeText("Use of this feature will require use of both overlay channels and the highlight overlay. Please save any data and return, or proceed if you do not need those overlays")
|
|
5760
|
+
msg.setWindowTitle("Proceed?")
|
|
5761
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
5762
|
+
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
5763
|
+
|
|
5764
|
+
|
|
5765
|
+
class MachineWindow(QMainWindow):
|
|
5766
|
+
|
|
5767
|
+
def __init__(self, parent=None):
|
|
5768
|
+
super().__init__(parent)
|
|
5769
|
+
|
|
5770
|
+
self.setWindowTitle("Threshold")
|
|
5276
5771
|
|
|
5277
|
-
#
|
|
5772
|
+
# Create central widget and layout
|
|
5773
|
+
central_widget = QWidget()
|
|
5774
|
+
self.setCentralWidget(central_widget)
|
|
5775
|
+
layout = QVBoxLayout(central_widget)
|
|
5776
|
+
|
|
5777
|
+
|
|
5778
|
+
# Create form layout for inputs
|
|
5779
|
+
form_layout = QFormLayout()
|
|
5780
|
+
|
|
5781
|
+
layout.addLayout(form_layout)
|
|
5782
|
+
|
|
5783
|
+
|
|
5784
|
+
|
|
5785
|
+
|
|
5786
|
+
if self.parent().active_channel == 0:
|
|
5787
|
+
if self.parent().channel_data[0] is not None:
|
|
5788
|
+
active_data = self.parent().channel_data[0]
|
|
5789
|
+
else:
|
|
5790
|
+
active_data = self.parent().channel_data[1]
|
|
5791
|
+
|
|
5792
|
+
|
|
5793
|
+
array1 = np.zeros_like(active_data)
|
|
5794
|
+
array2 = np.zeros_like(active_data)
|
|
5795
|
+
array3 = np.zeros_like(active_data)
|
|
5796
|
+
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
5797
|
+
|
|
5798
|
+
self.parent().load_channel(2, array1, True) #Temp for debugging
|
|
5799
|
+
self.parent().load_channel(3, array2, True) #Temp for debugging
|
|
5800
|
+
|
|
5801
|
+
self.parent().base_colors[2] = self.parent().color_dictionary['LIGHT_GREEN']
|
|
5802
|
+
self.parent().base_colors[3] = self.parent().color_dictionary['LIGHT_RED']
|
|
5803
|
+
|
|
5804
|
+
|
|
5805
|
+
# Set a reasonable default size
|
|
5806
|
+
self.setMinimumWidth(400)
|
|
5807
|
+
self.setMinimumHeight(400)
|
|
5808
|
+
|
|
5809
|
+
# Create zoom button and pan button
|
|
5810
|
+
buttons_widget = QWidget()
|
|
5811
|
+
buttons_layout = QHBoxLayout(buttons_widget)
|
|
5812
|
+
|
|
5813
|
+
# Create zoom button
|
|
5814
|
+
self.brush_button = QPushButton("🖌️")
|
|
5815
|
+
self.brush_button.setCheckable(True)
|
|
5816
|
+
self.brush_button.setFixedSize(40, 40)
|
|
5817
|
+
self.brush_button.clicked.connect(self.toggle_brush_mode)
|
|
5818
|
+
form_layout.addWidget(self.brush_button)
|
|
5819
|
+
self.brush_button.click()
|
|
5820
|
+
|
|
5821
|
+
self.fore_button = QPushButton("Foreground")
|
|
5822
|
+
self.fore_button.setCheckable(True)
|
|
5823
|
+
self.fore_button.setChecked(True)
|
|
5824
|
+
self.fore_button.clicked.connect(self.toggle_foreground)
|
|
5825
|
+
form_layout.addWidget(self.fore_button)
|
|
5826
|
+
|
|
5827
|
+
self.back_button = QPushButton("Background")
|
|
5828
|
+
self.back_button.setCheckable(True)
|
|
5829
|
+
self.back_button.setChecked(False)
|
|
5830
|
+
self.back_button.clicked.connect(self.toggle_background)
|
|
5831
|
+
form_layout.addWidget(self.back_button)
|
|
5832
|
+
|
|
5833
|
+
train_button = QPushButton("Train Model")
|
|
5834
|
+
train_button.clicked.connect(self.train_model)
|
|
5835
|
+
form_layout.addRow(train_button)
|
|
5836
|
+
|
|
5837
|
+
seg_button = QPushButton("Segment")
|
|
5838
|
+
seg_button.clicked.connect(self.segment)
|
|
5839
|
+
form_layout.addRow(seg_button)
|
|
5840
|
+
|
|
5841
|
+
self.trained = False
|
|
5842
|
+
|
|
5843
|
+
|
|
5844
|
+
self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=True)
|
|
5845
|
+
|
|
5846
|
+
|
|
5847
|
+
|
|
5848
|
+
|
|
5849
|
+
def toggle_foreground(self):
|
|
5850
|
+
|
|
5851
|
+
self.parent().foreground = self.fore_button.isChecked()
|
|
5852
|
+
|
|
5853
|
+
if self.parent().foreground:
|
|
5854
|
+
self.back_button.setChecked(False)
|
|
5855
|
+
else:
|
|
5856
|
+
self.back_button.setChecked(True)
|
|
5857
|
+
|
|
5858
|
+
def switch_foreground(self):
|
|
5859
|
+
|
|
5860
|
+
self.fore_button.click()
|
|
5861
|
+
|
|
5862
|
+
def toggle_background(self):
|
|
5863
|
+
|
|
5864
|
+
self.parent().foreground = not self.back_button.isChecked()
|
|
5865
|
+
|
|
5866
|
+
if not self.parent().foreground:
|
|
5867
|
+
self.fore_button.setChecked(False)
|
|
5868
|
+
else:
|
|
5869
|
+
self.fore_button.setChecked(True)
|
|
5870
|
+
|
|
5871
|
+
|
|
5872
|
+
|
|
5873
|
+
def toggle_brush_mode(self):
|
|
5874
|
+
"""Toggle brush mode on/off"""
|
|
5875
|
+
self.parent().brush_mode = self.brush_button.isChecked()
|
|
5876
|
+
if self.parent().brush_mode:
|
|
5877
|
+
self.parent().pan_button.setChecked(False)
|
|
5878
|
+
self.parent().zoom_button.setChecked(False)
|
|
5879
|
+
self.parent().pan_mode = False
|
|
5880
|
+
self.parent().zoom_mode = False
|
|
5881
|
+
self.parent().update_brush_cursor()
|
|
5882
|
+
else:
|
|
5883
|
+
self.parent().zoom_button.click()
|
|
5884
|
+
|
|
5885
|
+
def silence_button(self):
|
|
5886
|
+
self.brush_button.setChecked(False)
|
|
5887
|
+
|
|
5888
|
+
def toggle_brush_button(self):
|
|
5889
|
+
|
|
5890
|
+
self.brush_button.click()
|
|
5891
|
+
|
|
5892
|
+
def train_model(self):
|
|
5893
|
+
|
|
5894
|
+
self.segmenter.train_batch(self.parent().channel_data[2], self.parent().channel_data[3])
|
|
5895
|
+
self.trained = True
|
|
5896
|
+
|
|
5897
|
+
def segment(self):
|
|
5898
|
+
|
|
5899
|
+
if not self.trained:
|
|
5900
|
+
return
|
|
5901
|
+
else:
|
|
5902
|
+
foreground_coords, background_coords = self.segmenter.segment_volume()
|
|
5903
|
+
|
|
5904
|
+
# Clean up when done
|
|
5905
|
+
self.segmenter.cleanup()
|
|
5906
|
+
|
|
5907
|
+
for z,y,x in foreground_coords:
|
|
5908
|
+
self.parent().highlight_overlay[z,y,x] = True
|
|
5909
|
+
|
|
5910
|
+
self.parent().update_display()
|
|
5911
|
+
|
|
5912
|
+
def closeEvent(self, event):
|
|
5913
|
+
if self.brush_button.isChecked():
|
|
5914
|
+
self.silence_button()
|
|
5915
|
+
self.toggle_brush_mode()
|
|
5916
|
+
self.parent().brush_mode = False
|
|
5917
|
+
self.parent().machine_window = None
|
|
5918
|
+
|
|
5919
|
+
|
|
5920
|
+
|
|
5921
|
+
|
|
5922
|
+
|
|
5923
|
+
class ThresholdWindow(QMainWindow):
|
|
5924
|
+
def __init__(self, parent=None, accepted_mode=0):
|
|
5925
|
+
super().__init__(parent)
|
|
5926
|
+
self.setWindowTitle("Threshold")
|
|
5927
|
+
|
|
5928
|
+
# Create central widget and layout
|
|
5929
|
+
central_widget = QWidget()
|
|
5930
|
+
self.setCentralWidget(central_widget)
|
|
5931
|
+
layout = QVBoxLayout(central_widget)
|
|
5932
|
+
|
|
5933
|
+
# Get histogram data
|
|
5934
|
+
if accepted_mode == 1:
|
|
5935
|
+
self.histo_list = list(self.parent().volume_dict[self.parent().active_channel].values())
|
|
5936
|
+
self.bounds = False
|
|
5937
|
+
self.parent().bounds = False
|
|
5938
|
+
elif accepted_mode == 0:
|
|
5939
|
+
targ_shape = self.parent().channel_data[self.parent().active_channel].shape
|
|
5940
|
+
if (targ_shape[0] + targ_shape[1] + targ_shape[2]) > 2500: #Take a simpler histogram on big arrays
|
|
5941
|
+
temp_max = np.max(self.parent().channel_data[self.parent().active_channel])
|
|
5942
|
+
temp_min = np.min(self.parent().channel_data[self.parent().active_channel])
|
|
5943
|
+
temp_array = n3d.downsample(self.parent().channel_data[self.parent().active_channel], 5)
|
|
5944
|
+
self.histo_list = temp_array.flatten().tolist()
|
|
5945
|
+
self.histo_list.append(temp_min)
|
|
5946
|
+
self.histo_list.append(temp_max)
|
|
5947
|
+
else: #Otherwise just use full array data
|
|
5948
|
+
self.histo_list = self.parent().channel_data[self.parent().active_channel].flatten().tolist()
|
|
5949
|
+
self.bounds = True
|
|
5950
|
+
self.parent().bounds = True
|
|
5951
|
+
|
|
5952
|
+
# Create matplotlib figure
|
|
5953
|
+
fig = Figure(figsize=(5, 4))
|
|
5954
|
+
self.canvas = FigureCanvas(fig)
|
|
5955
|
+
layout.addWidget(self.canvas)
|
|
5956
|
+
|
|
5957
|
+
# Pre-compute histogram with numpy
|
|
5958
|
+
counts, bin_edges = np.histogram(self.histo_list, bins=50)
|
|
5959
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
5960
|
+
|
|
5961
|
+
# Plot pre-computed histogram
|
|
5962
|
+
self.ax = fig.add_subplot(111)
|
|
5963
|
+
self.ax.bar(bin_centers, counts, width=bin_edges[1] - bin_edges[0], alpha=0.5)
|
|
5964
|
+
|
|
5965
|
+
# Add vertical lines for thresholds
|
|
5966
|
+
self.min_line = self.ax.axvline(min(self.histo_list), color='r')
|
|
5967
|
+
self.max_line = self.ax.axvline(max(self.histo_list), color='b')
|
|
5968
|
+
|
|
5969
|
+
# Connect events for dragging
|
|
5970
|
+
self.canvas.mpl_connect('button_press_event', self.on_press)
|
|
5971
|
+
self.canvas.mpl_connect('motion_notify_event', self.on_motion)
|
|
5972
|
+
self.canvas.mpl_connect('button_release_event', self.on_release)
|
|
5973
|
+
|
|
5974
|
+
self.dragging = None
|
|
5975
|
+
|
|
5976
|
+
# Store histogram bounds
|
|
5977
|
+
if self.bounds:
|
|
5978
|
+
self.data_min = 0
|
|
5979
|
+
else:
|
|
5980
|
+
self.data_min = min(self.histo_list)
|
|
5981
|
+
self.data_max = max(self.histo_list)
|
|
5982
|
+
|
|
5983
|
+
# Create form layout for inputs
|
|
5984
|
+
form_layout = QFormLayout()
|
|
5985
|
+
|
|
5986
|
+
self.min = QLineEdit(f"{self.data_min}")
|
|
5987
|
+
self.min.editingFinished.connect(self.min_value_changed)
|
|
5988
|
+
form_layout.addRow("Minimum Value to retain:", self.min)
|
|
5989
|
+
self.prev_min = self.data_min
|
|
5990
|
+
|
|
5991
|
+
self.max = QLineEdit(f"{self.data_max}")
|
|
5992
|
+
self.max.editingFinished.connect(self.max_value_changed)
|
|
5993
|
+
form_layout.addRow("Maximum Value to retain:", self.max)
|
|
5994
|
+
self.prev_max = self.data_max
|
|
5995
|
+
|
|
5996
|
+
self.targs = [self.prev_min, self.prev_max]
|
|
5997
|
+
|
|
5998
|
+
# preview checkbox (default False)
|
|
5999
|
+
self.preview = QPushButton("Preview")
|
|
6000
|
+
self.preview.setCheckable(True)
|
|
6001
|
+
self.preview.setChecked(False)
|
|
6002
|
+
self.preview.clicked.connect(self.preview_mode)
|
|
6003
|
+
form_layout.addRow("Show Preview:", self.preview)
|
|
6004
|
+
|
|
5278
6005
|
run_button = QPushButton("Apply Threshold")
|
|
5279
6006
|
run_button.clicked.connect(self.thresh)
|
|
5280
|
-
|
|
6007
|
+
form_layout.addRow(run_button)
|
|
5281
6008
|
|
|
5282
|
-
|
|
5283
|
-
self.setMinimumWidth(300)
|
|
6009
|
+
layout.addLayout(form_layout)
|
|
5284
6010
|
|
|
5285
|
-
|
|
6011
|
+
# Set a reasonable default size
|
|
6012
|
+
self.setMinimumWidth(400)
|
|
6013
|
+
self.setMinimumHeight(400)
|
|
5286
6014
|
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
print(f"Invalid input: {text}")
|
|
5292
|
-
return default_value
|
|
6015
|
+
def closeEvent(self, event):
|
|
6016
|
+
self.parent().preview = False
|
|
6017
|
+
self.parent().targs = None
|
|
6018
|
+
self.parent().bounds = False
|
|
5293
6019
|
|
|
6020
|
+
def get_values_in_range(self, lst, min_val, max_val):
|
|
6021
|
+
values = [x for x in lst if min_val <= x <= max_val]
|
|
6022
|
+
output = []
|
|
6023
|
+
for item in self.parent().volume_dict[self.parent().active_channel]:
|
|
6024
|
+
if self.parent().volume_dict[self.parent().active_channel][item] in values:
|
|
6025
|
+
output.append(item)
|
|
6026
|
+
return output
|
|
6027
|
+
|
|
6028
|
+
|
|
6029
|
+
def min_value_changed(self):
|
|
5294
6030
|
try:
|
|
5295
|
-
|
|
5296
|
-
|
|
6031
|
+
text = self.min.text()
|
|
6032
|
+
if not text: # If empty, ignore
|
|
6033
|
+
return
|
|
5297
6034
|
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
6035
|
+
try:
|
|
6036
|
+
value = float(text)
|
|
6037
|
+
|
|
6038
|
+
# Bound check against data limits
|
|
6039
|
+
value = max(self.data_min, value)
|
|
6040
|
+
|
|
6041
|
+
# Check against max line
|
|
6042
|
+
max_val = float(self.max.text()) if self.max.text() else self.data_max
|
|
6043
|
+
if value > max_val:
|
|
6044
|
+
# If min would exceed max, set max to its highest possible value
|
|
6045
|
+
self.max.setText(str(round(self.data_max, 2)))
|
|
6046
|
+
self.max_line.set_xdata([self.data_max, self.data_max])
|
|
6047
|
+
# And set min to the previous max value
|
|
6048
|
+
value = max_val
|
|
6049
|
+
self.min.setText(str(round(value, 2)))
|
|
6050
|
+
|
|
6051
|
+
if value == self.prev_min:
|
|
6052
|
+
return
|
|
6053
|
+
else:
|
|
6054
|
+
self.prev_min = value
|
|
6055
|
+
if self.bounds:
|
|
6056
|
+
self.targs = [self.prev_min, self.prev_max]
|
|
6057
|
+
else:
|
|
6058
|
+
self.targs = self.get_values_in_range(self.histo_list, self.prev_min, self.prev_max)
|
|
6059
|
+
self.parent().targs = self.targs
|
|
6060
|
+
if self.preview.isChecked():
|
|
6061
|
+
self.parent().highlight_overlay = None
|
|
6062
|
+
self.parent().create_highlight_overlay_slice(self.targs, bounds = self.bounds)
|
|
6063
|
+
|
|
6064
|
+
# Update the line
|
|
6065
|
+
self.min_line.set_xdata([value, value])
|
|
6066
|
+
self.canvas.draw()
|
|
6067
|
+
|
|
5301
6068
|
|
|
5302
|
-
if self.parent().volume_dict[channel] is None:
|
|
5303
|
-
self.parent().volumes()
|
|
5304
|
-
|
|
5305
|
-
volumes = self.parent().volume_dict[channel]
|
|
5306
|
-
default_max = max(volumes.values())
|
|
5307
|
-
default_min = min(volumes.values())
|
|
5308
6069
|
|
|
5309
|
-
|
|
5310
|
-
|
|
6070
|
+
except ValueError:
|
|
6071
|
+
# If invalid number, reset to current line position
|
|
6072
|
+
self.min.setText(str(round(self.min_line.get_xself.data_mindata()[0], 2)))
|
|
6073
|
+
except:
|
|
6074
|
+
pass
|
|
6075
|
+
|
|
6076
|
+
def max_value_changed(self):
|
|
6077
|
+
try:
|
|
6078
|
+
text = self.max.text()
|
|
6079
|
+
if not text: # If empty, ignore
|
|
6080
|
+
return
|
|
5311
6081
|
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
elif accepted_mode == 1:
|
|
5316
|
-
channel_data = self.parent().channel_data[self.parent().active_channel]
|
|
5317
|
-
default_max = np.max(channel_data)
|
|
5318
|
-
default_min = np.min(channel_data)
|
|
6082
|
+
try:
|
|
6083
|
+
value = float(text)
|
|
5319
6084
|
|
|
5320
|
-
|
|
5321
|
-
|
|
6085
|
+
# Bound check against data limits
|
|
6086
|
+
value = min(self.data_max, value)
|
|
5322
6087
|
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
6088
|
+
# Check against min line
|
|
6089
|
+
min_val = float(self.min.text()) if self.min.text() else self.data_min
|
|
6090
|
+
if value < min_val:
|
|
6091
|
+
# If max would go below min, set min to its lowest possible value
|
|
6092
|
+
self.min.setText(str(round(self.data_min, 2)))
|
|
6093
|
+
self.min_line.set_xdata([self.data_min, self.data_min])
|
|
6094
|
+
# And set max to the previous min value
|
|
6095
|
+
value = min_val
|
|
6096
|
+
self.max.setText(str(round(value, 2)))
|
|
6097
|
+
|
|
6098
|
+
if value == self.prev_max:
|
|
6099
|
+
return
|
|
6100
|
+
else:
|
|
6101
|
+
self.prev_max = value
|
|
6102
|
+
if self.bounds:
|
|
6103
|
+
self.targs = [self.prev_min, self.prev_max]
|
|
6104
|
+
else:
|
|
6105
|
+
self.targs = self.get_values_in_range(self.histo_list, self.prev_min, self.prev_max)
|
|
6106
|
+
self.parent().targs = self.targs
|
|
6107
|
+
if self.preview.isChecked():
|
|
6108
|
+
self.parent().highlight_overlay = None
|
|
6109
|
+
self.parent().create_highlight_overlay_slice(self.targs, bounds = self.bounds)
|
|
6110
|
+
|
|
6111
|
+
# Update the line
|
|
6112
|
+
self.max_line.set_xdata([value, value])
|
|
6113
|
+
self.canvas.draw()
|
|
6114
|
+
|
|
6115
|
+
|
|
6116
|
+
|
|
6117
|
+
|
|
6118
|
+
|
|
6119
|
+
|
|
6120
|
+
except ValueError:
|
|
6121
|
+
# If invalid number, reset to current line position
|
|
6122
|
+
self.max.setText(str(round(self.max_line.get_xdata()[0], 2)))
|
|
6123
|
+
except:
|
|
6124
|
+
pass
|
|
6125
|
+
|
|
6126
|
+
def on_press(self, event):
|
|
6127
|
+
try:
|
|
6128
|
+
if event.inaxes != self.ax:
|
|
6129
|
+
return
|
|
5327
6130
|
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
elif
|
|
5333
|
-
self.
|
|
5334
|
-
|
|
5335
|
-
|
|
6131
|
+
# Left click controls left line
|
|
6132
|
+
if event.button == 1: # Left click
|
|
6133
|
+
self.dragging = 'min'
|
|
6134
|
+
# Right click controls right line
|
|
6135
|
+
elif event.button == 3: # Right click
|
|
6136
|
+
self.dragging = 'max'
|
|
6137
|
+
except:
|
|
6138
|
+
pass
|
|
6139
|
+
|
|
6140
|
+
def on_motion(self, event):
|
|
6141
|
+
try:
|
|
6142
|
+
if not self.dragging or event.inaxes != self.ax:
|
|
6143
|
+
return
|
|
6144
|
+
|
|
6145
|
+
if self.dragging == 'min':
|
|
6146
|
+
if event.xdata < self.max_line.get_xdata()[0]:
|
|
6147
|
+
self.min_line.set_xdata([event.xdata, event.xdata])
|
|
6148
|
+
self.min.setText(str(round(event.xdata, 2)))
|
|
6149
|
+
else:
|
|
6150
|
+
if event.xdata > self.min_line.get_xdata()[0]:
|
|
6151
|
+
self.max_line.set_xdata([event.xdata, event.xdata])
|
|
6152
|
+
self.max.setText(str(round(event.xdata, 2)))
|
|
6153
|
+
|
|
6154
|
+
self.canvas.draw()
|
|
6155
|
+
except:
|
|
6156
|
+
pass
|
|
6157
|
+
|
|
6158
|
+
def on_release(self, event):
|
|
6159
|
+
self.min_value_changed()
|
|
6160
|
+
self.max_value_changed()
|
|
6161
|
+
self.dragging = None
|
|
5336
6162
|
|
|
5337
|
-
|
|
5338
|
-
|
|
6163
|
+
def preview_mode(self):
|
|
6164
|
+
try:
|
|
6165
|
+
preview = self.preview.isChecked()
|
|
6166
|
+
self.parent().preview = preview
|
|
6167
|
+
self.parent().targs = self.targs
|
|
6168
|
+
|
|
6169
|
+
if preview and self.targs is not None:
|
|
6170
|
+
self.parent().create_highlight_overlay_slice(self.parent().targs, bounds = self.bounds)
|
|
6171
|
+
except:
|
|
6172
|
+
pass
|
|
5339
6173
|
|
|
5340
6174
|
def thresh(self):
|
|
5341
6175
|
try:
|
|
5342
6176
|
|
|
5343
|
-
self.
|
|
6177
|
+
if self.parent().active_channel == 0:
|
|
6178
|
+
self.parent().create_highlight_overlay(node_indices = self.targs, bounds = self.bounds)
|
|
6179
|
+
elif self.parent().active_channel == 1:
|
|
6180
|
+
self.parent().create_highlight_overlay(edge_indices = self.targs, bounds = self.bounds)
|
|
6181
|
+
elif self.parent().active_channel == 2:
|
|
6182
|
+
self.parent().create_highlight_overlay(overlay1_indices = self.targs, bounds = self.bounds)
|
|
6183
|
+
elif self.parent().active_channel == 3:
|
|
6184
|
+
self.parent().create_highlight_overlay(overlay2_indices = self.targs, bounds = self.bounds)
|
|
6185
|
+
|
|
5344
6186
|
channel_data = self.parent().channel_data[self.parent().active_channel]
|
|
5345
6187
|
mask = self.parent().highlight_overlay > 0
|
|
5346
6188
|
channel_data = channel_data * mask
|
|
@@ -5349,6 +6191,7 @@ class ThresholdWindow(QMainWindow):
|
|
|
5349
6191
|
self.close()
|
|
5350
6192
|
|
|
5351
6193
|
except Exception as e:
|
|
6194
|
+
|
|
5352
6195
|
QMessageBox.critical(
|
|
5353
6196
|
self,
|
|
5354
6197
|
"Error",
|
|
@@ -5775,9 +6618,17 @@ class WatershedDialog(QDialog):
|
|
|
5775
6618
|
self.directory = QLineEdit()
|
|
5776
6619
|
self.directory.setPlaceholderText("Leave empty for None")
|
|
5777
6620
|
layout.addRow("Output Directory:", self.directory)
|
|
6621
|
+
|
|
6622
|
+
active_shape = self.parent().channel_data[self.parent().active_channel].shape[0]
|
|
6623
|
+
|
|
6624
|
+
if active_shape == 1:
|
|
6625
|
+
self.default = 0.2
|
|
6626
|
+
else:
|
|
6627
|
+
self.default = 0.05
|
|
6628
|
+
|
|
5778
6629
|
|
|
5779
6630
|
# Proportion (default 0.1)
|
|
5780
|
-
self.proportion = QLineEdit("
|
|
6631
|
+
self.proportion = QLineEdit(f"{self.default}")
|
|
5781
6632
|
layout.addRow("Proportion:", self.proportion)
|
|
5782
6633
|
|
|
5783
6634
|
# GPU checkbox (default True)
|
|
@@ -5813,9 +6664,9 @@ class WatershedDialog(QDialog):
|
|
|
5813
6664
|
|
|
5814
6665
|
# Get proportion (0.1 if empty or invalid)
|
|
5815
6666
|
try:
|
|
5816
|
-
proportion = float(self.proportion.text()) if self.proportion.text() else
|
|
6667
|
+
proportion = float(self.proportion.text()) if self.proportion.text() else self.default
|
|
5817
6668
|
except ValueError:
|
|
5818
|
-
proportion =
|
|
6669
|
+
proportion = self.default
|
|
5819
6670
|
|
|
5820
6671
|
# Get GPU state
|
|
5821
6672
|
gpu = self.gpu.isChecked()
|
|
@@ -5842,7 +6693,8 @@ class WatershedDialog(QDialog):
|
|
|
5842
6693
|
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
5843
6694
|
if active_data is None:
|
|
5844
6695
|
raise ValueError("No active image selected")
|
|
5845
|
-
|
|
6696
|
+
|
|
6697
|
+
|
|
5846
6698
|
# Call watershed method with parameters
|
|
5847
6699
|
result = n3d.watershed(
|
|
5848
6700
|
active_data,
|
|
@@ -5865,6 +6717,8 @@ class WatershedDialog(QDialog):
|
|
|
5865
6717
|
self.accept()
|
|
5866
6718
|
|
|
5867
6719
|
except Exception as e:
|
|
6720
|
+
import traceback
|
|
6721
|
+
print(traceback.format_exc())
|
|
5868
6722
|
QMessageBox.critical(
|
|
5869
6723
|
self,
|
|
5870
6724
|
"Error",
|
|
@@ -6119,11 +6973,21 @@ class BranchDialog(QDialog):
|
|
|
6119
6973
|
self.nodes.setChecked(True)
|
|
6120
6974
|
layout.addRow("Generate nodes from edges? (Skip if already completed - presumes your edge skeleton from generate nodes is in Edges and that your original Edges are in Overlay 2):", self.nodes)
|
|
6121
6975
|
|
|
6122
|
-
# GPU checkbox (default
|
|
6976
|
+
# GPU checkbox (default False)
|
|
6123
6977
|
self.GPU = QPushButton("GPU")
|
|
6124
6978
|
self.GPU.setCheckable(True)
|
|
6125
6979
|
self.GPU.setChecked(False)
|
|
6126
|
-
layout.addRow("Use GPU (Note this may need to temporarily downsample your large images which may simplify outputs - Only memory errors but not permission errors for accessing
|
|
6980
|
+
layout.addRow("Use GPU (Note this may need to temporarily downsample your large images which may simplify outputs - Only memory errors but not permission errors for accessing VRAM are handled by default - CPU will never try to downsample):", self.GPU)
|
|
6981
|
+
|
|
6982
|
+
# Branch Fix checkbox (default False)
|
|
6983
|
+
self.fix = QPushButton("Auto-Correct Branches")
|
|
6984
|
+
self.fix.setCheckable(True)
|
|
6985
|
+
self.fix.setChecked(False)
|
|
6986
|
+
layout.addRow("Attempt to auto-correct branch labels:", self.fix)
|
|
6987
|
+
|
|
6988
|
+
self.fix_val = QLineEdit()
|
|
6989
|
+
self.fix_val.setPlaceholderText("Empty = default value...")
|
|
6990
|
+
layout.addRow("If checked above - Avg Degree of Nearby Branch Communities to Merge (Attempt to fix branch labeling - try 4 to 6 to start or leave empty):", self.fix_val)
|
|
6127
6991
|
|
|
6128
6992
|
self.down_factor = QLineEdit("0")
|
|
6129
6993
|
layout.addRow("Internal downsample (will have to recompute nodes)?:", self.down_factor)
|
|
@@ -6151,6 +7015,9 @@ class BranchDialog(QDialog):
|
|
|
6151
7015
|
nodes = self.nodes.isChecked()
|
|
6152
7016
|
GPU = self.GPU.isChecked()
|
|
6153
7017
|
cubic = self.cubic.isChecked()
|
|
7018
|
+
fix = self.fix.isChecked()
|
|
7019
|
+
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
7020
|
+
|
|
6154
7021
|
|
|
6155
7022
|
|
|
6156
7023
|
original_shape = my_network.edges.shape
|
|
@@ -6158,7 +7025,7 @@ class BranchDialog(QDialog):
|
|
|
6158
7025
|
|
|
6159
7026
|
if down_factor > 0:
|
|
6160
7027
|
self.parent().show_gennodes_dialog(down_factor = [down_factor, cubic], called = True)
|
|
6161
|
-
elif nodes:
|
|
7028
|
+
elif nodes or my_network.nodes is None:
|
|
6162
7029
|
self.parent().show_gennodes_dialog(called = True)
|
|
6163
7030
|
down_factor = None
|
|
6164
7031
|
|
|
@@ -6166,6 +7033,21 @@ class BranchDialog(QDialog):
|
|
|
6166
7033
|
|
|
6167
7034
|
output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
|
|
6168
7035
|
|
|
7036
|
+
if fix:
|
|
7037
|
+
|
|
7038
|
+
temp_network = n3d.Network_3D(nodes = output)
|
|
7039
|
+
|
|
7040
|
+
temp_network.morph_proximity(search = 1) #Detect network of nearby branches
|
|
7041
|
+
|
|
7042
|
+
temp_network.community_partition(weighted = False, style = 1, dostats = False) #Find communities with louvain, unweighted params
|
|
7043
|
+
|
|
7044
|
+
targs = n3d.fix_branches(temp_network.nodes, temp_network.network, temp_network.communities, fix_val)
|
|
7045
|
+
|
|
7046
|
+
temp_network.com_to_node(targs)
|
|
7047
|
+
|
|
7048
|
+
output = temp_network.nodes
|
|
7049
|
+
|
|
7050
|
+
|
|
6169
7051
|
if down_factor is not None:
|
|
6170
7052
|
|
|
6171
7053
|
self.parent().reset(nodes = True, id_overlay = True, edges = True)
|
|
@@ -6181,6 +7063,8 @@ class BranchDialog(QDialog):
|
|
|
6181
7063
|
|
|
6182
7064
|
except Exception as e:
|
|
6183
7065
|
print(f"Error labeling branches: {e}")
|
|
7066
|
+
import traceback
|
|
7067
|
+
print(traceback.format_exc())
|
|
6184
7068
|
|
|
6185
7069
|
|
|
6186
7070
|
|
|
@@ -6336,7 +7220,7 @@ class AlterDialog(QDialog):
|
|
|
6336
7220
|
class ModifyDialog(QDialog):
|
|
6337
7221
|
def __init__(self, parent=None):
|
|
6338
7222
|
super().__init__(parent)
|
|
6339
|
-
self.setWindowTitle("
|
|
7223
|
+
self.setWindowTitle("Modify Network Qualities")
|
|
6340
7224
|
self.setModal(True)
|
|
6341
7225
|
layout = QFormLayout(self)
|
|
6342
7226
|
|
|
@@ -6376,6 +7260,12 @@ class ModifyDialog(QDialog):
|
|
|
6376
7260
|
self.isolate.setChecked(False)
|
|
6377
7261
|
layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
|
|
6378
7262
|
|
|
7263
|
+
# Community collapse checkbox (default False)
|
|
7264
|
+
self.comcollapse = QPushButton("Communities -> nodes")
|
|
7265
|
+
self.comcollapse.setCheckable(True)
|
|
7266
|
+
self.comcollapse.setChecked(False)
|
|
7267
|
+
layout.addRow("Convert communities to nodes?:", self.comcollapse)
|
|
7268
|
+
|
|
6379
7269
|
#change button
|
|
6380
7270
|
change_button = QPushButton("Add/Remove Network Pairs")
|
|
6381
7271
|
change_button.clicked.connect(self.show_alter_dialog)
|
|
@@ -6409,6 +7299,8 @@ class ModifyDialog(QDialog):
|
|
|
6409
7299
|
edgeweight = self.edgeweight.isChecked()
|
|
6410
7300
|
prune = self.prune.isChecked()
|
|
6411
7301
|
isolate = self.isolate.isChecked()
|
|
7302
|
+
comcollapse = self.comcollapse.isChecked()
|
|
7303
|
+
|
|
6412
7304
|
|
|
6413
7305
|
if isolate and my_network.node_identities is not None:
|
|
6414
7306
|
self.show_isolate_dialog()
|
|
@@ -6434,6 +7326,15 @@ class ModifyDialog(QDialog):
|
|
|
6434
7326
|
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
6435
7327
|
except:
|
|
6436
7328
|
pass
|
|
7329
|
+
if comcollapse:
|
|
7330
|
+
if my_network.communities is None:
|
|
7331
|
+
self.parent().show_partition_dialog()
|
|
7332
|
+
if my_network.communities is None:
|
|
7333
|
+
return
|
|
7334
|
+
my_network.com_to_node()
|
|
7335
|
+
self.parent().load_channel(0, my_network.nodes, True)
|
|
7336
|
+
my_network.communities = None
|
|
7337
|
+
|
|
6437
7338
|
try:
|
|
6438
7339
|
if hasattr(my_network, 'network_lists'):
|
|
6439
7340
|
model = PandasModel(my_network.network_lists)
|
|
@@ -6552,8 +7453,7 @@ class CentroidDialog(QDialog):
|
|
|
6552
7453
|
"Error",
|
|
6553
7454
|
f"Error finding centroids: {str(e)}"
|
|
6554
7455
|
)
|
|
6555
|
-
|
|
6556
|
-
print(traceback.format_exc())
|
|
7456
|
+
|
|
6557
7457
|
|
|
6558
7458
|
|
|
6559
7459
|
|