Commit 6a432126093efeab3cc9233d8492a39d749bd968

Authored by Thiago Franco de Moraes
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
invesalius/gui/widgets/clut_imagedata.py 0 → 100644
... ... @@ -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()
... ...