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/logger.py CHANGED
@@ -1,20 +1,39 @@
1
1
  import logging
2
2
  import functools
3
+ import os
3
4
 
5
+ # Automatically configure logging
6
+ def configure_logger(log_file_name='spacr.log'):
7
+ # Determine a safe location for the log file
8
+ home_dir = os.path.expanduser("~") # Get the user's home directory
9
+ log_file_path = os.path.join(home_dir, log_file_name) # Save log file in home directory
10
+
11
+ # Setup logging configuration
12
+ logging.basicConfig(
13
+ filename=log_file_path,
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
+ )
17
+
18
+ # Call the function to configure the logger
19
+ configure_logger()
20
+
21
+ # Create a logger instance for this module
4
22
  logger = logging.getLogger(__name__)
5
23
 
24
+ # Decorator to log function calls
6
25
  def log_function_call(func):
7
26
  @functools.wraps(func)
8
27
  def wrapper(*args, **kwargs):
9
- args_repr = [repr(a) for a in args]
10
- kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
11
- signature = ", ".join(args_repr + kwargs_repr)
12
- logger.info(f"Calling {func.__name__}({signature})")
28
+ args_repr = [repr(a) for a in args] # Arguments passed to the function
29
+ kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] # Keyword arguments
30
+ signature = ", ".join(args_repr + kwargs_repr) # Construct the signature for logging
31
+ logger.info(f"Calling {func.__name__}({signature})") # Log function call
13
32
  try:
14
- result = func(*args, **kwargs)
15
- logger.info(f"{func.__name__} returned {result!r}")
33
+ result = func(*args, **kwargs) # Execute the function
34
+ logger.info(f"{func.__name__} returned {result!r}") # Log the result
16
35
  return result
17
36
  except Exception as e:
18
- logger.exception(f"Exception occurred in {func.__name__}")
19
- raise
20
- return wrapper
37
+ logger.exception(f"Exception occurred in {func.__name__}") # Log any exceptions
38
+ raise # Re-raise the exception after logging it
39
+ return wrapper
spacr/measure.py CHANGED
@@ -16,8 +16,6 @@ from skimage.util import img_as_bool
16
16
  import matplotlib.pyplot as plt
17
17
  from math import ceil, sqrt
18
18
 
19
- from .logger import log_function_call
20
-
21
19
  def get_components(cell_mask, nucleus_mask, pathogen_mask):
22
20
  """
23
21
  Get the components (nucleus and pathogens) for each cell in the given masks.
@@ -654,43 +652,6 @@ def img_list_to_grid(grid, titles=None):
654
652
  plt.tight_layout(pad=0.1)
655
653
  return fig
656
654
 
657
- def filepaths_to_database(img_paths, settings, source_folder, crop_mode):
658
- from. utils import _map_wells_png
659
- png_df = pd.DataFrame(img_paths, columns=['png_path'])
660
-
661
- png_df['file_name'] = png_df['png_path'].apply(lambda x: os.path.basename(x))
662
-
663
- parts = png_df['file_name'].apply(lambda x: pd.Series(_map_wells_png(x, timelapse=settings['timelapse'])))
664
-
665
- columns = ['plate', 'row', 'col', 'field']
666
-
667
- if settings['timelapse']:
668
- columns = columns + ['time_id']
669
-
670
- columns = columns + ['prcfo']
671
-
672
- if crop_mode == 'cell':
673
- columns = columns + ['cell_id']
674
-
675
- if crop_mode == 'nucleus':
676
- columns = columns + ['nucleus_id']
677
-
678
- if crop_mode == 'pathogen':
679
- columns = columns + ['pathogen_id']
680
-
681
- if crop_mode == 'cytoplasm':
682
- columns = columns + ['cytoplasm_id']
683
-
684
- png_df[columns] = parts
685
-
686
- try:
687
- conn = sqlite3.connect(f'{source_folder}/measurements/measurements.db', timeout=5)
688
- png_df.to_sql('png_list', conn, if_exists='append', index=False)
689
- conn.commit()
690
- except sqlite3.OperationalError as e:
691
- print(f"SQLite error: {e}", flush=True)
692
- traceback.print_exc()
693
-
694
655
  #@log_function_call
695
656
  def _measure_crop_core(index, time_ls, file, settings):
696
657
 
@@ -713,7 +674,7 @@ def _measure_crop_core(index, time_ls, file, settings):
713
674
  """
714
675
 
715
676
  from .plot import _plot_cropped_arrays
