License wxWindows Library License
Lines 328
Keywords
transfer function (2) volume rendering (2) widget (1) wxPython (7) wxWidgets (7)
Included in this Library
Permissions
Owner: Stou S.
Viewable by Everyone
Editable by All Siafoo Users
Hide
Need a quick chart or graph for your blog? Try our reStructured Text renderer. Join Siafoo Now or Learn More

wxPython Transfer Function Widget Atom Feed 0

In Brief A transfer function widget that allows you to map a value range to RGBA colors. It is useful for data visualization applications, especially volume rendering... more
# 's
  1import wx
2import numpy
3
4class TransferPoint(object):
5
6 def __init__(self, value, color, alpha, fixed = False):
7 self.value = value
8 # Color is 0 to 255
9 self.color = color
10 # Alpha is 0.0 to 1.0
11 self.alpha = alpha
12
13 if len(color) != 3:
14 raise Exception, 'Color should have length of 3'
15
16 self.selected = False
17 self.pix_size = 4
18 self.fixed = fixed
19
20 def __cmp__(self, pt):
21 ''' Used by the sort method'''
22 if self.value < pt.value: return -1
23 elif self.value > pt.value: return 1
24 return 0
25
26 def get_alpha(self):
27 return self.alpha
28
29 def get_rgba(self):
30 return [self.color[0], self.color[1], self.color[2], self.alpha]
31
32 def is_selected(self):
33 return self.selected
34
35 def set_selected(self, selected):
36 self.selected = selected
37
38
39class TransferFunctionWidget(wx.PyControl):
40
41 def __init__(self, parent, id=wx.ID_ANY, label="", pos=wx.DefaultPosition,
42 size=wx.DefaultSize, style=wx.NO_BORDER, validator=wx.DefaultValidator,
43 name="TransferFunction"):
44
45 wx.PyControl.__init__(self, parent, id, pos, size, style, validator, name)
46
47 # Local Variables
48 self.m_BorderLeft = 20
49 self.m_BorderUp = 5
50 self.m_BorderRight = 5
51 self.m_BorderDown = 13
52
53 self.m_GridLines = 10
54 self.m_TickSize = 4
55 self.m_TickBorder = 2
56 self.m_labelFontPt = 10;
57
58 # The transfer points
59 self.points = []
60 self.points.append(TransferPoint(0, [255, 255, 255], 0, fixed=True))
61 self.points.append(TransferPoint(255, [0, 0, 0], 1.0, fixed=True))
62
63 self.m_MinMax = (0.0, 255.0)
64
65 self.mouse_down = False
66 self.prev_x = 0
67 self.prev_y = 0
68 self.cur_pt = None
69
70 # This is always return -1 wtf.
71 self.x, self.y = self.GetPositionTuple()
72 width, height = self.GetClientSize()
73
74 # The number of usable pixels on the graph
75 self.r_fieldWidth = (width - (self.m_BorderRight + self.m_BorderLeft))
76 self.r_fieldHeight = (height - (self.m_BorderUp + self.m_BorderDown))
77
78 # The number of value data points
79 self.r_rangeWidth = (self.m_MinMax[1] - self.m_MinMax[0])
80 # Pixels per value
81 self.pixel_per_value = float(self.r_fieldWidth) / self.r_rangeWidth
82
83 # Bind events
84 self.Bind(wx.EVT_PAINT, self.OnPaint)
85 self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
86
87 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
88 self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
89 self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp)
90 self.Bind(wx.EVT_MOTION, self.OnMouseMotion)
91
92 def OnKeyDown(self, event):
93 """ Handles the wx.EVT_KEY_DOWN event for CustomCheckBox. """
94
95 if event.GetKeyCode() == wx.WXK_SPACE:
96 # The spacebar has been pressed: toggle our state
97 self.SendCheckBoxEvent()
98 event.Skip()
99 return
100
101 event.Skip()
102
103 def OnPaint(self, event):
104 """ Handles the wx.EVT_PAINT event for CustomCheckBox. """
105
106 dc = wx.BufferedPaintDC(self)
107 self.Draw(dc)
108
109 def DrawPoints(self, dc):
110
111 dc.SetBrush(wx.Brush(wx.Color(255,0,0), wx.SOLID))
112
113 x = self.x_from_value(self.m_MinMax[0])
114 y = self.y_from_alpha(0.0)
115
116 for pt in self.points:
117
118 x_c = self.x_from_value(pt.value)
119 y_c = self.y_from_alpha(pt.get_alpha())
120
121 dc.SetPen(wx.Pen(wx.Color(0,0,0)))
122 dc.DrawLine(x, y, x_c, y_c)
123
124 if pt.selected:
125 dc.SetPen(wx.Pen(wx.Color(0, 0, 255), 2))
126 dc.DrawRectangle(x_c - 3, y_c -3, 6, 6)
127
128 dc.SetPen(wx.Pen(wx.Color(255,0,0)))
129 dc.DrawRectangle(x_c - 2, y_c -2, 4, 4)
130
131 x = x_c
132 y = y_c
133
134 def DrawGrid(self, dc):
135 '''
136 Draw the grid lines
137 '''
138
139 width, height = self.GetClientSize()
140 x, y = self.GetPositionTuple()
141
142 spacing = height/(self.m_GridLines + 1)
143
144 dc.SetPen(wx.Pen(wx.Color(218, 218, 218)))
145
146 for i in range(self.m_GridLines):
147 dc.DrawLine(x + self.m_BorderLeft, y + (1 + i)*spacing,
148 x + self.m_BorderLeft + self.r_fieldWidth, y + (1 + i)*spacing)
149
150 def DrawAxis(self, dc):
151
152 dc.SetPen(wx.Pen(wx.Color(218,218,218)))
153
154 # Horizontal
155 dc.DrawLine(self.x + self.m_BorderLeft - 2, self.y + self.m_BorderUp,
156 self.x + self.m_BorderLeft + self.r_fieldWidth, self.y + self.m_BorderUp)
157 dc.DrawLine(self.x + self.m_BorderLeft - 2, self.y + self.r_fieldHeight + self.m_BorderUp,
158 self.x + self.m_BorderLeft + self.r_fieldWidth, self.y + self.r_fieldHeight + self.m_BorderUp)
159 # Vertical
160 dc.DrawLine(self.x + self.m_BorderLeft, self.y + self.m_BorderUp,
161 self.x + self.m_BorderLeft, self.y + self.m_BorderUp + self.r_fieldHeight);
162 dc.DrawLine(self.x + self.m_BorderLeft + self.r_fieldWidth, self.y + self.m_BorderUp,
163 self.x + self.m_BorderLeft + self.r_fieldWidth, self.y + self.m_BorderUp + self.r_fieldHeight);
164
165 dc.SetFont(wx.Font(self.m_labelFontPt, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
166
167 dc.DrawText('1.0', self.x + 2, self.y + self.m_labelFontPt/2 )
168 dc.DrawText('0.0', self.x + 2, self.y + self.m_BorderUp + self.r_fieldHeight)
169
170 for i, t in enumerate('Opacity'):
171 dc.DrawText(t, self.x + 6, self.y + self.m_BorderUp + (2+i)*self.m_labelFontPt)
172
173
174# int strw = 0;
175# int strh = 0;
176#
177# fl_measure(m_strRangeFrom.c_str(), strw, strh);
178# fl_draw(m_strRangeFrom.c_str(), x() + m_BorderLeft - strw/2, y() + m_BorderUp + r_fieldHeight + m_labelFontPt);
179# fl_measure(m_strRangeTo.c_str(), strw, strh);
180# fl_draw(m_strRangeTo.c_str(), x() + m_BorderLeft + r_fieldWidth - strw , y() + m_BorderUp + r_fieldHeight + m_labelFontPt);
181
182 dc.DrawText('Values', (self.m_BorderLeft + self.r_fieldWidth)/2, self.y + self.m_BorderUp + self.r_fieldHeight)
183
184 def DrawFill(self, dc):
185 ''' Draws the interpolated fill'''
186 yat = self.y_from_alpha(0.0)
187
188 for i in range(self.r_fieldWidth):
189 x = self.m_BorderLeft + self.x + i
190 rgba = self.rgba_from_value(self.value_from_x(x))
191 dc.SetPen(wx.Pen(wx.Color(rgba[0], rgba[1], rgba[2])))
192 dc.DrawLine(x, self.y_from_alpha(rgba[3]), x, yat)
193
194 def Draw(self, dc):
195
196 # Get the actual client size of ourselves
197 width, height = self.GetClientSize()
198
199 if not width or not height:
200 # Nothing to do, we still don't have dimensions!
201 return
202
203 # Initialize the wx.BufferedPaintDC, assigning a background
204 # colour and a foreground colour (to draw the text)
205 dc.SetBackground(wx.WHITE_BRUSH)
206 dc.Clear()
207
208 # Draw the transfer function fill
209 self.DrawFill(dc)
210 # Draw the Grid
211 self.DrawGrid(dc)
212 # Draw the Axis
213 self.DrawAxis(dc)
214 # Draw Points
215 self.DrawPoints(dc)
216
217 def OnEraseBackground(self, event):
218 ''' Called when the background is erased '''
219
220 # It is left blank to prevent flicker
221 pass
222
223 def OnMouseDown(self, event):
224
225 x = event.GetX()
226 y = event.GetY()
227
228 self.mouse_down = True
229
230 for pt in self.points:
231 if self.hit_test(x,y, pt):
232 self.cur_pt = pt
233 pt.selected = not pt.selected
234 self.prev_x = x
235 self.prev_y = y
236 return
237
238 self.cur_pt = None
239
240 def OnMouseUp(self, event):
241 x = event.GetX()
242 y = event.GetY()
243
244 if not self.cur_pt:
245
246 color_picker = wx.ColourDialog(self)
247
248 if color_picker.ShowModal() == wx.ID_OK:
249
250 color = color_picker.GetColourData().GetColour().Get()
251
252 self.points.append(TransferPoint(self.value_from_x(x), color, self.alpha_from_y(y)))
253 self.points.sort()
254 self.SendChangedEvent()
255
256 self.mouse_down = False
257 self.Refresh()
258
259 def OnMouseMotion(self, event):
260 x = event.GetX()
261 y = event.GetY()
262
263 if self.cur_pt and self.mouse_down:
264
265 self.cur_pt.alpha = self.alpha_from_y(y)
266
267 if not self.cur_pt.fixed:
268 self.cur_pt.value = self.value_from_x(x)
269 self.points.sort()
270
271 self.SendChangedEvent()
272 self.Refresh()
273
274 def SendChangedEvent(self):
275
276 event = wx.CommandEvent(wx.wxEVT_COMMAND_SLIDER_UPDATED, self.GetId())
277
278 self.GetEventHandler().ProcessEvent(event)
279
280 # Manipulation functions
281 def x_from_value(self, value):
282
283 if value > self.m_MinMax[1]:
284# print 'Warning x_from_value value out of range', value
285 return self.r_fieldWidth + self.m_BorderLeft + self.x
286
287 if value < self.m_MinMax[0]:
288# print 'Warning x_from_value value out of range', value
289 return self.m_BorderLeft + self.x
290
291 return self.m_BorderLeft + self.x + round(self.pixel_per_value * value)
292# return self.m_BorderLeft + self.x + int(self.r_fieldWidth*((value - self.m_MinMax[0])/self.r_rangeWidth))
293
294 def y_from_alpha(self, alpha):
295
296 if alpha < 0:
297 return self.m_BorderUp + self.y
298 if alpha > 1.0:
299 return self.y
300
301 return self.m_BorderUp + self.y + int(self.r_fieldHeight*(1 - alpha))
302
303 def value_from_x(self, xc):
304
305 if not (xc >= self.x + self.m_BorderLeft):
306 return float(self.m_MinMax[0])
307 if not (xc <= self.x + self.r_fieldWidth + self.m_BorderLeft):
308 return float(self.m_MinMax[1])
309
310 return float(self.m_MinMax[0]) + self.r_rangeWidth*float(float(xc - self.x - self.m_BorderLeft)/((self.r_fieldWidth)))
311
312 def alpha_from_y(self, yc):
313
314 if yc < self.y + self.m_BorderUp:
315 return 1.0
316 if yc > self.y + self.m_BorderUp + self.r_fieldHeight:
317 return 0.0
318
319 return 1.0 - float(yc - self.y - self.m_BorderUp)/self.r_fieldHeight;
320
321 def hit_test(self, x, y, pt):
322 x_c = self.x_from_value(pt.value)
323 y_c = self.y_from_alpha(pt.get_alpha())
324 sz = pt.pix_size
325
326 if x <= x_c + sz / 2 and x >= x_c - sz \
327 and y <= y_c + sz and y >= y_c - sz:
328 return True
329
330 return False
331
332 def rgba_from_value(self, value):
333
334 if not (value >= self.m_MinMax[0] and value <= self.m_MinMax[1]):
335 raise Exception, 'Value out of range: ' + str(value)
336
337 for pt_i in range(len(self.points)):
338 pt_cur = self.points[pt_i]
339 pt_next = self.points[pt_i + 1]
340
341 value_cur = pt_cur.value
342 value_next = pt_next.value
343
344 if value_cur < value:
345 if value_next > value:
346 target = (value - value_cur)/float(value_next - value_cur)
347
348 cur_color = pt_cur.get_rgba()
349 next_color = pt_next.get_rgba()
350 return self.interpolate(target, cur_color, next_color)
351 elif value_next == value:
352 return pt_next.get_rgba()
353 elif value_cur == value:
354 return pt_cur.get_rgba()
355 else:
356 print 'Value, ', value, value_cur
357 raise Exception, 'Value is weird'
358
359 return []
360
361 def interpolate(self, target, val1, val2):
362 return [target*(v2) + (1.0 - target)*v1 for v1, v2 in zip(val1, val2)]
363
364 def get_map(self):
365 ''' Get the interpolated values '''
366 return numpy.array([self.rgba_from_value(i) for i in range(int(self.r_rangeWidth) + 1)], dtype=numpy.float32)
367
368# You don't need anything below this
369class TGraphDemo(wx.Frame):
370
371 def __init__(self, parent, id=wx.ID_ANY, title="", pos=wx.DefaultPosition,
372 size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE):
373
374 wx.Frame.__init__(self, parent, id, title, pos, size, style)
375
376 panel = wx.Panel(self, -1)
377
378 # Initialize the widget
379 self.t_graph = TransferFunctionWidget(panel, -1, "Transfer Function", size=wx.Size(300, 150))
380
381 mainSizer = wx.BoxSizer(wx.VERTICAL)
382 mainSizer.Add(panel , 1)
383
384 self.SetSizer(mainSizer)
385 mainSizer.Layout()
386
387 # Bind the updated event
388 self.t_graph.Connect(-1, -1, wx.wxEVT_COMMAND_SLIDER_UPDATED, self.OnTGraphUpdate)
389
390 def OnTGraphUpdate(self, event):
391 # Use this to get an RGBA matrix
392 # self.t_graph.get_map()
393 print 'Yay new values!'
394
395if __name__ == '__main__':
396 app = wx.App()
397 frame = TGraphDemo(None, -1, 'Transfer Function Graph Demo',
398 wx.DefaultPosition, wx.Size(300,150))
399 frame.Show()
400 app.MainLoop()

A transfer function widget that allows you to map a value range to RGBA colors. It is useful for data visualization applications, especially volume rendering

/image/31

This is actually a wxPython port of an FLTK based widget I wrote for a volume rendering class. The code is a bit ugly (since it was ported from C++) and probably kind of buggy for ranges outside of 0-255. I ported it to Python for another application I was hacking together... so the quality of this code isn't that high... feel free to edit and improve it.

I might post the FLTK version of this code if I ever decouple it from the rest of it's host application and clean up some of the horrid code.