nettracer3d 0.5.9__py3-none-any.whl → 0.6.1__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.
@@ -840,7 +840,7 @@ class ImageViewerWindow(QMainWindow):
840
840
  select_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True))
841
841
  select_both.triggered.connect(lambda: self.handle_select_all(edges = True))
842
842
  select_edges.triggered.connect(lambda: self.handle_select_all(edges = True, nodes = False))
843
- if self.highlight_overlay is not None:
843
+ if self.highlight_overlay is not None or self.mini_overlay_data is not None:
844
844
  highlight_select = context_menu.addAction("Add highlight in network selection")
845
845
  highlight_select.triggered.connect(self.handle_highlight_select)
846
846
  show_remove_menu.triggered.connect(self.handle_remove_points)
@@ -1259,19 +1259,28 @@ class ImageViewerWindow(QMainWindow):
1259
1259
  nodes = list(np.unique(my_network.nodes))
1260
1260
  if nodes[0] == 0:
1261
1261
  del nodes[0]
1262
+ num = (self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2])
1263
+ print(f"Found {len(nodes)} node objects")
1262
1264
  else:
1263
1265
  nodes = []
1264
1266
  if edges:
1265
1267
  edges = list(np.unique(my_network.edges))
1268
+ num = (self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2])
1266
1269
  if edges[0] == 0:
1267
1270
  del edges[0]
1271
+ print(f"Found {len(edges)} edge objects")
1268
1272
  else:
1269
1273
  edges = []
1270
1274
 
1271
- self.clicked_values['nodes'] += nodes
1272
- self.clicked_values['edges'] += edges
1275
+ self.clicked_values['nodes'] = nodes
1276
+ self.clicked_values['edges'] = edges
1273
1277
 
1274
- self.create_highlight_overlay(edge_indices = self.clicked_values['edges'], node_indices = self.clicked_values['nodes'])
1278
+
1279
+ if num > self.mini_thresh:
1280
+ self.mini_overlay = True
1281
+ self.create_mini_overlay(node_indices = nodes, edge_indices = edges)
1282
+ else:
1283
+ self.create_highlight_overlay(edge_indices = self.clicked_values['edges'], node_indices = self.clicked_values['nodes'])
1275
1284
 
1276
1285
  except Exception as e:
1277
1286
  print(f"Error: {e}")
@@ -2466,6 +2475,8 @@ class ImageViewerWindow(QMainWindow):
2466
2475
  searchoverlay_action.triggered.connect(self.show_search_dialog)
2467
2476
  shuffle_action = overlay_menu.addAction("Shuffle")
2468
2477
  shuffle_action.triggered.connect(self.show_shuffle_dialog)
2478
+ arbitrary_action = image_menu.addAction("Select Objects")
2479
+ arbitrary_action.triggered.connect(self.show_arbitrary_dialog)
2469
2480
  show3d_action = image_menu.addAction("Show 3D (Napari)")
2470
2481
  show3d_action.triggered.connect(self.show3d_dialog)
2471
2482
 
@@ -2596,6 +2607,11 @@ class ImageViewerWindow(QMainWindow):
2596
2607
  dialog = WatershedDialog(self)
2597
2608
  dialog.exec()
2598
2609
 
2610
+ def show_arbitrary_dialog(self):
2611
+ """Show the arbitrary selection dialog."""
2612
+ dialog = ArbitraryDialog(self)
2613
+ dialog.exec()
2614
+
2599
2615
  def show_invert_dialog(self):
2600
2616
  """Show the watershed parameter dialog."""
2601
2617
  dialog = InvertDialog(self)
@@ -3504,8 +3520,8 @@ class ImageViewerWindow(QMainWindow):
3504
3520
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3505
3521
  # Convert slider values (0-100) to data values (0-1)
3506
3522
  min_val, max_val = values
3507
- self.channel_brightness[channel_index]['min'] = min_val / 255 #Accomodate 32 bit data?
3508
- self.channel_brightness[channel_index]['max'] = max_val / 255
3523
+ self.channel_brightness[channel_index]['min'] = min_val / 65535 #Accomodate 32 bit data?
3524
+ self.channel_brightness[channel_index]['max'] = max_val / 65535
3509
3525
  self.update_display(preserve_zoom = (current_xlim, current_ylim))
3510
3526
 
3511
3527
 
@@ -3514,206 +3530,211 @@ class ImageViewerWindow(QMainWindow):
3514
3530
  def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False, begin_paint = False):
3515
3531
  """Update the display with currently visible channels and highlight overlay."""
3516
3532
 
3517
- if begin_paint:
3518
- # Store/update the static background with current zoom level
3519
- self.static_background = self.canvas.copy_from_bbox(self.ax.bbox)
3533
+ try:
3520
3534
 
3535
+ if begin_paint:
3536
+ # Store/update the static background with current zoom level
3537
+ self.static_background = self.canvas.copy_from_bbox(self.ax.bbox)
3521
3538
 
3522
- self.figure.clear()
3523
3539
 
