nettracer3d 0.8.6__tar.gz → 0.8.7__tar.gz

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.

Files changed (31) hide show
  1. {nettracer3d-0.8.6/src/nettracer3d.egg-info → nettracer3d-0.8.7}/PKG-INFO +2 -2
  2. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/README.md +1 -1
  3. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/pyproject.toml +1 -1
  4. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/nettracer_gui.py +75 -71
  5. nettracer3d-0.8.7/src/nettracer3d/painting.py +549 -0
  6. {nettracer3d-0.8.6 → nettracer3d-0.8.7/src/nettracer3d.egg-info}/PKG-INFO +2 -2
  7. nettracer3d-0.8.6/src/nettracer3d/painting.py +0 -375
  8. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/LICENSE +0 -0
  9. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/setup.cfg +0 -0
  10. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/__init__.py +0 -0
  11. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/cellpose_manager.py +0 -0
  12. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/community_extractor.py +0 -0
  13. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/excelotron.py +0 -0
  14. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/modularity.py +0 -0
  15. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/morphology.py +0 -0
  16. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/neighborhoods.py +0 -0
  17. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/nettracer.py +0 -0
  18. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/network_analysis.py +0 -0
  19. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/network_draw.py +0 -0
  20. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/node_draw.py +0 -0
  21. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/proximity.py +0 -0
  22. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/run.py +0 -0
  23. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/segmenter.py +0 -0
  24. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/segmenter_GPU.py +0 -0
  25. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/simple_network.py +0 -0
  26. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/smart_dilate.py +0 -0
  27. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  28. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  29. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  30. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/requires.txt +0 -0
  31. {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.8.6
3
+ Version: 0.8.7
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <liamm@wustl.edu>
6
6
  Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
@@ -110,6 +110,6 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
110
110
 
111
111
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
112
112
 
113
- -- Version 0.8.6 Updates --
113
+ -- Version 0.8.7 Updates --
114
114
 
115
115
  * See Documentation Once Updated
@@ -65,6 +65,6 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
65
65
 
66
66
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
67
67
 
68
- -- Version 0.8.6 Updates --
68
+ -- Version 0.8.7 Updates --
69
69
 
70
70
  * See Documentation Once Updated
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.8.6"
3
+ version = "0.8.7"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="liamm@wustl.edu" },
6
6
  ]
@@ -2172,6 +2172,15 @@ class ImageViewerWindow(QMainWindow):
2172
2172
  if self.machine_window is not None:
2173
2173
  self.machine_window.silence_button()
2174
2174
  self.canvas.setCursor(Qt.CursorShape.CrossCursor)
2175
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
2176
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
2177
+ (hasattr(self, 'current_operation') and self.current_operation):
2178
+ # Finish current operation first
2179
+ if hasattr(self, 'current_operation') and self.current_operation:
2180
+ self.pm.finish_current_virtual_operation()
2181
+ # Now convert to real data
2182
+ self.pm.convert_virtual_strokes_to_data()
2183
+ self.update_display()
2175
2184
  if self.pan_mode or self.brush_mode:
2176
2185
  current_xlim = self.ax.get_xlim()
2177
2186
  current_ylim = self.ax.get_ylim()
@@ -2196,6 +2205,15 @@ class ImageViewerWindow(QMainWindow):
2196
2205
  self.threed = False
2197
2206
  self.last_change = None
2198
2207
  self.brush_mode = False
2208
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
2209
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
2210
+ (hasattr(self, 'current_operation') and self.current_operation):
2211
+ # Finish current operation first
2212
+ if hasattr(self, 'current_operation') and self.current_operation:
2213
+ self.pm.finish_current_virtual_operation()
2214
+ # Now convert to real data
2215
+ self.pm.convert_virtual_strokes_to_data()
2216
+ self.update_display()
2199
2217
  if self.machine_window is not None:
2200
2218
  self.machine_window.silence_button()
2201
2219
  self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
@@ -2229,8 +2247,6 @@ class ImageViewerWindow(QMainWindow):
2229
2247
  else:
2230
2248
  channel = 2
2231
2249
 
2232
- self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
2233
-
2234
2250
  if self.pan_mode:
2235
2251
  current_xlim = self.ax.get_xlim()
2236
2252
  current_ylim = self.ax.get_ylim()
@@ -2242,6 +2258,15 @@ class ImageViewerWindow(QMainWindow):
2242
2258
  self.zoom_mode = False
2243
2259
  self.update_brush_cursor()
