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
« 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
10logger = logging.getLogger(__name__)
13class AnnotateImage:
14 font_size = 14
16 def __init__(self, path):
17 self.path = path
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)
24 def annotate(self, image_center, beam_center, pixelsize, unit, subsample):
25 """Annotate the image
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
40 self.im = Image.open(self.path)
41 self.im_w, self.im_h = self.im.size
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)
48 if self.pixelsize[1] > 0:
49 self.im = ImageOps.flip(self.im)
51 self.origin_marking()
52 self.scalebar()
54 subret = None
55 if subsample:
56 subret = self.draw_subsample()
58 self.im.save(self.path)
59 self.im.close()
61 return {"subsample": subret}
63 def to_image_coords(self, pos):
64 """Convert from canvas units to image
66 Coordinates from the transfoms module assume that an image is x,y,
67 however in image space they are actually x, -y
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()
81 def origin_marking(self):
82 """Origin Marking
84 Draw a cross for the origin marking
85 """
86 colors = {"normal": "yellow", "invert": "green"}
88 beam = self.to_image_coords(self.beam_center)
89 # print("origin_marking", self.image_center, self.beam_center, beam)
91 color = colors["invert"] if self._invert(beam[0], beam[1]) else colors["normal"]
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 )
102 def scalebar(self):
103 """Draw Scalebar
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)
110 px = format_eng(abs(self.pixelsize[0]) * self.unit * sb_w)
112 # round to 10
113 rem = px["scalar"] % 10
114 fact = (px["scalar"] - rem) / px["scalar"]
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 )
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 )
135 def draw_subsample(self):
136 """Draw Subsample
138 For ROIs draw a rectangle,
139 LOIs a line
140 POIs a cross
142 Subsamples are marked relative to the origin marking
143 """
144 colors = {"normal": "cyan", "invert": "blue"}
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 # )
154 invert = self._invert(tl[0], tl[1])
155 color = colors["invert"] if invert else colors["normal"]
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"]])
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 )
172 return {"x": tl[0], "y": tl[1]}
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 )
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