spacr 0.2.4__py3-none-any.whl → 0.2.5__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 (60) hide show
  1. spacr/core.py +56 -67
  2. spacr/gui.py +20 -38
  3. spacr/gui_core.py +390 -489
  4. spacr/gui_elements.py +309 -59
  5. spacr/gui_utils.py +361 -73
  6. spacr/io.py +42 -46
  7. spacr/measure.py +198 -151
  8. spacr/plot.py +108 -42
  9. spacr/resources/font/open_sans/OFL.txt +93 -0
  10. spacr/resources/font/open_sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  11. spacr/resources/font/open_sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  12. spacr/resources/font/open_sans/README.txt +100 -0
  13. spacr/resources/font/open_sans/static/OpenSans-Bold.ttf +0 -0
  14. spacr/resources/font/open_sans/static/OpenSans-BoldItalic.ttf +0 -0
  15. spacr/resources/font/open_sans/static/OpenSans-ExtraBold.ttf +0 -0
  16. spacr/resources/font/open_sans/static/OpenSans-ExtraBoldItalic.ttf +0 -0
  17. spacr/resources/font/open_sans/static/OpenSans-Italic.ttf +0 -0
  18. spacr/resources/font/open_sans/static/OpenSans-Light.ttf +0 -0
  19. spacr/resources/font/open_sans/static/OpenSans-LightItalic.ttf +0 -0
  20. spacr/resources/font/open_sans/static/OpenSans-Medium.ttf +0 -0
  21. spacr/resources/font/open_sans/static/OpenSans-MediumItalic.ttf +0 -0
  22. spacr/resources/font/open_sans/static/OpenSans-Regular.ttf +0 -0
  23. spacr/resources/font/open_sans/static/OpenSans-SemiBold.ttf +0 -0
  24. spacr/resources/font/open_sans/static/OpenSans-SemiBoldItalic.ttf +0 -0
  25. spacr/resources/font/open_sans/static/OpenSans_Condensed-Bold.ttf +0 -0
  26. spacr/resources/font/open_sans/static/OpenSans_Condensed-BoldItalic.ttf +0 -0
  27. spacr/resources/font/open_sans/static/OpenSans_Condensed-ExtraBold.ttf +0 -0
  28. spacr/resources/font/open_sans/static/OpenSans_Condensed-ExtraBoldItalic.ttf +0 -0
  29. spacr/resources/font/open_sans/static/OpenSans_Condensed-Italic.ttf +0 -0
  30. spacr/resources/font/open_sans/static/OpenSans_Condensed-Light.ttf +0 -0
  31. spacr/resources/font/open_sans/static/OpenSans_Condensed-LightItalic.ttf +0 -0
  32. spacr/resources/font/open_sans/static/OpenSans_Condensed-Medium.ttf +0 -0
  33. spacr/resources/font/open_sans/static/OpenSans_Condensed-MediumItalic.ttf +0 -0
  34. spacr/resources/font/open_sans/static/OpenSans_Condensed-Regular.ttf +0 -0
  35. spacr/resources/font/open_sans/static/OpenSans_Condensed-SemiBold.ttf +0 -0
  36. spacr/resources/font/open_sans/static/OpenSans_Condensed-SemiBoldItalic.ttf +0 -0
  37. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Bold.ttf +0 -0
  38. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-BoldItalic.ttf +0 -0
  39. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-ExtraBold.ttf +0 -0
  40. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf +0 -0
  41. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Italic.ttf +0 -0
  42. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Light.ttf +0 -0
  43. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-LightItalic.ttf +0 -0
  44. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Medium.ttf +0 -0
  45. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-MediumItalic.ttf +0 -0
  46. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Regular.ttf +0 -0
  47. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-SemiBold.ttf +0 -0
  48. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
  49. spacr/resources/icons/logo.pdf +2786 -6
  50. spacr/resources/icons/logo_spacr.png +0 -0
  51. spacr/resources/icons/logo_spacr_1.png +0 -0
  52. spacr/settings.py +11 -83
  53. spacr/utils.py +13 -33
  54. {spacr-0.2.4.dist-info → spacr-0.2.5.dist-info}/METADATA +5 -1
  55. spacr-0.2.5.dist-info/RECORD +100 -0
  56. spacr-0.2.4.dist-info/RECORD +0 -58
  57. {spacr-0.2.4.dist-info → spacr-0.2.5.dist-info}/LICENSE +0 -0
  58. {spacr-0.2.4.dist-info → spacr-0.2.5.dist-info}/WHEEL +0 -0
  59. {spacr-0.2.4.dist-info → spacr-0.2.5.dist-info}/entry_points.txt +0 -0
  60. {spacr-0.2.4.dist-info → spacr-0.2.5.dist-info}/top_level.txt +0 -0
