nettracer3d 0.8.5__py3-none-any.whl → 0.8.7__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.

@@ -464,6 +464,8 @@ class ImageViewerWindow(QMainWindow):
464
464
  self.paint_batch = []
465
465
  self.last_paint_pos = None
466
466
 
467
+ self.resume = False
468
+
467
469
  def start_left_scroll(self):
468
470
  """Start scrolling left when left arrow is pressed."""
469
471
  # Single increment first
@@ -2170,6 +2172,15 @@ class ImageViewerWindow(QMainWindow):
2170
2172
  if self.machine_window is not None:
2171
2173
  self.machine_window.silence_button()
2172
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()
2173
2184
  if self.pan_mode or self.brush_mode:
2174
2185
  current_xlim = self.ax.get_xlim()
2175
2186
  current_ylim = self.ax.get_ylim()
@@ -2194,6 +2205,15 @@ class ImageViewerWindow(QMainWindow):
2194
2205
  self.threed = False
2195
2206
  self.last_change = None
2196
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()
2197
2217
  if self.machine_window is not None:
2198
2218
  self.machine_window.silence_button()
2199
2219
  self.canvas.setCursor(Qt.CursorShape.OpenHandCursor)
@@ -2227,8 +2247,6 @@ class ImageViewerWindow(QMainWindow):
2227
2247
  else:
2228
2248
  channel = 2
2229
2249
 
2230
- self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
2231
-
2232
2250
  if self.pan_mode:
2233
2251
  current_xlim = self.ax.get_xlim()
2234
2252
  current_ylim = self.ax.get_ylim()
@@ -2240,6 +2258,15 @@ class ImageViewerWindow(QMainWindow):
2240
2258
  self.zoom_mode = False
2241
2259
  self.update_brush_cursor()
2242
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()
2243
2270
  # Get current zoom and do display update
2244
2271
  current_xlim = self.ax.get_xlim()
2245
2272
  current_ylim = self.ax.get_ylim()
@@ -2498,6 +2525,8 @@ class ImageViewerWindow(QMainWindow):
2498
2525
 
2499
2526
  if self.machine_window is not None:
2500
2527
  if self.machine_window.segmentation_worker is not None:
2528
+ if not self.machine_window.segmentation_worker._paused:
2529
+ self.resume = True
2501
2530
  self.machine_window.segmentation_worker.pause()
2502
2531
 
2503
2532
  # Store current channel visibility state
@@ -2520,10 +2549,23 @@ class ImageViewerWindow(QMainWindow):
2520
2549
  """Handle brush mode with virtual painting."""
2521
2550
  if event.inaxes != self.ax:
2522
2551
  return
2552
+
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
2555
+
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)
2562
+
2523
2563
 
2524
2564
  if event.button == 1 or event.button == 3:
2525
2565
  if self.machine_window is not None:
2526
2566
  if self.machine_window.segmentation_worker is not None:
2567
+ if not self.machine_window.segmentation_worker._paused:
2568
+ self.resume = True
2527
2569
  self.machine_window.segmentation_worker.pause()
2528
2570
 
2529
2571
  x, y = int(event.xdata), int(event.ydata)
@@ -2533,7 +2575,7 @@ class ImageViewerWindow(QMainWindow):
2533
2575
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2534
2576
 
2535
2577
  if event.button == 1 and getattr(self, 'can', False):
2536
- self.update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = True)
2578
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
2537
2579
  self.handle_can(x, y)
2538
2580
  return
2539
2581
 
@@ -3099,29 +3141,30 @@ class ImageViewerWindow(QMainWindow):
3099
3141
 
3100
3142
  if not hasattr(self, 'zoom_changed'):
3101
3143
  self.zoom_changed = False
3102
-
3144
+
3103
3145
  self.canvas.draw()
3104
3146
 
3105
3147
  # Handle brush mode cleanup with paint session management
3106
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
+
3107
3154
  # Finish current operation
3155
+ self.pm.finish_current_stroke()
3108
3156
  self.pm.finish_current_virtual_operation()
3109
3157
 
3110
3158
  # Reset last position for next stroke
3111
- self.last_virtual_pos = None
3159
+ #self.last_virtual_pos = None
3112
3160
 
3113
3161
  # End this stroke but keep session active for continuous painting
3114
3162
  self.painting = False
3115
3163
 
