spacr 0.3.1__py3-none-any.whl → 0.3.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 (41) hide show
  1. spacr/__init__.py +19 -3
  2. spacr/cellpose.py +311 -0
  3. spacr/core.py +245 -2494
  4. spacr/deep_spacr.py +335 -163
  5. spacr/gui.py +2 -0
  6. spacr/gui_core.py +85 -65
  7. spacr/gui_elements.py +110 -5
  8. spacr/gui_utils.py +375 -7
  9. spacr/io.py +680 -141
  10. spacr/logger.py +28 -9
  11. spacr/measure.py +108 -133
  12. spacr/mediar.py +0 -3
  13. spacr/ml.py +1051 -0
  14. spacr/openai.py +37 -0
  15. spacr/plot.py +707 -20
  16. spacr/resources/data/lopit.csv +3833 -0
  17. spacr/resources/data/toxoplasma_metadata.csv +8843 -0
  18. spacr/resources/icons/convert.png +0 -0
  19. spacr/resources/{models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model → icons/dna_matrix.mp4} +0 -0
  20. spacr/sequencing.py +241 -1311
  21. spacr/settings.py +181 -50
  22. spacr/sim.py +0 -2
  23. spacr/submodules.py +349 -0
  24. spacr/timelapse.py +0 -2
  25. spacr/toxo.py +238 -0
  26. spacr/utils.py +776 -182
  27. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/METADATA +31 -22
  28. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/RECORD +32 -33
  29. spacr/chris.py +0 -50
  30. spacr/graph_learning.py +0 -340
  31. spacr/resources/MEDIAR/.git +0 -1
  32. spacr/resources/MEDIAR_weights/.DS_Store +0 -0
  33. spacr/resources/icons/.DS_Store +0 -0
  34. spacr/resources/icons/spacr_logo_rotation.gif +0 -0
  35. spacr/resources/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model_settings.csv +0 -23
  36. spacr/resources/models/cp/toxo_pv_lumen.CP_model +0 -0
  37. spacr/sim_app.py +0 -0
  38. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/LICENSE +0 -0
  39. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/WHEEL +0 -0
  40. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/entry_points.txt +0 -0
  41. {spacr-0.3.1.dist-info → spacr-0.3.3.dist-info}/top_level.txt +0 -0
spacr/gui_utils.py CHANGED
@@ -1,4 +1,4 @@
1
- import os, io, sys, ast, ctypes, ast, sqlite3, requests, time, traceback, torch
1
+ import os, io, sys, ast, ctypes, ast, sqlite3, requests, time, traceback, torch, cv2
2
2
  import tkinter as tk
3
3
  from tkinter import ttk
4
4
  import matplotlib
@@ -6,6 +6,8 @@ import matplotlib.pyplot as plt
6
6
  matplotlib.use('Agg')
7
7
  from huggingface_hub import list_repo_files
8
8
  import psutil
9
+ from PIL import Image, ImageTk
10
+ from screeninfo import get_monitors
9
11
 
10
12
  from .gui_elements import AnnotateApp, spacrEntry, spacrCheck, spacrCombo
11
13
 
@@ -75,7 +77,7 @@ def load_app(root, app_name, app_func):
75
77
  else:
76
78
  proceed_with_app(root, app_name, app_func)
77
79
 
