Coverage for /opt/conda/envs/apienv/lib/python3.10/site-packages/daiquiri/core/components/imageviewer/annotate.py: 92%

73 statements  

« prev     ^ index     » next       coverage.py v7.6.5, created at 2024-11-15 02:12 +0000

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3import logging 

4import numpy as np 

5from PIL import Image, ImageDraw, ImageFont, ImageOps 

6from daiquiri.core.utils import format_eng 

7from daiquiri.resources.utils import get_resource_provider 

8 

9 

10logger = logging.getLogger(__name__) 

11 

12 

13class AnnotateImage: 

14 font_size = 14 

15 

16 def __init__(self, path): 

17 self.path = path 

18 

19 provider = get_resource_provider() 

20 resource = provider.resource("fonts", "Poppins-Regular.ttf") 

21 with provider.open_resource(resource, "b") as f: 

22 self.font = ImageFont.truetype(f, self.font_size) 

23 

24 def annotate(self, image_center, beam_center, pixelsize, unit, subsample): 

25 """Annotate the image 

26 

27 Args: 

28 image_center (list): x, y positions for the image 

29 beam_center (list): x, y positions for the origin 

30 pizelsize (list): x, y pixel size in `unit` 

31 unit (int): the base unit for all calculations (1e-9 default) 

32 subsample (dict): (optionally) a subsample to annotate 

33 """ 

34 self.image_center = image_center 

35 self.beam_center = beam_center 

36 self.pixelsize = pixelsize 

37 self.unit = unit 

38 self.subsample = subsample 

39 

40 self.im = Image.open(self.path) 

41 self.im_w, self.im_h = self.im.size 

42 

43 # For a data colleciton image the image should be flipped for normal 

44 # viewing 

45 if self.pixelsize[0] < 0: 

46 self.im = ImageOps.mirror(self.im) 

47 

48 if self.pixelsize[1] > 0: 

49 self.im = ImageOps.flip(self.im) 

50 

51 self.origin_marking() 

52 self.scalebar() 

53 

54 subret = None 

55 if subsample: 

56 subret = self.draw_subsample() 

57 

58 self.im.save(self.path) 

59 self.im.close() 

60 

61 return {"subsample": subret} 

62 

63 def to_image_coords(self, pos): 

64 """Convert from canvas units to image 

65 

66 Coordinates from the transfoms module assume that an image is x,y, 

67 however in image space they are actually x, -y 

68 

69 If the image is flipped i.e. -pixelsize[0], or +pixelsize[1] this is accounted 

70 for by the flip/mirror above 

71 """ 

72 # TODO: Replace me with a src.transform 

73 return ( 

74 np.array([self.im_w / 2, self.im_h / 2]) 

75 - ( 

76 (self.image_center - np.array(pos)) 

77 / [abs(self.pixelsize[0]), -abs(self.pixelsize[1])] 

78 ) 

79 ).tolist() 

80 

81 def origin_marking(self): 

82 """Origin Marking 

83 

84 Draw a cross for the origin marking 

85 """ 

86 colors = {"normal": "yellow", "invert": "green"} 

87 

88 beam = self.to_image_coords(self.beam_center) 

89 # print("origin_marking", self.image_center, self.beam_center, beam) 

90 

91 color = colors["invert"] if self._invert(beam[0], beam[1]) else colors["normal"] 

92 

93 mark_size = 20 

94 mark = ImageDraw.Draw(self.im) 

95 mark.line( 

96 (beam[0] - mark_size, beam[1], beam[0] + mark_size, beam[1]), fill=color 

97 ) 

98 mark.line( 

99 (beam[0], beam[1] - mark_size, beam[0], beam[1] + mark_size), fill=color 

100 ) 

101 

102 def scalebar(self): 

103 """Draw Scalebar 

104 

105 Draw a single line for the scalebar with rounded size 

106 """ 

107 sb_w = int(0.1 * self.im_w) 

108 sb_h = int(0.1 * self.im_h) 

109 

110 px = format_eng(abs(self.pixelsize[0]) * self.unit * sb_w) 

111 

112 # round to 10 

113 rem = px["scalar"] % 10 

114 fact = (px["scalar"] - rem) / px["scalar"] 

115 

116 sb = ImageDraw.Draw(self.im) 

117 sb.line( 

118 ( 

119 sb_w / 2, 

120 self.im_h - sb_h / 2, 

121 (sb_w / 2) + (sb_w * fact), 

122 self.im_h - sb_h / 2, 

123 ), 

124 fill="grey", 

125 width=2, 

126 ) 

127 

128 sb.text( 

129 (sb_w / 2, self.im_h - sb_h / 2 - 20), 

130 f"{px['scalar'] - rem:.0f} {px['prefix']}m", 

131 font=self.font, 

132 fill="grey", 

133 ) 

134 

135 def draw_subsample(self): 

136 """Draw Subsample 

137 

138 For ROIs draw a rectangle, 

139 LOIs a line 

140 POIs a cross 

141 

142 Subsamples are marked relative to the origin marking 

143 """ 

144 colors = {"normal": "cyan", "invert": "blue"} 

145 

146 tl = self.to_image_coords([self.subsample["x"], -self.subsample["y"]]) 

147 # print( 

148 # "draw_subsample", 

149 # self.image_center, 

150 # [self.subsample["x"], -self.subsample["y"]], 

151 # tl, 

152 # ) 

153 

154 invert = self._invert(tl[0], tl[1]) 

155 color = colors["invert"] if invert else colors["normal"] 

156 

157 ss = ImageDraw.Draw(self.im) 

158 if self.subsample["type"] == "roi" or self.subsample["type"] == "loi": 

159 br = self.to_image_coords([self.subsample["x2"], -self.subsample["y2"]]) 

160 

161 if self.subsample["type"] == "roi": 

162 ss.rectangle([tl[0], tl[1], br[0], br[1]], outline=color) 

163 else: 

164 ss.line([tl[0], tl[1], br[0], br[1]], fill=color) 

165 ss.text( 

166 (br[0], br[1]), 

167 str(self.subsample["subsampleid"]), 

168 font=self.font, 

169 fill=color, 

170 ) 

171 

172 return {"x": tl[0], "y": tl[1]} 

173 

174 else: 

175 ss_size = 15 

176 ss.line((tl[0] - ss_size, tl[1], tl[0] + ss_size, tl[1]), fill=color) 

177 ss.line((tl[0], tl[1] - ss_size, tl[0], tl[1] + ss_size), fill=color) 

178 ss.text( 

179 (tl[0], tl[1]), 

180 str(self.subsample["subsampleid"]), 

181 font=self.font, 

182 fill=color, 

183 ) 

184 

185 def _invert(self, x, y): 

186 """Determine if a pixel is very bright""" 

187 pix = self.im.getpixel((x, y)) 

188 return (0.299 * pix[0] + 0.587 * pix[1] + 0.114 ** pix[2]) > 200