2244
2260
  else:
2261
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
2262
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
2263
+ (hasattr(self, 'current_operation') and self.current_operation):
2264
+ # Finish current operation first
2265
+ if hasattr(self, 'current_operation') and self.current_operation:
2266
+ self.pm.finish_current_virtual_operation()
2267
+ # Now convert to real data
2268
+ self.pm.convert_virtual_strokes_to_data()
2269
+ self.update_display()
2245
2270
  # Get current zoom and do display update
2246
2271
  current_xlim = self.ax.get_xlim()
2247
2272
  current_ylim = self.ax.get_ylim()
@@ -2525,21 +2550,16 @@ class ImageViewerWindow(QMainWindow):
2525
2550
  if event.inaxes != self.ax:
2526
2551
  return
2527
2552
 
2528
- """
2529
- try:
2530
- if self.machine_window is not None and not self.machine_window.segmentation_worker._paused:
2531
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2532
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2553
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2554
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2533
2555
 
2534
- if self.pen_button.isChecked():
2535
- channel = self.active_channel
2536
- else:
2537
- channel = 2
2556
+ if self.pen_button.isChecked():
2557
+ channel = self.active_channel
2558
+ else:
2559
+ channel = 2
2560
+
2561
+ self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
2538
2562
 
2539
- self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
2540
- except:
2541
- pass
2542
- """
2543
2563
 
2544
2564
  if event.button == 1 or event.button == 3:
2545
2565
  if self.machine_window is not None:
@@ -2555,7 +2575,7 @@ class ImageViewerWindow(QMainWindow):
2555
2575
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2556
2576
 
2557
2577
  if event.button == 1 and getattr(self, 'can', False):
2558
- self.update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = True)
2578
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
2559
2579
  self.handle_can(x, y)
2560
2580
  return
2561
2581
 
@@ -3121,24 +3141,26 @@ class ImageViewerWindow(QMainWindow):
3121
3141
 
3122
3142
  if not hasattr(self, 'zoom_changed'):
3123
3143
  self.zoom_changed = False
3124
-
3144
+
3125
3145
  self.canvas.draw()
3126
3146
 
3127
3147
  # Handle brush mode cleanup with paint session management
3128
3148
  if self.brush_mode and hasattr(self, 'painting') and self.painting:
3149
+
3150
+ self.pm.connect_virtual_paint_points()
3151
+ self.pm.update_virtual_paint_display()
3152
+
3153
+
3129
3154
  # Finish current operation
3155
+ self.pm.finish_current_stroke()
3130
3156
  self.pm.finish_current_virtual_operation()
3131
3157
 
3132
3158
  # Reset last position for next stroke
3133
- self.last_virtual_pos = None
3159
+ #self.last_virtual_pos = None
3134
3160
 
3135
3161
  # End this stroke but keep session active for continuous painting
3136
3162
  self.painting = False
3137
3163
 
3138
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3139
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3140
-
3141
- self.update_display(preserve_zoom=(current_xlim, current_ylim), continue_paint = True)
3142
3164
 
3143
3165
  if self.resume:
3144
3166
  self.machine_window.segmentation_worker.resume()
@@ -3264,7 +3286,6 @@ class ImageViewerWindow(QMainWindow):
3264
3286
 
3265
3287
  self.zoom_changed = False # Flag that zoom has changed
3266
3288
 
3267
-
3268
3289
 
3269
3290
  self.canvas.draw()
3270
3291
 
@@ -5110,6 +5131,14 @@ class ImageViewerWindow(QMainWindow):
5110
5131
  """Actually perform the slice update after debounce delay."""
5111
5132
  if self.pending_slice is not None:
5112
5133
  slice_value, view_settings = self.pending_slice
5134
+ if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
5135
+ (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
5136
+ (hasattr(self, 'current_operation') and self.current_operation):
5137
+ # Finish current operation first
5138
+ if hasattr(self, 'current_operation') and self.current_operation:
5139
+ self.pm.finish_current_virtual_operation()
5140
+ # Now convert to real data
5141
+ self.pm.convert_virtual_strokes_to_data()
5113
5142
  self.current_slice = slice_value
5114
5143
  if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
5115
5144
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
@@ -5133,7 +5162,7 @@ class ImageViewerWindow(QMainWindow):
5133
5162
 
5134
5163
 
5135
5164
 
5136
- def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False, continue_paint = False, skip_paint_reinit = False):
5165
+ def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False):
5137
5166
  """Update the display with currently visible channels and highlight overlay."""