3116
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3117
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3118
-
3119
- self.update_display(preserve_zoom=(current_xlim, current_ylim), continue_paint = True)
3120
-
3121
- if self.machine_window is not None:
3122
- if self.machine_window.segmentation_worker is not None:
3123
- self.machine_window.segmentation_worker.resume()
3124
3164
 
3165
+ if self.resume:
3166
+ self.machine_window.segmentation_worker.resume()
3167
+ self.resume = False
3125
3168
 
3126
3169
 
3127
3170
  def highlight_value_in_tables(self, clicked_value):
@@ -3243,7 +3286,6 @@ class ImageViewerWindow(QMainWindow):
3243
3286
 
3244
3287
  self.zoom_changed = False # Flag that zoom has changed
3245
3288
 
3246
-
3247
3289
 
3248
3290
  self.canvas.draw()
3249
3291
 
@@ -5089,6 +5131,14 @@ class ImageViewerWindow(QMainWindow):
5089
5131
  """Actually perform the slice update after debounce delay."""
5090
5132
  if self.pending_slice is not None:
5091
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()
5092
5142
  self.current_slice = slice_value
5093
5143
  if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
5094
5144
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
@@ -5112,7 +5162,7 @@ class ImageViewerWindow(QMainWindow):
5112
5162
 
5113
5163
 
5114
5164
 
5115
- 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):
5116
5166
  """Update the display with currently visible channels and highlight overlay."""
5117
5167
  try:
5118
5168
  self.figure.clear()
@@ -5121,9 +5171,9 @@ class ImageViewerWindow(QMainWindow):
5121
5171
  self.channel_visible = self.pre_pan_channel_state.copy()
5122
5172
  self.is_pan_preview = False
5123
5173
  self.pan_background_image = None
5124
- if self.machine_window is not None:
5125
- if self.machine_window.segmentation_worker is not None:
5126
- self.machine_window.segmentation_worker.resume()
5174
+ if self.resume:
5175
+ self.machine_window.segmentation_worker.resume()
5176
+ self.resume = False
5127
5177
  if self.static_background is not None:
5128
5178
  # NEW: Convert virtual strokes to real data before cleanup
5129
5179
  if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
@@ -5142,17 +5192,17 @@ class ImageViewerWindow(QMainWindow):
5142
5192
  self.restore_channels = []
5143
5193
  except:
5144
5194
  pass
5145
- if not continue_paint:
5146
- self.static_background = None
5147
5195
 
5148
- if self.machine_window is None:
5149
- try:
5150
- 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, :, :])
5151
- self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
5152
- self.channel_data[4] = None
5153
- self.channel_visible[4] = False
5154
- except:
5155
- 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
5156
5206
 
5157
5207
  # Get active channels and their dimensions
5158
5208
  active_channels = [i for i in range(4) if self.channel_data[i] is not None]
@@ -5383,17 +5433,6 @@ class ImageViewerWindow(QMainWindow):
5383
5433
 
5384
5434
  self.canvas.draw()
5385
5435
 
5386
- if self.brush_mode and not skip_paint_reinit:
5387
- # Get current zoom to preserve it
5388
- current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
5389
- current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
5390
-
5391
- if self.pen_button.isChecked():
5392
- channel = self.active_channel
5393
- else:
5394
- channel = 2
5395
-
5396
- self.pm.initiate_paint_session(channel, current_xlim, current_ylim)
5397
5436
 
5398
5437
  except:
5399
5438
  import traceback
@@ -9605,6 +9644,8 @@ class MachineWindow(QMainWindow):
9605
9644
  self.fore_button.click()
9606
9645
  self.fore_button.click()
9607
9646
 
9647
+ self.num_chunks = 0
9648
+
9608
9649
  except:
9609
9650
  return
9610
9651
 
@@ -9747,18 +9788,6 @@ class MachineWindow(QMainWindow):
9747
9788
 
9748
9789
  self.parent().pm = painting.PaintManager(parent = self.parent())
9749
9790
 
9750
- # Start virtual paint session
9751
- # Get current zoom to preserve it
9752
- current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
9753
- current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
9754
-
9755
- if self.parent().pen_button.isChecked():
9756
- channel = self.parent().active_channel
9757
- else:
9758
- channel = 2
9759
-
9760
- self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
9761
-
9762
9791
  self.parent().pan_button.setChecked(False)
9763
9792
  self.parent().zoom_button.setChecked(False)
