__init__.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import math
  2. import pathlib as pl
  3. from view.python_core.utils.colors import interpret_flag_SO_MV_colortable
  4. from view.python_core.utils.deduplicator import dedupilicate
  5. from ..areas import get_area_for_p1
  6. from ..flags import FlagsManager
  7. from ..p1_class import Default_P1_Getter
  8. from .ctv_handlers import get_ctv_handler
  9. from .roi_marker import get_roi_marker_2D
  10. from ..movies.excluder import Excluder2D
  11. from ..movies.spatial import get_spatial_processor
  12. from ..movies.data_limit import get_data_limit_decider_2D
  13. from ..movies.colorizer import get_colorizer_2D
  14. from ..movies.rotate import get_frame_rotator
  15. from ..movies.static_border import get_static_border_adder_2D
  16. from ..movies.data_to_01 import get_normalizer
  17. from ..movies.colorizer.aux_funcs import stack_duplicate_frames
  18. import numpy as np
  19. from matplotlib import pyplot as plt
  20. import logging
  21. class OverviewColorizerAnnotator(object):
  22. def __init__(self, flags, p1):
  23. frame_size = (p1.metadata.format_x, p1.metadata.format_y)
  24. self.colormap, self.bg_color_mpl_compliant, self.fg_color_mpl_compliant = \
  25. interpret_flag_SO_MV_colortable(SO_MV_colortable=flags["SO_MV_colortable"],
  26. fg_color=flags["SO_fgColor"],
  27. bg_color=flags["SO_bgColor"])
  28. # initialize excluder
  29. self.excluder = Excluder2D(cutborder_x=flags["SO_cutborder"], cutborder_y=flags["SO_cutborder"])
  30. # initialize area mask
  31. area_mask_2D = self.excluder.exclude_from_frame(p1.area_mask)
  32. # initialize object for handling preprocessing
  33. self.spatial_processor = get_spatial_processor(filter_space_flag=flags["Signal_FilterSpaceFlag"],
  34. filter_space_size=flags["Signal_FilterSpaceSize"])
  35. # initialize object for deciding data limit
  36. self.data_limit_decider = get_data_limit_decider_2D(flags=flags, frame_mask=area_mask_2D)
  37. # initialize scaler class
  38. self.individual_scale = flags["SO_individualScale"]
  39. # initialize colorizer
  40. self.colorizer = get_colorizer_2D(flags=flags, p1=p1, colormap=self.colormap,
  41. bg_color=self.bg_color_mpl_compliant, excluder=self.excluder,
  42. area_mask_2D_excluded=area_mask_2D)
  43. revised_frame_size = self.excluder.revise_frame_size(frame_size)
  44. # intialize ROI marker
  45. self.roi_marker = get_roi_marker_2D(
  46. flags=flags, measurement_label=p1.metadata.ex_name,
  47. fg_color=self.fg_color_mpl_compliant, bg_color=self.bg_color_mpl_compliant,
  48. unexcluded_frame_size=frame_size, excluder=self.excluder)
  49. # initialize object for rotations
  50. self.frame_rotater = get_frame_rotator(rotate=flags["SO_rotateImage"],
  51. reverse=flags["SO_reverseIt"])
  52. modified_frame_size = self.frame_rotater.transform_frame_size(revised_frame_size)
  53. # initialize object for adding colorbar
  54. self.colorbar_adder = get_static_border_adder_2D(flags=flags,
  55. bg_color_for_mpl=self.bg_color_mpl_compliant,
  56. fg_color_for_mpl=self.fg_color_mpl_compliant,
  57. frame_size=modified_frame_size,
  58. colormap=self.colormap,
  59. )
  60. def preprocess(self, data: np.ndarray):
  61. space_filtered_data = self.spatial_processor.filter_2D(data)
  62. data_cropped = self.excluder.exclude_from_frame(space_filtered_data)
  63. return data_cropped
  64. def get_normalizer(self, data: np.ndarray):
  65. vmin, vmax = self.data_limit_decider.get_data_limit(data)
  66. return get_normalizer(vmin=vmin, vmax=vmax, mv_individualScale=self.individual_scale)
  67. def colorize(self, data: np.ndarray, data_to_01_mapper):
  68. return self.colorizer.colorize(data=data, data_to_01_mapper=data_to_01_mapper)
  69. def rotate_frame(self, frame_data):
  70. return self.frame_rotater.transform(frame_data)
  71. def add_colorbar(self, frame_data, static_frame):
  72. return self.colorbar_adder.composite(frame_data=frame_data, static_frame=static_frame)
  73. def add_rois(self, frame_data):
  74. return self.roi_marker.draw(frame_data)
  75. def generate_overview_frame(flags, p1, feature_number=None):
  76. """
  77. Generate overview frames
  78. :param flags: view.python_core.FlagsManager object
  79. :param p1: pandas.Series
  80. :param str|None|list feature_number: one of the following:
  81. 'all': all features of the CTV specified by the flags, generating as many subplots
  82. None: use the feature number in the flag "CTV_FeatureNumber"
  83. list: containing a list of feature numbers to use (features are numbered 0, 1, 2...)
  84. :return: overview_frames, list of 2D numpy.ndarray, the overview image for features in XY format with origin at bottom left
  85. """
  86. ctv_handler = get_ctv_handler(flags=flags, p1=p1)
  87. overview_frames = ctv_handler.apply(p1.sig1)
  88. n_features = overview_frames.shape[0]
  89. if feature_number is None:
  90. feature_number = [flags["CTV_FeatureNumber"]]
  91. elif feature_number == "all":
  92. feature_number = slice(None)
  93. else:
  94. assert type(feature_number) is list and all(type(x) is int for x in feature_number), \
  95. f"feature_number can be either None, 'all' or a list of ints. Got {feature_number}"
  96. try:
  97. return overview_frames[feature_number, :, :]
  98. except IndexError as ie:
  99. raise IndexError(
  100. f"The specified CTV has {n_features} features, so feature_number can be in [0, {n_features - 1}]. "
  101. f"Some values of feature_number fall out of this range: {feature_number}")
  102. def generate_overview_image(flags, p1):
  103. """
  104. Generate overview image
  105. :param flags: view.python_core.FlagsManager object
  106. :param p1: pandas.Series
  107. :returns: image, data_limits
  108. image: numpy.ndarray, of dimension 3, format X x Y x Color
  109. data_limits: 2-member tuple, indicating frame values mapping to lower and upper end of colormap
  110. """
  111. overview_frames = generate_overview_frame(flags, p1)
  112. overview_frame = overview_frames[0, :, :]
  113. return colorize_overview_add_border_etc(overview_frame=overview_frame, flags=flags, p1=p1)
  114. def colorize_overview_add_border_etc(overview_frame, flags, p1=None):
  115. """
  116. Colorizes <overview_frame>, applies borders and border annotations according to <flags>. If <overview_frame> was
  117. generated from a p1 object, pass it to <p1>, else let it default to None. A p1 object is only required when <flags>
  118. specify that foto1 or stimuli information is to be used when colorizing or applying border annotations
  119. to <overview_frame>.
  120. :param overview_frame: numpy.ndarray, of dimension 2, format X x Y. input overview frame
  121. :param flags: FlagsManager object, containing flags, most importantly "SO..." flags
  122. :param p1: None or a p1 object with stimulus data.
  123. :rtype: tuple
  124. :returns: image, data_limits, overview_generator used
  125. image: numpy.ndarray, of dimension 3, format X x Y x Color
  126. data_limits: 2-member tuple, indicating frame values mapping to lower and upper end of colormap
  127. """
  128. if p1 is None:
  129. fake_raw1 = stack_duplicate_frames(overview_frame, 1)
  130. p1 = Default_P1_Getter().get_fake_p1_from_raw(raw1=fake_raw1)
  131. p1.metadata.format_x, p1.metadata.format_y = overview_frame.shape
  132. logging.getLogger("VIEW").warning(
  133. "Since original p1 object has not been specified, data in overview_frame will be used as foto1")
  134. p1.foto1 = overview_frame
  135. p1.area_mask = get_area_for_p1(frame_size=overview_frame.shape, flags=flags)
  136. overview_generator = OverviewColorizerAnnotator(flags=flags, p1=p1)
  137. # apply spatial filter and cut
  138. overview_frame_preprocessed = overview_generator.preprocess(overview_frame)
  139. # get data_to_01_mapper
  140. data_to_01_mapper = overview_generator.get_normalizer(overview_frame_preprocessed)
  141. # get static frame
  142. try:
  143. static_frame = overview_generator.colorbar_adder.get_static_frame(data_to_01_mapper)
  144. except ValueError as ve:
  145. if str(ve).startswith('Number of samples,'):
  146. raise ValueError("")
  147. else:
  148. raise ve
  149. # colorize the data
  150. overview_frame_colorized = overview_generator.colorize(data=overview_frame_preprocessed,
  151. data_to_01_mapper=data_to_01_mapper)
  152. # draw ROIs
  153. overview_frame_with_rois = overview_generator.add_rois(frame_data=overview_frame_colorized)
  154. # apply rotations
  155. overview_frame_rotated = overview_generator.rotate_frame(overview_frame_with_rois)
  156. # add colorbar
  157. overview_frame_final = overview_generator.add_colorbar(overview_frame_rotated, static_frame)
  158. logging.getLogger("VIEW").info(
  159. f"SO_individualScale set to:{flags['SO_individualScale']}. "
  160. f"Minimum and maximum are: {data_to_01_mapper.get_data_limits()}")
  161. return overview_frame_final, data_to_01_mapper.get_data_limits(), overview_generator
  162. def generate_overview_image_for_output(flags, p1):
  163. """
  164. Generates overview frame and transforms it so that it can be readily used either for plt.imshow or for
  165. saving with tifffile.imsave
  166. :param flags: FlagsManager object
  167. :param p1: p1 object
  168. :return: overview_frame, data_limits
  169. overview_frame: image as a 3D uint8 numpy.ndarray of format Y, X, Color with origin at top right
  170. data_limits: tuple, the lower and upper limits of data in overview image
  171. """
  172. # data is in X, Y, color format
  173. overview_frame, data_limits, overview_generator_used = generate_overview_image(flags, p1)
  174. # conversion to YX format, uint8 and flip Y
  175. return prep_overview_for_output(overview=overview_frame), data_limits
  176. def prep_overview_for_output(overview):
  177. """
  178. Prepare overview image for output as TIFs or as a frame of a movie
  179. :param numpy.ndarray overview: float64 X,Y,Color format with origin at bottom left
  180. :rtype: numpy.ndarray
  181. :returns: uint8; Y,X, Color format with origin at top left
  182. """
  183. # need to swap axes as tiff expects YX
  184. frame_data_numpy_swapped = overview.swapaxes(0, 1)
  185. # need to convert it to 8 bit from float
  186. frame_data_numpy_swapped_uint8 = np.array(frame_data_numpy_swapped * 255, dtype=np.uint8)
  187. # flip Y since origin in tiff is top left
  188. return np.flip(frame_data_numpy_swapped_uint8, axis=0)
  189. def get_current_pyplot_window_titles():
  190. """
  191. Returns the titles of all open pyplot windows as a list
  192. :return: list
  193. """
  194. titles = []
  195. for figure_number in plt.get_fignums():
  196. fig = plt.figure(figure_number)
  197. titles.append(fig.canvas.get_window_title())
  198. return titles
  199. def pop_show_overview(flags, p1, label, stimulus_number=None, feature_number=None):
  200. """
  201. Creates a new figure and plots overview frames in it. Subplots in tabular arrangement, with
  202. as many rows as features selected acc. to <feature_number> and as many columns as stimuli selected
  203. acc. to <stimulus_number>
  204. :param flags: FlagsManager object
  205. :param p1: p1 object
  206. :param label: internal label for the data in <p1> loaded with <flags>
  207. :param str|None|list stimulus_number: one of the following:
  208. 'all': all stimuli are used, generating as many subplots
  209. None: use the stimulus number in the flag "CTV_StimulusNumber"
  210. list: containing a list of stimulus numbers to use (stimuli are numbered 0, 1, 2...)
  211. :param str|None|list feature_number: one of the following:
  212. 'all': all features of the CTV specified by the flags, generating as many subplots
  213. None: use the feature number in the flag "CTV_FeatureNumber"
  214. list: containing a list of feature numbers to use (features are numbered 0, 1, 2...)
  215. """
  216. if not plt.isinteractive():
  217. plt.ion()
  218. n_stim = p1.pulsed_stimuli_handler.get_number_of_stimuli()
  219. if stimulus_number is None:
  220. stimulus_number = [flags['CTV_StimulusNumber']]
  221. elif stimulus_number == "all":
  222. stimulus_number = range(n_stim)
  223. else:
  224. assert type(stimulus_number) is list and all(type(x) is int for x in stimulus_number), \
  225. f"stimulus_number can be either None, 'all' or a list of ints. Got {stimulus_number}"
  226. n_stim_used = len(stimulus_number)
  227. overview_frames_columns = []
  228. for stim_ind in stimulus_number:
  229. assert type(stim_ind) is int, f"For flag 'CTV_StimulusNumber' Expected int, got {type(stim_ind)}({stim_ind})"
  230. if n_stim > 0:
  231. assert 0 <= stim_ind < n_stim, \
  232. f"IndexError: Current measurement has {n_stim} stimuli, " \
  233. f"therefore stimulus_number can be in [0, {n_stim - 1}]. Got {stim_ind}"
  234. flags_copy = flags.copy()
  235. flags_copy.update_flags({"CTV_StimulusNumber": stim_ind})
  236. overview_frames = generate_overview_frame(flags_copy, p1, feature_number=feature_number)
  237. overview_frames_columns.append(overview_frames)
  238. n_features = len(overview_frames_columns[0])
  239. frame_size = p1.get_frame_size()
  240. aspect_ratio = frame_size[0] / frame_size[1]
  241. temp_width = min(10, 5 * n_stim_used)
  242. temp_height = min(10, 5 * n_features)
  243. fig, axs = plt.subplots(
  244. figsize=(temp_width, temp_height / aspect_ratio),
  245. constrained_layout=True, nrows=n_features, ncols=n_stim_used, squeeze=False)
  246. for col_ind, stim_ind in enumerate(stimulus_number):
  247. for feature_ind, overview_frame_this_feature in enumerate(overview_frames_columns[col_ind]):
  248. row_ind = feature_ind
  249. ax = axs[row_ind, col_ind]
  250. overview_frame_colorized_with_frame, data_limits, overview_generator_used = \
  251. colorize_overview_add_border_etc(overview_frame=overview_frame_this_feature, flags=flags, p1=p1)
  252. overview_for_output = prep_overview_for_output(overview_frame_colorized_with_frame)
  253. # deduplicate the label
  254. label2use = dedupilicate(value=label, existing_values=get_current_pyplot_window_titles())
  255. fig.canvas.set_window_title(label2use)
  256. ax.imshow(np.flip(overview_for_output, axis=0), origin="lower")
  257. legendfactor = flags["SO_scaleLegendFactor"]
  258. ax.set_title(
  259. f'Feature Number: {feature_ind:d}; Stimulus Number: {stim_ind:d}\n'
  260. f'False color scale (CTV*{legendfactor:2.1f}) is '
  261. f'{data_limits[0] * legendfactor:2.3f} to {data_limits[1] * legendfactor:2.3f}'
  262. )
  263. def format_coord(x_img_data, y_img_data):
  264. y_overview = int(y_img_data + 0.5)
  265. x_overview = int(x_img_data + 0.5)
  266. if 0 <= y_overview < p1.metadata.format_y and 0 <= x_overview < p1.metadata.format_x:
  267. z = overview_frame_this_feature[x_overview, y_overview]*legendfactor
  268. reportline = 'Location: x=%.0i, y=%.0i, CTV Value (*%.0i)= %2.3f'
  269. return reportline % (x_img_data, y_img_data, legendfactor, z)
  270. else:
  271. return ''
  272. ax.format_coord = format_coord
  273. # add legend indicating the labels of ROIs marked if the flags "SO_showROIs" starts with a 2
  274. if np.floor(flags["SO_showROIs"] / 10) == 2:
  275. for roi_mask, color, label in overview_generator_used.roi_marker.roi_mask_color_label_tuples:
  276. ax.plot([-1], [-1], "-", color=color, label=label)
  277. ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
  278. plt.draw()
  279. def create_bw_image_from_frame(frame, extra_flags=None):
  280. """
  281. Creates a BW image from a 2D frame using flags in <extra_flags> if specified
  282. :param numpy.ndarray frame: 2D frame in XY format with origin at bottom left
  283. :param dict|None extra_flags: flags as key value pairs
  284. :rtype: numpy.ndarray
  285. :returns: float64 X,Y,Color format with origin at bottom left
  286. """
  287. flags_2_update = {"SO_MV_colortable": "gray",
  288. "SO_individualScale": 2,
  289. }
  290. flags = FlagsManager()
  291. flags.update_flags(flags_2_update)
  292. if type(extra_flags) is dict:
  293. flags.update_flags(extra_flags)
  294. frame_bw, data_limits, overview_generator_used = \
  295. colorize_overview_add_border_etc(overview_frame=frame, flags=flags)
  296. return frame_bw