Commit 6a432126093efeab3cc9233d8492a39d749bd968
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
| ... | ... | @@ -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() | ... | ... |