716
- from .utils import _merge_overlapping_objects, _filter_object, _relabel_parent_with_child_labels, _exclude_objects, normalize_to_dtype
677
+ from .utils import _merge_overlapping_objects, _filter_object, _relabel_parent_with_child_labels, _exclude_objects, normalize_to_dtype, filepaths_to_database
717
678
  from .utils import _merge_and_save_to_database, _crop_center, _find_bounding_box, _generate_names, _get_percentiles
718
679
 
719
680
  figs = {}
@@ -984,112 +945,126 @@ def measure_crop(settings):
984
945
  None
985
946
  """
986
947
 
987
- from .io import _save_settings_to_db, _create_database
948
+ from .io import _save_settings_to_db
988
949
  from .timelapse import _timelapse_masks_to_gif
989
950
  from .utils import measure_test_mode, print_progress
990
951
  from .settings import get_measure_crop_settings
991
952
 
992
- settings = get_measure_crop_settings(settings)
993
- settings = measure_test_mode(settings)
994
-
995
- src_fldr = settings['src']
996
- if not os.path.basename(src_fldr).endswith('merged'):
997
- print(f"WARNING: Source folder, settings: src: {src_fldr} should end with '/merged'")
998
- src_fldr = os.path.join(src_fldr, 'merged')
999
- print(f"Changed source folder to: {src_fldr}")
1000
-
1001
- #if settings['save_measurements']:
1002
- #source_folder = os.path.dirname(settings['src'])
1003
- #os.makedirs(source_folder+'/measurements', exist_ok=True)
1004
- #_create_database(source_folder+'/measurements/measurements.db')
953
+ if not isinstance(settings['src'], (str, list)):
954
+ ValueError(f'src must be a string or a list of strings')
955
+ return
1005
956
 
1006
- if settings['cell_mask_dim'] is None:
1007
- settings['include_uninfected'] = True
1008
- if settings['pathogen_mask_dim'] is None:
1009
- settings['include_uninfected'] = True
1010
- if settings['cell_mask_dim'] is not None and settings['pathogen_min_size'] is not None:
1011
- settings['cytoplasm'] = True
1012
- elif settings['cell_mask_dim'] is not None and settings['nucleus_min_size'] is not None:
1013
- settings['cytoplasm'] = True
1014
- else:
1015
- settings['cytoplasm'] = False
1016
-
1017
- spacr_cores = int(mp.cpu_count() - 6)
1018
- if spacr_cores <= 2:
1019
- spacr_cores = 1
1020
-
1021
- if settings['n_jobs'] > spacr_cores:
1022
- print(f'Warning reserving 6 CPU cores for other processes, setting n_jobs to {spacr_cores}')
1023
- settings['n_jobs'] = spacr_cores
957
+ if isinstance(settings['src'], str):
958
+ settings['src'] = [settings['src']]
959
+
960
+ if isinstance(settings['src'], list):
961
+ source_folders = settings['src']
962
+ for source_folder in source_folders:
963
+ print(f'Processing folder: {source_folder}')
964
+ settings['src'] = source_folder
965
+ src = source_folder
966
+
967
+ settings = get_measure_crop_settings(settings)
968
+ settings = measure_test_mode(settings)
969
+
970
+ src_fldr = settings['src']
971
+ if not os.path.basename(src_fldr).endswith('merged'):
972
+ print(f"WARNING: Source folder, settings: src: {src_fldr} should end with '/merged'")
973
+ src_fldr = os.path.join(src_fldr, 'merged')
974
+ print(f"Changed source folder to: {src_fldr}")
975
+
976
+ #if settings['save_measurements']:
977
+ #source_folder = os.path.dirname(settings['src'])
978
+ #os.makedirs(source_folder+'/measurements', exist_ok=True)
979
+ #_create_database(source_folder+'/measurements/measurements.db')
980
+
981
+ if settings['cell_mask_dim'] is None:
982
+ settings['include_uninfected'] = True
983
+ if settings['pathogen_mask_dim'] is None:
984
+ settings['include_uninfected'] = True
985
+ if settings['cell_mask_dim'] is not None and settings['pathogen_min_size'] is not None:
986
+ settings['cytoplasm'] = True
987
+ elif settings['cell_mask_dim'] is not None and settings['nucleus_min_size'] is not None:
988
+ settings['cytoplasm'] = True
989
+ else:
990
+ settings['cytoplasm'] = False
1024
991
 
1025
- dirname = os.path.dirname(settings['src'])
1026
- settings_df = pd.DataFrame(list(settings.items()), columns=['Key', 'Value'])
1027
- settings_csv = os.path.join(dirname,'settings','measure_crop_settings.csv')
1028
- os.makedirs(os.path.join(dirname,'settings'), exist_ok=True)
1029
- settings_df.to_csv(settings_csv, index=False)
992
+ spacr_cores = int(mp.cpu_count() - 6)
993
+ if spacr_cores <= 2:
994
+ spacr_cores = 1
1030
995
 
1031
- if settings['timelapse_objects'] == 'nucleus':
1032
- if not settings['cell_mask_dim'] is None:
1033
- tlo = settings['timelapse_objects']
1034
- print(f'timelapse object:{tlo}, cells will be relabeled to nucleus labels to track cells.')
996
+ if settings['n_jobs'] > spacr_cores:
997
+ print(f'Warning reserving 6 CPU cores for other processes, setting n_jobs to {spacr_cores}')
998
+ settings['n_jobs'] = spacr_cores
1035
999
 
1036
- int_setting_keys = ['cell_mask_dim', 'nucleus_mask_dim', 'pathogen_mask_dim', 'cell_min_size', 'nucleus_min_size', 'pathogen_min_size', 'cytoplasm_min_size']
1037
-
1038
- if isinstance(settings['normalize'], bool) and settings['normalize']:
1039
- print(f'WARNING: to notmalize single object pngs set normalize to a list of 2 integers, e.g. [1,99] (lower and upper percentiles)')
1040
- return
1041
-
1042
- if isinstance(settings['normalize'], list) or isinstance(settings['normalize'], bool) and settings['normalize']:
1043
- if settings['normalize_by'] not in ['png', 'fov']:
1044
- print("Warning: normalize_by should be either 'png' to notmalize each png to its own percentiles or 'fov' to normalize each png to the fov percentiles ")
1045
- return
1046
-
1047
- if not all(isinstance(settings[key], int) or settings[key] is None for key in int_setting_keys):
1048
- print(f"WARNING: {int_setting_keys} must all be integers")
1049
- return
1000
+ dirname = os.path.dirname(settings['src'])
1001
+ settings_df = pd.DataFrame(list(settings.items()), columns=['Key', 'Value'])
1002
+ settings_csv = os.path.join(dirname,'settings','measure_crop_settings.csv')
1003
+ os.makedirs(os.path.join(dirname,'settings'), exist_ok=True)
1004
+ settings_df.to_csv(settings_csv, index=False)
1050
1005
 
1051
- if not isinstance(settings['channels'], list):
1052
- print(f"WARNING: channels should be a list of integers representing channels e.g. [0,1,2,3]")
1053
- return
1006
+ if settings['timelapse_objects'] == 'nucleus':
1007
+ if not settings['cell_mask_dim'] is None:
1008
+ tlo = settings['timelapse_objects']
1009
+ print(f'timelapse object:{tlo}, cells will be relabeled to nucleus labels to track cells.')
1054
1010
 
1055
- if not isinstance(settings['crop_mode'], list):
1056
- print(f"WARNING: crop_mode should be a list with at least one element e.g. ['cell'] or ['cell','nucleus'] or [None]")
1057
- return
1058
-
1059
- _save_settings_to_db(settings)
1060
- files = [f for f in os.listdir(settings['src']) if f.endswith('.npy')]
1061
- n_jobs = settings['n_jobs']
1062
- print(f'using {n_jobs} cpu cores')
1063
- print_progress(files_processed=0, files_to_process=len(files), n_jobs=n_jobs, time_ls=[], operation_type='Measure and Crop')
1064
-
1065
- def job_callback(result):
1066
- completed_jobs.add(result[0])
1067
- process_meassure_crop_results([result], settings)
1068
- files_processed = len(completed_jobs)
1069
- files_to_process = len(files)
1070
- print_progress(files_processed, files_to_process, n_jobs, time_ls=time_ls, operation_type='Measure and Crop')
1071
- if files_processed >= files_to_process:
1072
- pool.terminate()
1073
-
1074
- with mp.Manager() as manager:
1075
- time_ls = manager.list()
1076
- completed_jobs = set() # Set to keep track of completed jobs
1077
-
1078
- with mp.Pool(n_jobs) as pool:
1079
- for index, file in enumerate(files):
1080
- pool.apply_async(_measure_crop_core, args=(index, time_ls, file, settings), callback=job_callback)
1011
+ int_setting_keys = ['cell_mask_dim', 'nucleus_mask_dim', 'pathogen_mask_dim', 'cell_min_size', 'nucleus_min_size', 'pathogen_min_size', 'cytoplasm_min_size']
1081
1012
 
1082
- pool.close()
1083
- pool.join()
1013
+ if isinstance(settings['normalize'], bool) and settings['normalize']:
1014
+ print(f'WARNING: to notmalize single object pngs set normalize to a list of 2 integers, e.g. [1,99] (lower and upper percentiles)')
1015
+ return
1016
+
1017
+ if isinstance(settings['normalize'], list) or isinstance(settings['normalize'], bool) and settings['normalize']:
1018
+ if settings['normalize_by'] not in ['png', 'fov']:
1019
+ print("Warning: normalize_by should be either 'png' to notmalize each png to its own percentiles or 'fov' to normalize each png to the fov percentiles ")
1020
+ return
1021
+
1022
+ if not all(isinstance(settings[key], int) or settings[key] is None for key in int_setting_keys):
1023
+ print(f"WARNING: {int_setting_keys} must all be integers")
1024
+ return
1025
+
1026
+ if not isinstance(settings['channels'], list):
1027
+ print(f"WARNING: channels should be a list of integers representing channels e.g. [0,1,2,3]")
1028
+ return
1029
+
1030
+ if not isinstance(settings['crop_mode'], list):
1031
+ print(f"WARNING: crop_mode should be a list with at least one element e.g. ['cell'] or ['cell','nucleus'] or [None]")
1032
+ return
1033
+
1034
+ _save_settings_to_db(settings)
1035
+ files = [f for f in os.listdir(settings['src']) if f.endswith('.npy')]
1036
+ n_jobs = settings['n_jobs']
1037
+ print(f'using {n_jobs} cpu cores')
1038
+ print_progress(files_processed=0, files_to_process=len(files), n_jobs=n_jobs, time_ls=[], operation_type='Measure and Crop')
1039
+
1040
+ def job_callback(result):
1041
+ completed_jobs.add(result[0])
1042
+ process_meassure_crop_results([result], settings)
1043
+ files_processed = len(completed_jobs)
1044
+ files_to_process = len(files)
1045
+ print_progress(files_processed, files_to_process, n_jobs, time_ls=time_ls, operation_type='Measure and Crop')
1046
+ if files_processed >= files_to_process:
1047
+ pool.terminate()
1048
+
1049
+ with mp.Manager() as manager:
1050
+ time_ls = manager.list()
1051
+ completed_jobs = set() # Set to keep track of completed jobs
1052
+
1053
+ with mp.Pool(n_jobs) as pool:
1054
+ for index, file in enumerate(files):
1055
+ pool.apply_async(_measure_crop_core, args=(index, time_ls, file, settings), callback=job_callback)
1056
+
1057
+ pool.close()
1058
+ pool.join()
1084
1059
 
1085
- if settings['timelapse']:
1086
- if settings['timelapse_objects'] == 'nucleus':
1087
- folder_path = settings['src']
1088
- mask_channels = [settings['nucleus_mask_dim'], settings['pathogen_mask_dim'], settings['cell_mask_dim']]
1089
- object_types = ['nucleus', 'pathogen', 'cell']
1090
- _timelapse_masks_to_gif(folder_path, mask_channels, object_types)
1060
+ if settings['timelapse']:
1061
+ if settings['timelapse_objects'] == 'nucleus':
1062
+ folder_path = settings['src']
1063
+ mask_channels = [settings['nucleus_mask_dim'], settings['pathogen_mask_dim'], settings['cell_mask_dim']]
1064
+ object_types = ['nucleus', 'pathogen', 'cell']
1065
+ _timelapse_masks_to_gif(folder_path, mask_channels, object_types)
1091
1066
 
1092
- print("Successfully completed run")
1067
+ print("Successfully completed run")
1093
1068
 
1094
1069
  def process_meassure_crop_results(partial_results, settings):
1095
1070
  """
spacr/mediar.py CHANGED
@@ -1,14 +1,11 @@
1
1
  import os, sys, gdown, cv2, torch
2
2
  import numpy as np
3
3
  import matplotlib.pyplot as plt
4
- from monai.inferers import sliding_window_inference
5
4
  import skimage.io as io
6
5
 
7
6
  # Path to the MEDIAR directory
8
7
  mediar_path = os.path.join(os.path.dirname(__file__), 'resources', 'MEDIAR')
9
8
 
10
- print('mediar path', mediar_path)
11
-
12
9
  # Temporarily create __init__.py to make MEDIAR a package
13
10
  init_file = os.path.join(mediar_path, '__init__.py')
14
11
  if not os.path.exists(init_file):