3524
- # Get active channels and their dimensions
3525
- active_channels = [i for i in range(4) if self.channel_data[i] is not None]
3526
- if dims is None:
3527
- if active_channels:
3528
- dims = [(self.channel_data[i].shape[1:3] if len(self.channel_data[i].shape) >= 3 else
3529
- self.channel_data[i].shape) for i in active_channels]
3530
- min_height = min(d[0] for d in dims)
3531
- min_width = min(d[1] for d in dims)
3532
- else:
3533
- min_height = 1
3534
- min_width = 1
3535
- else:
3536
- min_height = dims[0]
3537
- min_width = dims[1]
3540
+ self.figure.clear()
3538
3541
 
3539
- # Set axes limits before displaying any images
3540
- self.ax.set_xlim(-0.5, min_width - 0.5)
3541
- self.ax.set_ylim(min_height - 0.5, -0.5)
3542
-
3543
- # Create subplot with tight layout and white figure background
3544
- self.figure.patch.set_facecolor('white')
3545
- self.ax = self.figure.add_subplot(111)
3546
-
3547
- # Store current zoom limits if they exist and weren't provided
3548
-
3549
- current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
3550
-
3551
- # Define base colors for each channel with increased intensity
3552
- base_colors = self.base_colors
3553
- # Set only the axes (image area) background to black
3554
- self.ax.set_facecolor('black')
3555
-
3556
- # Display each visible channel
3557
- for channel in range(4):
3558
- if (self.channel_visible[channel] and
3559
- self.channel_data[channel] is not None):
3560
-
3561
- # Check if we're dealing with RGB data
3562
- is_rgb = len(self.channel_data[channel].shape) == 4 and (self.channel_data[channel].shape[-1] == 3 or self.channel_data[channel].shape[-1] == 4)
3563
-
3564
- if len(self.channel_data[channel].shape) == 3 and not is_rgb:
3565
- current_image = self.channel_data[channel][self.current_slice, :, :]
3566
- elif is_rgb:
3567
- current_image = self.channel_data[channel][self.current_slice] # Already has RGB channels
3542
+ # Get active channels and their dimensions
3543
+ active_channels = [i for i in range(4) if self.channel_data[i] is not None]
3544
+ if dims is None:
3545
+ if active_channels:
3546
+ dims = [(self.channel_data[i].shape[1:3] if len(self.channel_data[i].shape) >= 3 else
3547
+ self.channel_data[i].shape) for i in active_channels]
3548
+ min_height = min(d[0] for d in dims)
3549
+ min_width = min(d[1] for d in dims)
3568
3550
  else:
3569
- current_image = self.channel_data[channel]
3570
-
3571
- if is_rgb and self.channel_data[channel].shape[-1] == 3:
3572
- # For RGB images, just display directly without colormap
3573
- self.ax.imshow(current_image,
3574
- alpha=0.7)
3575
- elif is_rgb and self.channel_data[channel].shape[-1] == 4:
3576
- self.ax.imshow(current_image) #For images that already have an alpha value and RGB, don't update alpha
3551
+ min_height = 1
3552
+ min_width = 1
3553
+ else:
3554
+ min_height = dims[0]
3555
+ min_width = dims[1]
3577
3556
 
3578
- else:
3579
- # Regular channel processing with colormap
3580
- # Calculate brightness/contrast limits from entire volume
3581
- img_min = self.min_max[channel][0]
3582
- img_max = self.min_max[channel][1]
3557
+ # Set axes limits before displaying any images
3558
+ self.ax.set_xlim(-0.5, min_width - 0.5)
3559
+ self.ax.set_ylim(min_height - 0.5, -0.5)
3560
+
3561
+ # Create subplot with tight layout and white figure background
3562
+ self.figure.patch.set_facecolor('white')
3563
+ self.ax = self.figure.add_subplot(111)
3564
+
3565
+ # Store current zoom limits if they exist and weren't provided
3566
+
3567
+ current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
3568
+
3569
+ # Define base colors for each channel with increased intensity
3570
+ base_colors = self.base_colors
3571
+ # Set only the axes (image area) background to black
3572
+ self.ax.set_facecolor('black')
3573
+
3574
+ # Display each visible channel
3575
+ for channel in range(4):
3576
+ if (self.channel_visible[channel] and
3577
+ self.channel_data[channel] is not None):
3583
3578
 
3584
- # Calculate vmin and vmax, ensuring we don't get a zero range
3585
- if img_min == img_max:
3586
- vmin = img_min
3587
- vmax = img_min + 1
3588
- else:
3589
- vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
3590
- vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
3579
+ # Check if we're dealing with RGB data
3580
+ is_rgb = len(self.channel_data[channel].shape) == 4 and (self.channel_data[channel].shape[-1] == 3 or self.channel_data[channel].shape[-1] == 4)
3591
3581
 
3592
- # Normalize the image safely
3593
- if vmin == vmax:
3594
- normalized_image = np.zeros_like(current_image)
3582
+ if len(self.channel_data[channel].shape) == 3 and not is_rgb:
3583
+ current_image = self.channel_data[channel][self.current_slice, :, :]
3584
+ elif is_rgb:
3585
+ current_image = self.channel_data[channel][self.current_slice] # Already has RGB channels
3595
3586
  else:
3596
- normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
3597
-
3598
- if channel == 2 and self.machine_window is not None:
3599
- custom_cmap = LinearSegmentedColormap.from_list(
3600
- f'custom_{channel}',
3601
- [(0, 0, 0, 0), # transparent for 0
3602
- (0.5, 1, 0.5, 1), # light green for 1
3603
- (1, 0.5, 0.5, 1)] # light red for 2
3604
- )
3587
+ current_image = self.channel_data[channel]
3588
+
3589
+ if is_rgb and self.channel_data[channel].shape[-1] == 3:
3590
+ # For RGB images, just display directly without colormap
3605
3591
  self.ax.imshow(current_image,
3606
- cmap=custom_cmap,
3607
- vmin=0,
3608
- vmax=2,
3609
- alpha=0.7,
3610
- interpolation='nearest',
3611
- extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
3592
+ alpha=0.7)
3593
+ elif is_rgb and self.channel_data[channel].shape[-1] == 4:
3594
+ self.ax.imshow(current_image) #For images that already have an alpha value and RGB, don't update alpha
3595
+
3612
3596
  else:
3613
- # Create custom colormap with higher intensity
3614
- color = base_colors[channel]
3615
- custom_cmap = LinearSegmentedColormap.from_list(
3616
- f'custom_{channel}',
3617
- [(0,0,0,0), (*color,1)]
3618
- )
3597
+ # Regular channel processing with colormap
3598
+ # Calculate brightness/contrast limits from entire volume
3599
+ img_min = self.min_max[channel][0]
3600
+ img_max = self.min_max[channel][1]
3619
3601
 