spacr/measure.py CHANGED
@@ -13,6 +13,8 @@ from skimage.feature import graycomatrix, graycoprops
13
13
  from mahotas.features import zernike_moments
14
14
  from skimage import morphology, measure, filters
15
15
  from skimage.util import img_as_bool
16
+ import matplotlib.pyplot as plt
17
+ from math import ceil, sqrt
16
18
 
17
19
  from .logger import log_function_call
18
20
 
@@ -582,6 +584,113 @@ def _intensity_measurements(cell_mask, nucleus_mask, pathogen_mask, cytoplasm_ma
582
584
 
583
585
  return pd.concat(cell_dfs, axis=1), pd.concat(nucleus_dfs, axis=1), pd.concat(pathogen_dfs, axis=1), pd.concat(cytoplasm_dfs, axis=1)
584
586
 
587
+ def save_and_add_image_to_grid(png_channels, img_path, grid, plot=False):
588
+ """
589
+ Add an image to a grid and save it as PNG.
590
+
591
+ Args:
592
+ png_channels (ndarray): The array representing the image channels.
593
+ img_path (str): The path to save the image as PNG.
594
+ grid (list): The grid of images to be plotted later.
595
+
596
+ Returns:
597
+ grid (list): Updated grid with the new image added.
598
+ """
599
+
600
+ # Save the image as a PNG
601
+ cv2.imwrite(img_path, png_channels)
602
+
603
+ if plot:
604
+
605
+ # Ensure the image is in uint8 format for cv2 functions
606
+ if png_channels.dtype == np.uint16:
607
+ png_channels = (png_channels / 256).astype(np.uint8)
608
+
609
+ # Get the filename without the extension
610
+ filename = os.path.splitext(os.path.basename(img_path))[0]
611
+
612
+ # Add the label to the image
613
+ #labeled_image = cv2.putText(png_channels.copy(), filename, (10, 30),
614
+ # cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA)
615
+
616
+ # Add the labeled image to the grid
617
+ grid.append(png_channels)
618
+
619
+ return grid
620
+
621
+ def img_list_to_grid(grid, titles=None):
622
+ """
623
+ Plot a grid of images with optional titles.
624
+
625
+ Args:
626
+ grid (list): List of images to be plotted.
627
+ titles (list): List of titles for the images.
628
+
629
+ Returns:
630
+ fig (Figure): The matplotlib figure object containing the image grid.
631
+ """
632
+ n_images = len(grid)
633
+ grid_size = ceil(sqrt(n_images))
634
+
635
+ fig, axs = plt.subplots(grid_size, grid_size, figsize=(15, 15), facecolor='black')
636
+
637
+ for i, ax in enumerate(axs.flat):
638
+ if i < n_images:
639
+ image = grid[i]
640
+ ax.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
641
+ ax.axis('off')
642
+ ax.set_facecolor('black')
643
+
644
+ if titles:
645
+ # Determine text size
646
+ img_height, img_width = image.shape[:2]
647
+ text_size = max(min(img_width / (len(titles[i]) * 1.5), img_height / 10), 4)
648
+ ax.text(5, 5, titles[i], color='white', fontsize=text_size, ha='left', va='top', fontweight='bold')
649
+ else:
650
+ fig.delaxes(ax)
651
+
652
+ # Adjust spacing
653
+ plt.subplots_adjust(wspace=0.05, hspace=0.05)
654
+ plt.tight_layout(pad=0.1)
655
+ return fig
656
+
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
+
585
694
  #@log_function_call
586
695
  def _measure_crop_core(index, time_ls, file, settings):
587
696
 
@@ -603,20 +712,15 @@ def _measure_crop_core(index, time_ls, file, settings):
603
712
  A list of cropped images.
604
713
  """
605
714
 
606
- from .io import _create_database
607
715
  from .plot import _plot_cropped_arrays
608
716
  from .utils import _merge_overlapping_objects, _filter_object, _relabel_parent_with_child_labels, _exclude_objects, normalize_to_dtype
609
- from .utils import _merge_and_save_to_database, _crop_center, _find_bounding_box, _generate_names, _get_percentiles, _map_wells_png
610
-
717
+ from .utils import _merge_and_save_to_database, _crop_center, _find_bounding_box, _generate_names, _get_percentiles
718
+
719
+ figs = {}
720
+ grid = []
611
721
  start = time.time()
612
722
  try:
613
723
  source_folder = os.path.dirname(settings['src'])
614
- #if not os.path.basename(source_folder).endswith('merged'):
615
- # source_folder = os.path.join(source_folder, 'merged')
616
- # print(f'changed source_folder to {source_folder}')
617
-
618
- #if not os.path.exists(source_folder):
619
- # return
620
724
 
621
725
  file_name = os.path.splitext(file)[0]
622
726
  data = np.load(os.path.join(settings['src'], file))
@@ -628,18 +732,13 @@ def _measure_crop_core(index, time_ls, file, settings):
628
732
  if settings['verbose']:
629
733
  print(f'Converted data from {data_type_before} to {data_type}')
630
734
 
631
- if settings['save_measurements']:
632
- os.makedirs(source_folder+'/measurements', exist_ok=True)
633
- _create_database(source_folder+'/measurements/measurements.db')
634
-
635
- if settings['plot_filtration']:
636
-
735
+ if settings['plot']:
637
736
  if len(data.shape) == 3:
638
737
  figuresize = data.shape[2]*10
639
738
  else:
640
739
  figuresize = 10
641
- print('')
642
- _plot_cropped_arrays(data, file, figuresize)
740
+ fig = _plot_cropped_arrays(data, file, figuresize)
741
+ figs[f'{file_name}__before_filtration'] = fig
643
742
 
644
743
  channel_arrays = data[:, :, settings['channels']].astype(data_type)
645
744
  if settings['cell_mask_dim'] is not None:
@@ -714,9 +813,9 @@ def _measure_crop_core(index, time_ls, file, settings):
714
813
  if settings['cytoplasm']:
715
814
  data = np.concatenate((data, cytoplasm_mask[:, :, np.newaxis]), axis=2)
716
815
 
717
- if settings['plot_filtration']:
718
- _plot_cropped_arrays(data, file, figuresize)
719
- #_plot_cropped_arrays(data)
816
+ if settings['plot']:
817
+ fig = _plot_cropped_arrays(data, file, figuresize)
818
+ figs[f'{file_name}__after_filtration'] = fig
720
819
 
721
820
  if settings['save_measurements']:
722
821
 
@@ -837,53 +936,12 @@ def _measure_crop_core(index, time_ls, file, settings):
837
936
  if png_channels.shape[2] == 2:
838
937
  dummy_channel = np.zeros_like(png_channels[:,:,0]) # Create a 2D zero array with same shape as one channel
839
938
  png_channels = np.dstack((png_channels, dummy_channel))
840
- cv2.imwrite(img_path, png_channels)
939
+ grid = save_and_add_image_to_grid(png_channels, img_path, grid, settings['plot'])
841
940
  else:
842
- cv2.imwrite(img_path, png_channels)
941
+ grid = save_and_add_image_to_grid(png_channels, img_path, grid, settings['plot'])
843
942
 
844
943
  if len(img_paths) == len(objects_in_image):
845
-
846
- png_df = pd.DataFrame(img_paths, columns=['png_path'])
847
-
848
- png_df['file_name'] = png_df['png_path'].apply(lambda x: os.path.basename(x))
849
-
850
- parts = png_df['file_name'].apply(lambda x: pd.Series(_map_wells_png(x, timelapse=settings['timelapse'])))
851
-
852
- columns = ['plate', 'row', 'col', 'field']
853
-
854
- if settings['timelapse']:
855
- columns = columns + ['time_id']
856
-
857
- columns = columns + ['prcfo']
858
-
859
- if crop_mode == 'cell':
860
- columns = columns + ['cell_id']
861
-
862
- if crop_mode == 'nucleus':
863
- columns = columns + ['nucleus_id']
864
-
865
- if crop_mode == 'pathogen':
866
- columns = columns + ['pathogen_id']
867
-
868
- if crop_mode == 'cytoplasm':
869
- columns = columns + ['cytoplasm_id']
870
-
871
- png_df[columns] = parts
872
-
873
- try:
874
- conn = sqlite3.connect(f'{source_folder}/measurements/measurements.db', timeout=5)
875
- png_df.to_sql('png_list', conn, if_exists='append', index=False)
876
- conn.commit()
877
- except sqlite3.OperationalError as e:
878
- print(f"SQLite error: {e}", flush=True)
879
- traceback.print_exc()
880
-
881
- if settings['plot']:
882
- if len(png_channels.shape) == 3:
883
- figuresize = png_channels.shape[2]*10
884
- else:
885
- figuresize = 10
886
- _plot_cropped_arrays(png_channels, img_name, figuresize, threshold=1)
944
+ filepaths_to_database(img_paths, settings, source_folder, crop_mode)
887
945
 
888
946
  if settings['save_arrays']:
889
947
  row_idx, col_idx = np.where(region)
@@ -891,21 +949,12 @@ def _measure_crop_core(index, time_ls, file, settings):
891
949
  array_folder = f"{fldr}/region_array/"
892
950
  os.makedirs(array_folder, exist_ok=True)
893
951
  np.save(os.path.join(array_folder, img_name), region_array)
894
- if settings['plot']:
895
- if len(png_channels.shape) == 3:
896
- figuresize = png_channels.shape[2]*10
897
- else:
898
- figuresize = 10
899
- _plot_cropped_arrays(png_channels, img_name, figuresize, threshold=1)
900
-
901
- if not settings['save_arrays'] and not settings['save_png'] and settings['plot']:
902
- row_idx, col_idx = np.where(region)
903
- region_array = data[row_idx.min():row_idx.max()+1, col_idx.min():col_idx.max()+1, :]
904
- if len(png_channels.shape) == 3:
905
- figuresize = png_channels.shape[2]*10
906
- else:
907
- figuresize = 10
908
- _plot_cropped_arrays(png_channels, file, figuresize, threshold=1)
952
+
953
+ grid = save_and_add_image_to_grid(png_channels, img_path, grid, settings['plot'])
954
+
955
+ img_paths.append(img_path)
956
+ if len(img_paths) == len(objects_in_image):
957
+ filepaths_to_database(img_paths, settings, source_folder, crop_mode)
909
958
 
910
959
  cells = np.unique(cell_mask)
911
960
  except Exception as e:
@@ -917,7 +966,10 @@ def _measure_crop_core(index, time_ls, file, settings):
917
966
  duration = end-start
918
967
  time_ls.append(duration)
919
968
  average_time = np.mean(time_ls) if len(time_ls) > 0 else 0
920
- return average_time, cells
969
+ if settings['plot']:
970
+ fig = img_list_to_grid(grid)
971
+ figs[f'{file_name}__pngs'] = fig
972
+ return index, average_time, cells, figs
921
973
 
922
974
  #@log_function_call
923
975
  def measure_crop(settings):
@@ -932,23 +984,24 @@ def measure_crop(settings):
932
984
  None
933
985
  """
934
986
 
935
- from .io import _save_settings_to_db
936
- from .timelapse import _timelapse_masks_to_gif, _scmovie
937
- from .plot import _save_scimg_plot
938
- from .utils import _list_endpoint_subdirectories, _generate_representative_images, measure_test_mode, print_progress
987
+ from .io import _save_settings_to_db, _create_database
988
+ from .timelapse import _timelapse_masks_to_gif
989
+ from .utils import measure_test_mode, print_progress
939
990
  from .settings import get_measure_crop_settings
940
991
 
941
992
  settings = get_measure_crop_settings(settings)
942
993
  settings = measure_test_mode(settings)
943
994
 
944
- #src_fldr = settings['src']
945
- #if not os.path.basename(src_fldr).endswith('merged'):
946
- # settings['src'] = os.path.join(src_fldr, 'merged')
947
- # print(f"changed src to {src_fldr}")
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}")
948
1000
 
