nettracer3d 0.8.4__py3-none-any.whl → 0.8.6__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.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- nettracer3d/community_extractor.py +3 -2
- nettracer3d/neighborhoods.py +140 -31
- nettracer3d/nettracer.py +10 -3
- nettracer3d/nettracer_gui.py +496 -706
- nettracer3d/painting.py +375 -0
- nettracer3d/proximity.py +2 -2
- nettracer3d/segmenter.py +849 -851
- nettracer3d/segmenter_GPU.py +806 -658
- nettracer3d/smart_dilate.py +2 -2
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/METADATA +5 -2
- nettracer3d-0.8.6.dist-info/RECORD +25 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/licenses/LICENSE +2 -4
- nettracer3d-0.8.4.dist-info/RECORD +0 -24
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.6.dist-info}/top_level.txt +0 -0
nettracer3d/painting.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
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
|
+
self.resume = False
|
|
12
|
+
|
|
13
|
+
def get_line_points(self, x0, y0, x1, y1):
|
|
14
|
+
"""Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm."""
|
|
15
|
+
points = []
|
|
16
|
+
dx = abs(x1 - x0)
|
|
17
|
+
dy = abs(y1 - y0)
|
|
18
|
+
x, y = x0, y0
|
|
19
|
+
sx = 1 if x0 < x1 else -1
|
|
20
|
+
sy = 1 if y0 < y1 else -1
|
|
21
|
+
|
|
22
|
+
if dx > dy:
|
|
23
|
+
err = dx / 2.0
|
|
24
|
+
while x != x1:
|
|
25
|
+
points.append((x, y))
|
|
26
|
+
err -= dy
|
|
27
|
+
if err < 0:
|
|
28
|
+
y += sy
|
|
29
|
+
err += dx
|
|
30
|
+
x += sx
|
|
31
|
+
else:
|
|
32
|
+
err = dy / 2.0
|
|
33
|
+
while y != y1:
|
|
34
|
+
points.append((x, y))
|
|
35
|
+
err -= dx
|
|
36
|
+
if err < 0:
|
|
37
|
+
x += sx
|
|
38
|
+
err += dy
|
|
39
|
+
y += sy
|
|
40
|
+
|
|
41
|
+
points.append((x, y))
|
|
42
|
+
return points
|
|
43
|
+
|
|
44
|
+
def initiate_paint_session(self, channel, current_xlim, current_ylim):
|
|
45
|
+
# Create static background (same as selection rectangle)
|
|
46
|
+
|
|
47
|
+
if self.parent().machine_window is not None:
|
|
48
|
+
if self.parent().machine_window.segmentation_worker is not None:
|
|
49
|
+
if not self.parent().machine_window.segmentation_worker._paused:
|
|
50
|
+
self.resume = True
|
|
51
|
+
self.parent().machine_window.segmentation_worker.pause()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if not self.parent().channel_visible[channel]:
|
|
55
|
+
self.parent().channel_visible[channel] = True
|
|
56
|
+
|
|
57
|
+
# Capture the background once
|
|
58
|
+
self.parent().static_background = self.parent().canvas.copy_from_bbox(self.parent().ax.bbox)
|
|
59
|
+
|
|
60
|
+
if self.resume:
|
|
61
|
+
self.parent().machine_window.segmentation_worker.resume()
|
|
62
|
+
self.resume = False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def start_virtual_paint_session(self, channel, current_xlim, current_ylim):
|
|
67
|
+
"""Start a virtual paint session that doesn't modify arrays until the end."""
|
|
68
|
+
self.parent().painting = True
|
|
69
|
+
self.parent().paint_channel = channel
|
|
70
|
+
|
|
71
|
+
# Store original state
|
|
72
|
+
if not self.parent().channel_visible[channel]:
|
|
73
|
+
self.parent().channel_visible[channel] = True
|
|
74
|
+
|
|
75
|
+
# Initialize virtual paint storage - separate draw and erase operations
|
|
76
|
+
self.parent().virtual_draw_operations = [] # Stores drawing operations
|
|
77
|
+
self.parent().virtual_erase_operations = [] # Stores erase operations
|
|
78
|
+
self.parent().current_operation = []
|
|
79
|
+
self.parent().current_operation_type = None # 'draw' or 'erase'
|
|
80
|
+
|
|
81
|
+
def add_virtual_paint_point(self, x, y, brush_size, erase=False, foreground=True):
|
|
82
|
+
"""Add a single paint point to the virtual layer."""
|
|
83
|
+
|
|
84
|
+
# Determine operation type and visual properties
|
|
85
|
+
if erase:
|
|
86
|
+
paint_color = 'black' # Visual indicator for erase
|
|
87
|
+
alpha = 0.5
|
|
88
|
+
operation_type = 'erase'
|
|
89
|
+
else:
|
|
90
|
+
if self.parent().machine_window is not None:
|
|
91
|
+
if foreground:
|
|
92
|
+
paint_color = 'green' # Visual for foreground (value 1)
|
|
93
|
+
alpha = 0.7
|
|
94
|
+
else:
|
|
95
|
+
paint_color = 'red' # Visual for background (value 2)
|
|
96
|
+
alpha = 0.7
|
|
97
|
+
else:
|
|
98
|
+
paint_color = 'white' # Normal paint
|
|
99
|
+
alpha = 0.7
|
|
100
|
+
operation_type = 'draw'
|
|
101
|
+
|
|
102
|
+
# Store the operation data (for later conversion to real paint)
|
|
103
|
+
operation_data = {
|
|
104
|
+
'x': x,
|
|
105
|
+
'y': y,
|
|
106
|
+
'brush_size': brush_size,
|
|
107
|
+
'erase': erase,
|
|
108
|
+
'foreground': foreground,
|
|
109
|
+
'channel': self.parent().paint_channel,
|
|
110
|
+
'threed': getattr(self.parent(), 'threed', False),
|
|
111
|
+
'threedthresh': getattr(self.parent(), 'threedthresh', 1)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Create visual circle
|
|
115
|
+
circle = plt.Circle((x, y), brush_size/2,
|
|
116
|
+
color=paint_color, alpha=alpha, animated=True)
|
|
117
|
+
|
|
118
|
+
# Add to current operation
|
|
119
|
+
if self.parent().current_operation_type != operation_type:
|
|
120
|
+
# Finish previous operation if switching between draw/erase
|
|
121
|
+
self.finish_current_virtual_operation()
|
|
122
|
+
self.parent().current_operation_type = operation_type
|
|
123
|
+
|
|
124
|
+
self.parent().current_operation.append({
|
|
125
|
+
'circle': circle,
|
|
126
|
+
'data': operation_data
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
self.parent().ax.add_patch(circle)
|
|
130
|
+
|
|
131
|
+
def add_virtual_paint_stroke(self, x, y, brush_size, erase=False, foreground=True):
|
|
132
|
+
"""Add a paint stroke - simple visual, interpolation happens during data conversion."""
|
|
133
|
+
# Just add the current point for visual display (no interpolation yet)
|
|
134
|
+
self.add_virtual_paint_point(x, y, brush_size, erase, foreground)
|
|
135
|
+
|
|
136
|
+
# Store the last position for data conversion later
|
|
137
|
+
self.parent().last_virtual_pos = (x, y)
|
|
138
|
+
|
|
139
|
+
def finish_current_virtual_operation(self):
|
|
140
|
+
"""Finish the current operation (draw or erase) and add it to the appropriate list."""
|
|
141
|
+
|
|
142
|
+
if not self.parent().current_operation:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
if self.parent().current_operation_type == 'draw':
|
|
146
|
+
self.parent().virtual_draw_operations.append(self.parent().current_operation)
|
|
147
|
+
elif self.parent().current_operation_type == 'erase':
|
|
148
|
+
self.parent().virtual_erase_operations.append(self.parent().current_operation)
|
|
149
|
+
|
|
150
|
+
self.parent().current_operation = []
|
|
151
|
+
self.parent().current_operation_type = None
|
|
152
|
+
|
|
153
|
+
def update_virtual_paint_display(self):
|
|
154
|
+
"""Update display with virtual paint strokes - super fast like selection rectangle."""
|
|
155
|
+
if not hasattr(self.parent(), 'static_background') or self.parent().static_background is None:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# Restore the clean background
|
|
159
|
+
self.parent().canvas.restore_region(self.parent().static_background)
|
|
160
|
+
|
|
161
|
+
# Draw all completed operations
|
|
162
|
+
for operation_list in [self.parent().virtual_draw_operations, self.parent().virtual_erase_operations]:
|
|
163
|
+
for operation in operation_list:
|
|
164
|
+
for item in operation:
|
|
165
|
+
self.parent().ax.draw_artist(item['circle'])
|
|
166
|
+
|
|
167
|
+
# Draw current operation being painted
|
|
168
|
+
if hasattr(self.parent(), 'current_operation'):
|
|
169
|
+
for item in self.parent().current_operation:
|
|
170
|
+
self.parent().ax.draw_artist(item['circle'])
|
|
171
|
+
|
|
172
|
+
# Blit everything at once
|
|
173
|
+
self.parent().canvas.blit(self.parent().ax.bbox)
|
|
174
|
+
|
|
175
|
+
def convert_virtual_strokes_to_data(self):
|
|
176
|
+
"""Convert virtual paint strokes to actual array data with interpolation applied here."""
|
|
177
|
+
|
|
178
|
+
# First, apply all drawing operations with interpolation
|
|
179
|
+
for operation in self.parent().virtual_draw_operations:
|
|
180
|
+
last_pos = None
|
|
181
|
+
for item in operation:
|
|
182
|
+
data = item['data']
|
|
183
|
+
current_pos = (data['x'], data['y'])
|
|
184
|
+
|
|
185
|
+
if last_pos is not None:
|
|
186
|
+
points = self.get_line_points(last_pos[0], last_pos[1], current_pos[0], current_pos[1])
|
|
187
|
+
for px, py in points:
|
|
188
|
+
self.paint_at_position_vectorized(
|
|
189
|
+
px, py,
|
|
190
|
+
erase=False,
|
|
191
|
+
channel=data['channel'],
|
|
192
|
+
brush_size=data['brush_size'],
|
|
193
|
+
threed=data['threed'], # Add this
|
|
194
|
+
threedthresh=data['threedthresh'], # Add this
|
|
195
|
+
foreground=data['foreground'],
|
|
196
|
+
machine_window=self.parent().machine_window
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
self.paint_at_position_vectorized(
|
|
200
|
+
data['x'], data['y'],
|
|
201
|
+
erase=False,
|
|
202
|
+
channel=data['channel'],
|
|
203
|
+
brush_size=data['brush_size'],
|
|
204
|
+
threed=data['threed'], # Add this
|
|
205
|
+
threedthresh=data['threedthresh'], # Add this
|
|
206
|
+
foreground=data['foreground'],
|
|
207
|
+
machine_window=self.parent().machine_window
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
last_pos = current_pos
|
|
211
|
+
try:
|
|
212
|
+
item['circle'].remove()
|
|
213
|
+
except:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
# Then, apply all erase operations with interpolation (same changes)
|
|
217
|
+
for operation in self.parent().virtual_erase_operations:
|
|
218
|
+
last_pos = None
|
|
219
|
+
for item in operation:
|
|
220
|
+
data = item['data']
|
|
221
|
+
current_pos = (data['x'], data['y'])
|
|
222
|
+
|
|
223
|
+
if last_pos is not None:
|
|
224
|
+
points = self.get_line_points(last_pos[0], last_pos[1], current_pos[0], current_pos[1])
|
|
225
|
+
for px, py in points:
|
|
226
|
+
self.paint_at_position_vectorized(
|
|
227
|
+
px, py,
|
|
228
|
+
erase=True,
|
|
229
|
+
channel=data['channel'],
|
|
230
|
+
brush_size=data['brush_size'],
|
|
231
|
+
threed=data['threed'], # Add this
|
|
232
|
+
threedthresh=data['threedthresh'], # Add this
|
|
233
|
+
foreground=data['foreground'],
|
|
234
|
+
machine_window=self.parent().machine_window
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
self.paint_at_position_vectorized(
|
|
238
|
+
data['x'], data['y'],
|
|
239
|
+
erase=True,
|
|
240
|
+
channel=data['channel'],
|
|
241
|
+
brush_size=data['brush_size'],
|
|
242
|
+
threed=data['threed'], # Add this
|
|
243
|
+
threedthresh=data['threedthresh'], # Add this
|
|
244
|
+
foreground=data['foreground'],
|
|
245
|
+
machine_window=self.parent().machine_window
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
last_pos = current_pos
|
|
249
|
+
try:
|
|
250
|
+
item['circle'].remove()
|
|
251
|
+
except:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
# Clean up
|
|
255
|
+
self.parent().virtual_draw_operations = []
|
|
256
|
+
self.parent().virtual_erase_operations = []
|
|
257
|
+
if hasattr(self.parent(), 'current_operation'):
|
|
258
|
+
for item in self.parent().current_operation:
|
|
259
|
+
try:
|
|
260
|
+
item['circle'].remove()
|
|
261
|
+
except:
|
|
262
|
+
pass
|
|
263
|
+
self.parent().current_operation = []
|
|
264
|
+
self.parent().current_operation_type = None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def end_virtual_paint_session(self):
|
|
268
|
+
"""Convert virtual paint to actual array modifications when exiting paint mode."""
|
|
269
|
+
if not hasattr(self.parent(), 'virtual_paint_strokes'):
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
# Now apply all the virtual strokes to the actual arrays
|
|
273
|
+
for stroke in self.parent().virtual_paint_strokes:
|
|
274
|
+
for circle in stroke:
|
|
275
|
+
center = circle.center
|
|
276
|
+
radius = circle.radius
|
|
277
|
+
is_erase = circle.get_facecolor()[0] == 0 # Black = erase
|
|
278
|
+
|
|
279
|
+
# Apply to actual array
|
|
280
|
+
self.paint_at_position_vectorized(
|
|
281
|
+
int(center[0]), int(center[1]),
|
|
282
|
+
erase=is_erase,
|
|
283
|
+
channel=self.paint_channel,
|
|
284
|
+
brush_size=int(radius * 2)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Remove the virtual circle
|
|
288
|
+
circle.remove()
|
|
289
|
+
|
|
290
|
+
# Clean up virtual paint data
|
|
291
|
+
self.virtual_paint_strokes = []
|
|
292
|
+
self.current_stroke = []
|
|
293
|
+
|
|
294
|
+
# Reset background
|
|
295
|
+
self.static_background = None
|
|
296
|
+
self.painting = False
|
|
297
|
+
|
|
298
|
+
# Full refresh to show final result
|
|
299
|
+
self.update_display()
|
|
300
|
+
|
|
301
|
+
def paint_at_position_vectorized(self, center_x, center_y, erase=False, channel=2,
|
|
302
|
+
slice_idx=None, brush_size=None, threed=None,
|
|
303
|
+
threedthresh=None, foreground=True, machine_window=None):
|
|
304
|
+
"""Vectorized paint operation for better performance."""
|
|
305
|
+
if self.parent().channel_data[channel] is None:
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# Use provided parameters or fall back to instance variables
|
|
309
|
+
slice_idx = slice_idx if slice_idx is not None else self.parent().current_slice
|
|
310
|
+
brush_size = brush_size if brush_size is not None else getattr(self.parent(), 'brush_size', 5)
|
|
311
|
+
threed = threed if threed is not None else getattr(self.parent(), 'threed', False)
|
|
312
|
+
threedthresh = threedthresh if threedthresh is not None else getattr(self.parent(), 'threedthresh', 1)
|
|
313
|
+
|
|
314
|
+
# Handle 3D painting by recursively calling for each slice
|
|
315
|
+
if threed and threedthresh > 1:
|
|
316
|
+
half_range = (threedthresh - 1) // 2
|
|
317
|
+
low = max(0, slice_idx - half_range)
|
|
318
|
+
high = min(self.parent().channel_data[channel].shape[0] - 1, slice_idx + half_range)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
for i in range(low, high + 1):
|
|
322
|
+
|
|
323
|
+
# Recursive call for each slice, but with threed=False to avoid infinite recursion
|
|
324
|
+
self.paint_at_position_vectorized(
|
|
325
|
+
center_x, center_y,
|
|
326
|
+
erase=erase,
|
|
327
|
+
channel=channel,
|
|
328
|
+
slice_idx=i, # Paint on slice i
|
|
329
|
+
brush_size=brush_size,
|
|
330
|
+
threed=False, # Important: turn off 3D for recursive calls
|
|
331
|
+
threedthresh=1,
|
|
332
|
+
foreground=foreground,
|
|
333
|
+
machine_window=machine_window
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
return # Exit early, recursive calls handle everything
|
|
338
|
+
|
|
339
|
+
# Regular 2D painting (single slice)
|
|
340
|
+
|
|
341
|
+
# Determine paint value
|
|
342
|
+
if erase:
|
|
343
|
+
val = 0
|
|
344
|
+
elif machine_window is None:
|
|
345
|
+
try:
|
|
346
|
+
val = self.parent().min_max[channel][1]
|
|
347
|
+
except:
|
|
348
|
+
val = 255
|
|
349
|
+
elif foreground:
|
|
350
|
+
val = 1
|
|
351
|
+
else:
|
|
352
|
+
val = 2
|
|
353
|
+
|
|
354
|
+
height, width = self.parent().channel_data[channel][slice_idx].shape
|
|
355
|
+
radius = brush_size // 2
|
|
356
|
+
|
|
357
|
+
# Calculate affected region bounds
|
|
358
|
+
y_min = max(0, center_y - radius)
|
|
359
|
+
y_max = min(height, center_y + radius + 1)
|
|
360
|
+
x_min = max(0, center_x - radius)
|
|
361
|
+
x_max = min(width, center_x + radius + 1)
|
|
362
|
+
|
|
363
|
+
if y_min >= y_max or x_min >= x_max:
|
|
364
|
+
return # No valid region to paint
|
|
365
|
+
|
|
366
|
+
# Create coordinate grids for the affected region
|
|
367
|
+
y_coords, x_coords = np.mgrid[y_min:y_max, x_min:x_max]
|
|
368
|
+
|
|
369
|
+
# Calculate distances squared (avoid sqrt for performance)
|
|
370
|
+
distances_sq = (x_coords - center_x) ** 2 + (y_coords - center_y) ** 2
|
|
371
|
+
mask = distances_sq <= radius ** 2
|
|
372
|
+
|
|
373
|
+
# Paint on this single slice
|
|
374
|
+
|
|
375
|
+
self.parent().channel_data[channel][slice_idx][y_min:y_max, x_min:x_max][mask] = val
|
nettracer3d/proximity.py
CHANGED
|
@@ -88,7 +88,7 @@ def process_label(args):
|
|
|
88
88
|
print(f"Processing node {label}")
|
|
89
89
|
|
|
90
90
|
# Get the pre-computed bounding box for this label
|
|
91
|
-
slice_obj = bounding_boxes[label-1] # -1 because label numbers start at 1
|
|
91
|
+
slice_obj = bounding_boxes[int(label)-1] # -1 because label numbers start at 1
|
|
92
92
|
if slice_obj is None:
|
|
93
93
|
return None, None
|
|
94
94
|
|
|
@@ -113,7 +113,7 @@ def create_node_dictionary(nodes, num_nodes, dilate_xy, dilate_z, targets=None,
|
|
|
113
113
|
with ThreadPoolExecutor(max_workers=mp.cpu_count()) as executor:
|
|
114
114
|
# Create args list with bounding_boxes included
|
|
115
115
|
args_list = [(nodes, i, dilate_xy, dilate_z, array_shape, bounding_boxes)
|
|
116
|
-
for i in range(1, num_nodes + 1)]
|
|
116
|
+
for i in range(1, int(num_nodes) + 1)]
|
|
117
117
|
|
|
118
118
|
if targets is not None:
|
|
119
119
|
args_list = [tup for tup in args_list if tup[1] in targets]
|