78
- def parse_list(value):
80
+ def parse_list_v1(value):
79
81
  """
80
82
  Parses a string representation of a list and returns the parsed list.
81
83
 
@@ -96,6 +98,34 @@ def parse_list(value):
96
98
  return parsed_value
97
99
  elif all(isinstance(item, str) for item in parsed_value):
98
100
  return parsed_value
101
+ elif all(isinstance(item, float) for item in parsed_value):
102
+ return parsed_value
103
+ else:
104
+ raise ValueError("List contains mixed types or unsupported types")
105
+ else:
106
+ raise ValueError(f"Expected a list but got {type(parsed_value).__name__}")
107
+ except (ValueError, SyntaxError) as e:
108
+ raise ValueError(f"Invalid format for list: {value}. Error: {e}")
109
+
110
+ def parse_list(value):
111
+ """
112
+ Parses a string representation of a list and returns the parsed list.
113
+
114
+ Args:
115
+ value (str): The string representation of the list.
116
+
117
+ Returns:
118
+ list: The parsed list, which can contain integers, floats, or strings.
119
+
120
+ Raises:
121
+ ValueError: If the input value is not a valid list format or contains mixed types or unsupported types.
122
+ """
123
+ try:
124
+ parsed_value = ast.literal_eval(value)
125
+ if isinstance(parsed_value, list):
126
+ # Check if all elements are homogeneous (either all int, float, or str)
127
+ if all(isinstance(item, (int, float, str)) for item in parsed_value):
128
+ return parsed_value
99
129
  else:
100
130
  raise ValueError("List contains mixed types or unsupported types")
101
131
  else:
@@ -394,6 +424,7 @@ def convert_settings_dict_for_gui(settings):
394
424
  'train_channels': ('combo', ["['r','g','b']", "['r','g']", "['r','b']", "['g','b']", "['r']", "['g']", "['b']"], "['r','g','b']"),
395
425
  'channel_dims': ('combo', ['[0,1,2,3]', '[0,1,2]', '[0,1]', '[0]'], '[0,1,2,3]'),
396
426
  'dataset_mode': ('combo', ['annotation', 'metadata', 'recruitment'], 'metadata'),
427
+ 'cov_type': ('combo', ['HC0', 'HC1', 'HC2', 'HC3', None], None),
397
428
  'cell_mask_dim': ('combo', chans, None),
398
429
  'cell_chann_dim': ('combo', chans, None),
399
430
  'nucleus_mask_dim': ('combo', chans, None),
@@ -486,12 +517,16 @@ def function_gui_wrapper(function=None, settings={}, q=None, fig_queue=None, imp
486
517
  def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
487
518
 
488
519
  from .gui_utils import process_stdout_stderr
489
- from .core import generate_image_umap, preprocess_generate_masks, generate_ml_scores, identify_masks_finetune, check_cellpose_models, analyze_recruitment, train_cellpose, analyze_plaques, compare_cellpose_masks, generate_dataset, apply_model_to_tar
490
- from .io import generate_cellpose_train_test
520
+ from .core import generate_image_umap, preprocess_generate_masks
521
+ from .cellpose import identify_masks_finetune, check_cellpose_models, compare_cellpose_masks
522
+ from .submodules import analyze_recruitment
523
+ from .ml import generate_ml_scores, perform_regression
524
+ from .submodules import train_cellpose, analyze_plaques
525
+ from .io import process_non_tif_non_2D_images, generate_cellpose_train_test, generate_dataset
491
526
  from .measure import measure_crop
492
527
  from .sim import run_multiple_simulations
493
- from .deep_spacr import deep_spacr
494
- from .sequencing import generate_barecode_mapping, perform_regression
528
+ from .deep_spacr import deep_spacr, apply_model_to_tar
529
+ from .sequencing import generate_barecode_mapping
495
530
  process_stdout_stderr(q)
496
531
 
497
532
  print(f'run_function_gui settings_type: {settings_type}')
@@ -535,6 +570,9 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
535
570
  elif settings_type == 'analyze_plaques':
536
571
  function = analyze_plaques
537
572
  imports = 1
573
+ elif settings_type == 'convert':
574
+ function = process_non_tif_non_2D_images
575
+ imports = 1
538
576
  else:
539
577
  raise ValueError(f"Invalid settings type: {settings_type}")
540
578
  try:
@@ -697,4 +735,334 @@ def download_dataset(q, repo_id, subfolder, local_dir=None, retries=5, delay=5):
697
735
 
698
736
  def ensure_after_tasks(frame):
699
737
  if not hasattr(frame, 'after_tasks'):
700
- frame.after_tasks = []
738
+ frame.after_tasks = []
739
+
740
+ def display_gif_in_plot_frame_v1(gif_path, parent_frame):
741
+ """Display and zoom a GIF to fill the entire parent_frame, maintaining aspect ratio, with lazy resizing and caching."""
742
+ # Clear parent_frame if it contains any previous widgets
743
+ for widget in parent_frame.winfo_children():
744
+ widget.destroy()
745
+
746
+ # Load the GIF
747
+ gif = Image.open(gif_path)
748
+
749
+ # Get the aspect ratio of the GIF
750
+ gif_width, gif_height = gif.size
751
+ gif_aspect_ratio = gif_width / gif_height
752
+
753
+ # Create a label to display the GIF and configure it to fill the parent_frame
754
+ label = tk.Label(parent_frame, bg="black")
755
+ label.grid(row=0, column=0, sticky="nsew") # Expands in all directions (north, south, east, west)
756
+
757
+ # Configure parent_frame to stretch the label to fill available space
758
+ parent_frame.grid_rowconfigure(0, weight=1)
759
+ parent_frame.grid_columnconfigure(0, weight=1)
760
+
761
+ # Cache for storing resized frames (lazily filled)
762
+ resized_frames_cache = {}
763
+
764
+ # Last frame dimensions
765
+ last_frame_width = 0
766
+ last_frame_height = 0
767
+
768
+ def resize_and_crop_frame(frame_idx, frame_width, frame_height):
769
+ """Resize and crop the current frame of the GIF to fit the parent_frame while maintaining the aspect ratio."""
770
+ # If the frame is already cached at the current size, return it
771
+ if (frame_idx, frame_width, frame_height) in resized_frames_cache:
772
+ return resized_frames_cache[(frame_idx, frame_width, frame_height)]
773
+
774
+ # Calculate the scaling factor to zoom in on the GIF
775
+ scale_factor = max(frame_width / gif_width, frame_height / gif_height)
776
+
777
+ # Calculate new dimensions while maintaining the aspect ratio
778
+ new_width = int(gif_width * scale_factor)
779
+ new_height = int(gif_height * scale_factor)
780
+
781
+ # Resize the GIF to fit the frame
782
+ gif.seek(frame_idx)
783
+ resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.LANCZOS)
784
+
785
+ # Calculate the cropping box to center the resized GIF in the frame
786
+ crop_left = (new_width - frame_width) // 2
787
+ crop_top = (new_height - frame_height) // 2
788
+ crop_right = crop_left + frame_width
789
+ crop_bottom = crop_top + frame_height
790
+
791
+ # Crop the resized GIF to exactly fit the frame
792
+ cropped_gif = resized_gif.crop((crop_left, crop_top, crop_right, crop_bottom))
793
+
794
+ # Convert the cropped frame to a Tkinter-compatible format
795
+ frame_image = ImageTk.PhotoImage(cropped_gif)
796
+
797
+ # Cache the resized frame
798
+ resized_frames_cache[(frame_idx, frame_width, frame_height)] = frame_image
799
+
800
+ return frame_image
801
+
802
+ def update_frame(frame_idx):
803
+ """Update the GIF frame using lazy resizing and caching."""
804
+ # Get the current size of the parent_frame
805
+ frame_width = parent_frame.winfo_width()
806
+ frame_height = parent_frame.winfo_height()
807
+
808
+ # Only resize if the frame size has changed
809
+ nonlocal last_frame_width, last_frame_height
810
+ if frame_width != last_frame_width or frame_height != last_frame_height:
811
+ last_frame_width, last_frame_height = frame_width, frame_height
812
+
813
+ # Get the resized and cropped frame image
814
+ frame_image = resize_and_crop_frame(frame_idx, frame_width, frame_height)
815
+ label.config(image=frame_image)
816
+ label.image = frame_image # Keep a reference to avoid garbage collection
817
+
818
+ # Move to the next frame, or loop back to the beginning
819
+ next_frame_idx = (frame_idx + 1) % gif.n_frames
820
+ parent_frame.after(gif.info['duration'], update_frame, next_frame_idx)
821
+
822
+ # Start the GIF animation from frame 0
823
+ update_frame(0)
824
+
825
+ def display_gif_in_plot_frame(gif_path, parent_frame):
826
+ """Display and zoom a GIF to fill the entire parent_frame, maintaining aspect ratio, with lazy resizing and caching."""
827
+ # Clear parent_frame if it contains any previous widgets
828
+ for widget in parent_frame.winfo_children():
829
+ widget.destroy()
830
+
831
+ # Load the GIF
832
+ gif = Image.open(gif_path)
833
+
834
+ # Get the aspect ratio of the GIF
835
+ gif_width, gif_height = gif.size
836
+ gif_aspect_ratio = gif_width / gif_height
837
+
838
+ # Create a label to display the GIF and configure it to fill the parent_frame
839
+ label = tk.Label(parent_frame, bg="black")
840
+ label.grid(row=0, column=0, sticky="nsew") # Expands in all directions (north, south, east, west)
841
+
842
+ # Configure parent_frame to stretch the label to fill available space
843
+ parent_frame.grid_rowconfigure(0, weight=1)
844
+ parent_frame.grid_columnconfigure(0, weight=1)
845
+
846
+ # Cache for storing resized frames (lazily filled)
847
+ resized_frames_cache = {}
848
+
849
+ # Store last frame size and aspect ratio
850
+ last_frame_width = 0
851
+ last_frame_height = 0
852
+
853
+ def resize_and_crop_frame(frame_idx, frame_width, frame_height):
854
+ """Resize and crop the current frame of the GIF to fit the parent_frame while maintaining the aspect ratio."""
855
+ # If the frame is already cached at the current size, return it
856
+ if (frame_idx, frame_width, frame_height) in resized_frames_cache:
857
+ return resized_frames_cache[(frame_idx, frame_width, frame_height)]
858
+
859
+ # Calculate the scaling factor to zoom in on the GIF
860
+ scale_factor = max(frame_width / gif_width, frame_height / gif_height)
861
+
862
+ # Calculate new dimensions while maintaining the aspect ratio
863
+ new_width = int(gif_width * scale_factor)
864
+ new_height = int(gif_height * scale_factor)
865
+
866
+ # Resize the GIF to fit the frame using NEAREST for faster resizing
867
+ gif.seek(frame_idx)
868
+ resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.NEAREST if scale_factor > 2 else Image.Resampling.LANCZOS)
869
+
870
+ # Calculate the cropping box to center the resized GIF in the frame
871
+ crop_left = (new_width - frame_width) // 2
872
+ crop_top = (new_height - frame_height) // 2
873
+ crop_right = crop_left + frame_width
874
+ crop_bottom = crop_top + frame_height
875
+
876
+ # Crop the resized GIF to exactly fit the frame
877
+ cropped_gif = resized_gif.crop((crop_left, crop_top, crop_right, crop_bottom))
878
+
879
+ # Convert the cropped frame to a Tkinter-compatible format
880
+ frame_image = ImageTk.PhotoImage(cropped_gif)
881
+
882
+ # Cache the resized frame
883
+ resized_frames_cache[(frame_idx, frame_width, frame_height)] = frame_image
884
+
885
+ return frame_image
886
+
887
+ def update_frame(frame_idx):
888
+ """Update the GIF frame using lazy resizing and caching."""
889
+ # Get the current size of the parent_frame
890
+ frame_width = parent_frame.winfo_width()
891
+ frame_height = parent_frame.winfo_height()
892
+
893
+ # Only resize if the frame size has changed
894
+ nonlocal last_frame_width, last_frame_height
895
+ if frame_width != last_frame_width or frame_height != last_frame_height:
896
+ last_frame_width, last_frame_height = frame_width, frame_height
897
+
898
+ # Get the resized and cropped frame image
899
+ frame_image = resize_and_crop_frame(frame_idx, frame_width, frame_height)
900
+ label.config(image=frame_image)
901
+ label.image = frame_image # Keep a reference to avoid garbage collection
902
+
903
+ # Move to the next frame, or loop back to the beginning
904
+ next_frame_idx = (frame_idx + 1) % gif.n_frames
905
+ parent_frame.after(gif.info['duration'], update_frame, next_frame_idx)
906
+
907
+ # Start the GIF animation from frame 0
908
+ update_frame(0)
909
+
910
+ def display_media_in_plot_frame(media_path, parent_frame):
911
+ """Display an MP4, AVI, or GIF and play it on repeat in the parent_frame, fully filling the frame while maintaining aspect ratio."""
912
+ # Clear parent_frame if it contains any previous widgets
913
+ for widget in parent_frame.winfo_children():
914
+ widget.destroy()
915
+
916
+ # Check file extension to decide between video (mp4/avi) or gif
917
+ file_extension = os.path.splitext(media_path)[1].lower()
918
+
919
+ if file_extension in ['.mp4', '.avi']:
920
+ # Handle video formats (mp4, avi) using OpenCV
921
+ video = cv2.VideoCapture(media_path)
922
+
923
+ # Create a label to display the video
924
+ label = tk.Label(parent_frame, bg="black")
925
+ label.grid(row=0, column=0, sticky="nsew")
926
+
927
+ # Configure the parent_frame to expand
928
+ parent_frame.grid_rowconfigure(0, weight=1)
929
+ parent_frame.grid_columnconfigure(0, weight=1)
930
+
931
+ def update_frame():
932
+ """Update function for playing video."""
933
+ ret, frame = video.read()
934
+ if ret:
935
+ # Get the frame dimensions
936
+ frame_height, frame_width, _ = frame.shape
937
+
938
+ # Get parent frame dimensions
939
+ parent_width = parent_frame.winfo_width()
940
+ parent_height = parent_frame.winfo_height()
941
+
942
+ # Ensure dimensions are greater than 0
943
+ if parent_width > 0 and parent_height > 0:
944
+ # Calculate the aspect ratio of the media
945
+ frame_aspect_ratio = frame_width / frame_height
946
+ parent_aspect_ratio = parent_width / parent_height
947
+
948
+ # Determine whether to scale based on width or height to cover the parent frame
949
+ if parent_aspect_ratio > frame_aspect_ratio:
950
+ # The parent frame is wider than the video aspect ratio
951
+ # Fit to width, crop height
952
+ new_width = parent_width
953
+ new_height = int(parent_width / frame_aspect_ratio)
954
+ else:
955
+ # The parent frame is taller than the video aspect ratio
956
+ # Fit to height, crop width
957
+ new_width = int(parent_height * frame_aspect_ratio)
958
+ new_height = parent_height
959
+
960
+ # Resize the frame to the new dimensions (cover the parent frame)
961
+ resized_frame = cv2.resize(frame, (new_width, new_height))
962
+
963
+ # Crop the frame to fit exactly within the parent frame
964
+ x_offset = (new_width - parent_width) // 2
965
+ y_offset = (new_height - parent_height) // 2
966
+ cropped_frame = resized_frame[y_offset:y_offset + parent_height, x_offset:x_offset + parent_width]
967
+
968
+ # Convert the frame to RGB (OpenCV uses BGR by default)
969
+ cropped_frame = cv2.cvtColor(cropped_frame, cv2.COLOR_BGR2RGB)
970
+
971
+ # Convert the frame to a Tkinter-compatible format
972
+ frame_image = ImageTk.PhotoImage(Image.fromarray(cropped_frame))
973
+
974
+ # Update the label with the new frame
975
+ label.config(image=frame_image)
976
+ label.image = frame_image # Keep a reference to avoid garbage collection
977
+
978
+ # Call update_frame again after a delay to match the video's frame rate
979
+ parent_frame.after(int(1000 / video.get(cv2.CAP_PROP_FPS)), update_frame)
980
+ else:
981
+ # Restart the video if it reaches the end
982
+ video.set(cv2.CAP_PROP_POS_FRAMES, 0)
983
+ update_frame()
984
+
985
+ # Start the video playback
986
+ update_frame()
987
+
988
+ elif file_extension == '.gif':
989
+ # Handle GIF format using PIL
990
+ gif = Image.open(media_path)
991
+
992
+ # Create a label to display the GIF
993
+ label = tk.Label(parent_frame, bg="black")
994
+ label.grid(row=0, column=0, sticky="nsew")
995
+
996
+ # Configure the parent_frame to expand
997
+ parent_frame.grid_rowconfigure(0, weight=1)
998
+ parent_frame.grid_columnconfigure(0, weight=1)
999
+
1000
+ def update_gif_frame(frame_idx):
1001
+ """Update function for playing GIF."""
1002
+ try:
1003
+ gif.seek(frame_idx) # Move to the next frame
1004
+
1005
+ # Get the frame dimensions
1006
+ gif_width, gif_height = gif.size
1007
+
1008
+ # Get parent frame dimensions
1009
+ parent_width = parent_frame.winfo_width()
1010
+ parent_height = parent_frame.winfo_height()
1011
+
1012
+ # Ensure dimensions are greater than 0
1013
+ if parent_width > 0 and parent_height > 0:
1014
+ # Calculate the aspect ratio of the GIF
1015
+ gif_aspect_ratio = gif_width / gif_height
1016
+ parent_aspect_ratio = parent_width / parent_height
1017
+
1018
+ # Determine whether to scale based on width or height to cover the parent frame
1019
+ if parent_aspect_ratio > gif_aspect_ratio:
1020
+ # Fit to width, crop height
1021
+ new_width = parent_width
1022
+ new_height = int(parent_width / gif_aspect_ratio)
1023
+ else:
1024
+ # Fit to height, crop width
1025
+ new_width = int(parent_height * gif_aspect_ratio)
1026
+ new_height = parent_height
1027
+
1028
+ # Resize the GIF frame to cover the parent frame
1029
+ resized_gif = gif.copy().resize((new_width, new_height), Image.Resampling.LANCZOS)
1030
+
1031
+ # Crop the resized GIF to fit the exact parent frame dimensions
1032
+ x_offset = (new_width - parent_width) // 2
1033
+ y_offset = (new_height - parent_height) // 2
1034
+ cropped_gif = resized_gif.crop((x_offset, y_offset, x_offset + parent_width, y_offset + parent_height))
1035
+
1036
+ # Convert the frame to a Tkinter-compatible format
1037
+ frame_image = ImageTk.PhotoImage(cropped_gif)
1038
+
1039
+ # Update the label with the new frame
1040
+ label.config(image=frame_image)
1041
+ label.image = frame_image # Keep a reference to avoid garbage collection
1042
+ frame_idx += 1
1043
+ except EOFError:
1044
+ frame_idx = 0 # Restart the GIF if at the end
1045
+
1046
+ # Schedule the next frame update
1047
+ parent_frame.after(gif.info['duration'], update_gif_frame, frame_idx)
1048
+
1049
+ # Start the GIF animation from frame 0
1050
+ update_gif_frame(0)
1051
+
1052
+ else:
1053
+ raise ValueError("Unsupported file format. Only .mp4, .avi, and .gif are supported.")
1054
+
1055
+ def print_widget_structure(widget, indent=0):
1056
+ """Recursively print the widget structure."""
1057
+ # Print the widget's name and class
1058
+ print(" " * indent + f"{widget}: {widget.winfo_class()}")
1059
+
1060
+ # Recursively print all child widgets
1061
+ for child_name, child_widget in widget.children.items():
1062
+ print_widget_structure(child_widget, indent + 2)
1063
+
1064
+ def get_screen_dimensions():
1065
+ monitor = get_monitors()[0] # Get the primary monitor
1066
+ screen_width = monitor.width
1067
+ screen_height = monitor.height
1068
+ return screen_width, screen_height