949
- #if not os.path.exists(settings['src']):
950
- # print(f'src: {settings["src"]} does not exist')
951
- # return
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')
952
1005
 
953
1006
  if settings['cell_mask_dim'] is None:
954
1007
  settings['include_uninfected'] = True
@@ -960,7 +1013,15 @@ def measure_crop(settings):
960
1013
  settings['cytoplasm'] = True
961
1014
  else:
962
1015
  settings['cytoplasm'] = False
963
-
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
1024
+
964
1025
  dirname = os.path.dirname(settings['src'])
965
1026
  settings_df = pd.DataFrame(list(settings.items()), columns=['Key', 'Value'])
966
1027
  settings_csv = os.path.join(dirname,'settings','measure_crop_settings.csv')
@@ -997,76 +1058,62 @@ def measure_crop(settings):
997
1058
 
998
1059
  _save_settings_to_db(settings)
999
1060
  files = [f for f in os.listdir(settings['src']) if f.endswith('.npy')]
1000
- n_jobs = settings['n_jobs'] or mp.cpu_count()-4
1061
+ n_jobs = settings['n_jobs']
1001
1062
  print(f'using {n_jobs} cpu cores')
1063
+
1064
+ def job_callback(result):
1065
+ completed_jobs.add(result[0])
1066
+ process_meassure_crop_results([result], settings)
1067
+ files_processed = len(completed_jobs)
1068
+ files_to_process = len(files)
1069
+ print_progress(files_processed, files_to_process, n_jobs, time_ls=time_ls, operation_type='Measure and Crop')
1070
+ if files_processed >= files_to_process:
1071
+ pool.terminate()
1072
+
1002
1073
  with mp.Manager() as manager:
1003
1074
  time_ls = manager.list()
1075
+ completed_jobs = set() # Set to keep track of completed jobs
1076
+
1004
1077
  with mp.Pool(n_jobs) as pool:
1005
- result = pool.starmap_async(_measure_crop_core, [(index, time_ls, file, settings) for index, file in enumerate(files)])
1006
-
1007
- # Track progress in the main process
1008
- while not result.ready():
1009
- time.sleep(1)
1010
- files_processed = len(time_ls)
1011
- files_to_process = len(files)
1012
- print_progress(files_processed, files_to_process, n_jobs, time_ls=None)
1013
- result.get()
1014
-
1015
- if settings['representative_images']:
1016
- if settings['save_png']:
1017
- img_fldr = os.path.join(os.path.dirname(settings['src']), 'data')
1018
- sc_img_fldrs = _list_endpoint_subdirectories(img_fldr)
1019
-
1020
- for i, well_src in enumerate(sc_img_fldrs):
1021
- if len(os.listdir(well_src)) < 16:
1022
- nr_imgs = len(os.listdir(well_src))
1023
- standardize = False
1024
- else:
1025
- nr_imgs = 16
1026
- standardize = True
1027
- try:
1028
- all_folders = len(sc_img_fldrs)
1029
- _save_scimg_plot(src=well_src, nr_imgs=nr_imgs, channel_indices=settings['png_dims'], um_per_pixel=0.1, scale_bar_length_um=10, standardize=standardize, fontsize=12, show_filename=True, channel_names=['red','green','blue'], dpi=300, plot=False, i=i, all_folders=all_folders)
1030
-
1031
- except Exception as e:
1032
- print(f"Unable to generate figure for folder {well_src}: {e}", end='\r', flush=True)
1033
- #traceback.print_exc()
1034
-
1035
- if settings['save_measurements']:
1036
- db_path = os.path.join(os.path.dirname(settings['src']), 'measurements', 'measurements.db')
1037
- channel_indices = settings['png_dims']
1038
- channel_indices = [min(value, 2) for value in channel_indices]
1039
- _generate_representative_images(db_path,
1040
- cells=settings['cells'],
1041
- cell_loc=settings['cell_loc'],
1042
- pathogens=settings['pathogens'],
1043
- pathogen_loc=settings['pathogen_loc'],
1044
- treatments=settings['treatments'],
1045
- treatment_loc=settings['treatment_loc'],
1046
- channel_of_interest=settings['channel_of_interest'],
1047
- compartments = settings['compartments'],
1048
- measurement = settings['measurement'],
1049
- nr_imgs=settings['nr_imgs'],
1050
- channel_indices=channel_indices,
1051
- um_per_pixel=settings['um_per_pixel'],
1052
- scale_bar_length_um=10,
1053
- plot=False,
1054
- fontsize=12,
1055
- show_filename=True,
1056
- channel_names=None)
1078
+ for index, file in enumerate(files):
1079
+ pool.apply_async(_measure_crop_core, args=(index, time_ls, file, settings), callback=job_callback)
1080
+
1081
+ pool.close()
1082
+ pool.join()
1057
1083
 
