static_border.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. from view.python_core.utils.pil_helpers import numpy_to_pil_image, pil_image_to_numpy
  2. from view.python_core.utils.pil_helpers import add_string
  3. import numpy as np
  4. from view.python_core.utils.colors import mpl_color_to_PIL
  5. from PIL.ImageDraw import Draw
  6. from view.python_core.utils.fonts import resolve_font_size, resolve_font_file
  7. class StaticBorder(object):
  8. def __init__(self, mv_xgap_left, mv_xgap_right, mv_ygap, bg_color_for_mpl, frame_size):
  9. self.frame_size = frame_size
  10. self.mv_xgap_left = mv_xgap_left
  11. self.mv_xgap_right = mv_xgap_right
  12. self.mv_ygap = mv_ygap
  13. self.bg_color_for_mpl = bg_color_for_mpl
  14. # make sure the frame sizes along x and y after padding is even
  15. possible_frame_Xsize = self.frame_size[0] + mv_xgap_left + mv_xgap_right
  16. frame_Xsize = possible_frame_Xsize + possible_frame_Xsize % 2
  17. possible_frame_Ysize = self.frame_size[1] + 2 * self.mv_ygap
  18. frame_Ysize = possible_frame_Ysize + possible_frame_Ysize % 2
  19. buffer_shape = list(self.frame_size) + [len(bg_color_for_mpl)]
  20. buffer_shape[0] = frame_Xsize
  21. buffer_shape[1] = frame_Ysize
  22. self.frame_with_blank_border = np.full(buffer_shape, self.bg_color_for_mpl)
  23. self.frame_with_annotations = None
  24. def get_static_frame(self, data_to_01_mapper):
  25. return self.frame_with_blank_border
  26. def composite(self, frame_data, static_frame):
  27. data_shape = frame_data.shape[:2]
  28. assert data_shape == self.frame_size, f"Frame data of shape {self.frame_size} expected, " \
  29. f"got {data_shape}"
  30. new_frame = static_frame.astype(dtype=frame_data.dtype, copy=True)
  31. new_frame[self.mv_xgap_left: self.mv_xgap_left + data_shape[0],
  32. self.mv_ygap: self.mv_ygap + data_shape[1]] = frame_data
  33. return new_frame
  34. class StaticBorderWithColorbar(StaticBorder):
  35. def __init__(self, mv_xgap_left, mv_xgap_right, mv_ygap, bg_color_for_mpl, frame_size, colormap,
  36. fg_color_for_mpl, font_file, font_size, legend_scale_factor):
  37. super().__init__(mv_xgap_left, mv_xgap_right, mv_ygap, bg_color_for_mpl, frame_size)
  38. self.fg_color_for_pil = mpl_color_to_PIL(fg_color_for_mpl)
  39. self.font_file = font_file
  40. self.colormap = colormap
  41. self.font_size = resolve_font_size(text="-0.000", maximum_width=mv_xgap_right,
  42. font_name=font_file, suggested_font_size=font_size)
  43. self.legend_scale_factor = legend_scale_factor
  44. current_frame_size = self.frame_with_blank_border.shape
  45. self.colorbar_x_end = current_frame_size[0] - int(0.35 * mv_xgap_right)
  46. self.colorbar_x_start = current_frame_size[0] - int(0.65 * mv_xgap_right)
  47. # these are for when origin in bottom left and positive Y axis is upwards
  48. self.colorbar_y_start = int(max(mv_ygap, 1 * self.font_size))
  49. self.colorbar_y_end = int(min(frame_size[1] + mv_ygap, current_frame_size[1] - 1 * self.font_size))
  50. def get_static_frame(self, data_to_01_mapper):
  51. vmin, vmax = data_to_01_mapper.get_data_limits()
  52. # draw scalebar
  53. data_for_colorbar = np.linspace(vmin, vmax, self.colorbar_y_end - self.colorbar_y_start + 1)
  54. data_for_colorbar_scaled = data_to_01_mapper.normalize(data_for_colorbar)
  55. self.frame_with_blank_border[self.colorbar_x_start: self.colorbar_x_end + 1,
  56. self.colorbar_y_start: self.colorbar_y_end + 1] \
  57. = self.colormap(data_for_colorbar_scaled)
  58. pil_image = numpy_to_pil_image(self.frame_with_blank_border)
  59. upper_lower_text_x = 0.8 * self.colorbar_x_start + 0.2 * self.colorbar_x_end
  60. # add string for upper limit
  61. upper_limit_to_print = vmax * self.legend_scale_factor
  62. upper_limit_to_print_str = f"{upper_limit_to_print: 1.2f}"
  63. pil_image = add_string(pil_image,
  64. # in PIL, origin in at top left and positive Y is downwards
  65. # needs to convert coordinates, plus half a font size margin
  66. position=(upper_lower_text_x,
  67. pil_image.height - (self.colorbar_y_end + 0.25 * self.font_size)),
  68. font_size=self.font_size, text=upper_limit_to_print_str,
  69. horizontal_alignment="center", vertical_alignment="bottom",
  70. fill_color_for_pil=self.fg_color_for_pil, font_file=self.font_file)
  71. # add string for lower limit
  72. lower_limit_to_print = vmin * self.legend_scale_factor
  73. lower_limit_to_print_str = f"{lower_limit_to_print: 1.2f}"
  74. pil_image = add_string(pil_image,
  75. position=(upper_lower_text_x,
  76. pil_image.height - (self.colorbar_y_start - 0.25 * self.font_size)),
  77. font_size=self.font_size,
  78. text=lower_limit_to_print_str, horizontal_alignment="center", vertical_alignment="top",
  79. fill_color_for_pil=self.fg_color_for_pil, font_file=self.font_file)
  80. # if required, add a line on colormap and '0' string
  81. if vmin <= 0 <= vmax:
  82. # calculate the position of 0 according to the normalization in <data_to_01_mapper>
  83. colorbar_y_range = self.colorbar_y_end - self.colorbar_y_start
  84. # normalize function only works with arrays, hence passing one element array and indexing the result
  85. # also, normalized vmin=0, normalized vmax=1
  86. normalized_0 = data_to_01_mapper.normalize(np.array([0]))[0]
  87. zero_y = self.colorbar_y_start + (normalized_0 - 0) / 1 * colorbar_y_range
  88. pil_image = add_string(pil_image, position=(1.025 * self.colorbar_x_end, pil_image.height - zero_y),
  89. font_size=self.font_size,
  90. text="0", horizontal_alignment="left", vertical_alignment="center",
  91. fill_color_for_pil=self.fg_color_for_pil, font_file=self.font_file)
  92. img_draw_obj = Draw(pil_image)
  93. img_draw_obj.line(xy=((self.colorbar_x_start, pil_image.height - zero_y),
  94. (self.colorbar_x_end, pil_image.height - zero_y)),
  95. fill="rgb(0, 0, 0)", width=int(np.ceil(colorbar_y_range / 128)))
  96. return pil_image_to_numpy(pil_image)
  97. def check_frame_height(frame_height, SO_cutborder):
  98. """
  99. raises a Value error if <frame_height> is not large enough to draw a colorbar
  100. """
  101. original_frame_height = frame_height + 2 * SO_cutborder
  102. if frame_height < 30:
  103. if SO_cutborder == 0:
  104. raise ValueError(
  105. f"Current frame height ({original_frame_height}) is too small to draw a scalebar."
  106. f"Please set the flag 'CTV_scalebar' to False and try again!"
  107. )
  108. else:
  109. raise ValueError(
  110. f"The current value of cutborder ({SO_cutborder}) removes too much of "
  111. f"the frame height ({2 * SO_cutborder} out of {original_frame_height}) to be able to "
  112. f"add a colorbar to it. Please consider reducing the value of "
  113. f"cutborder to {SO_cutborder - (30 - frame_height)} pixels, below which adding a "
  114. f"colorbar becomes possible")
  115. def get_static_border_adder_3D(flags, fg_color_for_mpl, bg_color_for_mpl, colormap,
  116. frame_size, font_file, font_size):
  117. mv_xgap = flags["mv_xgap"]
  118. mv_ygap = flags["mv_ygap"]
  119. if flags["CTV_scalebar"]:
  120. check_frame_height(frame_height=frame_size[1], SO_cutborder=flags["mv_cutborder"])
  121. mv_xgap_right = 2 * mv_xgap
  122. static_border_adder = StaticBorderWithColorbar(
  123. mv_xgap_left=mv_xgap, mv_xgap_right=mv_xgap_right,
  124. mv_ygap=mv_ygap, bg_color_for_mpl=bg_color_for_mpl,
  125. fg_color_for_mpl=fg_color_for_mpl, colormap=colormap,
  126. frame_size=frame_size, font_file=font_file,
  127. font_size=font_size, legend_scale_factor=flags["SO_scaleLegendFactor"])
  128. return static_border_adder, mv_xgap_right
  129. else:
  130. mv_xgap_right = mv_xgap
  131. static_border_adder = StaticBorder(
  132. mv_xgap_left=mv_xgap, mv_xgap_right=mv_xgap_right, mv_ygap=mv_ygap,
  133. bg_color_for_mpl=bg_color_for_mpl, frame_size=frame_size)
  134. return static_border_adder, mv_xgap_right
  135. def get_static_border_adder_2D(flags, fg_color_for_mpl, bg_color_for_mpl, colormap,
  136. frame_size):
  137. if flags["CTV_scalebar"] and (flags["SO_xgap"] > 0) and (np.floor(flags["SO_showROIs"] / 10) != 2):
  138. check_frame_height(frame_height=frame_size[1], SO_cutborder=flags["SO_cutborder"])
  139. font_file = resolve_font_file(flags["SO_fontName"])
  140. mv_xgap_right = 2 * flags["SO_xgap"]
  141. # <font_size> passed here is the maximum allowed font size, passing infinity to disable ceiling
  142. static_border_adder = StaticBorderWithColorbar(
  143. mv_xgap_left=0, mv_xgap_right=mv_xgap_right,
  144. mv_ygap=0, bg_color_for_mpl=bg_color_for_mpl,
  145. fg_color_for_mpl=fg_color_for_mpl, colormap=colormap,
  146. frame_size=frame_size, font_file=font_file,
  147. font_size=np.inf,
  148. legend_scale_factor=flags["SO_scaleLegendFactor"])
  149. return static_border_adder
  150. else:
  151. static_border_adder = StaticBorder(
  152. mv_xgap_left=0, mv_xgap_right=0, mv_ygap=0,
  153. bg_color_for_mpl=bg_color_for_mpl, frame_size=frame_size)
  154. return static_border_adder