Commit a5e04a63aa32e508c6b710c2636b70be4947d89e
1 parent
41a8eeff
Exists in
master
and in
6 other branches
ADD: Added the clut_raycasting widget
Showing
2 changed files
with
417 additions
and
0 deletions
Show diff stats
.gitattributes
@@ -102,6 +102,7 @@ invesalius/gui/task_slice.py -text | @@ -102,6 +102,7 @@ invesalius/gui/task_slice.py -text | ||
102 | invesalius/gui/task_surface.py -text | 102 | invesalius/gui/task_surface.py -text |
103 | invesalius/gui/task_tools.py -text | 103 | invesalius/gui/task_tools.py -text |
104 | invesalius/gui/widgets/__init__.py -text | 104 | invesalius/gui/widgets/__init__.py -text |
105 | +invesalius/gui/widgets/clut_raycasting.py -text | ||
105 | invesalius/gui/widgets/foldpanelbar.py -text | 106 | invesalius/gui/widgets/foldpanelbar.py -text |
106 | invesalius/gui/widgets/gradient.py -text | 107 | invesalius/gui/widgets/gradient.py -text |
107 | invesalius/gui/widgets/listctrl.py -text | 108 | invesalius/gui/widgets/listctrl.py -text |
@@ -0,0 +1,416 @@ | @@ -0,0 +1,416 @@ | ||
1 | +import bisect | ||
2 | +import math | ||
3 | +import plistlib | ||
4 | +import sys | ||
5 | + | ||
6 | +import cairo | ||
7 | +import numpy | ||
8 | +import wx | ||
9 | +import wx.lib.pubsub as ps | ||
10 | +import wx.lib.wxcairo | ||
11 | + | ||
12 | +FONT_COLOUR = (1, 1, 1) | ||
13 | +LINE_COLOUR = (0.5, 0.5, 0.5) | ||
14 | +BACKGROUND_TEXT_COLOUR_RGBA = (1, 0, 0, 0.5) | ||
15 | +GRADIENT_RGBA = 0.75 | ||
16 | +RADIUS = 5 | ||
17 | + | ||
18 | +class CLUTRaycastingWidget(wx.Panel): | ||
19 | + """ | ||
20 | + This class represents the frame where images is showed | ||
21 | + """ | ||
22 | + | ||
23 | + def __init__(self, parent, id): | ||
24 | + """ | ||
25 | + Constructor. | ||
26 | + | ||
27 | + parent -- parent of this frame | ||
28 | + """ | ||
29 | + super(CLUTRaycastingWidget, self).__init__(parent, id) | ||
30 | + self.points = []#plistlib.readPlist(sys.argv[-1])['16bitClutCurves'] | ||
31 | + self.colours = []#plistlib.readPlist(sys.argv[-1])['16bitClutColors'] | ||
32 | + self.init = -1024 | ||
33 | + self.end = 2000 | ||
34 | + self.padding = 10 | ||
35 | + self.to_render = False | ||
36 | + self.histogram_pixel_points = [[0,0]] | ||
37 | + self.histogram_array = [[100,100],[100,100]] | ||
38 | + self.CreatePixelArray() | ||
39 | + #self.sizer = wx.BoxSizer(wx.HORIZONTAL) | ||
40 | + #self.SetSizer(self.sizer) | ||
41 | + #self.DrawControls() | ||
42 | + self.dragged = False | ||
43 | + self.point_dragged = None | ||
44 | + self.DoBind() | ||
45 | + #self.__bind_events() | ||
46 | + #self.SetAutoLayout(True) | ||
47 | + #self.sizer.Fit(self) | ||
48 | + self.Show() | ||
49 | + #self.LoadVolume() | ||
50 | + | ||
51 | + def SetRange(self, range): | ||
52 | + self.init, self.end = range | ||
53 | + print "Range", range | ||
54 | + self.CreatePixelArray() | ||
55 | + | ||
56 | + def DoBind(self): | ||
57 | + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) | ||
58 | + self.Bind(wx.EVT_LEFT_DOWN , self.OnClick) | ||
59 | + self.Bind(wx.EVT_LEFT_DCLICK , self.OnDoubleClick) | ||
60 | + self.Bind(wx.EVT_LEFT_UP , self.OnRelease) | ||
61 | + self.Bind(wx.EVT_RIGHT_DOWN , self.OnRighClick) | ||
62 | + self.Bind(wx.EVT_MOTION, self.OnMotion) | ||
63 | + self.Bind(wx.EVT_PAINT, self.OnPaint) | ||
64 | + self.Bind(wx.EVT_SIZE, self.OnSize) | ||
65 | + self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) | ||
66 | + | ||
67 | + ps.Publisher().subscribe(self.SetRaycastPreset, | ||
68 | + 'Set raycasting preset') | ||
69 | + | ||
70 | + def OnEraseBackground(self, evt): | ||
71 | + pass | ||
72 | + | ||
73 | + def OnClick(self, evt): | ||
74 | + point = self._has_clicked_in_a_point(evt.GetPositionTuple()) | ||
75 | + if point: | ||
76 | + self.dragged = True | ||
77 | + self.point_dragged = point | ||
78 | + self.Refresh() | ||
79 | + return | ||
80 | + else: | ||
81 | + p = self._has_clicked_in_line(evt.GetPositionTuple()) | ||
82 | + if p: | ||
83 | + n, p = p | ||
84 | + self.points[n].insert(p, {'x': 0, 'y': 0}) | ||
85 | + self.pixels_points[n].insert(p, list(evt.GetPositionTuple())) | ||
86 | + self.colours[n].insert(p, {'red': 0, 'green': 0, 'blue': 0}) | ||
87 | + self.PixelToHounsfield(n, p) | ||
88 | + self.Refresh() | ||
89 | + nevt = CLUTEvent(myEVT_CLUT_POINT_CHANGED, self.GetId()) | ||
90 | + self.GetEventHandler().ProcessEvent(nevt) | ||
91 | + return | ||
92 | + evt.Skip() | ||
93 | + | ||
94 | + def OnDoubleClick(self, evt): | ||
95 | + point = self._has_clicked_in_a_point(evt.GetPositionTuple()) | ||
96 | + if point: | ||
97 | + colour = wx.GetColourFromUser(self) | ||
98 | + if colour.IsOk(): | ||
99 | + i,j = self.point_dragged | ||
100 | + r, g, b = [x/255.0 for x in colour.Get()] | ||
101 | + self.colours[i][j]['red'] = r | ||
102 | + self.colours[i][j]['green'] = g | ||
103 | + self.colours[i][j]['blue'] = b | ||
104 | + self.Refresh() | ||
105 | + nevt = CLUTEvent(myEVT_CLUT_POINT_CHANGED, self.GetId()) | ||
106 | + self.GetEventHandler().ProcessEvent(nevt) | ||
107 | + return | ||
108 | + evt.Skip() | ||
109 | + | ||
110 | + def _has_clicked_in_a_point(self, position): | ||
111 | + """ | ||
112 | + returns the index from the selected point | ||
113 | + """ | ||
114 | + for i,curve in enumerate(self.pixels_points): | ||
115 | + for j,point in enumerate(curve): | ||
116 | + if self._calculate_distance(point, position) <= RADIUS: | ||
117 | + return (i, j) | ||
118 | + return None | ||
119 | + | ||
120 | + def _has_clicked_in_line(self, position): | ||
121 | + for n, point in enumerate(self.pixels_points): | ||
122 | + p = bisect.bisect([i[0] for i in point], position[0]) | ||
123 | + print p | ||
124 | + if p != 0 and p != len(point): | ||
125 | + x1, y1 = point[p-1] | ||
126 | + x2, y2 = position | ||
127 | + x3, y3 = point[p] | ||
128 | + if int(float(x2 - x1) / (x3 - x2)) - int(float(y2 - y1) / (y3 - y2)) == 0: | ||
129 | + return (n, p) | ||
130 | + return None | ||
131 | + | ||
132 | + def _calculate_distance(self, p1, p2): | ||
133 | + return ((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2) ** 0.5 | ||
134 | + | ||
135 | + def OnRighClick(self, evt): | ||
136 | + point = self._has_clicked_in_a_point(evt.GetPositionTuple()) | ||
137 | + if point: | ||
138 | + i, j = point | ||
139 | + print "RightClick", i, j | ||
140 | + self.pixels_points[i].pop(j) | ||
141 | + self.points[i].pop(j) | ||
142 | + self.colours[i].pop(j) | ||
143 | + if (i, j) == self.point_dragged: | ||
144 | + self.point_dragged = None | ||
145 | + if len(self.points[i]) == 1: | ||
146 | + self.points.pop(i) | ||
147 | + self.pixels_points.pop(i) | ||
148 | + self.colours.pop(i) | ||
149 | + self.Refresh() | ||
150 | + nevt = CLUTEvent(myEVT_CLUT_POINT_CHANGED, self.GetId()) | ||
151 | + self.GetEventHandler().ProcessEvent(nevt) | ||
152 | + return | ||
153 | + evt.Skip() | ||
154 | + | ||
155 | + def OnRelease(self, evt): | ||
156 | + if self.to_render: | ||
157 | + evt = CLUTEvent(myEVT_CLUT_POINT_CHANGED, self.GetId()) | ||
158 | + self.GetEventHandler().ProcessEvent(evt) | ||
159 | + self.dragged = False | ||
160 | + self.to_render = False | ||
161 | + | ||
162 | + def OnWheel(self, evt): | ||
163 | + direction = evt.GetWheelRotation() / evt.GetWheelDelta() | ||
164 | + init = self.init - 10 * direction | ||
165 | + end = self.end + 10 * direction | ||
166 | + print direction, init, end | ||
167 | + self.SetRange((init, end)) | ||
168 | + self.Refresh() | ||
169 | + | ||
170 | + def OnMotion(self, evt): | ||
171 | + if self.dragged: | ||
172 | + self.to_render = True | ||
173 | + i,j = self.point_dragged | ||
174 | + x = evt.GetX() | ||
175 | + y = evt.GetY() | ||
176 | + | ||
177 | + if y > self.GetVirtualSizeTuple()[1] - self.padding: | ||
178 | + y = self.GetVirtualSizeTuple()[1] - self.padding | ||
179 | + | ||
180 | + if y <= 0: | ||
181 | + y = 0 | ||
182 | + | ||
183 | + if x < 0: | ||
184 | + x = 0 | ||
185 | + | ||
186 | + if x > self.GetVirtualSizeTuple()[0]: | ||
187 | + x = self.GetVirtualSizeTuple()[0] | ||
188 | + | ||
189 | + # A point must be greater than the previous one, but the first one | ||
190 | + if j > 0 and x <= self.pixels_points[i][j-1][0]: | ||
191 | + x = self.pixels_points[i][j-1][0] + 1 | ||
192 | + | ||
193 | + # A point must be lower than the previous one, but the last one | ||
194 | + if j < len(self.pixels_points[i]) -1 \ | ||
195 | + and x >= self.pixels_points[i][j+1][0]: | ||
196 | + x = self.pixels_points[i][j+1][0] - 1 | ||
197 | + | ||
198 | + self.pixels_points[i][j][0] = x | ||
199 | + self.pixels_points[i][j][1] = y | ||
200 | + self.PixelToHounsfield(i,j) | ||
201 | + self.Refresh() | ||
202 | + evt = CLUTEvent(myEVT_CLUT_POINT , self.GetId()) | ||
203 | + self.GetEventHandler().ProcessEvent(evt) | ||
204 | + else: | ||
205 | + evt.Skip() | ||
206 | + | ||
207 | + def OnPaint(self, evt): | ||
208 | + dc = wx.BufferedPaintDC(self) | ||
209 | + dc.SetBackground(wx.Brush('Black')) | ||
210 | + dc.Clear() | ||
211 | + self.Render(dc) | ||
212 | + | ||
213 | + def OnSize(self, evt): | ||
214 | + print "Resizing" | ||
215 | + self.CreatePixelArray() | ||
216 | + self.Refresh() | ||
217 | + | ||
218 | + def _draw_gradient(self, ctx, height): | ||
219 | + #The gradient | ||
220 | + for i, curve in enumerate(self.pixels_points): | ||
221 | + x, y = curve[0] | ||
222 | + xini, yini = curve[0] | ||
223 | + xend, yend = curve[-1] | ||
224 | + gradient = cairo.LinearGradient(xini, height, xend, height) | ||
225 | + ctx.move_to(x, y) | ||
226 | + for j, point in enumerate(curve): | ||
227 | + x, y = point | ||
228 | + ctx.line_to(x, y) | ||
229 | + r = self.colours[i][j]['red'] | ||
230 | + g = self.colours[i][j]['green'] | ||
231 | + b = self.colours[i][j]['blue'] | ||
232 | + gradient.add_color_stop_rgba((x - xini) * 1.0 / (xend - xini), | ||
233 | + r, g, b, GRADIENT_RGBA) | ||
234 | + ctx.line_to(x, height) | ||
235 | + ctx.line_to(xini, height) | ||
236 | + ctx.close_path() | ||
237 | + ctx.set_source(gradient) | ||
238 | + ctx.fill() | ||
239 | + | ||
240 | + def _draw_curves(self, ctx): | ||
241 | + #Drawing the lines | ||
242 | + for curve in self.pixels_points: | ||
243 | + x,y = curve[0] | ||
244 | + ctx.move_to(x, y) | ||
245 | + for point in curve: | ||
246 | + x,y = point | ||
247 | + ctx.line_to(x, y) | ||
248 | + ctx.set_source_rgb(*LINE_COLOUR) | ||
249 | + ctx.stroke() | ||
250 | + | ||
251 | + def _draw_points(self, ctx): | ||
252 | + #Drawing the circles that represents the points | ||
253 | + for i, curve in enumerate(self.pixels_points): | ||
254 | + for j, point in enumerate(curve): | ||
255 | + x,y = point | ||
256 | + r = self.colours[i][j]['red'] | ||
257 | + g = self.colours[i][j]['green'] | ||
258 | + b = self.colours[i][j]['blue'] | ||
259 | + ctx.arc(x, y, RADIUS, 0, math.pi * 2) | ||
260 | + ctx.set_source_rgb(r, g, b) | ||
261 | + ctx.fill_preserve() | ||
262 | + ctx.set_source_rgb(*LINE_COLOUR) | ||
263 | + ctx.stroke() | ||
264 | + #ctx.move_to(x, y) | ||
265 | + | ||
266 | + def _draw_selected_point_text(self, ctx): | ||
267 | + ctx.select_font_face('Sans') | ||
268 | + ctx.set_font_size(15) | ||
269 | + i,j = self.point_dragged | ||
270 | + x,y = self.pixels_points[i][j] | ||
271 | + value = self.points[i][j]['x'] | ||
272 | + alpha = self.points[i][j]['y'] | ||
273 | + x_bearing, y_bearing, width, height, x_advance, y_advance\ | ||
274 | + = ctx.text_extents("Value %6d" % value) | ||
275 | + | ||
276 | + fheight = ctx.font_extents()[2] | ||
277 | + print "font", x_bearing, y_bearing | ||
278 | + | ||
279 | + ctx.set_source_rgba(*BACKGROUND_TEXT_COLOUR_RGBA) | ||
280 | + ctx.rectangle(x + RADIUS + 1 + x_bearing, y - RADIUS * 2 - 2 + | ||
281 | + y_bearing * 2, width + RADIUS + 1, fheight * 2) | ||
282 | + ctx.fill() | ||
283 | + | ||
284 | + ctx.set_source_rgb(1, 1, 1) | ||
285 | + ctx.move_to(x + RADIUS + 1, y - RADIUS - 1) | ||
286 | + ctx.show_text("Alpha: %.3f" % alpha) | ||
287 | + ctx.move_to(x + RADIUS + 1, y - RADIUS - 1 - fheight) | ||
288 | + ctx.show_text("Value: %6d" % value) | ||
289 | + | ||
290 | + def _draw_histogram(self, ctx): | ||
291 | + # The histogram | ||
292 | + ctx.set_source_rgb(0.5, 0.5, 0.5) | ||
293 | + x,y = self.histogram_pixel_points[0] | ||
294 | + print "=>", x,y | ||
295 | + ctx.move_to(x,y) | ||
296 | + for x,y in self.histogram_pixel_points: | ||
297 | + ctx.line_to(x,y) | ||
298 | + ctx.stroke() | ||
299 | + | ||
300 | + def _draw_selection_curve(self, ctx, width): | ||
301 | + for curve in self.pixels_points: | ||
302 | + x_center = (curve[0][0] + curve[-1][0])/2.0 | ||
303 | + print "x_center", curve[0][0], curve[-1][0], x_center | ||
304 | + ctx.set_source_rgb(*LINE_COLOUR) | ||
305 | + ctx.stroke() | ||
306 | + ctx.rectangle(x_center-5, width-5, 10, 10) | ||
307 | + ctx.set_source_rgb(0,0,0) | ||
308 | + ctx.fill_preserve() | ||
309 | + | ||
310 | + def Render(self, dc): | ||
311 | + ctx = wx.lib.wxcairo.ContextFromDC(dc) | ||
312 | + width, height= self.GetVirtualSizeTuple() | ||
313 | + height -= self.padding | ||
314 | + width -= self.padding | ||
315 | + | ||
316 | + self._draw_gradient(ctx, height) | ||
317 | + self._draw_curves(ctx) | ||
318 | + self._draw_points(ctx) | ||
319 | + self._draw_selection_curve(ctx, width) | ||
320 | + if self.point_dragged: | ||
321 | + self._draw_selected_point_text(ctx) | ||
322 | + | ||
323 | + | ||
324 | + def _build_histogram(self): | ||
325 | + width, height= self.GetVirtualSizeTuple() | ||
326 | + width -= self.padding | ||
327 | + height -= self.padding | ||
328 | + y_init = 0 | ||
329 | + y_end = max(self.histogram_array[0]) | ||
330 | + proportion_x = width * 1.0 / (self.end - self.init) | ||
331 | + proportion_y = height * 1.0 / (y_end - y_init) | ||
332 | + print ":) ", y_end, proportion_y | ||
333 | + self.histogram_pixel_points = [] | ||
334 | + for i in xrange(len(self.histogram_array[0])): | ||
335 | + x = self.histogram_array[1][i] | ||
336 | + y = self.histogram_array[0][i] | ||
337 | + print x, y | ||
338 | + x = (x + abs(self.init)) * proportion_x | ||
339 | + y = height - (y + abs(y_init)) * proportion_y | ||
340 | + self.histogram_pixel_points.append((x, y)) | ||
341 | + | ||
342 | + | ||
343 | + def CreatePixelArray(self): | ||
344 | + self.pixels_points = [] | ||
345 | + for curve in self.points: | ||
346 | + self.pixels_points.append([self.HounsfieldToPixel(i) for i in curve]) | ||
347 | + #self._build_histogram() | ||
348 | + | ||
349 | + def HounsfieldToPixel(self, h_pt): | ||
350 | + """ | ||
351 | + Given a Hounsfield point(graylevel, opacity), returns a pixel point in the canvas. | ||
352 | + """ | ||
353 | + width, height= self.GetVirtualSizeTuple() | ||
354 | + width -= self.padding | ||
355 | + height -= self.padding | ||
356 | + proportion = width * 1.0 / (self.end - self.init) | ||
357 | + x = (h_pt['x'] - self.init) * proportion | ||
358 | + y = height - (h_pt['y'] * height) | ||
359 | + return [x,y] | ||
360 | + | ||
361 | + def PixelToHounsfield(self, i, j): | ||
362 | + """ | ||
363 | + Given a Hounsfield point(graylevel, opacity), returns a pixel point in the canvas. | ||
364 | + """ | ||
365 | + width, height= self.GetVirtualSizeTuple() | ||
366 | + width -= self.padding | ||
367 | + height -= self.padding | ||
368 | + proportion = width * 1.0 / (self.end - self.init) | ||
369 | + x = self.pixels_points[i][j][0] / proportion - abs(self.init) | ||
370 | + y = (height - self.pixels_points[i][j][1]) * 1.0 / height | ||
371 | + self.points[i][j]['x'] = x | ||
372 | + self.points[i][j]['y'] = y | ||
373 | + self.colours[i][j] | ||
374 | + print x,y | ||
375 | + | ||
376 | + def SetRaycastPreset(self, preset): | ||
377 | + self.points = preset.data['16bitClutCurves'] | ||
378 | + self.colours = preset.data['16bitClutColors'] | ||
379 | + self.CreatePixelArray() | ||
380 | + | ||
381 | + def SetHistrogramArray(self, h_array): | ||
382 | + self.histogram_array = h_array | ||
383 | + | ||
384 | +class CLUTEvent(wx.PyCommandEvent): | ||
385 | + def __init__(self , evtType, id): | ||
386 | + wx.PyCommandEvent.__init__(self, evtType, id) | ||
387 | + | ||
388 | + | ||
389 | +# Occurs when CLUT is sliding | ||
390 | +myEVT_CLUT_SLIDER = wx.NewEventType() | ||
391 | +EVT_CLUT_SLIDER = wx.PyEventBinder(myEVT_CLUT_SLIDER, 1) | ||
392 | + | ||
393 | +# Occurs when CLUT was slided | ||
394 | +myEVT_CLUT_SLIDER_CHANGED = wx.NewEventType() | ||
395 | +EVT_CLUT_SLIDER_CHANGED = wx.PyEventBinder(myEVT_CLUT_SLIDER_CHANGED, 1) | ||
396 | + | ||
397 | +# Occurs when CLUT point is changing | ||
398 | +myEVT_CLUT_POINT = wx.NewEventType() | ||
399 | +EVT_CLUT_POINT = wx.PyEventBinder(myEVT_CLUT_POINT, 1) | ||
400 | + | ||
401 | +# Occurs when a CLUT point was changed | ||
402 | +myEVT_CLUT_POINT_CHANGED = wx.NewEventType() | ||
403 | +EVT_CLUT_POINT_CHANGED = wx.PyEventBinder(myEVT_CLUT_POINT_CHANGED, 1) | ||
404 | + | ||
405 | +class App(wx.App): | ||
406 | + def OnInit(self): | ||
407 | + str_type = sys.argv[-1].split("/")[-1].split(".")[0] | ||
408 | + self.frame = CLUTRaycastingWidget(None, -1, "InVesalius 3 - Raycasting: "+ str_type) | ||
409 | + self.frame.SetPreset(plistlib.readPlist(sys.argv[-1])) | ||
410 | + self.frame.Center() | ||
411 | + self.SetTopWindow(self.frame) | ||
412 | + return True | ||
413 | + | ||
414 | +if __name__ == '__main__': | ||
415 | + app = App() | ||
416 | + app.MainLoop() |