1058
1084
  if settings['timelapse']:
1059
1085
  if settings['timelapse_objects'] == 'nucleus':
1060
1086
  folder_path = settings['src']
1061
- mask_channels = [settings['nucleus_mask_dim'], settings['pathogen_mask_dim'],settings['cell_mask_dim']]
1062
- object_types = ['nucleus','pathogen','cell']
1087
+ mask_channels = [settings['nucleus_mask_dim'], settings['pathogen_mask_dim'], settings['cell_mask_dim']]
1088
+ object_types = ['nucleus', 'pathogen', 'cell']
1063
1089
  _timelapse_masks_to_gif(folder_path, mask_channels, object_types)
1064
1090
 
1065
- #if settings['save_png']:
1066
- img_fldr = os.path.join(os.path.dirname(settings['src']), 'data')
1067
- sc_img_fldrs = _list_endpoint_subdirectories(img_fldr)
1068
- _scmovie(sc_img_fldrs)
1069
1091
  print("Successfully completed run")
1092
+
1093
+ def process_meassure_crop_results(partial_results, settings):
1094
+ """
1095
+ Process the results, display, and optionally save the figures.
1096
+
1097
+ Args:
1098
+ partial_results (list): List of partial results.
1099
+ settings (dict): Settings dictionary.
1100
+ save_figures (bool): Flag to save figures or not.
1101
+ """
1102
+ for result in partial_results:
1103
+ if result is None:
1104
+ continue
1105
+ index, avg_time, cells, figs = result
1106
+ if figs is not None:
1107
+ for key, fig in figs.items():
1108
+ part_1, part_2 = key.split('__')
1109
+ save_dir = os.path.join(os.path.dirname(settings['src']), 'results', f"{part_1}")
1110
+ os.makedirs(save_dir, exist_ok=True)
1111
+ fig_path = os.path.join(save_dir, f"{part_2}.pdf")
1112
+ fig.savefig(fig_path)
1113
+ plt.figure(fig.number)
1114
+ plt.show()
1115
+ plt.close(fig)
1116
+ result = (index, None, None, None)
1070
1117
 