9764
9793
  if self.parent().pan_mode:
@@ -9786,6 +9815,15 @@ class MachineWindow(QMainWindow):
9786
9815
  # Wait a bit for cleanup
9787
9816
  time.sleep(0.1)
9788
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
+
9789
9827
  self.previewing = True
9790
9828
  try:
9791
9829
  try:
@@ -9829,7 +9867,7 @@ class MachineWindow(QMainWindow):
9829
9867
  return
9830
9868
  else:
9831
9869
  self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
9832
- 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
9833
9871
  current_xlim = self.parent().ax.get_xlim()
9834
9872
  current_ylim = self.parent().ax.get_ylim()
9835
9873
  try:
@@ -9882,7 +9920,7 @@ class MachineWindow(QMainWindow):
9882
9920
 
9883
9921
  return changed
9884
9922
 
9885
- def update_display(self, skip_paint_reinit = False):
9923
+ def update_display(self):
9886
9924
  if not hasattr(self, '_last_update'):
9887
9925
  self._last_update = 0
9888
9926
 
@@ -9892,6 +9930,7 @@ class MachineWindow(QMainWindow):
9892
9930
 
9893
9931
  self._last_z = current_z
9894
9932
 
9933
+ self.num_chunks += 1
9895
9934
 
9896
9935
  current_time = time.time()
9897
9936
  if current_time - self._last_update >= 1: # Match worker's interval
@@ -9908,18 +9947,7 @@ class MachineWindow(QMainWindow):
9908
9947
 
9909
9948
  if not self.parent().painting:
9910
9949
  # Only update if view limits are valid
9911
- self.parent().update_display(preserve_zoom=(current_xlim, current_ylim), skip_paint_reinit = skip_paint_reinit)
9912
-
9913
- if self.parent().brush_mode:
9914
- current_xlim = self.parent().ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
9915
- current_ylim = self.parent().ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
9916
-
9917
- if self.parent().pen_button.isChecked():
9918
- channel = self.parent().active_channel
9919
- else:
9920
- channel = 2
9921
-
9922
- self.parent().pm.initiate_paint_session(channel, current_xlim, current_ylim)
9950
+ self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
9923
9951
 
9924
9952
  self._last_update = current_time
9925
9953
  except Exception as e:
@@ -10110,6 +10138,8 @@ class SegmentationWorker(QThread):
10110
10138
  current_time = time.time()
10111
10139
  if (self.chunks_since_update >= self.chunks_per_update and
10112
10140
  current_time - self.last_update >= self.update_interval):
10141
+ if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000: #arbitrary throttle for large arrays.
10142
+ self.msleep(3000)
10113
10143
  self.chunk_processed.emit()
10114
10144
  self.chunks_since_update = 0
10115
10145
  self.last_update = current_time
nettracer3d/painting.py CHANGED
@@ -7,6 +7,26 @@ import numpy as np
7
7
  class PaintManager(QMainWindow):
8
8
  def __init__(self, parent = None):
9
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
10
30
 
11
31
  def get_line_points(self, x0, y0, x1, y1):
12
32
  """Get all points in a line between (x0,y0) and (x1,y1) using Bresenham's algorithm."""
@@ -44,7 +64,8 @@ class PaintManager(QMainWindow):
44
64
 
45
65
  if self.parent().machine_window is not None:
46
66
  if self.parent().machine_window.segmentation_worker is not None:
47
- # Instead of just pausing, completely stop and clean up the worker
67
+ if not self.parent().machine_window.segmentation_worker._paused:
68
+ self.resume = True
48
69
  self.parent().machine_window.segmentation_worker.pause()
49
70
 
50
71
 
@@ -54,10 +75,9 @@ class PaintManager(QMainWindow):
54
75
  # Capture the background once
55
76
  self.parent().static_background = self.parent().canvas.copy_from_bbox(self.parent().ax.bbox)
56
77
 
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()
78
+ if self.resume:
79
+ self.parent().machine_window.segmentation_worker.resume()
80
+ self.resume = False
61
81
 
62
82
 
63
83
 
@@ -70,11 +90,35 @@ class PaintManager(QMainWindow):
70
90
  if not self.parent().channel_visible[channel]:
71
91
  self.parent().channel_visible[channel] = True
72
92
 
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
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 = []
76
118
  self.parent().current_operation = []
