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
Siafoo is here to make coding less frustrating and to save you time. Join Siafoo Now or Learn More
Note: You are viewing an old version of this snippet. View Latest Version

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 4'
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.