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.
@@ -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
- def get_label_indices(binary_stack, label, y_offset):
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
- Get indices for all labels in a given chunk of the 3D array.
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
- for label in label_list:
573
- indices = get_label_indices(chunk, label, y_offset)
574
- indices_dict_chunk[label] = indices
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 into smaller subarrays
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 of indices across chunks
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
- try:
626
- del centroid_dict[0]
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
- return_nodes = np.zeros_like(nodes) # Start with all zeros
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
@@ -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