1071
1118
  def generate_cellpose_train_set(folders, dst, min_objects=5):
1072
1119
  os.makedirs(dst, exist_ok=True)
spacr/plot.py CHANGED
@@ -19,6 +19,112 @@ from IPython.display import Image as ipyimage
19
19
 
20
20
  from .logger import log_function_call
21
21
 
22
+ def plot_image_mask_overlay(file, channels, cell_channel, nucleus_channel, pathogen_channel, figuresize=10, normalize=True, thickness=3, save_pdf=True):
23
+ """Plot image and mask overlays."""
24
+
25
+ def _plot_merged_plot(image, outlines, outline_colors, figuresize, thickness):
26
+ """Plot the merged plot with overlay, image channels, and masks."""
27
+
28
+ def _normalize_image(image, percentiles=(2, 98)):
29
+ """Normalize the image to the given percentiles."""
30
+ v_min, v_max = np.percentile(image, percentiles)
31
+ image_normalized = np.clip((image - v_min) / (v_max - v_min), 0, 1)
32
+ return image_normalized
33
+
34
+ def _generate_contours(mask):
35
+ """Generate contours for the given mask using OpenCV."""
36
+ contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
37
+ return contours
38
+
39
+ def _apply_contours(image, mask, color, thickness):
40
+ """Apply the contours to the RGB image for each unique label."""
41
+ unique_labels = np.unique(mask)
42
+ for label in unique_labels:
43
+ if label == 0:
44
+ continue # Skip background
45
+ label_mask = np.where(mask == label, 1, 0).astype(np.uint8)
46
+ contours = _generate_contours(label_mask)
47
+ for contour in contours:
48
+ cv2.drawContours(image, [contour], -1, mpl.colors.to_rgb(color), thickness)
49
+ return image
50
+
51
+ num_channels = image.shape[-1]
52
+ fig, ax = plt.subplots(1, num_channels + 1, figsize=(4 * figuresize, figuresize))
53
+
54
+ # Plot each channel with its corresponding outlines
55
+ for v in range(num_channels):
56
+ channel_image = image[..., v]
57
+ channel_image_normalized = _normalize_image(channel_image)
58
+ channel_image_rgb = np.dstack((channel_image_normalized, channel_image_normalized, channel_image_normalized))
59
+
60
+ for outline, color in zip(outlines, outline_colors):
61
+ channel_image_rgb = _apply_contours(channel_image_rgb, outline, color, thickness)
62
+
63
+ ax[v].imshow(channel_image_rgb)
64
+ ax[v].set_title(f'Image - Channel {v}')
65
+
66
+ # Plot the combined RGB image with all outlines
67
+ rgb_image = np.zeros((*image.shape[:2], 3), dtype=float)
68
+ rgb_channels = min(3, num_channels)
69
+ for i in range(rgb_channels):
70
+ channel_image = image[..., i]
71
+ channel_image_normalized = _normalize_image(channel_image)
72
+ rgb_image[..., i] = channel_image_normalized
73
+
74
+ for outline, color in zip(outlines, outline_colors):
75
+ rgb_image = _apply_contours(rgb_image, outline, color, thickness)
76
+
77
+ ax[-1].imshow(rgb_image)
78
+ ax[-1].set_title('Combined RGB Image')
79
+
80
+ plt.tight_layout()
81
+
82
+ # Save the figure as a PDF
83
+ if save_pdf:
84
+ pdf_dir = os.path.join(os.path.dirname(os.path.dirname(file)), 'results', 'overlay')
85
+ os.makedirs(pdf_dir, exist_ok=True)
86
+ pdf_path = os.path.join(pdf_dir, os.path.basename(file).replace('.npy', '.pdf'))
87
+ fig.savefig(pdf_path, format='pdf')
88
+
89
+ plt.show()
90
+ return fig
91
+
92
+ stack = np.load(file)
93
+
94
+ # Convert to float for normalization and ensure correct handling of both 8-bit and 16-bit arrays
95
+ if stack.dtype == np.uint16:
96
+ stack = stack.astype(np.float32)
97
+ elif stack.dtype == np.uint8:
98
+ stack = stack.astype(np.float32)
99
+
100
+ image = stack[..., channels]
101
+ outlines = []
102
+ outline_colors = []
103
+
104
+ if pathogen_channel is not None:
105
+ pathogen_mask_dim = -1 # last dimension
106
+ outlines.append(np.take(stack, pathogen_mask_dim, axis=2))
107
+ outline_colors.append('blue')
108
+
109
+ if nucleus_channel is not None:
110
+ nucleus_mask_dim = -2 if pathogen_channel is not None else -1
111
+ outlines.append(np.take(stack, nucleus_mask_dim, axis=2))
112
+ outline_colors.append('green')
113
+
114
+ if cell_channel is not None:
115
+ if nucleus_channel is not None and pathogen_channel is not None:
116
+ cell_mask_dim = -3
117
+ elif nucleus_channel is not None or pathogen_channel is not None:
118
+ cell_mask_dim = -2
119
+ else:
120
+ cell_mask_dim = -1
121
+ outlines.append(np.take(stack, cell_mask_dim, axis=2))
122
+ outline_colors.append('red')
123
+
124
+ fig = _plot_merged_plot(image=image, outlines=outlines, outline_colors=outline_colors, figuresize=figuresize, thickness=thickness)
125
+
126
+ return
127
+
22
128
  def plot_masks(batch, masks, flows, cmap='inferno', figuresize=20, nr=1, file_type='.npz', print_object_number=True):