77
- self.parent().current_operation_type = None # 'draw' or 'erase'
119
+ self.parent().current_operation_type = None
120
+
121
+
78
122
 
79
123
  def add_virtual_paint_point(self, x, y, brush_size, erase=False, foreground=True):
80
124
  """Add a single paint point to the virtual layer."""
@@ -109,11 +153,19 @@ class PaintManager(QMainWindow):
109
153
  'threedthresh': getattr(self.parent(), 'threedthresh', 1)
110
154
  }
111
155
 
112
- # Create visual circle
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
113
165
  circle = plt.Circle((x, y), brush_size/2,
114
166
  color=paint_color, alpha=alpha, animated=True)
115
167
 
116
- # Add to current operation
168
+ # Add to display operations (OLD - for visual display)
117
169
  if self.parent().current_operation_type != operation_type:
118
170
  # Finish previous operation if switching between draw/erase
119
171
  self.finish_current_virtual_operation()
@@ -126,6 +178,23 @@ class PaintManager(QMainWindow):
126
178
 
127
179
  self.parent().ax.add_patch(circle)
128
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
+
129
198
  def add_virtual_paint_stroke(self, x, y, brush_size, erase=False, foreground=True):
130
199
  """Add a paint stroke - simple visual, interpolation happens during data conversion."""
131
200
  # Just add the current point for visual display (no interpolation yet)
@@ -134,22 +203,119 @@ class PaintManager(QMainWindow):
134
203
  # Store the last position for data conversion later
135
204
  self.parent().last_virtual_pos = (x, y)
136
205
 
137
- def finish_current_virtual_operation(self):
138
- """Finish the current operation (draw or erase) and add it to the appropriate list."""
206
+ def connect_virtual_paint_points(self):
207
+ """Connect points with lines matching the circle size by converting to screen coordinates."""
139
208
 
140
- if not self.parent().current_operation:
209
+ if not hasattr(self.parent(), 'current_operation') or len(self.parent().current_operation) < 2:
141
210
  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
211
 
148
- self.parent().current_operation = []
149
- self.parent().current_operation_type = None
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
150
316
 
151
317
  def update_virtual_paint_display(self):
152
- """Update display with virtual paint strokes - super fast like selection rectangle."""
318
+ """Update display with virtual paint strokes - handles different object types."""
153
319
  if not hasattr(self.parent(), 'static_background') or self.parent().static_background is None:
154
320
  return
155
321
 
@@ -160,108 +326,118 @@ class PaintManager(QMainWindow):
160
326
  for operation_list in [self.parent().virtual_draw_operations, self.parent().virtual_erase_operations]:
161
327
  for operation in operation_list:
162
328
  for item in operation:
163
- self.parent().ax.draw_artist(item['circle'])
329
+ self._draw_virtual_item(item)
164
330
 
165
331
  # Draw current operation being painted
166
332
  if hasattr(self.parent(), 'current_operation'):
167
333
  for item in self.parent().current_operation:
168
- self.parent().ax.draw_artist(item['circle'])
334
+ self._draw_virtual_item(item)
169
335
 
170
336
  # Blit everything at once
171
337
  self.parent().canvas.blit(self.parent().ax.bbox)
172
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
+
173
358
  def convert_virtual_strokes_to_data(self):
174
- """Convert virtual paint strokes to actual array data with interpolation applied here."""
359
+ """Convert each stroke separately to actual array data using ONLY the new stroke tracking system."""
175
360
 
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
361
+ # Finish the current stroke first
362
+ self.finish_current_stroke()
213
363
 
214
- # Then, apply all erase operations with interpolation (same changes)
215
- for operation in self.parent().virtual_erase_operations:
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
216
373
  last_pos = None
217
- for item in operation:
218
- data = item['data']
219
- current_pos = (data['x'], data['y'])
374
+ for point_data in stroke_points:
375
+ current_pos = (point_data['x'], point_data['y'])
220
376
 
221
377
  if last_pos is not None:
378
+ # Interpolate between consecutive points in this stroke
222
379
  points = self.get_line_points(last_pos[0], last_pos[1], current_pos[0], current_pos[1])
223
380
  for px, py in points:
224
381
  self.paint_at_position_vectorized(
225
382
  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'],
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'],
232
389
  machine_window=self.parent().machine_window
233
390
  )
234
391
  else:
392
+ # First point in stroke
235
393
  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'],
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'],
243
401
  machine_window=self.parent().machine_window
