small-fish-gui 2.0.2__py3-none-any.whl → 2.0.3__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.
Files changed (51) hide show
  1. small_fish_gui/__init__.py +2 -2
  2. small_fish_gui/batch/integrity.py +2 -2
  3. small_fish_gui/batch/pipeline.py +46 -11
  4. small_fish_gui/batch/prompt.py +102 -41
  5. small_fish_gui/batch/update.py +26 -13
  6. small_fish_gui/batch/utils.py +1 -1
  7. small_fish_gui/gui/__init__.py +1 -0
  8. small_fish_gui/gui/_napari_widgets.py +418 -6
  9. small_fish_gui/gui/layout.py +332 -112
  10. small_fish_gui/gui/napari_visualiser.py +107 -22
  11. small_fish_gui/gui/prompts.py +161 -48
  12. small_fish_gui/gui/testing.ipynb +231 -24
  13. small_fish_gui/gui/tooltips.py +7 -1
  14. small_fish_gui/hints.py +23 -7
  15. small_fish_gui/interface/__init__.py +7 -1
  16. small_fish_gui/interface/default_settings.py +118 -0
  17. small_fish_gui/interface/image.py +43 -11
  18. small_fish_gui/interface/settings.json +50 -0
  19. small_fish_gui/interface/testing.ipynb +4354 -0
  20. small_fish_gui/interface/user_settings.py +96 -0
  21. small_fish_gui/main_menu.py +13 -1
  22. small_fish_gui/pipeline/{_signaltonoise.py → _bigfish_wrapers.py} +59 -7
  23. small_fish_gui/pipeline/_colocalisation.py +23 -24
  24. small_fish_gui/pipeline/_preprocess.py +46 -32
  25. small_fish_gui/pipeline/actions.py +48 -5
  26. small_fish_gui/pipeline/detection.py +71 -141
  27. small_fish_gui/pipeline/segmentation.py +360 -268
  28. small_fish_gui/pipeline/spots.py +3 -3
  29. small_fish_gui/pipeline/utils.py +5 -1
  30. small_fish_gui/README.md → small_fish_gui-2.0.3.dist-info/METADATA +35 -0
  31. small_fish_gui-2.0.3.dist-info/RECORD +46 -0
  32. {small_fish_gui-2.0.2.dist-info → small_fish_gui-2.0.3.dist-info}/WHEEL +1 -1
  33. small_fish_gui/.github/workflows/python-publish.yml +0 -39
  34. small_fish_gui/LICENSE +0 -24
  35. small_fish_gui/batch/values.txt +0 -65
  36. small_fish_gui/default_values.py +0 -51
  37. small_fish_gui/gui/screenshot/general_help_screenshot.png +0 -0
  38. small_fish_gui/gui/screenshot/mapping_help_screenshot.png +0 -0
  39. small_fish_gui/gui/screenshot/segmentation_help_screenshot.png +0 -0
  40. small_fish_gui/illustrations/DetectionVitrine_filtre.png +0 -0
  41. small_fish_gui/illustrations/DetectionVitrine_signal.png +0 -0
  42. small_fish_gui/illustrations/FocciVitrine.png +0 -0
  43. small_fish_gui/illustrations/FocciVitrine_no_spots.png +0 -0
  44. small_fish_gui/illustrations/Segmentation2D.png +0 -0
  45. small_fish_gui/illustrations/Segmentation2D_with_labels.png +0 -0
  46. small_fish_gui/logo.png +0 -0
  47. small_fish_gui/pipeline/testing.ipynb +0 -3636
  48. small_fish_gui/requirements.txt +0 -19
  49. small_fish_gui-2.0.2.dist-info/METADATA +0 -75
  50. small_fish_gui-2.0.2.dist-info/RECORD +0 -59
  51. {small_fish_gui-2.0.2.dist-info → small_fish_gui-2.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,14 +1,29 @@
1
1
  """
2
2
  Submodule containing custom class for napari widgets
3
3
  """
4
+ import os
4
5
  import numpy as np
5
6
  import pandas as pd
6
7
  import bigfish.detection as detection
7
- from napari.layers import Labels, Points
8
+ import bigfish.stack as stack
9
+ from skimage.segmentation import find_boundaries
10
+ from ..pipeline._bigfish_wrapers import _apply_log_filter, _local_maxima_mask
11
+
12
+ from napari.layers import Labels, Points, Image
13
+ from napari.utils.events import Event, EmitterGroup
8
14
  from magicgui import magicgui
15
+ from magicgui.widgets import SpinBox, Container
16
+ from bigfish.detection import spots_thresholding, automated_threshold_setting
17
+ from napari.types import LayerDataTuple
9
18
 
10
19
  from abc import ABC, abstractmethod
11
- from typing import Tuple
20
+ from typing import Tuple, List
21
+ from ..utils import compute_anisotropy_coef
22
+
23
+ from AF_eraser import remove_autofluorescence_RANSACfit
24
+ from pathlib import Path
25
+ from ..interface import open_image
26
+
12
27
 
13
28
  class NapariWidget(ABC) :
14
29
  """
@@ -25,6 +40,8 @@ class NapariWidget(ABC) :
25
40
  """
26
41
  pass
27
42
 
43
+ # Corrector widgets
44
+
28
45
  class ClusterWidget(NapariWidget) :
29
46
  """
30
47
  Widget for clusters interaction are all init with cluster_layer and single_layer
@@ -262,9 +279,6 @@ class ClusterMerger(ClusterWidget) :
262
279
 
263
280
  return merge_cluster
264
281
 
265
-
266
-
267
-
268
282
  class ClusterUpdater(NapariWidget) :
269
283
  """
270
284
  Relaunch clustering algorithm taking into consideration new spots, new clusters and deleted clusters.
@@ -510,4 +524,402 @@ class ClusterCleaner(ClusterWizard) :
510
524
  self.cluster_layer.refresh()
511
525
 
512
526
  self.cluster_layer.events.features.connect(delete_empty_cluster)
513
-
527
+
528
+
529
+
530
+ #Detection widgets
531
+ class BackgroundRemover(NapariWidget) :
532
+ def __init__(
533
+ self,
534
+ signal : Image,
535
+ voxel_size : tuple,
536
+ other_image : np.ndarray = None,
537
+ ) :
538
+
539
+ self.other_image = other_image
540
+ self.signal_layer = signal
541
+ self.signal_data_raw = np.array(signal.data)
542
+ self.voxel_size = voxel_size
543
+ self.scale = compute_anisotropy_coef(self.voxel_size)
544
+
545
+ self.signal_args = {
546
+ "name" : "raw signal",
547
+ "colormap" : 'green',
548
+ "scale" : self.scale,
549
+ "blending" : 'additive'
550
+ }
551
+
552
+ self.events = EmitterGroup(source=self.signal_layer, auto_connect=False, background_substraction_event = None)
553
+
554
+ super().__init__()
555
+ if self.other_image is None : self.disable_channel() #Image stack is None when image stack is not is_multichannel
556
+ self.reset_widget = self._create_reset_button()
557
+
558
+ def disable_channel(self) :
559
+ self.widget.channel.enabled = False
560
+
561
+ def _create_widget(self) :
562
+ @magicgui(
563
+ channel = {'min' : 0, 'max' : 0 if self.other_image is None else self.other_image.shape[0] - 1},
564
+ max_trial = {'min' : 0},
565
+ )
566
+ def remove_background(
567
+ background_path : Path,
568
+ channel : int,
569
+ max_trial : int = 100,
570
+ )-> LayerDataTuple :
571
+
572
+ print("Substracting background ...", end="", flush=True)
573
+ self.gui = remove_background
574
+
575
+ if os.path.isfile(background_path) :
576
+ background = open_image(str(background_path))
577
+ elif self.other_image is None :
578
+ raise FileNotFoundError(f"{background_path} is not a valid file.")
579
+ else :
580
+ background = self.other_image[channel]
581
+ if not background.shape == self.signal_data_raw.shape : raise ValueError(f"Shape missmatch between signal and background : {self.signal_data_raw.shape} ; {background.shape}")
582
+
583
+ result, score = remove_autofluorescence_RANSACfit(
584
+ signal=self.signal_data_raw.copy(),
585
+ background=background,
586
+ max_trials=max_trial
587
+ )
588
+
589
+ print("\rBackground substraction done.")
590
+ self.events.background_substraction_event(new_signal_array = result)
591
+
592
+ return (result, self.signal_args, 'image')
593
+ return remove_background
594
+
595
+ def _create_reset_button(self) :
596
+
597
+ @magicgui(call_button= "Reset signal")
598
+ def reset_signal() -> LayerDataTuple :
599
+ self.events.background_substraction_event(new_signal_array = self.signal_data_raw)
600
+ return (self.signal_data_raw, self.signal_args, 'image')
601
+ return reset_signal
602
+
603
+
604
+ class SpotDetector(NapariWidget) :
605
+ """
606
+ Widget aimed at helping user to set detection parameters : threshold, spot radius and so on...
607
+ """
608
+
609
+ def __init__(
610
+ self,
611
+ image: np.ndarray,
612
+ default_threshold : int,
613
+ default_spot_size : tuple,
614
+ default_kernel_size : tuple,
615
+ default_min_distance : tuple,
616
+ voxel_size : tuple,
617
+ background_remover_instance : BackgroundRemover,
618
+ ) :
619
+
620
+ self.image = image
621
+ self.voxel_size = voxel_size
622
+ self.dim = len(voxel_size)
623
+ self.default_threshold = default_threshold
624
+ self.spot_radius = default_spot_size
625
+ self.kernel_size = default_kernel_size
626
+ self.min_distance = default_min_distance
627
+ self._update_filtered_image()
628
+ self.maximum_threshold = self.filtered_image.max()
629
+ self.do_update = False
630
+
631
+ super().__init__()
632
+ background_remover_instance.events.background_substraction_event.connect(self.on_background_updated)
633
+
634
+ def _update_filtered_image(self) :
635
+
636
+ print("Re-computing filtered image with new parameters : ...", end="", flush=True)
637
+ self.filtered_image = _apply_log_filter(
638
+ image=self.image,
639
+ voxel_size=self.voxel_size,
640
+ spot_radius=self.spot_radius,
641
+ log_kernel_size=self.kernel_size
642
+ )
643
+ print("\rRe-computing filtered image with new parameters : done")
644
+
645
+ self.local_maxima = _local_maxima_mask(
646
+ image_filtered=self.filtered_image,
647
+ voxel_size=self.voxel_size,
648
+ spot_radius=self.spot_radius,
649
+ minimum_distance=self.min_distance
650
+ )
651
+
652
+ def _create_widget(self) :
653
+
654
+ dim = len(self.voxel_size)
655
+ if dim == 2 :
656
+ tuple_hint = Tuple[int,int]
657
+ else :
658
+ tuple_hint = Tuple[int,int,int]
659
+
660
+ if not self.default_threshold is None :
661
+ default_threshold = min(self.default_threshold, self.filtered_image.max())
662
+ else :
663
+ default_threshold = None
664
+
665
+ @magicgui(
666
+ threshold = {"widget_type" : SpinBox, "min" : 0, "value" : default_threshold, "max" : self.filtered_image.max() + 1},
667
+ spot_radius = {"label" : "spot radius(zyx)", "value" : self.spot_radius},
668
+ kernel_size = {"label" : "LoG kernel size(zyx)"},
669
+ minimum_distance = {"label" : "Distance min between spots"},
670
+ )
671
+ def find_spots(
672
+ threshold : int,
673
+ spot_radius : tuple_hint,
674
+ kernel_size : tuple_hint,
675
+ minimum_distance : tuple_hint,
676
+ ) -> List[LayerDataTuple] :
677
+
678
+ if (np.array(spot_radius) < 0).any() :
679
+ raise ValueError("Spot radius : set value > 0 (0 to ignore argument)")
680
+
681
+ if (np.array(kernel_size) < 0).any() :
682
+ raise ValueError("Spot radius : set value > 0 (0 to ignore argument)")
683
+
684
+ if (np.array(minimum_distance) < 0).any() :
685
+ raise ValueError("Spot radius : set value > 0 (0 to ignore argument)")
686
+
687
+ if isinstance(spot_radius, tuple) :
688
+ if not all(spot_radius) : spot_radius = None #any value set to 0
689
+ if isinstance(kernel_size, tuple) :
690
+ if not all(kernel_size) : kernel_size = None #any value set to 0
691
+ if isinstance(minimum_distance, tuple) :
692
+ if not all(minimum_distance) : minimum_distance = None #any value set to 0
693
+
694
+ if spot_radius != self.spot_radius :
695
+ self.spot_radius = spot_radius
696
+ self.do_update = True
697
+ if kernel_size != self.kernel_size :
698
+ self.kernel_size = kernel_size
699
+ self.do_update = True
700
+ if minimum_distance != self.min_distance :
701
+ self.min_distance = minimum_distance
702
+ self.do_update = True
703
+
704
+ try :
705
+ if self.do_update :
706
+ self._update_filtered_image()
707
+ self.do_update = False
708
+ self.widget.threshold.max = self.filtered_image.max() + 1
709
+
710
+ print("Computing automated threshold : ...", end="", flush=True)
711
+ if threshold == 0 :
712
+ threshold = automated_threshold_setting(
713
+ self.filtered_image,
714
+ mask_local_max=self.local_maxima
715
+ )
716
+ self.widget.threshold.value = threshold
717
+ print("\rComputing automated threshold : done.")
718
+
719
+ spots = spots_thresholding(
720
+ image=self.filtered_image,
721
+ mask_local_max=self.local_maxima,
722
+ threshold=threshold
723
+ )[0]
724
+ except ValueError as e :
725
+ print(str(e))
726
+
727
+
728
+ scale = compute_anisotropy_coef(self.voxel_size)
729
+
730
+ spot_layer_args = {
731
+ 'size': 5,
732
+ 'scale' : scale,
733
+ 'face_color' : 'transparent',
734
+ 'border_color' : 'red',
735
+ 'symbol' : 'disc',
736
+ 'opacity' : 0.7,
737
+ 'blending' : 'translucent',
738
+ 'name': 'single spots',
739
+ 'visible' : True,
740
+ }
741
+
742
+ filtered_image_layer_args = {
743
+ "colormap" : 'gray',
744
+ "scale" : scale,
745
+ "blending" : 'additive',
746
+ "name" : "filtered image"
747
+ }
748
+
749
+ return [
750
+ (self.filtered_image, filtered_image_layer_args, 'image'),
751
+ (spots, spot_layer_args, 'points')
752
+ ]
753
+
754
+ return find_spots
755
+
756
+ def on_background_updated(self, event):
757
+ print("Background was updated — recomputing filtered image...")
758
+ self.image = event.new_signal_array
759
+ self.do_update = True
760
+ self.widget()
761
+
762
+ def get_detection_parameters(self) :
763
+ detection_parameters = {"threshold" : self.widget.threshold.value}
764
+ if self.spot_radius is not None :
765
+ detection_parameters.update({
766
+ "spot_size" : self.spot_radius,
767
+ "spot_size_z" : self.spot_radius[0] if self.dim == 3 else None,
768
+ "spot_size_y" : self.spot_radius[0 + (self.dim==3)],
769
+ "spot_size_x" : self.spot_radius[1 + (self.dim==3)]
770
+ })
771
+ if self.kernel_size is not None :
772
+ detection_parameters.update({
773
+ "log_kernel_size" : self.kernel_size,
774
+ "log_kernel_size_z" : self.kernel_size[0] if self.dim == 3 else None,
775
+ "log_kernel_size_y" : self.kernel_size[0 + (self.dim==3)],
776
+ "log_kernel_size_x" : self.kernel_size[1 + (self.dim==3)]
777
+ })
778
+ if self.min_distance is not None :
779
+ detection_parameters.update({
780
+ "minimum_distance" : self.min_distance,
781
+ "minimum_distance" : self.min_distance[0] if self.dim == 3 else None,
782
+ "minimum_distance" : self.min_distance[0 + (self.dim==3)],
783
+ "minimum_distance" : self.min_distance[1 + (self.dim==3)],
784
+ })
785
+
786
+ return detection_parameters
787
+
788
+
789
+
790
+
791
+
792
+ class DenseRegionDeconvolver(NapariWidget) :
793
+ """
794
+ Widget for interactive detection. Create 2 layes : Labels layer representing dense region that could be deconvoluted and Points layer with deconvoluted spots
795
+ """
796
+ def __init__(
797
+ self,
798
+ image : Image,
799
+ spots : Points,
800
+ alpha : float,
801
+ beta : float,
802
+ gamma : float,
803
+ spot_radius : tuple,
804
+ kernel_size : tuple,
805
+ voxel_size : tuple
806
+ ) :
807
+
808
+ self.image = image
809
+ self.alpha = alpha
810
+ self.beta = beta
811
+ self.gamma = gamma
812
+ self.spots = spots
813
+ self.spot_radius = spot_radius
814
+ self.kernel_size = kernel_size
815
+ self.voxel_size = voxel_size
816
+ self.update_dense_regions()
817
+ super().__init__()
818
+
819
+ def update_dense_regions(self) :
820
+ dense_regions, spot_out_regions,max_size = detection.get_dense_region(
821
+ image=self.image.data,
822
+ spots=self.spots.data,
823
+ voxel_size = self.voxel_size,
824
+ beta=self.beta,
825
+ spot_radius=self.spot_radius
826
+ )
827
+ del spot_out_regions,max_size
828
+
829
+ mask = np.zeros(shape=self.image.data.shape, dtype= np.int16)
830
+ for label, region in enumerate(dense_regions) :
831
+ reg_im = region.image
832
+ coordinates = np.argwhere(reg_im)
833
+ z,y,x = coordinates.T
834
+ min_z,min_y,min_x,*_ = region.bbox
835
+ z += min_z
836
+ y += min_y
837
+ x += min_x
838
+
839
+ mask[z,y,x] = label + 1
840
+
841
+ self.dense_regions = mask
842
+
843
+ def _create_widget(self) :
844
+
845
+ dim = len(self.voxel_size)
846
+ tuple_hint = Tuple[int,int] if dim == 2 else Tuple[int,int,int]
847
+ tuple_dummy = tuple(0 for i in range(dim))
848
+
849
+ @magicgui
850
+ def dense_region_deconvolution(
851
+ alpha : float = self.alpha,
852
+ beta : float = self.beta,
853
+ gamma : float = self.gamma,
854
+ spot_radius : tuple_hint = tuple_dummy if self.spot_radius is None else self.spot_radius,
855
+ kernel_size : tuple_hint = tuple_dummy if self.kernel_size is None else self.kernel_size,
856
+ ) -> List[LayerDataTuple] :
857
+
858
+ if (np.array(spot_radius) < 0).any() :
859
+ raise ValueError("Spot radius : set value > 0 (0 to ignore argument)")
860
+ if (np.array(kernel_size) < 0).any() :
861
+ raise ValueError("kernel size : set value > 0 (0 to ignore argument)")
862
+
863
+ if isinstance(spot_radius,tuple) :
864
+ if not all(spot_radius) : spot_radius = None #any value set to 0
865
+ if isinstance(kernel_size,tuple) :
866
+ if not all(kernel_size) : kernel_size = None #any value set to 0
867
+
868
+ self.do_update = False
869
+ if spot_radius != self.spot_radius :
870
+ self.spot_radius = spot_radius
871
+ self.do_update = True
872
+ if beta != self.beta :
873
+ self.beta = beta
874
+ self.do_update=True
875
+ if self.do_update :
876
+ print("Updating dense regions...", end="", flush=True)
877
+ self.update_dense_regions()
878
+ print("\rUpdating dense regions : done.")
879
+ self.alpha = alpha
880
+ self.gamma = gamma
881
+ self.kernel_size = kernel_size
882
+
883
+ print("Decomposing dense regions...", end="", flush=True)
884
+ spots, _dense_region, _reference_spot = detection.decompose_dense(
885
+ image= self.image.data,
886
+ spots= self.spots.data,
887
+ voxel_size=self.voxel_size,
888
+ spot_radius=self.spot_radius,
889
+ kernel_size=self.kernel_size,
890
+ alpha=self.alpha,
891
+ beta=self.beta,
892
+ gamma=self.gamma
893
+ )
894
+ print("\rDecomposing dense regions : done")
895
+ del _dense_region, _reference_spot
896
+
897
+ scale = compute_anisotropy_coef(self.voxel_size)
898
+ spot_layer_args = {
899
+ 'size': 5,
900
+ 'scale' : scale,
901
+ 'face_color' : 'transparent',
902
+ 'border_color' : 'blue',
903
+ 'symbol' : 'disc',
904
+ 'opacity' : 0.7,
905
+ 'blending' : 'translucent',
906
+ 'name': 'decovoluted spots',
907
+ 'visible' : True,
908
+ }
909
+
910
+ dense_region_args = {
911
+ "scale" : scale,
912
+ "name": "Dense regions",
913
+ "colormap" : ["red"] * self.dense_regions.max()
914
+ }
915
+
916
+ return [(self.dense_regions, dense_region_args, 'labels'), (spots, spot_layer_args, 'points')]
917
+ return dense_region_deconvolution
918
+
919
+ def get_detection_parameters(self) :
920
+ return {
921
+ "alpha" : self.alpha,
922
+ "beta" : self.beta,
923
+ "gamma" : self.gamma
924
+ }
925
+