Commit 6a432126093efeab3cc9233d8492a39d749bd968
1 parent
7c80274f
Exists in
measure
Added (but not integrated) the clut_imagedata widget
Showing
1 changed file
with
377 additions
and
0 deletions
Show diff stats
... | ... | @@ -0,0 +1,377 @@ |
1 | +import glob | |
2 | +import math | |
3 | +import os | |
4 | + | |
5 | +import wx | |
6 | + | |
7 | +HISTOGRAM_LINE_COLOUR = (128, 128, 128) | |
8 | +HISTOGRAM_FILL_COLOUR = (64, 64, 64) | |
9 | +HISTOGRAM_LINE_WIDTH = 1 | |
10 | + | |
11 | +DEFAULT_COLOUR = (0, 0, 0) | |
12 | + | |
13 | +TEXT_COLOUR = (255, 255, 255) | |
14 | +BACKGROUND_TEXT_COLOUR_RGBA = (255, 0, 0, 128) | |
15 | + | |
16 | +GRADIENT_RGBA = 0.75 * 255 | |
17 | + | |
18 | +LINE_COLOUR = (128, 128, 128) | |
19 | +LINE_WIDTH = 2 | |
20 | +RADIUS = 5 | |
21 | + | |
22 | +PADDING = 2 | |
23 | + | |
24 | + | |
25 | +class CLUTEvent(wx.PyCommandEvent): | |
26 | + def __init__(self, evtType, id, nodes): | |
27 | + wx.PyCommandEvent.__init__(self, evtType, id) | |
28 | + self.nodes = nodes | |
29 | + | |
30 | + def GetNodes(self): | |
31 | + return self.nodes | |
32 | + | |
33 | + | |
34 | +# Occurs when CLUT point is changing | |
35 | +myEVT_CLUT_POINT_MOVE = wx.NewEventType() | |
36 | +EVT_CLUT_POINT_MOVE = wx.PyEventBinder(myEVT_CLUT_POINT_MOVE, 1) | |
37 | + | |
38 | + | |
39 | +class Node(object): | |
40 | + def __init__(self, value, colour): | |
41 | + self.value = value | |
42 | + self.colour = colour | |
43 | + | |
44 | + def __cmp__(self, o): | |
45 | + return cmp(self.value, o.value) | |
46 | + | |
47 | + def __repr__(self): | |
48 | + return "(%d %s)" % (self.value, self.colour) | |
49 | + | |
50 | + | |
51 | +class CLUTImageDataWidget(wx.Panel): | |
52 | + """ | |
53 | + Widget used to config the Lookup table from imagedata. | |
54 | + """ | |
55 | + def __init__(self, parent, id, histogram, init, end, wl, ww): | |
56 | + super(CLUTImageDataWidget, self).__init__(parent, id) | |
57 | + | |
58 | + self.SetMinSize((300, 200)) | |
59 | + | |
60 | + self.histogram = histogram | |
61 | + | |
62 | + self._init = init | |
63 | + self._end = end | |
64 | + | |
65 | + self.i_init = init | |
66 | + self.i_end = end | |
67 | + | |
68 | + self._range = 0.05 * (end - init) | |
69 | + | |
70 | + self.wl = wl | |
71 | + self.ww = ww | |
72 | + | |
73 | + wi = wl - ww / 2 | |
74 | + wf = wl + ww / 2 | |
75 | + | |
76 | + self.nodes = [Node(wi, (0, 0, 0)), | |
77 | + Node(wf, (255, 255, 255))] | |
78 | + | |
79 | + self.middle_pressed = False | |
80 | + self.right_pressed = False | |
81 | + self.left_pressed = False | |
82 | + | |
83 | + self.selected_node = None | |
84 | + self.last_selected = None | |
85 | + | |
86 | + self._d_hist = [] | |
87 | + | |
88 | + self._build_drawn_hist() | |
89 | + | |
90 | + self.__bind_events_wx() | |
91 | + | |
92 | + def __bind_events_wx(self): | |
93 | + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackGround) | |
94 | + self.Bind(wx.EVT_PAINT, self.OnPaint) | |
95 | + self.Bind(wx.EVT_SIZE, self.OnSize) | |
96 | + | |
97 | + self.Bind(wx.EVT_MOTION, self.OnMotion) | |
98 | + | |
99 | + self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) | |
100 | + self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMiddleClick) | |
101 | + self.Bind(wx.EVT_MIDDLE_UP, self.OnMiddleRelease) | |
102 | + | |
103 | + self.Bind(wx.EVT_LEFT_DOWN, self.OnClick) | |
104 | + self.Bind(wx.EVT_LEFT_UP, self.OnRelease) | |
105 | + self.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick) | |
106 | + | |
107 | + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightClick) | |
108 | + | |
109 | + def _build_drawn_hist(self): | |
110 | + #w, h = self.GetVirtualSize() | |
111 | + w = len(self.histogram) | |
112 | + h = 1080 | |
113 | + | |
114 | + x_init = self._init | |
115 | + x_end = self._end | |
116 | + | |
117 | + y_init = 0 | |
118 | + y_end = math.log(self.histogram.max() + 1) | |
119 | + | |
120 | + prop_x = (w) * 1.0 / (x_end - x_init) | |
121 | + prop_y = (h) * 1.0 / (y_end - y_init) | |
122 | + | |
123 | + self._d_hist = [] | |
124 | + for i in xrange(w): | |
125 | + x = i / prop_x + x_init - 1 | |
126 | + if self.i_init <= x < self.i_end: | |
127 | + try: | |
128 | + y = math.log(self.histogram[int(x - self.i_init)] + 1) * prop_y | |
129 | + except IndexError: | |
130 | + print x, self.histogram.shape, x_init, self.i_init, self.i_end | |
131 | + | |
132 | + self._d_hist.append((i, y)) | |
133 | + | |
134 | + def _interpolation(self, x): | |
135 | + f = math.floor(x) | |
136 | + c = math.ceil(x) | |
137 | + h = self.histogram | |
138 | + | |
139 | + if f != c: | |
140 | + return h[f] + (h[c] - h[f]) / (c - f) * (x - f) | |
141 | + else: | |
142 | + return h[int(x)] | |
143 | + | |
144 | + def OnEraseBackGround(self, evt): | |
145 | + pass | |
146 | + | |
147 | + def OnSize(self, evt): | |
148 | + #self._build_drawn_hist() | |
149 | + self.Refresh() | |
150 | + evt.Skip() | |
151 | + | |
152 | + def OnPaint(self, evt): | |
153 | + dc = wx.BufferedPaintDC(self) | |
154 | + dc.SetBackground(wx.Brush('Black')) | |
155 | + dc.Clear() | |
156 | + | |
157 | + self.draw_histogram(dc) | |
158 | + self.draw_gradient(dc) | |
159 | + | |
160 | + if self.last_selected is not None: | |
161 | + self.draw_text(dc) | |
162 | + | |
163 | + def OnWheel(self, evt): | |
164 | + """ | |
165 | + Increase or decrease the range from hounsfield scale showed. It | |
166 | + doesn't change values in preset, only to visualization. | |
167 | + """ | |
168 | + direction = evt.GetWheelRotation() / evt.GetWheelDelta() | |
169 | + init = self._init - direction * self._range | |
170 | + end = self._end + direction * self._range | |
171 | + self.SetRange(init, end) | |
172 | + self.Refresh() | |
173 | + | |
174 | + def OnMiddleClick(self, evt): | |
175 | + self.middle_pressed = True | |
176 | + self.last_x = self.pixel_to_hounsfield(evt.GetX()) | |
177 | + | |
178 | + def OnMiddleRelease(self, evt): | |
179 | + self.middle_pressed = False | |
180 | + | |
181 | + def OnClick(self, evt): | |
182 | + px, py = evt.GetPositionTuple() | |
183 | + self.left_pressed = True | |
184 | + self.selected_node = self.get_node_clicked(px, py) | |
185 | + self.last_selected = self.selected_node | |
186 | + if self.selected_node is not None: | |
187 | + self.Refresh() | |
188 | + | |
189 | + def OnRelease(self, evt): | |
190 | + self.left_pressed = False | |
191 | + self.selected_node = None | |
192 | + | |
193 | + def OnDoubleClick(self, evt): | |
194 | + w, h = self.GetVirtualSize() | |
195 | + px, py = evt.GetPositionTuple() | |
196 | + | |
197 | + # Verifying if the user double-click in a node-colour. | |
198 | + selected_node = self.get_node_clicked(px, py) | |
199 | + if selected_node: | |
200 | + # The user double-clicked a node colour. Give the user the | |
201 | + # option to change the color from this node. | |
202 | + colour_dialog = wx.GetColourFromUser(self, (0, 0, 0)) | |
203 | + if colour_dialog.IsOk(): | |
204 | + r, g, b = colour_dialog.Get() | |
205 | + selected_node.colour = r, g, b | |
206 | + self._generate_event() | |
207 | + else: | |
208 | + # The user doesn't clicked in a node colour. Creates a new node | |
209 | + # colour with the DEFAULT_COLOUR | |
210 | + vx = self.pixel_to_hounsfield(px) | |
211 | + node = Node(vx, DEFAULT_COLOUR) | |
212 | + self.nodes.append(node) | |
213 | + self._generate_event() | |
214 | + | |
215 | + self.Refresh() | |
216 | + | |
217 | + def OnRightClick(self, evt): | |
218 | + w, h = self.GetVirtualSize() | |
219 | + px, py = evt.GetPositionTuple() | |
220 | + selected_node = self.get_node_clicked(px, py) | |
221 | + | |
222 | + if selected_node: | |
223 | + self.nodes.remove(selected_node) | |
224 | + self._generate_event() | |
225 | + self.Refresh() | |
226 | + | |
227 | + def OnMotion(self, evt): | |
228 | + if self.middle_pressed: | |
229 | + x = self.pixel_to_hounsfield(evt.GetX()) | |
230 | + dx = x - self.last_x | |
231 | + init = self._init - dx | |
232 | + end = self._end - dx | |
233 | + self.SetRange(init, end) | |
234 | + self.Refresh() | |
235 | + self.last_x = x | |
236 | + | |
237 | + # The user is dragging a colour node | |
238 | + elif self.left_pressed and self.selected_node: | |
239 | + x = self.pixel_to_hounsfield(evt.GetX()) | |
240 | + print x, self.selected_node, type(x) | |
241 | + self.selected_node.value = float(x) | |
242 | + self.Refresh() | |
243 | + | |
244 | + # A point in the preset has been changed, raising a event | |
245 | + self._generate_event() | |
246 | + | |
247 | + def draw_histogram(self, dc): | |
248 | + w, h = self.GetVirtualSize() | |
249 | + ctx = wx.GraphicsContext.Create(dc) | |
250 | + | |
251 | + ctx.SetPen(wx.Pen(HISTOGRAM_LINE_COLOUR, HISTOGRAM_LINE_WIDTH)) | |
252 | + ctx.SetBrush(wx.Brush(HISTOGRAM_FILL_COLOUR)) | |
253 | + | |
254 | + path = ctx.CreatePath() | |
255 | + xi, yi = self._d_hist[0] | |
256 | + path.MoveToPoint(xi, h - yi) | |
257 | + for x, y in self._d_hist: | |
258 | + path.AddLineToPoint(x, h - y) | |
259 | + | |
260 | + ctx.Translate(self.hounsfield_to_pixel(self.i_init), 0) | |
261 | + ctx.Translate(0, h) | |
262 | + ctx.Scale(w * 1.0 / (self._end - self._init), h / 1080.) | |
263 | + ctx.Translate(0, -h) | |
264 | + #ctx.Translate(0, h * h/1080.0 ) | |
265 | + ctx.PushState() | |
266 | + ctx.StrokePath(path) | |
267 | + ctx.PopState() | |
268 | + path.AddLineToPoint(x, h) | |
269 | + path.AddLineToPoint(xi, h) | |
270 | + path.AddLineToPoint(*self._d_hist[0]) | |
271 | + ctx.FillPath(path) | |
272 | + | |
273 | + def draw_gradient(self, dc): | |
274 | + w, h = self.GetVirtualSize() | |
275 | + ctx = wx.GraphicsContext.Create(dc) | |
276 | + knodes = sorted(self.nodes) | |
277 | + for ni, nj in zip(knodes[:-1], knodes[1:]): | |
278 | + vi = round(self.hounsfield_to_pixel(ni.value)) | |
279 | + vj = round(self.hounsfield_to_pixel(nj.value)) | |
280 | + | |
281 | + path = ctx.CreatePath() | |
282 | + path.AddRectangle(vi, 0, vj - vi, h) | |
283 | + | |
284 | + ci = ni.colour + (GRADIENT_RGBA,) | |
285 | + cj = nj.colour + (GRADIENT_RGBA,) | |
286 | + b = ctx.CreateLinearGradientBrush(vi, h, | |
287 | + vj, h, | |
288 | + ci, cj) | |
289 | + ctx.SetBrush(b) | |
290 | + ctx.SetPen(wx.TRANSPARENT_PEN) | |
291 | + ctx.FillPath(path) | |
292 | + | |
293 | + self._draw_circle(vi, ni.colour, ctx) | |
294 | + self._draw_circle(vj, nj.colour, ctx) | |
295 | + | |
296 | + def _draw_circle(self, px, color, ctx): | |
297 | + w, h = self.GetVirtualSize() | |
298 | + | |
299 | + path = ctx.CreatePath() | |
300 | + path.AddCircle(px, h / 2, RADIUS) | |
301 | + | |
302 | + path.AddCircle(px, h / 2, RADIUS) | |
303 | + ctx.SetPen(wx.Pen('white', LINE_WIDTH + 1)) | |
304 | + ctx.StrokePath(path) | |
305 | + | |
306 | + ctx.SetPen(wx.Pen(LINE_COLOUR, LINE_WIDTH - 1)) | |
307 | + ctx.SetBrush(wx.Brush(color)) | |
308 | + ctx.StrokePath(path) | |
309 | + ctx.FillPath(path) | |
310 | + | |
311 | + def draw_text(self, dc): | |
312 | + w, h = self.GetVirtualSize() | |
313 | + ctx = wx.GraphicsContext.Create(dc) | |
314 | + | |
315 | + value = self.last_selected.value | |
316 | + | |
317 | + x = self.hounsfield_to_pixel(value) | |
318 | + y = h / 2 | |
319 | + | |
320 | + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) | |
321 | + font.SetWeight(wx.BOLD) | |
322 | + font = ctx.CreateFont(font, TEXT_COLOUR) | |
323 | + ctx.SetFont(font) | |
324 | + | |
325 | + text = 'Value: %-6d' % value | |
326 | + | |
327 | + wt, ht = ctx.GetTextExtent(text) | |
328 | + | |
329 | + wr, hr = wt + 2 * PADDING, ht + 2 * PADDING | |
330 | + xr, yr = x + RADIUS, y - RADIUS - hr | |
331 | + | |
332 | + if xr + wr > w: | |
333 | + xr = x - RADIUS - wr | |
334 | + if yr < 0: | |
335 | + yr = y + RADIUS | |
336 | + | |
337 | + xf, yf = xr + PADDING, yr + PADDING | |
338 | + ctx.SetBrush(wx.Brush(BACKGROUND_TEXT_COLOUR_RGBA)) | |
339 | + ctx.SetPen(wx.Pen(BACKGROUND_TEXT_COLOUR_RGBA)) | |
340 | + ctx.DrawRectangle(xr, yr, wr, hr) | |
341 | + ctx.DrawText(text, xf, yf) | |
342 | + | |
343 | + def _generate_event(self): | |
344 | + evt = CLUTEvent(myEVT_CLUT_POINT_MOVE, self.GetId(), self.nodes) | |
345 | + self.GetEventHandler().ProcessEvent(evt) | |
346 | + | |
347 | + def hounsfield_to_pixel(self, x): | |
348 | + w, h = self.GetVirtualSize() | |
349 | + p = (x - self._init) * w * 1.0 / (self._end - self._init) | |
350 | + print "h->p", x, p, type(p) | |
351 | + return p | |
352 | + | |
353 | + def pixel_to_hounsfield(self, x): | |
354 | + w, h = self.GetVirtualSize() | |
355 | + prop_x = (self._end - self._init) / (w * 1.0) | |
356 | + p = x * prop_x + self._init | |
357 | + print "p->h", x, p | |
358 | + return p | |
359 | + | |
360 | + def get_node_clicked(self, px, py): | |
361 | + w, h = self.GetVirtualSize() | |
362 | + for n in self.nodes: | |
363 | + x = self.hounsfield_to_pixel(n.value) | |
364 | + y = h / 2 | |
365 | + | |
366 | + if ((px - x)**2 + (py - y)**2)**0.5 <= RADIUS: | |
367 | + return n | |
368 | + | |
369 | + return None | |
370 | + | |
371 | + def SetRange(self, init, end): | |
372 | + """ | |
373 | + Sets the range from hounsfield | |
374 | + """ | |
375 | + self._init, self._end = init, end | |
376 | + print self._init, self._end | |
377 | + #self._build_drawn_hist() | ... | ... |