3620
- # Display the image with slightly higher alpha
3621
- self.ax.imshow(normalized_image,
3622
- alpha=0.7,
3623
- cmap=custom_cmap,
3624
- vmin=0,
3625
- vmax=1,
3626
- extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
3627
-
3628
- if self.preview and not called:
3629
- self.create_highlight_overlay_slice(self.targs, bounds = self.bounds)
3630
-
3631
- # Add highlight overlay if it exists
3632
- if self.mini_overlay and self.highlight and self.machine_window is None:
3633
- highlight_cmap = LinearSegmentedColormap.from_list(
3634
- 'highlight',
3635
- [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
3636
- )
3637
- self.ax.imshow(self.mini_overlay_data,
3638
- cmap=highlight_cmap,
3639
- alpha=0.5)
3640
- elif self.highlight_overlay is not None and self.highlight and self.machine_window is None:
3641
- highlight_slice = self.highlight_overlay[self.current_slice]
3642
- highlight_cmap = LinearSegmentedColormap.from_list(
3643
- 'highlight',
3644
- [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
3645
- )
3646
- self.ax.imshow(highlight_slice,
3647
- cmap=highlight_cmap,
3648
- alpha=0.5)
3649
- elif self.highlight_overlay is not None and self.highlight:
3650
- highlight_slice = self.highlight_overlay[self.current_slice]
3651
- highlight_cmap = LinearSegmentedColormap.from_list(
3652
- 'highlight',
3653
- [(0, 0, 0, 0), # transparent for 0
3654
- (1, 1, 0, 1), # bright yellow for 1
3655
- (0, 0.7, 1, 1)] # cool blue for 2
3656
- )
3657
- self.ax.imshow(highlight_slice,
3658
- cmap=highlight_cmap,
3659
- vmin=0,
3660
- vmax=2, # Important: set vmax to 2 to accommodate both values
3661
- alpha=0.5)
3662
-
3663
-
3664
-
3665
-
3666
- # Style the axes
3667
- self.ax.set_xlabel('X')
3668
- self.ax.set_ylabel('Y')
3669
- self.ax.set_title(f'Slice {self.current_slice}')
3670
-
3671
- # Make axis labels and ticks black for visibility against white background
3672
- self.ax.xaxis.label.set_color('black')
3673
- self.ax.yaxis.label.set_color('black')
3674
- self.ax.title.set_color('black')
3675
- self.ax.tick_params(colors='black')
3676
- for spine in self.ax.spines.values():
3677
- spine.set_color('black')
3678
-
3679
- # Adjust the layout to ensure the plot fits well in the figure
3680
- self.figure.tight_layout()
3681
-
3682
- # Redraw measurement points and their labels
3683
- for point in self.measurement_points:
3684
- x1, y1, z1 = point['point1']
3685
- x2, y2, z2 = point['point2']
3686
- pair_idx = point['pair_index']
3687
-
3688
- # Draw points and labels if they're on current slice
3689
- if z1 == self.current_slice:
3690
- self.ax.plot(x1, y1, 'yo', markersize=8)
3691
- self.ax.text(x1, y1+5, str(pair_idx),
3692
- color='white', ha='center', va='bottom')
3693
- if z2 == self.current_slice:
3694
- self.ax.plot(x2, y2, 'yo', markersize=8)
3695
- self.ax.text(x2, y2+5, str(pair_idx),
3696
- color='white', ha='center', va='bottom')
3697
-
3698
- # Draw line if both points are on current slice
3699
- if z1 == z2 == self.current_slice:
3700
- self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
3701
-
3702
- if active_channels:
3703
- self.ax.set_xlim(-0.5, min_width - 0.5)
3704
- self.ax.set_ylim(min_height - 0.5, -0.5)
3602
+ # Calculate vmin and vmax, ensuring we don't get a zero range
3603
+ if img_min == img_max:
3604
+ vmin = img_min
3605
+ vmax = img_min + 1
3606
+ else:
3607
+ vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
3608
+ vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
3609
+
3610
+ # Normalize the image safely
3611
+ if vmin == vmax:
3612
+ normalized_image = np.zeros_like(current_image)
3613
+ else:
3614
+ normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
3615
+
3616
+ if channel == 2 and self.machine_window is not None:
3617
+ custom_cmap = LinearSegmentedColormap.from_list(
3618
+ f'custom_{channel}',
3619
+ [(0, 0, 0, 0), # transparent for 0
3620
+ (0.5, 1, 0.5, 1), # light green for 1
3621
+ (1, 0.5, 0.5, 1)] # light red for 2
3622
+ )
3623
+ self.ax.imshow(current_image,
3624
+ cmap=custom_cmap,
3625
+ vmin=0,
3626
+ vmax=2,
3627
+ alpha=0.7,
3628
+ interpolation='nearest',
3629
+ extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
3630
+ else:
3631
+ # Create custom colormap with higher intensity
3632
+ color = base_colors[channel]
3633
+ custom_cmap = LinearSegmentedColormap.from_list(
3634
+ f'custom_{channel}',
3635
+ [(0,0,0,0), (*color,1)]
3636
+ )
3637
+
3638
+ # Display the image with slightly higher alpha
3639
+ self.ax.imshow(normalized_image,
3640
+ alpha=0.7,
3641
+ cmap=custom_cmap,
3642
+ vmin=0,
3643
+ vmax=1,
3644
+ extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
3645
+
3646
+ if self.preview and not called:
3647
+ self.create_highlight_overlay_slice(self.targs, bounds = self.bounds)
3648
+
3649
+ # Add highlight overlay if it exists
3650
+ if self.mini_overlay and self.highlight and self.machine_window is None:
3651
+ highlight_cmap = LinearSegmentedColormap.from_list(
3652
+ 'highlight',
3653
+ [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
3654
+ )
3655
+ self.ax.imshow(self.mini_overlay_data,
3656
+ cmap=highlight_cmap,
3657
+ alpha=0.5)
3658
+ elif self.highlight_overlay is not None and self.highlight and self.machine_window is None:
3659
+ highlight_slice = self.highlight_overlay[self.current_slice]
3660
+ highlight_cmap = LinearSegmentedColormap.from_list(
3661
+ 'highlight',
3662
+ [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
3663
+ )
3664
+ self.ax.imshow(highlight_slice,
3665
+ cmap=highlight_cmap,
3666
+ alpha=0.5)
3667
+ elif self.highlight_overlay is not None and self.highlight:
3668
+ highlight_slice = self.highlight_overlay[self.current_slice]
3669
+ highlight_cmap = LinearSegmentedColormap.from_list(
3670
+ 'highlight',
3671
+ [(0, 0, 0, 0), # transparent for 0
3672
+ (1, 1, 0, 1), # bright yellow for 1
3673
+ (0, 0.7, 1, 1)] # cool blue for 2
3674
+ )
3675
+ self.ax.imshow(highlight_slice,
3676
+ cmap=highlight_cmap,
3677
+ vmin=0,
3678
+ vmax=2, # Important: set vmax to 2 to accommodate both values
3679
+ alpha=0.5)
3705
3680
 
3706
- if self.resizing:
3707
- self.original_xlim = self.ax.get_xlim()
3708
- self.original_ylim = self.ax.get_ylim()
3709
- # Restore zoom limits if they existed
3710
- if current_xlim is not None and current_ylim is not None:
3711
- self.ax.set_xlim(current_xlim)
3712
- self.ax.set_ylim(current_ylim)
3713
- if reset_resize:
3714
- self.resizing = False
3681
+
3682
+
3683
+
3684
+ # Style the axes
3685
+ self.ax.set_xlabel('X')
3686
+ self.ax.set_ylabel('Y')
3687
+ self.ax.set_title(f'Slice {self.current_slice}')
3688
+
3689
+ # Make axis labels and ticks black for visibility against white background
3690
+ self.ax.xaxis.label.set_color('black')
3691
+ self.ax.yaxis.label.set_color('black')
3692
+ self.ax.title.set_color('black')
3693
+ self.ax.tick_params(colors='black')
3694
+ for spine in self.ax.spines.values():
3695
+ spine.set_color('black')
3696
+
3697
+ # Adjust the layout to ensure the plot fits well in the figure
3698
+ self.figure.tight_layout()
3699
+
3700
+ # Redraw measurement points and their labels
3701
+ for point in self.measurement_points:
3702
+ x1, y1, z1 = point['point1']
3703
+ x2, y2, z2 = point['point2']
3704
+ pair_idx = point['pair_index']
3705
+
3706
+ # Draw points and labels if they're on current slice
3707
+ if z1 == self.current_slice:
3708
+ self.ax.plot(x1, y1, 'yo', markersize=8)
3709
+ self.ax.text(x1, y1+5, str(pair_idx),
3710
+ color='white', ha='center', va='bottom')
3711
+ if z2 == self.current_slice:
3712
+ self.ax.plot(x2, y2, 'yo', markersize=8)
3713
+ self.ax.text(x2, y2+5, str(pair_idx),
3714
+ color='white', ha='center', va='bottom')
3715
+
3716
+ # Draw line if both points are on current slice
3717
+ if z1 == z2 == self.current_slice:
3718
+ self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
3715
3719
 
3716
- self.canvas.draw()
3720
+ if active_channels:
3721
+ self.ax.set_xlim(-0.5, min_width - 0.5)
3722
+ self.ax.set_ylim(min_height - 0.5, -0.5)
3723
+
3724
+ if self.resizing:
3725
+ self.original_xlim = self.ax.get_xlim()
3726
+ self.original_ylim = self.ax.get_ylim()
3727
+ # Restore zoom limits if they existed
3728
+ if current_xlim is not None and current_ylim is not None:
3729
+ self.ax.set_xlim(current_xlim)
3730
+ self.ax.set_ylim(current_ylim)
3731
+ if reset_resize:
3732
+ self.resizing = False
3733
+
3734
+ self.canvas.draw()
3735
+
3736
+ except:
3737
+ pass
3717
3738
 
3718
3739
  def update_display_slice(self, channel, preserve_zoom=None):
3719
3740
  """Ultra minimal update that only changes the paint channel's data"""
@@ -4631,14 +4652,14 @@ class BrightnessContrastDialog(QDialog):
4631
4652
  # Create range slider
4632
4653
  slider = QRangeSlider(Qt.Orientation.Horizontal)
4633
4654
  slider.setMinimum(0)
4634
- slider.setMaximum(255)
4635
- slider.setValue((0, 255))
4655
+ slider.setMaximum(65535)
4656
+ slider.setValue((0, 65535))
4636
4657
  self.brightness_sliders.append(slider)
4637
4658
 
4638
4659
  # Create max value input
4639
4660
  max_input = QLineEdit()
4640
4661
  max_input.setFixedWidth(50)
4641
- max_input.setText("255")
4662
+ max_input.setText("65535")
4642
4663
  self.max_inputs.append(max_input)
4643
4664
 
4644
4665
  # Add all components to slider container
@@ -4687,8 +4708,8 @@ class BrightnessContrastDialog(QDialog):
4687
4708
  max_val = self.parse_input_value(self.max_inputs[channel].text())
4688
4709
  current_min, current_max = self.brightness_sliders[channel].value()
4689
4710
 
4690
- if max_val > 255:
4691
- max_val = 255
4711
+ if max_val > 65535:
4712
+ max_val = 65535
4692
4713
  # Ensure max doesn't go below min
4693
4714
  max_val = max(max_val, current_min + 1)
4694
4715
 
@@ -4708,8 +4729,8 @@ class BrightnessContrastDialog(QDialog):
4708
4729
  value = float(text)
4709
4730
  # Round to nearest integer
4710
4731
  value = int(round(value))
4711
- # Clamp between 0 and 255
4712
- return max(0, min(255, value))
4732
+ # Clamp between 0 and 65535
4733
+ return max(0, min(65535, value))
4713
4734
  except ValueError:
4714
4735
  raise ValueError("Invalid input")
4715
4736
 
@@ -4756,7 +4777,168 @@ class ColorDialog(QDialog):
4756
4777
 
4757
4778
  # Update the display
4758
4779
  self.parent().update_display()
4759
- self.accept()
4780
+ self.accept()
4781
+
4782
+ class ArbitraryDialog(QDialog):
4783
+ def __init__(self, parent=None):
4784
+ super().__init__(parent)
4785
+ self.setWindowTitle("Arbitrary Selector")
4786
+ self.setModal(True)
4787
+
4788
+ # Main layout
4789
+ main_layout = QVBoxLayout(self)
4790
+
4791
+ # Form layout for inputs
4792
+ layout = QFormLayout()
4793
+ main_layout.addLayout(layout)
4794
+
4795
+ self.mode_selector = QComboBox()
4796
+ self.mode_selector.addItems(["nodes", "edges"])
4797
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
4798
+ layout.addRow("Type to select:", self.mode_selector)
4799
+
4800
+ # Selection section
4801
+ excel_button = QPushButton("Import selection from spreadsheet (Col 1)")
4802
+ excel_button.clicked.connect(self.import_excel)
4803
+ layout.addWidget(excel_button)
4804
+
4805
+ self.select = QLineEdit("")
4806
+ layout.addRow("Select the following? (Use this format - '1,2,3,4' etc:", self.select)
4807
+
4808
+ # Deselection section
4809
+ deexcel_button = QPushButton("Import deselection from spreadsheet (Col 1)")
4810
+ deexcel_button.clicked.connect(self.import_deexcel)
4811
+ layout.addWidget(deexcel_button)
4812
+
4813
+ self.deselect = QLineEdit("")
4814
+ layout.addRow("Deselect the following? (Use this format - '1,2,3,4' etc:", self.deselect)
4815
+
4816
+ # Run button
4817
+ run_button = QPushButton("Run")
4818
+ run_button.clicked.connect(self.process_selections)
4819
+ main_layout.addWidget(run_button)
4820
+
4821
+ def import_excel(self):
4822
+ """Import selection from Excel/CSV and populate the select QLineEdit."""
4823
+ file_path, _ = QFileDialog.getOpenFileName(
4824
+ self, "Select File", "", "Spreadsheet Files (*.xlsx *.xls *.csv)"
4825
+ )
4826
+
4827
+ if file_path:
4828
+ try:
4829
+ selection_list = self.read_selection_from_file(file_path)
4830
+ selection_string = ",".join(map(str, selection_list))
4831
+ self.select.setText(selection_string)
4832
+ except Exception as e:
4833
+ QMessageBox.critical(self, "Error", f"Failed to import: {str(e)}")
4834
+
4835
+ def import_deexcel(self):
4836
+ """Import deselection from Excel/CSV and populate the deselect QLineEdit."""
4837
+ file_path, _ = QFileDialog.getOpenFileName(
4838
+ self, "Select File", "", "Spreadsheet Files (*.xlsx *.xls *.csv)"
4839
+ )
4840
+
4841
+ if file_path:
4842
+ try:
4843
+ deselection_list = self.read_selection_from_file(file_path)
4844
+ deselection_string = ",".join(map(str, deselection_list))
4845
+ self.deselect.setText(deselection_string)
4846
+ except Exception as e:
4847
+ QMessageBox.critical(self, "Error", f"Failed to import: {str(e)}")
4848
+
4849
+ def read_selection_from_file(self, file_path):
4850
+ """Read selection IDs from Excel/CSV file and return as a list."""
4851
+ # Determine file type and read accordingly
4852
+ if file_path.lower().endswith('.csv'):
4853
+ # Read CSV file
4854
+ df = pd.read_csv(file_path, header=None)
4855
+ else:
4856
+ # Read Excel file
4857
+ df = pd.read_excel(file_path, header=None)
4858
+
4859
+ # Check if first row looks like a header
4860
+ first_row = df.iloc[0]
4861
+ if all(isinstance(x, str) for x in first_row):
4862
+ # First row is likely a header, skip it
4863
+ values = df.iloc[1:, 0].dropna().tolist()
4864
+ else:
4865
+ # No header, use all rows
4866
+ values = df.iloc[:, 0].dropna().tolist()
4867
+
4868
+ # Convert to integers when possible, keep floats when necessary
4869
+ processed_values = []
4870
+ for val in values:
4871
+ try:
4872
+ # Try to convert to int first
4873
+ processed_values.append(int(val))
4874
+ except ValueError:
4875
+ try:
4876
+ # If int fails, try float
4877
+ processed_values.append(float(val))
4878
+ except ValueError:
4879
+ # Skip values that can't be converted to numbers
4880
+ continue
4881
+
4882
+ return processed_values
4883
+
4884
+ def process_selections(self):
4885
+ """Process the selection and deselection inputs."""
4886
+ try:
4887
+ from ast import literal_eval
4888
+ # Get values from QLineEdit fields
4889
+ select_text = self.select.text()
4890
+ deselect_text = self.deselect.text()
4891
+
4892
+ # Format text for literal_eval by adding brackets
4893
+ if select_text:
4894
+ select_list = literal_eval(f"[{select_text}]")
4895
+ else:
4896
+ select_list = []
4897
+
4898
+ if deselect_text:
4899
+ deselect_list = literal_eval(f"[{deselect_text}]")
4900
+ else:
4901
+ deselect_list = []
4902
+
4903
+ # Get the current mode
4904
+ mode = self.mode_selector.currentText()
4905
+
4906
+ if mode == 'nodes':
4907
+ num = self.parent().channel_data[0].shape[0] * self.parent().channel_data[0].shape[1] * self.parent().channel_data[0].shape[2]
4908
+ else:
4909
+ num = self.parent().channel_data[1].shape[0] * self.parent().channel_data[1].shape[1] * self.parent().channel_data[1].shape[2]
4910
+
4911
+
4912
+ for item in deselect_list:
4913
+ try:
4914
+ self.parent().clicked_values[mode].remove(item)
4915
+ except:
4916
+ pass #Forgive mistakes
4917
+
4918
+ for item in select_list:
4919
+ try:
4920
+ self.parent().clicked_values[mode].append(item)
4921
+ except:
4922
+ pass
4923
+
4924
+ self.parent().clicked_values[mode] = list(set(self.parent().clicked_values[mode]))
4925
+
4926
+ if num > self.parent().mini_thresh:
4927
+ self.parent().mini_overlay = True
4928
+ self.parent().create_mini_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
4929
+ else:
4930
+ self.parent().create_highlight_overlay(
4931
+ node_indices=self.parent().clicked_values['nodes'],
4932
+ edge_indices=self.parent().clicked_values['edges']
4933
+ )
4934
+
4935
+
4936
+
4937
+ # Close the dialog after processing
4938
+ self.accept()
4939
+
4940
+ except Exception as e:
4941
+ QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
4760
4942
 
4761
4943
  class Show3dDialog(QDialog):
4762
4944
  def __init__(self, parent=None):
@@ -4818,7 +5000,7 @@ class Show3dDialog(QDialog):
4818
5000
  if visible:
4819
5001
  arrays_4d.append(channel)
4820
5002
 
4821
- if self.parent().highlight_overlay is not None:
5003
+ if self.parent().highlight_overlay is not None or self.parent().mini_overlay_data is not None:
4822
5004
  if self.parent().mini_overlay == True:
4823
5005
  self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
4824
5006
  arrays_3d.append(self.parent().highlight_overlay)
@@ -5048,19 +5230,24 @@ class ShuffleDialog(QDialog):
5048
5230
  target_data = self.parent().channel_data[accepted_target]
5049
5231
 
5050
5232
 
5051
- if accepted_mode == 4:
5052
-
5053
- self.parent().highlight_overlay = n3d.binarize(target_data)
5054
- else:
5055
- self.parent().load_channel(accepted_mode, channel_data = target_data, data = True)
5233
+ try:
5234
+ if accepted_mode == 4:
5056
5235
 
5236
+ self.parent().highlight_overlay = n3d.binarize(target_data)
5237
+ else:
5238
+ self.parent().load_channel(accepted_mode, channel_data = target_data, data = True)
5239
+ except:
5240
+ pass
5057
5241
 
5058
5242
 
5059
- if accepted_target == 4:
5060
- try:
5061
- self.parent().highlight_overlay = n3d.binarize(active_data)
5062
- except:
5063
- self.parent().highlight_overlay = None
5243
+ try:
5244
+ if accepted_target == 4:
5245
+ try:
5246
+ self.parent().highlight_overlay = n3d.binarize(active_data)
5247
+ except:
5248
+ self.parent().highlight_overlay = None
5249
+ except:
5250
+ pass
5064
5251
 
5065
5252
  else:
5066
5253
  self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
@@ -5862,6 +6049,9 @@ class ResizeDialog(QDialog):
5862
6049
 
5863
6050
 
5864
6051
  # Process highlight overlay if it exists
6052
+ if self.parent().mini_overlay_data is not None:
6053
+ self.parent().create_highlight_overlay(self.parent().clicked_values['nodes'], self.parent().clicked_values['edges'])
6054
+
5865
6055
  if self.parent().highlight_overlay is not None:
5866
6056
  self.parent().highlight_overlay = n3d.resize(self.parent().highlight_overlay, resize, order)
5867
6057
  if my_network.search_region is not None:
@@ -5877,6 +6067,10 @@ class ResizeDialog(QDialog):
5877
6067
  resized_data = n3d.upsample_with_padding(self.parent().channel_data[channel], original_shape = self.parent().original_shape)
5878
6068
  self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
5879
6069
 
6070
+ if self.parent().mini_overlay_data is not None:
6071
+
6072
+ self.parent().create_highlight_overlay(self.parent().clicked_values['nodes'], self.parent().clicked_values['edges'])
6073
+
5880
6074
 
5881
6075
  # Process highlight overlay if it exists
5882
6076
  if self.parent().highlight_overlay is not None:
@@ -6105,6 +6299,9 @@ class ThresholdDialog(QDialog):
6105
6299
  if self.parent().volume_dict[self.parent().active_channel] is None:
6106
6300
  self.parent().volumes()
6107
6301
 
6302
+ if self.parent().mini_overlay_data is not None:
6303
+ self.parent().mini_overlay_data = None
6304
+
6108
6305
  thresh_window = ThresholdWindow(self.parent(), accepted_mode)
6109
6306
  thresh_window.show() # Non-modal window
6110
6307
  self.highlight_overlay = None
@@ -6128,6 +6325,8 @@ class ThresholdDialog(QDialog):
6128
6325
  )
6129
6326
  return
6130
6327
 
6328
+ if self.parent().mini_overlay_data is not None:
6329
+ self.parent().mini_overlay_data = None
6131
6330
 
6132
6331
  self.parent().machine_window = MachineWindow(self.parent())
6133
6332
  self.parent().machine_window.show() # Non-modal window
@@ -6240,7 +6439,7 @@ class MachineWindow(QMainWindow):
6240
6439
  # Group 2: Processing Options (GPU)
6241
6440
  processing_group = QGroupBox("Processing Options")
6242
6441
  processing_layout = QHBoxLayout()
6243
- self.GPU = QPushButton("GPU")
6442
+ self.GPU = QPushButton("GPU (Beta)")
6244
6443
  self.GPU.setCheckable(True)
6245
6444
  self.GPU.setChecked(False)
6246
6445
  self.GPU.clicked.connect(self.toggle_GPU)
@@ -6276,9 +6475,15 @@ class MachineWindow(QMainWindow):
6276
6475
  seg_button = QPushButton("Preview Segment")
6277
6476
  self.seg_button = seg_button
6278
6477
  seg_button.clicked.connect(self.start_segmentation)
6478
+ self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM. Recommended unless you have a lot)")
6479
+ self.lock_button.setCheckable(True)
6480
+ self.lock_button.setChecked(True)
6481
+ self.lock_button.clicked.connect(self.toggle_lock)
6482
+ self.mem_lock = True
6279
6483
  full_button = QPushButton("Segment All")
6280
6484
  full_button.clicked.connect(self.segment)
6281
6485
  segmentation_layout.addWidget(seg_button)
6486
+ segmentation_layout.addWidget(self.lock_button)
6282
6487
  segmentation_layout.addWidget(full_button)
6283
6488
  segmentation_group.setLayout(segmentation_layout)
6284
6489
 
@@ -6298,6 +6503,9 @@ class MachineWindow(QMainWindow):
6298
6503
  self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=True)
6299
6504
  self.segmentation_worker = None
6300
6505
 
6506
+ def toggle_lock(self):
6507
+
6508
+ self.mem_lock = self.lock_button.isChecked()
6301
6509
 
6302
6510
 
6303
6511
  def toggle_GPU(self):
@@ -6377,13 +6585,16 @@ class MachineWindow(QMainWindow):
6377
6585
  if not self.use_two:
6378
6586
  self.previewing = False
6379
6587
  try:
6380
- self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu, use_two = self.use_two)
6381
- self.trained = True
6588
+ try:
6589
+ self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu, use_two = self.use_two, mem_lock = self.mem_lock)
6590
+ self.trained = True
6591
+ except Exception as e:
6592
+ print("Error training. Perhaps you forgot both foreground and background markers? I need both!")
6382
6593
  except MemoryError:
6383
6594
  QMessageBox.critical(
6384
6595
  self,
6385
6596
  "Alert",
6386
- "Out of memory computing feature maps. Note these for 3D require 7x the RAM of the active image (or 9x for the detailed map).\n Please use 2D slice models if you do not have enough RAM."
6597
+ "Out of memory computing feature maps. Note these for 3D require 7x the RAM of the active image (or 9x for the detailed map).\n Please use 2D slice models or RAM lock if you do not have enough RAM."
6387
6598
  )
6388
6599
 
6389
6600
 
@@ -6410,7 +6621,7 @@ class MachineWindow(QMainWindow):
6410
6621
  if not self.trained:
6411
6622
  return
6412
6623
  else:
6413
- self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self)
6624
+ self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu, self.use_two, self.previewing, self, self.mem_lock)
6414
6625
  self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