5138
5167
  try:
5139
5168
  self.figure.clear()
@@ -5163,17 +5192,17 @@ class ImageViewerWindow(QMainWindow):
5163
5192
  self.restore_channels = []
5164
5193
  except:
5165
5194
  pass
5166
- if not continue_paint:
5167
- self.static_background = None
5168
5195
 
5169
- if self.machine_window is None:
5170
- try:
5171
- self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
5172
- self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
5173
- self.channel_data[4] = None
5174
- self.channel_visible[4] = False
5175
- except:
5176
- pass
5196
+ self.static_background = None
5197
+
5198
+ if self.machine_window is None:
5199
+ try:
5200
+ self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
5201
+ self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
5202
+ self.channel_data[4] = None
5203
+ self.channel_visible[4] = False
5204
+ except:
5205
+ pass
5177
5206
 
5178
5207
  # Get active channels and their dimensions
5179
5208
  active_channels = [i for i in range(4) if self.channel_data[i] is not None]
@@ -5404,17 +5433,6 @@ class ImageViewerWindow(QMainWindow):
5404
5433
 
5405
5434
  self.canvas.draw()
5406
5435
 
5407
- if self.brush_mode and not skip_paint_reinit:
5408
- # Get current zoom to preserve it
5409
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
5410
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
5411
-
5412
- if self.pen_button.isChecked():
5413
- channel = self.active_channel
5414
- else:
5415
- channel = 2
5416
-
5417
- self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
5418
5436
 
5419
5437
  except:
5420
5438
  import traceback
@@ -9770,18 +9788,6 @@ class MachineWindow(QMainWindow):
9770
9788
 
9771
9789
  self.parent().pm = painting.PaintManager(parent = self.parent())
9772
9790
 
9773
- # Start virtual paint session
9774
- # Get current zoom to preserve it
9775
- current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
9776
- current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
9777
-
9778
- if self.parent().pen_button.isChecked():
9779
- channel = self.parent().active_channel
9780
- else:
9781
- channel = 2
9782
-
9783
- self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
9784
-
9785
9791
  self.parent().pan_button.setChecked(False)
9786
9792
  self.parent().zoom_button.setChecked(False)
9787
9793
  if self.parent().pan_mode:
@@ -9809,6 +9815,15 @@ class MachineWindow(QMainWindow):
9809
9815
  # Wait a bit for cleanup
9810
9816
  time.sleep(0.1)
9811
9817
 
9818
+ if (hasattr(self.parent(), 'virtual_draw_operations') and self.parent().virtual_draw_operations) or \
9819
+ (hasattr(self.parent(), 'virtual_erase_operations') and self.parent().virtual_erase_operations) or \
9820
+ (hasattr(self.parent(), 'current_operation') and self.parent().current_operation):
9821
+ # Finish current operation first
9822
+ if hasattr(self.parent(), 'current_operation') and self.parent().current_operation:
9823
+ self.parent().pm.finish_current_virtual_operation()
9824
+ # Now convert to real data
9825
+ self.parent().pm.convert_virtual_strokes_to_data()
9826
+
9812
9827
  self.previewing = True
9813
9828
  try:
9814
9829
  try:
@@ -9852,7 +9867,7 @@ class MachineWindow(QMainWindow):
9852
9867
  return
9853
9868
  else:
9854
9869
  self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
9855
- self.segmentation_worker.chunk_processed.connect(lambda: self.update_display(skip_paint_reinit = True)) # Just update display
9870
+ self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
9856
9871
  current_xlim = self.parent().ax.get_xlim()
9857
9872
  current_ylim = self.parent().ax.get_ylim()
9858
9873
  try:
@@ -9905,7 +9920,7 @@ class MachineWindow(QMainWindow):
9905
9920
 
9906
9921
  return changed
9907
9922
 
9908
- def update_display(self, skip_paint_reinit = False):
9923
+ def update_display(self):
9909
9924
  if not hasattr(self, '_last_update'):
9910
9925
  self._last_update = 0
9911
9926
 
@@ -9932,18 +9947,7 @@ class MachineWindow(QMainWindow):
9932
9947
 
9933
9948
  if not self.parent().painting:
9934
9949
  # Only update if view limits are valid
9935
- self.parent().update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = skip_paint_reinit)
9936
-
9937
- if self.parent().brush_mode:
9938
- current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
9939
- current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
9940
-
9941
- if self.parent().pen_button.isChecked():
9942
- channel = self.parent().active_channel
9943
- else:
9944
- channel = 2
9945
-
9946
- self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
9950
+ self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
9947
9951
 
