diff --git a/.gitattributes b/.gitattributes index ef7a9f4..812e1b7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -102,6 +102,7 @@ invesalius/gui/task_slice.py -text invesalius/gui/task_surface.py -text invesalius/gui/task_tools.py -text invesalius/gui/widgets/__init__.py -text +invesalius/gui/widgets/clut_raycasting.py -text invesalius/gui/widgets/foldpanelbar.py -text invesalius/gui/widgets/gradient.py -text invesalius/gui/widgets/listctrl.py -text diff --git a/invesalius/gui/widgets/clut_raycasting.py b/invesalius/gui/widgets/clut_raycasting.py new file mode 100644 index 0000000..7587aa7 --- /dev/null +++ b/invesalius/gui/widgets/clut_raycasting.py @@ -0,0 +1,416 @@ +import bisect +import math +import plistlib +import sys + +import cairo +import numpy +import wx +import wx.lib.pubsub as ps +import wx.lib.wxcairo + +FONT_COLOUR = (1, 1, 1) +LINE_COLOUR = (0.5, 0.5, 0.5) +BACKGROUND_TEXT_COLOUR_RGBA = (1, 0, 0, 0.5) +GRADIENT_RGBA = 0.75 +RADIUS = 5 + +class CLUTRaycastingWidget(wx.Panel): + """ + This class represents the frame where images is showed + """ + + def __init__(self, parent, id): + """ + Constructor. + + parent -- parent of this frame + """ + super(CLUTRaycastingWidget, self).__init__(parent, id) + self.points = []#plistlib.readPlist(sys.argv[-1])['16bitClutCurves'] + self.colours = []#plistlib.readPlist(sys.argv[-1])['16bitClutColors'] + self.init = -1024 + self.end = 2000 + self.padding = 10 + self.to_render = False + self.histogram_pixel_points = [[0,0]] + self.histogram_array = [[100,100],[100,100]] + self.CreatePixelArray() + #self.sizer = wx.BoxSizer(wx.HORIZONTAL) + #self.SetSizer(self.sizer) + #self.DrawControls() + self.dragged = False + self.point_dragged = None + self.DoBind() + #self.__bind_events() + #self.SetAutoLayout(True) + #self.sizer.Fit(self) + self.Show() + #self.LoadVolume() + + def SetRange(self, range): + self.init, self.end = range + print "Range", range + self.CreatePixelArray() + + def DoBind(self): + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + self.Bind(wx.EVT_LEFT_DOWN , self.OnClick) + self.Bind(wx.EVT_LEFT_DCLICK , self.OnDoubleClick) + self.Bind(wx.EVT_LEFT_UP , self.OnRelease) + self.Bind(wx.EVT_RIGHT_DOWN , self.OnRighClick) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) + + ps.Publisher().subscribe(self.SetRaycastPreset, + 'Set raycasting preset') + + def OnEraseBackground(self, evt): + pass + + def OnClick(self, evt): + point = self._has_clicked_in_a_point(evt.GetPositionTuple()) + if point: + self.dragged = True + self.point_dragged = point + self.Refresh() + return + else: + p = self._has_clicked_in_line(evt.GetPositionTuple()) + if p: + n, p = p + self.points[n].insert(p, {'x': 0, 'y': 0}) + self.pixels_points[n].insert(p, list(evt.GetPositionTuple())) + self.colours[n].insert(p, {'red': 0, 'green': 0, 'blue': 0}) + self.PixelToHounsfield(n, p) + self.Refresh() + nevt = CLUTEvent(myEVT_CLUT_POINT_CHANGED, self.GetId()) + self.GetEventHandler().ProcessEvent(nevt) + return + evt.Skip() + + def OnDoubleClick(self, evt): + point = self._has_clicked_in_a_point(evt.GetPositionTuple()) + if point: + colour = wx.GetColourFromUser(self) + if colour.IsOk(): + i,j = self.point_dragged + r, g, b = [x/255.0 for x in colour.Get()] + self.colours[i][j]['red'] = r + self.colours[i][j]['green'] = g + self.colours[i][j]['blue'] = b + self.Refresh() + nevt = CLUTEvent(myEVT_CLUT_POINT_CHANGED, self.GetId()) + self.GetEventHandler().ProcessEvent(nevt) + return + evt.Skip() + + def _has_clicked_in_a_point(self, position): + """ + returns the index from the selected point + """ + for i,curve in enumerate(self.pixels_points): + for j,point in enumerate(curve): + if self._calculate_distance(point, position) <= RADIUS: + return (i, j) + return None + + def _has_clicked_in_line(self, position): + for n, point in enumerate(self.pixels_points): + p = bisect.bisect([i[0] for i in point], position[0]) + print p + if p != 0 and p != len(point): + x1, y1 = point[p-1] + x2, y2 = position + x3, y3 = point[p] + if int(float(x2 - x1) / (x3 - x2)) - int(float(y2 - y1) / (y3 - y2)) == 0: + return (n, p) + return None + + def _calculate_distance(self, p1, p2): + return ((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2) ** 0.5 + + def OnRighClick(self, evt): + point = self._has_clicked_in_a_point(evt.GetPositionTuple()) + if point: + i, j = point + print "RightClick", i, j + self.pixels_points[i].pop(j) + self.points[i].pop(j) + self.colours[i].pop(j) + if (i, j) == self.point_dragged: + self.point_dragged = None + if len(self.points[i]) == 1: + self.points.pop(i) + self.pixels_points.pop(i) + self.colours.pop(i) + self.Refresh() + nevt = CLUTEvent(myEVT_CLUT_POINT_CHANGED, self.GetId()) + self.GetEventHandler().ProcessEvent(nevt) + return + evt.Skip() + + def OnRelease(self, evt): + if self.to_render: + evt = CLUTEvent(myEVT_CLUT_POINT_CHANGED, self.GetId()) + self.GetEventHandler().ProcessEvent(evt) + self.dragged = False + self.to_render = False + + def OnWheel(self, evt): + direction = evt.GetWheelRotation() / evt.GetWheelDelta() + init = self.init - 10 * direction + end = self.end + 10 * direction + print direction, init, end + self.SetRange((init, end)) + self.Refresh() + + def OnMotion(self, evt): + if self.dragged: + self.to_render = True + i,j = self.point_dragged + x = evt.GetX() + y = evt.GetY() + + if y > self.GetVirtualSizeTuple()[1] - self.padding: + y = self.GetVirtualSizeTuple()[1] - self.padding + + if y <= 0: + y = 0 + + if x < 0: + x = 0 + + if x > self.GetVirtualSizeTuple()[0]: + x = self.GetVirtualSizeTuple()[0] + + # A point must be greater than the previous one, but the first one + if j > 0 and x <= self.pixels_points[i][j-1][0]: + x = self.pixels_points[i][j-1][0] + 1 + + # A point must be lower than the previous one, but the last one + if j < len(self.pixels_points[i]) -1 \ + and x >= self.pixels_points[i][j+1][0]: + x = self.pixels_points[i][j+1][0] - 1 + + self.pixels_points[i][j][0] = x + self.pixels_points[i][j][1] = y + self.PixelToHounsfield(i,j) + self.Refresh() + evt = CLUTEvent(myEVT_CLUT_POINT , self.GetId()) + self.GetEventHandler().ProcessEvent(evt) + else: + evt.Skip() + + def OnPaint(self, evt): + dc = wx.BufferedPaintDC(self) + dc.SetBackground(wx.Brush('Black')) + dc.Clear() + self.Render(dc) + + def OnSize(self, evt): + print "Resizing" + self.CreatePixelArray() + self.Refresh() + + def _draw_gradient(self, ctx, height): + #The gradient + for i, curve in enumerate(self.pixels_points): + x, y = curve[0] + xini, yini = curve[0] + xend, yend = curve[-1] + gradient = cairo.LinearGradient(xini, height, xend, height) + ctx.move_to(x, y) + for j, point in enumerate(curve): + x, y = point + ctx.line_to(x, y) + r = self.colours[i][j]['red'] + g = self.colours[i][j]['green'] + b = self.colours[i][j]['blue'] + gradient.add_color_stop_rgba((x - xini) * 1.0 / (xend - xini), + r, g, b, GRADIENT_RGBA) + ctx.line_to(x, height) + ctx.line_to(xini, height) + ctx.close_path() + ctx.set_source(gradient) + ctx.fill() + + def _draw_curves(self, ctx): + #Drawing the lines + for curve in self.pixels_points: + x,y = curve[0] + ctx.move_to(x, y) + for point in curve: + x,y = point + ctx.line_to(x, y) + ctx.set_source_rgb(*LINE_COLOUR) + ctx.stroke() + + def _draw_points(self, ctx): + #Drawing the circles that represents the points + for i, curve in enumerate(self.pixels_points): + for j, point in enumerate(curve): + x,y = point + r = self.colours[i][j]['red'] + g = self.colours[i][j]['green'] + b = self.colours[i][j]['blue'] + ctx.arc(x, y, RADIUS, 0, math.pi * 2) + ctx.set_source_rgb(r, g, b) + ctx.fill_preserve() + ctx.set_source_rgb(*LINE_COLOUR) + ctx.stroke() + #ctx.move_to(x, y) + + def _draw_selected_point_text(self, ctx): + ctx.select_font_face('Sans') + ctx.set_font_size(15) + i,j = self.point_dragged + x,y = self.pixels_points[i][j] + value = self.points[i][j]['x'] + alpha = self.points[i][j]['y'] + x_bearing, y_bearing, width, height, x_advance, y_advance\ + = ctx.text_extents("Value %6d" % value) + + fheight = ctx.font_extents()[2] + print "font", x_bearing, y_bearing + + ctx.set_source_rgba(*BACKGROUND_TEXT_COLOUR_RGBA) + ctx.rectangle(x + RADIUS + 1 + x_bearing, y - RADIUS * 2 - 2 + + y_bearing * 2, width + RADIUS + 1, fheight * 2) + ctx.fill() + + ctx.set_source_rgb(1, 1, 1) + ctx.move_to(x + RADIUS + 1, y - RADIUS - 1) + ctx.show_text("Alpha: %.3f" % alpha) + ctx.move_to(x + RADIUS + 1, y - RADIUS - 1 - fheight) + ctx.show_text("Value: %6d" % value) + + def _draw_histogram(self, ctx): + # The histogram + ctx.set_source_rgb(0.5, 0.5, 0.5) + x,y = self.histogram_pixel_points[0] + print "=>", x,y + ctx.move_to(x,y) + for x,y in self.histogram_pixel_points: + ctx.line_to(x,y) + ctx.stroke() + + def _draw_selection_curve(self, ctx, width): + for curve in self.pixels_points: + x_center = (curve[0][0] + curve[-1][0])/2.0 + print "x_center", curve[0][0], curve[-1][0], x_center + ctx.set_source_rgb(*LINE_COLOUR) + ctx.stroke() + ctx.rectangle(x_center-5, width-5, 10, 10) + ctx.set_source_rgb(0,0,0) + ctx.fill_preserve() + + def Render(self, dc): + ctx = wx.lib.wxcairo.ContextFromDC(dc) + width, height= self.GetVirtualSizeTuple() + height -= self.padding + width -= self.padding + + self._draw_gradient(ctx, height) + self._draw_curves(ctx) + self._draw_points(ctx) + self._draw_selection_curve(ctx, width) + if self.point_dragged: + self._draw_selected_point_text(ctx) + + + def _build_histogram(self): + width, height= self.GetVirtualSizeTuple() + width -= self.padding + height -= self.padding + y_init = 0 + y_end = max(self.histogram_array[0]) + proportion_x = width * 1.0 / (self.end - self.init) + proportion_y = height * 1.0 / (y_end - y_init) + print ":) ", y_end, proportion_y + self.histogram_pixel_points = [] + for i in xrange(len(self.histogram_array[0])): + x = self.histogram_array[1][i] + y = self.histogram_array[0][i] + print x, y + x = (x + abs(self.init)) * proportion_x + y = height - (y + abs(y_init)) * proportion_y + self.histogram_pixel_points.append((x, y)) + + + def CreatePixelArray(self): + self.pixels_points = [] + for curve in self.points: + self.pixels_points.append([self.HounsfieldToPixel(i) for i in curve]) + #self._build_histogram() + + def HounsfieldToPixel(self, h_pt): + """ + Given a Hounsfield point(graylevel, opacity), returns a pixel point in the canvas. + """ + width, height= self.GetVirtualSizeTuple() + width -= self.padding + height -= self.padding + proportion = width * 1.0 / (self.end - self.init) + x = (h_pt['x'] - self.init) * proportion + y = height - (h_pt['y'] * height) + return [x,y] + + def PixelToHounsfield(self, i, j): + """ + Given a Hounsfield point(graylevel, opacity), returns a pixel point in the canvas. + """ + width, height= self.GetVirtualSizeTuple() + width -= self.padding + height -= self.padding + proportion = width * 1.0 / (self.end - self.init) + x = self.pixels_points[i][j][0] / proportion - abs(self.init) + y = (height - self.pixels_points[i][j][1]) * 1.0 / height + self.points[i][j]['x'] = x + self.points[i][j]['y'] = y + self.colours[i][j] + print x,y + + def SetRaycastPreset(self, preset): + self.points = preset.data['16bitClutCurves'] + self.colours = preset.data['16bitClutColors'] + self.CreatePixelArray() + + def SetHistrogramArray(self, h_array): + self.histogram_array = h_array + +class CLUTEvent(wx.PyCommandEvent): + def __init__(self , evtType, id): + wx.PyCommandEvent.__init__(self, evtType, id) + + +# Occurs when CLUT is sliding +myEVT_CLUT_SLIDER = wx.NewEventType() +EVT_CLUT_SLIDER = wx.PyEventBinder(myEVT_CLUT_SLIDER, 1) + +# Occurs when CLUT was slided +myEVT_CLUT_SLIDER_CHANGED = wx.NewEventType() +EVT_CLUT_SLIDER_CHANGED = wx.PyEventBinder(myEVT_CLUT_SLIDER_CHANGED, 1) + +# Occurs when CLUT point is changing +myEVT_CLUT_POINT = wx.NewEventType() +EVT_CLUT_POINT = wx.PyEventBinder(myEVT_CLUT_POINT, 1) + +# Occurs when a CLUT point was changed +myEVT_CLUT_POINT_CHANGED = wx.NewEventType() +EVT_CLUT_POINT_CHANGED = wx.PyEventBinder(myEVT_CLUT_POINT_CHANGED, 1) + +class App(wx.App): + def OnInit(self): + str_type = sys.argv[-1].split("/")[-1].split(".")[0] + self.frame = CLUTRaycastingWidget(None, -1, "InVesalius 3 - Raycasting: "+ str_type) + self.frame.SetPreset(plistlib.readPlist(sys.argv[-1])) + self.frame.Center() + self.SetTopWindow(self.frame) + return True + +if __name__ == '__main__': + app = App() + app.MainLoop() -- libgit2 0.21.2