nettracer3d 0.8.4__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.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

@@ -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
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]