9948
9952
  self._last_update = current_time
9949
9953
  except Exception as e:
@@ -0,0 +1,549 @@
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
+ self.resume = False
11
+
12
+ # Initialize stroke tracking storage once
13
+ if parent is not None:
14
+ if not hasattr(parent, 'completed_paint_strokes'):
15
+ parent.completed_paint_strokes = [] # List of individual completed strokes
16
+ if not hasattr(parent, 'current_stroke_points'):
17
+ parent.current_stroke_points = [] # Current stroke being drawn
18
+ if not hasattr(parent, 'current_stroke_type'):
19
+ parent.current_stroke_type = None # 'draw' or 'erase'
20
+
21
+ # Keep the old properties for display purposes
22
+ if not hasattr(parent, 'virtual_draw_operations'):
23
+ parent.virtual_draw_operations = []
24
+ if not hasattr(parent, 'virtual_erase_operations'):
25
+ parent.virtual_erase_operations = []
26
+ if not hasattr(parent, 'current_operation'):
27
+ parent.current_operation = []
28
+ if not hasattr(parent, 'current_operation_type'):
29
+ parent.current_operation_type = None
30
+
31
+ def get_line_points(self, x0, y0, x1, y1):
32
+ """Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm."""
33
+ points = []
34
+ dx = abs(x1 - x0)
35
+ dy = abs(y1 - y0)
36
+ x, y = x0, y0
37
+ sx = 1 if x0 < x1 else -1
38
+ sy = 1 if y0 < y1 else -1
39
+
40
+ if dx > dy:
41
+ err = dx / 2.0
42
+ while x != x1:
43
+ points.append((x, y))
44
+ err -= dy
45
+ if err < 0:
46
+ y += sy
47
+ err += dx
48
+ x += sx
49
+ else:
50
+ err = dy / 2.0
51
+ while y != y1:
52
+ points.append((x, y))
53
+ err -= dx
54
+ if err < 0:
55
+ x += sx
56
+ err += dy
57
+ y += sy
58
+
59
+ points.append((x, y))
60
+ return points
61
+
62
+ def initiate_paint_session(self, channel, current_xlim, current_ylim):
63
+ # Create static background (same as selection rectangle)
64
+
65
+ if self.parent().machine_window is not None:
66
+ if self.parent().machine_window.segmentation_worker is not None:
67
+ if not self.parent().machine_window.segmentation_worker._paused:
68
+ self.resume = True
69
+ self.parent().machine_window.segmentation_worker.pause()
70
+
71
+
72
+ if not self.parent().channel_visible[channel]:
73
+ self.parent().channel_visible[channel] = True
74
+
75
+ # Capture the background once
76
+ self.parent().static_background = self.parent().canvas.copy_from_bbox(self.parent().ax.bbox)
77
+
78
+ if self.resume:
79
+ self.parent().machine_window.segmentation_worker.resume()
80
+ self.resume = False
81
+
82
+
83
+
84
+ def start_virtual_paint_session(self, channel, current_xlim, current_ylim):
85
+ """Start a virtual paint session that doesn't modify arrays until the end."""
86
+ self.parent().painting = True
87
+ self.parent().paint_channel = channel
88
+
89
+ # Store original state
90
+ if not self.parent().channel_visible[channel]:
91
+ self.parent().channel_visible[channel] = True
92
+
93
+ # Initialize stroke tracking storage ONLY if they don't exist
94
+ if not hasattr(self.parent(), 'completed_paint_strokes'):
95
+ self.parent().completed_paint_strokes = []
96
+ if not hasattr(self.parent(), 'current_stroke_points'):
97
+ self.parent().current_stroke_points = []
98
+ if not hasattr(self.parent(), 'current_stroke_type'):
99
+ self.parent().current_stroke_type = None
100
+
101
+ # Initialize display storage ONLY if they don't exist
102
+ if not hasattr(self.parent(), 'virtual_draw_operations'):
103
+ self.parent().virtual_draw_operations = []
104
+ if not hasattr(self.parent(), 'virtual_erase_operations'):
105
+ self.parent().virtual_erase_operations = []
106
+ if not hasattr(self.parent(), 'current_operation'):
107
+ self.parent().current_operation = []
108
+ if not hasattr(self.parent(), 'current_operation_type'):
109
+ self.parent().current_operation_type = None
110
+
111
+ def reset_all_paint_storage(self):
112
+ """Reset all paint storage - call this when you want to start completely fresh."""
113
+ self.parent().completed_paint_strokes = []
114
+ self.parent().current_stroke_points = []
115
+ self.parent().current_stroke_type = None
116
+ self.parent().virtual_draw_operations = []
117
+ self.parent().virtual_erase_operations = []
118
+ self.parent().current_operation = []
119
+ self.parent().current_operation_type = None
120
+
121
+
122
+
123
+ def add_virtual_paint_point(self, x, y, brush_size, erase=False, foreground=True):
124
+ """Add a single paint point to the virtual layer."""
125
+
126
+ # Determine operation type and visual properties
127
+ if erase:
128
+ paint_color = 'black' # Visual indicator for erase
129
+ alpha = 0.5
130
+ operation_type = 'erase'
131
+ else:
132
+ if self.parent().machine_window is not None:
133
+ if foreground:
134
+ paint_color = 'green' # Visual for foreground (value 1)
135
+ alpha = 0.7
136
+ else:
137
+ paint_color = 'red' # Visual for background (value 2)
138
+ alpha = 0.7
139
+ else:
140
+ paint_color = 'white' # Normal paint
141
+ alpha = 0.7
142
+ operation_type = 'draw'
143
+
144
+ # Store the operation data (for later conversion to real paint)
145
+ operation_data = {
146
+ 'x': x,
147
+ 'y': y,
148
+ 'brush_size': brush_size,
149
+ 'erase': erase,
150
+ 'foreground': foreground,
151
+ 'channel': self.parent().paint_channel,
152
+ 'threed': getattr(self.parent(), 'threed', False),
153
+ 'threedthresh': getattr(self.parent(), 'threedthresh', 1)
154
+ }
155
+
156
+ # Add to stroke tracking (NEW - separate stroke tracking)
157
+ if self.parent().current_stroke_type != operation_type:
158
+ # Finish previous stroke if switching between draw/erase
159
+ self.finish_current_stroke()
160
+ self.parent().current_stroke_type = operation_type
161
+
162
+ self.parent().current_stroke_points.append(operation_data)
163
+
164
+ # Create visual circle for display
165
+ circle = plt.Circle((x, y), brush_size/2,
166
+ color=paint_color, alpha=alpha, animated=True)
167
+
168
+ # Add to display operations (OLD - for visual display)
169
+ if self.parent().current_operation_type != operation_type:
170
+ # Finish previous operation if switching between draw/erase
171
+ self.finish_current_virtual_operation()
172
+ self.parent().current_operation_type = operation_type
173
+
174
+ self.parent().current_operation.append({
175
+ 'circle': circle,
176
+ 'data': operation_data
177
+ })
178
+
179
+ self.parent().ax.add_patch(circle)
180
+
181
+ def finish_current_stroke(self):
182
+ """Finish the current stroke and add it to completed strokes."""
183
+ if not self.parent().current_stroke_points:
184
+ return
185
+
186
+ # Store the completed stroke with its type
187
+ stroke_data = {
188
+ 'points': self.parent().current_stroke_points.copy(),
189
+ 'type': self.parent().current_stroke_type
190
+ }
191
+
192
+ self.parent().completed_paint_strokes.append(stroke_data)
193
+
194
+ # Clear current stroke
195
+ self.parent().current_stroke_points = []
196
+ self.parent().current_stroke_type = None
197
+
198
+ def add_virtual_paint_stroke(self, x, y, brush_size, erase=False, foreground=True):
199
+ """Add a paint stroke - simple visual, interpolation happens during data conversion."""
200
+ # Just add the current point for visual display (no interpolation yet)
201
+ self.add_virtual_paint_point(x, y, brush_size, erase, foreground)
202
+
203
+ # Store the last position for data conversion later
204
+ self.parent().last_virtual_pos = (x, y)
205
+
206
+ def connect_virtual_paint_points(self):
207
+ """Connect points with lines matching the circle size by converting to screen coordinates."""
208
+
209
+ if not hasattr(self.parent(), 'current_operation') or len(self.parent().current_operation) < 2:
210
+ return
211
+
212
+ # Get existing points but DON'T remove them
213
+ existing_points = self.parent().current_operation.copy()
214
+ point_data = [item['data'] for item in existing_points if 'data' in item]
215
+
216
+ if len(point_data) < 2:
217
+ return
218
+
219
+ # Get visual properties and brush size from first point
220
+ first_data = point_data[0]
221
+ brush_size_data = first_data['brush_size'] # This is in data coordinates
222
+
223
+ # Convert brush size from data coordinates to points for linewidth
224
+ # Get the transformation from data to display coordinates
225
+ ax = self.parent().ax
226
+
227
+ # Transform two points to see the scaling
228
+ p1_data = [0, 0]
229
+ p2_data = [brush_size_data, 0] # One brush_size unit away
230
+
231
+ p1_display = ax.transData.transform(p1_data)
232
+ p2_display = ax.transData.transform(p2_data)
233
+
234
+ # Calculate pixels per data unit
235
+ pixels_per_data_unit = abs(p2_display[0] - p1_display[0])
236
+
237
+ # Convert to points (matplotlib uses 72 points per inch, figure.dpi pixels per inch)
238
+ fig = ax.figure
239
+ points_per_pixel = 72.0 / fig.dpi
240
+ brush_size_points = pixels_per_data_unit * points_per_pixel
241
+
242
+ if first_data['erase']:
243
+ line_color = 'black'
244
+ alpha = 0.5
245
+ else:
246
+ if self.parent().machine_window is not None:
247
+ if first_data['foreground']:
248
+ line_color = 'green'
249
+ alpha = 0.7
250
+ else:
251
+ line_color = 'red'
252
+ alpha = 0.7
253
+ else:
254
+ line_color = 'white'
255
+ alpha = 0.7
256
+
257
+ # Create line segments for connections using LineCollection
258
+ from matplotlib.collections import LineCollection
259
+
260
+ segments = []
261
+ for i in range(len(point_data) - 1):
262
+ x1, y1 = point_data[i]['x'], point_data[i]['y']
263
+ x2, y2 = point_data[i+1]['x'], point_data[i+1]['y']
264
+ segments.append([(x1, y1), (x2, y2)])
265
+
266
+ # Create line collection with converted linewidth
267
+ if segments:
268
+ lc = LineCollection(segments,
269
+ colors=line_color,
270
+ alpha=alpha,
271
+ linewidths=brush_size_points, # Now in points, matching circles
272
+ animated=True)
273
+ self.parent().ax.add_collection(lc)
274
+
275
+ # Add the line collection as a visual-only element
276
+ self.parent().current_operation.append({
277
+ 'line_collection': lc,
278
+ 'is_connection_visual': True
279
+ })
280
+
281
+ def finish_current_virtual_operation(self):
282
+ """Finish the current operation (draw or erase) and add it to the appropriate list."""
283
+
284
+ if not self.parent().current_operation:
285
+ return
286
+
287
+ # Filter out connection visuals from the operation before storing
288
+ data_items = []
289
+ visual_items = []
290
+
291
+ for item in self.parent().current_operation:
292
+ if item.get('is_connection_visual', False):
293
+ visual_items.append(item)
294
+ else:
295
+ data_items.append(item)
296
+
297
+ # Only store the data items for this specific stroke
298
+ if data_items:
299
+ if self.parent().current_operation_type == 'draw':
300
+ self.parent().virtual_draw_operations.append(data_items)
301
+ elif self.parent().current_operation_type == 'erase':
302
+ self.parent().virtual_erase_operations.append(data_items)
303
+
304
+ # Clean up visual items that are connection-only
305
+ for item in visual_items:
306
+ try:
307
+ if 'line_collection' in item:
308
+ item['line_collection'].remove()
309
+ elif 'line' in item:
310
+ item['line'].remove()
311
+ except:
312
+ pass
313
+
314
+ self.parent().current_operation = []
315
+ self.parent().current_operation_type = None
316
+
317
+ def update_virtual_paint_display(self):
318
+ """Update display with virtual paint strokes - handles different object types."""
319
+ if not hasattr(self.parent(), 'static_background') or self.parent().static_background is None:
320
+ return
321
+
322
+ # Restore the clean background
323
+ self.parent().canvas.restore_region(self.parent().static_background)
324
+
325
+ # Draw all completed operations
326
+ for operation_list in [self.parent().virtual_draw_operations, self.parent().virtual_erase_operations]:
327
+ for operation in operation_list:
328
+ for item in operation:
329
+ self._draw_virtual_item(item)
330
+
331
+ # Draw current operation being painted
332
+ if hasattr(self.parent(), 'current_operation'):
333
+ for item in self.parent().current_operation:
334
+ self._draw_virtual_item(item)
335
+
336
+ # Blit everything at once
337
+ self.parent().canvas.blit(self.parent().ax.bbox)
338
+
339
+ def _draw_virtual_item(self, item):
340
+ """Helper method to draw different types of virtual paint items."""
341
+ try:
342
+ # Skip items that are marked as visual-only connections
343
+ if item.get('is_connection_visual', False):
344
+ if 'line' in item:
345
+ self.parent().ax.draw_artist(item['line'])
346
+ elif 'line_collection' in item:
347
+ self.parent().ax.draw_artist(item['line_collection'])
348
+ elif 'circle' in item:
349
+ self.parent().ax.draw_artist(item['circle'])
350
+ elif 'line' in item:
351
+ self.parent().ax.draw_artist(item['line'])
352
+ elif 'line_collection' in item:
353
+ self.parent().ax.draw_artist(item['line_collection'])
354
+ except Exception as e:
355
+ # Skip items that can't be drawn (might have been removed)
356
+ pass
357
+
358
+ def convert_virtual_strokes_to_data(self):
359
+ """Convert each stroke separately to actual array data using ONLY the new stroke tracking system."""
360
+
361
+ # Finish the current stroke first
362
+ self.finish_current_stroke()
363
+
364
+ # Process ONLY the completed_paint_strokes (ignore old display operations)
365
+ for stroke in self.parent().completed_paint_strokes:
366
+ stroke_points = stroke['points']
367
+ stroke_type = stroke['type']
368
+
369
+ if len(stroke_points) == 0:
370
+ continue
371
+
372
+ # Apply interpolation within this stroke only
373
+ last_pos = None
374
+ for point_data in stroke_points:
375
+ current_pos = (point_data['x'], point_data['y'])
376
+
377
+ if last_pos is not None:
378
+ # Interpolate between consecutive points in this stroke
379
+ points = self.get_line_points(last_pos[0], last_pos[1], current_pos[0], current_pos[1])
380
+ for px, py in points:
381
+ self.paint_at_position_vectorized(
382
+ px, py,
383
+ erase=point_data['erase'],
384
+ channel=point_data['channel'],
385
+ brush_size=point_data['brush_size'],
386
+ threed=point_data['threed'],
387
+ threedthresh=point_data['threedthresh'],
388
+ foreground=point_data['foreground'],
389
+ machine_window=self.parent().machine_window
390
+ )
391
+ else:
392
+ # First point in stroke
393
+ self.paint_at_position_vectorized(
394
+ point_data['x'], point_data['y'],
395
+ erase=point_data['erase'],
396
+ channel=point_data['channel'],
397
+ brush_size=point_data['brush_size'],
398
+ threed=point_data['threed'],
399
+ threedthresh=point_data['threedthresh'],
400
+ foreground=point_data['foreground'],
401
+ machine_window=self.parent().machine_window
402
+ )
403
+
404
+ last_pos = current_pos
405
+
406
+ # Clean up ALL visual elements (both old and new systems)
407
+ for operation_list in [self.parent().virtual_draw_operations, self.parent().virtual_erase_operations]:
408
+ for operation in operation_list:
409
+ for item in operation:
410
+ try:
411
+ if 'circle' in item:
412
+ item['circle'].remove()
413
+ elif 'line_collection' in item:
414
+ item['line_collection'].remove()
415
+ elif 'line' in item:
416
+ item['line'].remove()
417
+ except:
418
+ pass
419
+
420
+ if hasattr(self.parent(), 'current_operation'):
421
+ for item in self.parent().current_operation:
422
+ try:
423
+ if 'circle' in item:
424
+ item['circle'].remove()
425
+ elif 'line_collection' in item:
426
+ item['line_collection'].remove()
427
+ elif 'line' in item:
428
+ item['line'].remove()
429
+ except:
430
+ pass
431
+
432
+ # Reset all storage for next paint session
433
+ self.parent().completed_paint_strokes = []
434
+ self.parent().current_stroke_points = []
435
+ self.parent().current_stroke_type = None
436
+ self.parent().virtual_draw_operations = []
437
+ self.parent().virtual_erase_operations = []
438
+ self.parent().current_operation = []
439
+ self.parent().current_operation_type = None
440
+
441
+ def end_virtual_paint_session(self):
442
+ """Convert virtual paint to actual array modifications when exiting paint mode."""
443
+ if not hasattr(self.parent(), 'virtual_paint_strokes'):
444
+ return
445
+
446
+ # Now apply all the virtual strokes to the actual arrays
447
+ for stroke in self.parent().virtual_paint_strokes:
448
+ for circle in stroke:
449
+ center = circle.center
450
+ radius = circle.radius
451
+ is_erase = circle.get_facecolor()[0] == 0 # Black = erase
452
+
453
+ # Apply to actual array
454
+ self.paint_at_position_vectorized(
455
+ int(center[0]), int(center[1]),
456
+ erase=is_erase,
457
+ channel=self.paint_channel,
458
+ brush_size=int(radius * 2)
459
+ )
460
+
461
+ # Remove the virtual circle
462
+ circle.remove()
463
+
464
+ # Clean up virtual paint data
465
+ self.virtual_paint_strokes = []
466
+ self.current_stroke = []
467
+
468
+ # Reset background
469
+ self.static_background = None
470
+ self.painting = False
471
+
472
+ # Full refresh to show final result
473
+ self.update_display()
474
+
475
+ def paint_at_position_vectorized(self, center_x, center_y, erase=False, channel=2,
476
+ slice_idx=None, brush_size=None, threed=None,
477
+ threedthresh=None, foreground=True, machine_window=None):
478
+ """Vectorized paint operation for better performance."""
479
+ if self.parent().channel_data[channel] is None:
480
+ return
481
+
482
+ # Use provided parameters or fall back to instance variables
483
+ slice_idx = slice_idx if slice_idx is not None else self.parent().current_slice
484
+ brush_size = brush_size if brush_size is not None else getattr(self.parent(), 'brush_size', 5)
485
+ threed = threed if threed is not None else getattr(self.parent(), 'threed', False)
486
+ threedthresh = threedthresh if threedthresh is not None else getattr(self.parent(), 'threedthresh', 1)
487
+
488
+ # Handle 3D painting by recursively calling for each slice
489
+ if threed and threedthresh > 1:
490
+ half_range = (threedthresh - 1) // 2
491
+ low = max(0, slice_idx - half_range)
492
+ high = min(self.parent().channel_data[channel].shape[0] - 1, slice_idx + half_range)
493
+
494
+
495
+ for i in range(low, high + 1):
496
+
497
+ # Recursive call for each slice, but with threed=False to avoid infinite recursion
498
+ self.paint_at_position_vectorized(
499
+ center_x, center_y,
500
+ erase=erase,
501
+ channel=channel,
502
+ slice_idx=i, # Paint on slice i
503
+ brush_size=brush_size,
504
+ threed=False, # Important: turn off 3D for recursive calls
505
+ threedthresh=1,
506
+ foreground=foreground,
507
+ machine_window=machine_window
508
+ )
509
+
510
+
511
+ return # Exit early, recursive calls handle everything
512
+
513
+ # Regular 2D painting (single slice)
514
+
515
+ # Determine paint value
516
+ if erase:
517
+ val = 0
518
+ elif machine_window is None:
519
+ try:
520
+ val = self.parent().min_max[channel][1]
521
+ except:
522
+ val = 255
523
+ elif foreground:
524
+ val = 1
525
+ else:
526
+ val = 2
527
+
528
+ height, width = self.parent().channel_data[channel][slice_idx].shape
529
+ radius = brush_size // 2
530
+
531
+ # Calculate affected region bounds
532
+ y_min = max(0, center_y - radius)
533
+ y_max = min(height, center_y + radius + 1)
534
+ x_min = max(0, center_x - radius)
535
+ x_max = min(width, center_x + radius + 1)
536
+
537
+ if y_min >= y_max or x_min >= x_max:
538
+ return # No valid region to paint
539
+
540
+ # Create coordinate grids for the affected region
541
+ y_coords, x_coords = np.mgrid[y_min:y_max, x_min:x_max]
542
+
543
+ # Calculate distances squared (avoid sqrt for performance)
544
+ distances_sq = (x_coords - center_x) ** 2 + (y_coords - center_y) ** 2
545
+ mask = distances_sq <= radius ** 2
546
+
547
+ # Paint on this single slice
548
+
549
+ self.parent().channel_data[channel][slice_idx][y_min:y_max, x_min:x_max][mask] = val
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.8.6
3
+ Version: 0.8.7
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <liamm@wustl.edu>
6
6
  Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
@@ -110,6 +110,6 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
110
110
 
111
111
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
112
112
 
113
- -- Version 0.8.6 Updates --
113
+ -- Version 0.8.7 Updates --
114
114
 
115
115
  * See Documentation Once Updated
@@ -1,375 +0,0 @@
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
File without changes
File without changes