nettracer3d 0.8.3__py3-none-any.whl → 0.8.5__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/community_extractor.py +3 -3
- nettracer3d/excelotron.py +21 -2
- nettracer3d/neighborhoods.py +140 -31
- nettracer3d/nettracer.py +516 -82
- nettracer3d/nettracer_gui.py +1072 -842
- nettracer3d/network_analysis.py +90 -29
- nettracer3d/node_draw.py +6 -2
- nettracer3d/painting.py +373 -0
- nettracer3d/proximity.py +52 -103
- nettracer3d/segmenter.py +849 -851
- nettracer3d/segmenter_GPU.py +806 -658
- nettracer3d/smart_dilate.py +44 -10
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/METADATA +6 -3
- nettracer3d-0.8.5.dist-info/RECORD +25 -0
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/licenses/LICENSE +2 -4
- nettracer3d-0.8.3.dist-info/RECORD +0 -24
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.3.dist-info → nettracer3d-0.8.5.dist-info}/top_level.txt +0 -0
nettracer3d/network_analysis.py
CHANGED
|
@@ -136,7 +136,6 @@ def open_network(excel_file_path):
|
|
|
136
136
|
G.add_edge(nodes_a[i], nodes_b[i])
|
|
137
137
|
|
|
138
138
|
return G
|
|
139
|
-
|
|
140
139
|
def read_excel_to_lists(file_path, sheet_name=0):
|
|
141
140
|
"""Convert a pd dataframe to lists. Handles both .xlsx and .csv files"""
|
|
142
141
|
def load_json_to_list(filename):
|
|
@@ -157,6 +156,61 @@ def read_excel_to_lists(file_path, sheet_name=0):
|
|
|
157
156
|
converted_data[k] = v
|
|
158
157
|
|
|
159
158
|
return converted_data
|
|
159
|
+
|
|
160
|
+
if type(file_path) == str:
|
|
161
|
+
# Check file extension
|
|
162
|
+
if file_path.lower().endswith('.xlsx'):
|
|
163
|
+
# Read the Excel file with headers (since your new save method includes them)
|
|
164
|
+
df = pd.read_excel(file_path, sheet_name=sheet_name)
|
|
165
|
+
elif file_path.lower().endswith('.csv'):
|
|
166
|
+
# Read the CSV file with headers and specify dtype to avoid the warning
|
|
167
|
+
df = pd.read_csv(file_path, dtype=str, low_memory=False)
|
|
168
|
+
elif file_path.lower().endswith('.json'):
|
|
169
|
+
df = load_json_to_list(file_path)
|
|
170
|
+
return df
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError("File must be either .xlsx, .csv, or .json format")
|
|
173
|
+
else:
|
|
174
|
+
df = file_path
|
|
175
|
+
|
|
176
|
+
# Initialize an empty list to store the lists of values
|
|
177
|
+
data_lists = []
|
|
178
|
+
# Iterate over each column in the DataFrame
|
|
179
|
+
for column_name, column_data in df.items():
|
|
180
|
+
# Convert the column values to a list and append to the data_lists
|
|
181
|
+
data_lists.append(column_data.tolist())
|
|
182
|
+
|
|
183
|
+
master_list = [[], [], []]
|
|
184
|
+
for i in range(0, len(data_lists), 3):
|
|
185
|
+
master_list[0].extend([int(x) for x in data_lists[i]])
|
|
186
|
+
master_list[1].extend([int(x) for x in data_lists[i+1]])
|
|
187
|
+
try:
|
|
188
|
+
master_list[2].extend([int(x) for x in data_lists[i+2]])
|
|
189
|
+
except IndexError:
|
|
190
|
+
master_list[2].extend([0]) # Note: Changed to list with single int 0
|
|
191
|
+
|
|
192
|
+
return master_list
|
|
193
|
+
|
|
194
|
+
def read_excel_to_lists_old(file_path, sheet_name=0):
|
|
195
|
+
"""Convert a pd dataframe to lists. Handles both .xlsx and .csv files"""
|
|
196
|
+
def load_json_to_list(filename):
|
|
197
|
+
with open(filename, 'r') as f:
|
|
198
|
+
data = json.load(f)
|
|
199
|
+
|
|
200
|
+
# Convert only numeric strings to integers, leave other strings as is
|
|
201
|
+
converted_data = [[],[],[]]
|
|
202
|
+
for i in data[0]:
|
|
203
|
+
try:
|
|
204
|
+
converted_data[0].append(int(data[0][i]))
|
|
205
|
+
converted_data[1].append(int(data[1][i]))
|
|
206
|
+
try:
|
|
207
|
+
converted_data[2].append(int(data[2][i]))
|
|
208
|
+
except IndexError:
|
|
209
|
+
converted_data[2].append(0)
|
|
210
|
+
except ValueError:
|
|
211
|
+
converted_data[k] = v
|
|
212
|
+
|
|
213
|
+
return converted_data
|
|
160
214
|
|
|
161
215
|
if type(file_path) == str:
|
|
162
216
|
# Check file extension
|
|
@@ -545,42 +599,51 @@ def _find_centroids_old(nodes, node_list = None, down_factor = None):
|
|
|
545
599
|
|
|
546
600
|
return centroid_dict
|
|
547
601
|
|
|
602
|
+
|
|
548
603
|
def _find_centroids(nodes, node_list=None, down_factor=None):
|
|
549
604
|
"""Internal use version to get centroids without saving"""
|
|
550
|
-
|
|
551
|
-
"""
|
|
552
|
-
Finds indices of labelled object in array and adjusts for the Y-offset.
|
|
553
|
-
"""
|
|
554
|
-
indices = np.argwhere(binary_stack == label)
|
|
555
|
-
# Adjust the Y coordinate by the y_offset
|
|
556
|
-
indices[:, 1] += y_offset
|
|
557
|
-
return indices
|
|
605
|
+
|
|
558
606
|
|
|
559
607
|
def compute_indices_in_chunk(chunk, y_offset):
|
|
560
608
|
"""
|
|
561
|
-
|
|
562
|
-
Adjust Y-coordinate based on the y_offset for each chunk.
|
|
609
|
+
Alternative approach using np.where for even better performance on sparse arrays.
|
|
563
610
|
"""
|
|
564
611
|
indices_dict_chunk = {}
|
|
565
|
-
label_list = np.unique(chunk)
|
|
566
|
-
try:
|
|
567
|
-
if label_list[0] == 0:
|
|
568
|
-
label_list = np.delete(label_list, 0)
|
|
569
|
-
except:
|
|
570
|
-
pass
|
|
571
612
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
613
|
+
# Get all coordinates where chunk is non-zero
|
|
614
|
+
z_coords, y_coords, x_coords = np.where(chunk != 0)
|
|
615
|
+
|
|
616
|
+
if len(z_coords) == 0:
|
|
617
|
+
return indices_dict_chunk
|
|
618
|
+
|
|
619
|
+
# Adjust Y coordinates
|
|
620
|
+
y_coords_adjusted = y_coords + y_offset
|
|
621
|
+
|
|
622
|
+
# Get labels at these coordinates
|
|
623
|
+
labels = chunk[z_coords, y_coords, x_coords]
|
|
624
|
+
|
|
625
|
+
# Group by unique labels
|
|
626
|
+
unique_labels = np.unique(labels)
|
|
627
|
+
|
|
628
|
+
for label in unique_labels:
|
|
629
|
+
if label == 0: # Skip background
|
|
630
|
+
continue
|
|
631
|
+
mask = (labels == label)
|
|
632
|
+
# Stack coordinates into the expected format [z, y, x]
|
|
633
|
+
indices_dict_chunk[label] = np.column_stack((
|
|
634
|
+
z_coords[mask],
|
|
635
|
+
y_coords_adjusted[mask],
|
|
636
|
+
x_coords[mask]
|
|
637
|
+
))
|
|
638
|
+
|
|
575
639
|
return indices_dict_chunk
|
|
576
640
|
|
|
577
641
|
def chunk_3d_array(array, num_chunks):
|
|
578
|
-
"""
|
|
579
|
-
Split the 3D array into smaller chunks along the y-axis.
|
|
580
|
-
"""
|
|
642
|
+
"""Split the 3D array into smaller chunks along the y-axis."""
|
|
581
643
|
y_slices = np.array_split(array, num_chunks, axis=1)
|
|
582
644
|
return y_slices
|
|
583
645
|
|
|
646
|
+
# Handle input processing
|
|
584
647
|
if isinstance(nodes, str): # Open into numpy array if filepath
|
|
585
648
|
nodes = tifffile.imread(nodes)
|
|
586
649
|
if len(np.unique(nodes)) == 2: # Label if binary
|
|
@@ -595,14 +658,14 @@ def _find_centroids(nodes, node_list=None, down_factor=None):
|
|
|
595
658
|
indices_dict = {}
|
|
596
659
|
num_cpus = mp.cpu_count()
|
|
597
660
|
|
|
598
|
-
# Chunk the 3D array along the y-axis
|
|
661
|
+
# Chunk the 3D array along the y-axis
|
|
599
662
|
node_chunks = chunk_3d_array(nodes, num_cpus)
|
|
600
663
|
|
|
601
664
|
# Calculate Y offset for each chunk
|
|
602
665
|
chunk_sizes = [chunk.shape[1] for chunk in node_chunks]
|
|
603
666
|
y_offsets = np.cumsum([0] + chunk_sizes[:-1])
|
|
604
667
|
|
|
605
|
-
# Parallel computation
|
|
668
|
+
# Parallel computation using the optimized single-pass approach
|
|
606
669
|
with ThreadPoolExecutor(max_workers=num_cpus) as executor:
|
|
607
670
|
futures = {executor.submit(compute_indices_in_chunk, chunk, y_offset): chunk_id
|
|
608
671
|
for chunk_id, (chunk, y_offset) in enumerate(zip(node_chunks, y_offsets))}
|
|
@@ -622,10 +685,8 @@ def _find_centroids(nodes, node_list=None, down_factor=None):
|
|
|
622
685
|
centroid = np.round(np.mean(indices, axis=0)).astype(int)
|
|
623
686
|
centroid_dict[label] = centroid
|
|
624
687
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
except:
|
|
628
|
-
pass
|
|
688
|
+
# Remove background label if it exists
|
|
689
|
+
centroid_dict.pop(0, None)
|
|
629
690
|
|
|
630
691
|
return centroid_dict
|
|
631
692
|
|
nettracer3d/node_draw.py
CHANGED
|
@@ -200,8 +200,12 @@ def degree_draw(degree_dict, centroid_dict, nodes):
|
|
|
200
200
|
|
|
201
201
|
return draw_array
|
|
202
202
|
|
|
203
|
-
def degree_infect(degree_dict, nodes):
|
|
204
|
-
|
|
203
|
+
def degree_infect(degree_dict, nodes, make_floats = False):
|
|
204
|
+
|
|
205
|
+
if not make_floats:
|
|
206
|
+
return_nodes = np.zeros_like(nodes) # Start with all zeros
|
|
207
|
+
else:
|
|
208
|
+
return_nodes = np.zeros(nodes.shape, dtype=np.float32)
|
|
205
209
|
|
|
206
210
|
if not degree_dict: # Handle empty dict
|
|
207
211
|
return return_nodes
|
nettracer3d/painting.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import QApplication, QMainWindow
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import copy
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PaintManager(QMainWindow):
|
|
8
|
+
def __init__(self, parent = None):
|
|
9
|
+
super().__init__(parent)
|
|
10
|
+
|
|
11
|
+
def get_line_points(self, x0, y0, x1, y1):
|
|
12
|
+
"""Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm."""
|
|
13
|
+
points = []
|
|
14
|
+
dx = abs(x1 - x0)
|
|
15
|
+
dy = abs(y1 - y0)
|
|
16
|
+
x, y = x0, y0
|
|
17
|
+
sx = 1 if x0 < x1 else -1
|
|
18
|
+
sy = 1 if y0 < y1 else -1
|
|
19
|
+
|
|
20
|
+
if dx > dy:
|
|
21
|
+
err = dx / 2.0
|
|
22
|
+
while x != x1:
|
|
23
|
+
points.append((x, y))
|
|
24
|
+
err -= dy
|
|
25
|
+
if err < 0:
|
|
26
|
+
y += sy
|
|
27
|
+
err += dx
|
|
28
|
+
x += sx
|
|
29
|
+
else:
|
|
30
|
+
err = dy / 2.0
|
|
31
|
+
while y != y1:
|
|
32
|
+
points.append((x, y))
|
|
33
|
+
err -= dx
|
|
34
|
+
if err < 0:
|
|
35
|
+
x += sx
|
|
36
|
+
err += dy
|
|
37
|
+
y += sy
|
|
38
|
+
|
|
39
|
+
points.append((x, y))
|
|
40
|
+
return points
|
|
41
|
+
|
|
42
|
+
def initiate_paint_session(self, channel, current_xlim, current_ylim):
|
|
43
|
+
# Create static background (same as selection rectangle)
|
|
44
|
+
|
|
45
|
+
if self.parent().machine_window is not None:
|
|
46
|
+
if self.parent().machine_window.segmentation_worker is not None:
|
|
47
|
+
# Instead of just pausing, completely stop and clean up the worker
|
|
48
|
+
self.parent().machine_window.segmentation_worker.pause()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if not self.parent().channel_visible[channel]:
|
|
52
|
+
self.parent().channel_visible[channel] = True
|
|
53
|
+
|
|
54
|
+
# Capture the background once
|
|
55
|
+
self.parent().static_background = self.parent().canvas.copy_from_bbox(self.parent().ax.bbox)
|
|
56
|
+
|
|
57
|
+
if self.parent().machine_window is not None:
|
|
58
|
+
if self.parent().machine_window.segmentation_worker is not None:
|
|
59
|
+
# Instead of just pausing, completely stop and clean up the worker
|
|
60
|
+
self.parent().machine_window.segmentation_worker.resume()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def start_virtual_paint_session(self, channel, current_xlim, current_ylim):
|
|
65
|
+
"""Start a virtual paint session that doesn't modify arrays until the end."""
|
|
66
|
+
self.parent().painting = True
|
|
67
|
+
self.parent().paint_channel = channel
|
|
68
|
+
|
|
69
|
+
# Store original state
|
|
70
|
+
if not self.parent().channel_visible[channel]:
|
|
71
|
+
self.parent().channel_visible[channel] = True
|
|
72
|
+
|
|
73
|
+
# Initialize virtual paint storage - separate draw and erase operations
|
|
74
|
+
self.parent().virtual_draw_operations = [] # Stores drawing operations
|
|
75
|
+
self.parent().virtual_erase_operations = [] # Stores erase operations
|
|
76
|
+
self.parent().current_operation = []
|
|
77
|
+
self.parent().current_operation_type = None # 'draw' or 'erase'
|
|
78
|
+
|
|
79
|
+
def add_virtual_paint_point(self, x, y, brush_size, erase=False, foreground=True):
|
|
80
|
+
"""Add a single paint point to the virtual layer."""
|
|
81
|
+
|
|
82
|
+
# Determine operation type and visual properties
|
|
83
|
+
if erase:
|
|
84
|
+
paint_color = 'black' # Visual indicator for erase
|
|
85
|
+
alpha = 0.5
|
|
86
|
+
operation_type = 'erase'
|
|
87
|
+
else:
|
|
88
|
+
if self.parent().machine_window is not None:
|
|
89
|
+
if foreground:
|
|
90
|
+
paint_color = 'green' # Visual for foreground (value 1)
|
|
91
|
+
alpha = 0.7
|
|
92
|
+
else:
|
|
93
|
+
paint_color = 'red' # Visual for background (value 2)
|
|
94
|
+
alpha = 0.7
|
|
95
|
+
else:
|
|
96
|
+
paint_color = 'white' # Normal paint
|
|
97
|
+
alpha = 0.7
|
|
98
|
+
operation_type = 'draw'
|
|
99
|
+
|
|
100
|
+
# Store the operation data (for later conversion to real paint)
|
|
101
|
+
operation_data = {
|
|
102
|
+
'x': x,
|
|
103
|
+
'y': y,
|
|
104
|
+
'brush_size': brush_size,
|
|
105
|
+
'erase': erase,
|
|
106
|
+
'foreground': foreground,
|
|
107
|
+
'channel': self.parent().paint_channel,
|
|
108
|
+
'threed': getattr(self.parent(), 'threed', False),
|
|
109
|
+
'threedthresh': getattr(self.parent(), 'threedthresh', 1)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Create visual circle
|
|
113
|
+
circle = plt.Circle((x, y), brush_size/2,
|
|
114
|
+
color=paint_color, alpha=alpha, animated=True)
|
|
115
|
+
|
|
116
|
+
# Add to current operation
|
|
117
|
+
if self.parent().current_operation_type != operation_type:
|
|
118
|
+
# Finish previous operation if switching between draw/erase
|
|
119
|
+
self.finish_current_virtual_operation()
|
|
120
|
+
self.parent().current_operation_type = operation_type
|
|
121
|
+
|
|
122
|
+
self.parent().current_operation.append({
|
|
123
|
+
'circle': circle,
|
|
124
|
+
'data': operation_data
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
self.parent().ax.add_patch(circle)
|
|
128
|
+
|
|
129
|
+
def add_virtual_paint_stroke(self, x, y, brush_size, erase=False, foreground=True):
|
|
130
|
+
"""Add a paint stroke - simple visual, interpolation happens during data conversion."""
|
|
131
|
+
# Just add the current point for visual display (no interpolation yet)
|
|
132
|
+
self.add_virtual_paint_point(x, y, brush_size, erase, foreground)
|
|
133
|
+
|
|
134
|
+
# Store the last position for data conversion later
|
|
135
|
+
self.parent().last_virtual_pos = (x, y)
|
|
136
|
+
|
|
137
|
+
def finish_current_virtual_operation(self):
|
|
138
|
+
"""Finish the current operation (draw or erase) and add it to the appropriate list."""
|
|
139
|
+
|
|
140
|
+
if not self.parent().current_operation:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
if self.parent().current_operation_type == 'draw':
|
|
144
|
+
self.parent().virtual_draw_operations.append(self.parent().current_operation)
|
|
145
|
+
elif self.parent().current_operation_type == 'erase':
|
|
146
|
+
self.parent().virtual_erase_operations.append(self.parent().current_operation)
|
|
147
|
+
|
|
148
|
+
self.parent().current_operation = []
|
|
149
|
+
self.parent().current_operation_type = None
|
|
150
|
+
|
|
151
|
+
def update_virtual_paint_display(self):
|
|
152
|
+
"""Update display with virtual paint strokes - super fast like selection rectangle."""
|
|
153
|
+
if not hasattr(self.parent(), 'static_background') or self.parent().static_background is None:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Restore the clean background
|
|
157
|
+
self.parent().canvas.restore_region(self.parent().static_background)
|
|
158
|
+
|
|
159
|
+
# Draw all completed operations
|
|
160
|
+
for operation_list in [self.parent().virtual_draw_operations, self.parent().virtual_erase_operations]:
|
|
161
|
+
for operation in operation_list:
|
|
162
|
+
for item in operation:
|
|
163
|
+
self.parent().ax.draw_artist(item['circle'])
|
|
164
|
+
|
|
165
|
+
# Draw current operation being painted
|
|
166
|
+
if hasattr(self.parent(), 'current_operation'):
|
|
167
|
+
for item in self.parent().current_operation:
|
|
168
|
+
self.parent().ax.draw_artist(item['circle'])
|
|
169
|
+
|
|
170
|
+
# Blit everything at once
|
|
171
|
+
self.parent().canvas.blit(self.parent().ax.bbox)
|
|
172
|
+
|
|
173
|
+
def convert_virtual_strokes_to_data(self):
|
|
174
|
+
"""Convert virtual paint strokes to actual array data with interpolation applied here."""
|
|
175
|
+
|
|
176
|
+
# First, apply all drawing operations with interpolation
|
|
177
|
+
for operation in self.parent().virtual_draw_operations:
|
|
178
|
+
last_pos = None
|
|
179
|
+
for item in operation:
|
|
180
|
+
data = item['data']
|
|
181
|
+
current_pos = (data['x'], data['y'])
|
|
182
|
+
|
|
183
|
+
if last_pos is not None:
|
|
184
|
+
points = self.get_line_points(last_pos[0], last_pos[1], current_pos[0], current_pos[1])
|
|
185
|
+
for px, py in points:
|
|
186
|
+
self.paint_at_position_vectorized(
|
|
187
|
+
px, py,
|
|
188
|
+
erase=False,
|
|
189
|
+
channel=data['channel'],
|
|
190
|
+
brush_size=data['brush_size'],
|
|
191
|
+
threed=data['threed'], # Add this
|
|
192
|
+
threedthresh=data['threedthresh'], # Add this
|
|
193
|
+
foreground=data['foreground'],
|
|
194
|
+
machine_window=self.parent().machine_window
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
self.paint_at_position_vectorized(
|
|
198
|
+
data['x'], data['y'],
|
|
199
|
+
erase=False,
|
|
200
|
+
channel=data['channel'],
|
|
201
|
+
brush_size=data['brush_size'],
|
|
202
|
+
threed=data['threed'], # Add this
|
|
203
|
+
threedthresh=data['threedthresh'], # Add this
|
|
204
|
+
foreground=data['foreground'],
|
|
205
|
+
machine_window=self.parent().machine_window
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
last_pos = current_pos
|
|
209
|
+
try:
|
|
210
|
+
item['circle'].remove()
|
|
211
|
+
except:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# Then, apply all erase operations with interpolation (same changes)
|
|
215
|
+
for operation in self.parent().virtual_erase_operations:
|
|
216
|
+
last_pos = None
|
|
217
|
+
for item in operation:
|
|
218
|
+
data = item['data']
|
|
219
|
+
current_pos = (data['x'], data['y'])
|
|
220
|
+
|
|
221
|
+
if last_pos is not None:
|
|
222
|
+
points = self.get_line_points(last_pos[0], last_pos[1], current_pos[0], current_pos[1])
|
|
223
|
+
for px, py in points:
|
|
224
|
+
self.paint_at_position_vectorized(
|
|
225
|
+
px, py,
|
|
226
|
+
erase=True,
|
|
227
|
+
channel=data['channel'],
|
|
228
|
+
brush_size=data['brush_size'],
|
|
229
|
+
threed=data['threed'], # Add this
|
|
230
|
+
threedthresh=data['threedthresh'], # Add this
|
|
231
|
+
foreground=data['foreground'],
|
|
232
|
+
machine_window=self.parent().machine_window
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
self.paint_at_position_vectorized(
|
|
236
|
+
data['x'], data['y'],
|
|
237
|
+
erase=True,
|
|
238
|
+
channel=data['channel'],
|
|
239
|
+
brush_size=data['brush_size'],
|
|
240
|
+
threed=data['threed'], # Add this
|
|
241
|
+
threedthresh=data['threedthresh'], # Add this
|
|
242
|
+
foreground=data['foreground'],
|
|
243
|
+
machine_window=self.parent().machine_window
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
last_pos = current_pos
|
|
247
|
+
try:
|
|
248
|
+
item['circle'].remove()
|
|
249
|
+
except:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
# Clean up
|
|
253
|
+
self.parent().virtual_draw_operations = []
|
|
254
|
+
self.parent().virtual_erase_operations = []
|
|
255
|
+
if hasattr(self.parent(), 'current_operation'):
|
|
256
|
+
for item in self.parent().current_operation:
|
|
257
|
+
try:
|
|
258
|
+
item['circle'].remove()
|
|
259
|
+
except:
|
|
260
|
+
pass
|
|
261
|
+
self.parent().current_operation = []
|
|
262
|
+
self.parent().current_operation_type = None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def end_virtual_paint_session(self):
|
|
266
|
+
"""Convert virtual paint to actual array modifications when exiting paint mode."""
|
|
267
|
+
if not hasattr(self.parent(), 'virtual_paint_strokes'):
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
# Now apply all the virtual strokes to the actual arrays
|
|
271
|
+
for stroke in self.parent().virtual_paint_strokes:
|
|
272
|
+
for circle in stroke:
|
|
273
|
+
center = circle.center
|
|
274
|
+
radius = circle.radius
|
|
275
|
+
is_erase = circle.get_facecolor()[0] == 0 # Black = erase
|
|
276
|
+
|
|
277
|
+
# Apply to actual array
|
|
278
|
+
self.paint_at_position_vectorized(
|
|
279
|
+
int(center[0]), int(center[1]),
|
|
280
|
+
erase=is_erase,
|
|
281
|
+
channel=self.paint_channel,
|
|
282
|
+
brush_size=int(radius * 2)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Remove the virtual circle
|
|
286
|
+
circle.remove()
|
|
287
|
+
|
|
288
|
+
# Clean up virtual paint data
|
|
289
|
+
self.virtual_paint_strokes = []
|
|
290
|
+
self.current_stroke = []
|
|
291
|
+
|
|
292
|
+
# Reset background
|
|
293
|
+
self.static_background = None
|
|
294
|
+
self.painting = False
|
|
295
|
+
|
|
296
|
+
# Full refresh to show final result
|
|
297
|
+
self.update_display()
|
|
298
|
+
|
|
299
|
+
def paint_at_position_vectorized(self, center_x, center_y, erase=False, channel=2,
|
|
300
|
+
slice_idx=None, brush_size=None, threed=None,
|
|
301
|
+
threedthresh=None, foreground=True, machine_window=None):
|
|
302
|
+
"""Vectorized paint operation for better performance."""
|
|
303
|
+
if self.parent().channel_data[channel] is None:
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
# Use provided parameters or fall back to instance variables
|
|
307
|
+
slice_idx = slice_idx if slice_idx is not None else self.parent().current_slice
|
|
308
|
+
brush_size = brush_size if brush_size is not None else getattr(self.parent(), 'brush_size', 5)
|
|
309
|
+
threed = threed if threed is not None else getattr(self.parent(), 'threed', False)
|
|
310
|
+
threedthresh = threedthresh if threedthresh is not None else getattr(self.parent(), 'threedthresh', 1)
|
|
311
|
+
|
|
312
|
+
# Handle 3D painting by recursively calling for each slice
|
|
313
|
+
if threed and threedthresh > 1:
|
|
314
|
+
half_range = (threedthresh - 1) // 2
|
|
315
|
+
low = max(0, slice_idx - half_range)
|
|
316
|
+
high = min(self.parent().channel_data[channel].shape[0] - 1, slice_idx + half_range)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
for i in range(low, high + 1):
|
|
320
|
+
|
|
321
|
+
# Recursive call for each slice, but with threed=False to avoid infinite recursion
|
|
322
|
+
self.paint_at_position_vectorized(
|
|
323
|
+
center_x, center_y,
|
|
324
|
+
erase=erase,
|
|
325
|
+
channel=channel,
|
|
326
|
+
slice_idx=i, # Paint on slice i
|
|
327
|
+
brush_size=brush_size,
|
|
328
|
+
threed=False, # Important: turn off 3D for recursive calls
|
|
329
|
+
threedthresh=1,
|
|
330
|
+
foreground=foreground,
|
|
331
|
+
machine_window=machine_window
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
return # Exit early, recursive calls handle everything
|
|
336
|
+
|
|
337
|
+
# Regular 2D painting (single slice)
|
|
338
|
+
|
|
339
|
+
# Determine paint value
|
|
340
|
+
if erase:
|
|
341
|
+
val = 0
|
|
342
|
+
elif machine_window is None:
|
|
343
|
+
try:
|
|
344
|
+
val = self.parent().min_max[channel][1]
|
|
345
|
+
except:
|
|
346
|
+
val = 255
|
|
347
|
+
elif foreground:
|
|
348
|
+
val = 1
|
|
349
|
+
else:
|
|
350
|
+
val = 2
|
|
351
|
+
|
|
352
|
+
height, width = self.parent().channel_data[channel][slice_idx].shape
|
|
353
|
+
radius = brush_size // 2
|
|
354
|
+
|
|
355
|
+
# Calculate affected region bounds
|
|
356
|
+
y_min = max(0, center_y - radius)
|
|
357
|
+
y_max = min(height, center_y + radius + 1)
|
|
358
|
+
x_min = max(0, center_x - radius)
|
|
359
|
+
x_max = min(width, center_x + radius + 1)
|
|
360
|
+
|
|
361
|
+
if y_min >= y_max or x_min >= x_max:
|
|
362
|
+
return # No valid region to paint
|
|
363
|
+
|
|
364
|
+
# Create coordinate grids for the affected region
|
|
365
|
+
y_coords, x_coords = np.mgrid[y_min:y_max, x_min:x_max]
|
|
366
|
+
|
|
367
|
+
# Calculate distances squared (avoid sqrt for performance)
|
|
368
|
+
distances_sq = (x_coords - center_x) ** 2 + (y_coords - center_y) ** 2
|
|
369
|
+
mask = distances_sq <= radius ** 2
|
|
370
|
+
|
|
371
|
+
# Paint on this single slice
|
|
372
|
+
|
|
373
|
+
self.parent().channel_data[channel][slice_idx][y_min:y_max, x_min:x_max][mask] = val
|