244
402
  )
245
403
 
246
404
  last_pos = current_pos
247
- try:
248
- item['circle'].remove()
249
- except:
250
- pass
251
405
 
252
- # Clean up
253
- self.parent().virtual_draw_operations = []
254
- self.parent().virtual_erase_operations = []
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
+
255
420
  if hasattr(self.parent(), 'current_operation'):
256
421
  for item in self.parent().current_operation:
257
422
  try:
258
- item['circle'].remove()
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()
259
429
  except:
260
430
  pass
261
- self.parent().current_operation = []
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 = []
262
439
  self.parent().current_operation_type = None
263
440
 
264
-
265
441
  def end_virtual_paint_session(self):
266
442
  """Convert virtual paint to actual array modifications when exiting paint mode."""
267
443
  if not hasattr(self.parent(), 'virtual_paint_strokes'):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.8.5
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.5 Updates --
113
+ -- Version 0.8.7 Updates --
114
114
 
115
115
  * See Documentation Once Updated
@@ -6,20 +6,20 @@ nettracer3d/modularity.py,sha256=O9OeKbjD3v6gSFz9K2GzP6LsxlpQaPfeJbM1pyIEigw,217
6
6
  nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
7
7
  nettracer3d/neighborhoods.py,sha256=VWubD5CBu9aNPhUea7FbAk9aTOq0FLKR9y-1VT7YkAc,39677
8
8
  nettracer3d/nettracer.py,sha256=TEV-nDmkcGP3UjWEor1LtEwm5mFBQu2nB0VRyz9Lt08,253649
9
- nettracer3d/nettracer_gui.py,sha256=Uf8a9DkK475sAwOq45S1okuiQtVwU7_Ovv8NuWDQ99M,538823
9
+ nettracer3d/nettracer_gui.py,sha256=yrWkpWVt7IvgGFkvc8tbMWy8t6O8P_2ulOKW-eOe9B4,540425
10
10
  nettracer3d/network_analysis.py,sha256=kBzsVaq4dZkMe0k-VGvQIUvM-tK0ZZ8bvb-wtsugZRQ,46150
11
11
  nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
12
12
  nettracer3d/node_draw.py,sha256=kZcR1PekLg0riioNeGcALIXQyZ5PtHA_9MT6z7Zovdk,10401
13
- nettracer3d/painting.py,sha256=_8QyiDhU0T4B6virPxfm1HGo-XGgDTavuQx_pbs5_Tc,15916
13
+ nettracer3d/painting.py,sha256=K_dwngivw80r-Yyg4btKMsWGn566ZE9PnrQl986uxJE,23497
14
14
  nettracer3d/proximity.py,sha256=bTaucn_InQ-v1GIk8ug-dXvDhIO59rnBMl5nIwAmNyw,35335
15
15
  nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
16
16
  nettracer3d/segmenter.py,sha256=O3xjCimPwoL8LM1w4cKVTB7saY-UptFuYC8qOIo3iWg,61637
17
17
  nettracer3d/segmenter_GPU.py,sha256=3CJLXCiySZP2dJbkpfBoXwAYbV4TnvIYAm6oQv-T-y4,63479
18
18
  nettracer3d/simple_network.py,sha256=dkG4jpc4zzdeuoaQobgGfL3PNo6N8dGKQ5hEEubFIvA,9947
19
19
  nettracer3d/smart_dilate.py,sha256=TvRUh6B4q4zIdCO1BWH-xgTdND5OUNmo99eyxG9oIAU,27145
20
- nettracer3d-0.8.5.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
21
- nettracer3d-0.8.5.dist-info/METADATA,sha256=AiCE19SzTrqneRhGlwFase_EbjCaiLTn0FOGDmHIN0Q,7008
22
- nettracer3d-0.8.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- nettracer3d-0.8.5.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
24
- nettracer3d-0.8.5.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
25
- nettracer3d-0.8.5.dist-info/RECORD,,
20
+ nettracer3d-0.8.7.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
21
+ nettracer3d-0.8.7.dist-info/METADATA,sha256=6Q68XlohpUOpHNK6V7doYlQ4R9twu5D9qoJNwoyfs48,7008
22
+ nettracer3d-0.8.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ nettracer3d-0.8.7.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
24
+ nettracer3d-0.8.7.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
25
+ nettracer3d-0.8.7.dist-info/RECORD,,