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.
- {nettracer3d-0.8.6/src/nettracer3d.egg-info → nettracer3d-0.8.7}/PKG-INFO +2 -2
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/README.md +1 -1
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/pyproject.toml +1 -1
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/nettracer_gui.py +75 -71
- nettracer3d-0.8.7/src/nettracer3d/painting.py +549 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7/src/nettracer3d.egg-info}/PKG-INFO +2 -2
- nettracer3d-0.8.6/src/nettracer3d/painting.py +0 -375
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/LICENSE +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/setup.cfg +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/cellpose_manager.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/excelotron.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/neighborhoods.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/nettracer.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/segmenter.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/segmenter_GPU.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-0.8.6 → nettracer3d-0.8.7}/src/nettracer3d.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
68
|
+
-- Version 0.8.7 Updates --
|
|
69
69
|
|
|
70
70
|
* See Documentation Once Updated
|
|
@@ -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
|
-
|
|
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
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
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(
|
|
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
|
|
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)
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|