6415
6626
  self.segmentation_worker.finished.connect(self.segmentation_finished)
6416
6627
  current_xlim = self.parent().ax.get_xlim()
@@ -6647,7 +6858,7 @@ class SegmentationWorker(QThread):
6647
6858
  finished = pyqtSignal()
6648
6859
  chunk_processed = pyqtSignal()
6649
6860
 
6650
- def __init__(self, highlight_overlay, segmenter, use_gpu, use_two, previewing, machine_window):
6861
+ def __init__(self, highlight_overlay, segmenter, use_gpu, use_two, previewing, machine_window, mem_lock):
6651
6862
  super().__init__()
6652
6863
  self.overlay = highlight_overlay
6653
6864
  self.segmenter = segmenter
@@ -6655,6 +6866,7 @@ class SegmentationWorker(QThread):
6655
6866
  self.use_two = use_two
6656
6867
  self.previewing = previewing
6657
6868
  self.machine_window = machine_window
6869
+ self.mem_lock = mem_lock
6658
6870
  self._stop = False
6659
6871
  self.update_interval = 1 # Increased to 500ms
6660
6872
  self.chunks_since_update = 0
@@ -6774,6 +6986,7 @@ class ThresholdWindow(QMainWindow):
6774
6986
  self.histo_list = self.parent().channel_data[self.parent().active_channel].flatten().tolist()
6775
6987
  self.bounds = True
6776
6988
  self.parent().bounds = True
6989
+
6777
6990
 
6778
6991
  # Create matplotlib figure
6779
6992
  fig = Figure(figsize=(5, 4))
@@ -7589,9 +7802,9 @@ class InvertDialog(QDialog):
7589
7802
  if active_data.dtype == 'uint8' or 'int8':
7590
7803
  num = 255
7591
7804
  elif active_data.dtype == 'uint16' or 'int16':
7592
- num = 65,535
7805
+ num = 65535
7593
7806
  elif active_data.dtype == 'uint32' or 'int32':
7594
- num = 2,147,483,647
7807
+ num = 2147483647
7595
7808
 
7596
7809
  result = (num - active_data
7597
7810
  )