23
129
  """
24
130
  Plot the masks and flows for a given batch of images.
@@ -802,49 +908,9 @@ def _plot_cropped_arrays(stack, filename, figuresize=20, cmap='inferno', thresho
802
908
  fig, axs = plt.subplots(1, num_channels, figsize=(figuresize, figuresize))
803
909
  for channel in range(num_channels):
804
910
  plot_single_array(stack[:, :, channel], axs[channel], f'C. {channel}', plt.get_cmap(cmap))
805
- fig.tight_layout()
806
- plt.show()
807
-
808
- #stop = time.time()
809
- #duration = stop - start
810
- #print('plot_cropped_arrays', duration)
911
+ fig.tight_layout()
811
912
  print(f'{filename}')
812
-
813
- def _plot_cropped_arrays_v1(stack, figuresize=20, cmap='inferno'):
814
- """
815
- Plot cropped arrays.
816
-
817
- Args:
818
- stack (ndarray): The array to be plotted.
819
- figuresize (int, optional): The size of the figure. Defaults to 20.
820
- cmap (str, optional): The colormap to be used. Defaults to 'inferno'.
821
-
822
- Returns:
823
- None
824
- """
825
- start = time.time()
826
- dim = stack.shape
827
- channel=min(dim)
828
- if len(stack.shape) == 2:
829
- f, a = plt.subplots(1, 1,figsize=(figuresize,figuresize))
830
- a.imshow(stack, cmap=plt.get_cmap(cmap))
831
- a.set_title('Channel one',size=18)
832
- a.axis('off')
833
- f.tight_layout()
834
- plt.show()
835
- if len(stack.shape) > 2:
836
- anr = stack.shape[2]
837
- f, a = plt.subplots(1, anr,figsize=(figuresize,figuresize))
838
- for channel in range(anr):
839
- a[channel].imshow(stack[:,:,channel], cmap=plt.get_cmap(cmap))
840
- a[channel].set_title('Channel '+str(channel),size=18)
841
- a[channel].axis('off')
842
- f.tight_layout()
843
- plt.show()
844
- stop = time.time()
845
- duration = stop - start
846
- print('plot_cropped_arrays', duration)
847
- return
913
+ return fig
848
914
 
849
915
  def _visualize_and_save_timelapse_stack_with_tracks(masks, tracks_df, save, src, name, plot, filenames, object_type, mode='btrack', interactive=False):
850
916
  """