diff --git a/fsl/fslview/lightboxcanvas.py b/fsl/fslview/lightboxcanvas.py index 5a3612a82842c18c18796b7379bd882abb8cb33e..1e5e7396d62e198b1674c07fea8981e2f9443d4c 100644 --- a/fsl/fslview/lightboxcanvas.py +++ b/fsl/fslview/lightboxcanvas.py @@ -47,22 +47,86 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): 'zmax' : 'Last slice', 'sliceSpacing' : 'Slice spacing', 'ncols' : 'Number of columns', + 'showCursor' : 'Show cursor', 'zax' : 'Z axis'} - _view = props.VGroup(('zmin', + _view = props.VGroup(('showCursor', + 'zmin', 'zmax', 'sliceSpacing', 'ncols', 'zax')) + def worldToCanvas(self, xpos, ypos, zpos): + """ + Given an x/y/z location in the image list world (with xpos + corresponding to the horizontal screen axis, ypos to the + vertical axis, and zpos to the depth axis), converts + it into an x/y position, in world coordinates, on the + canvas. + """ + sliceno = int(np.floor((zpos - self.zmin) / self.sliceSpacing)) + + xlen = self.imageList.bounds.getLen(self.xax) + ylen = self.imageList.bounds.getLen(self.yax) + + row = self._nrows - int(np.floor(sliceno / self.ncols)) - 1 + col = int(np.floor(sliceno % self.ncols)) + + xpos = xpos + xlen * col + ypos = ypos + ylen * row + + return xpos, ypos + + def canvasToWorld(self, xpos, ypos): """ - Given pixel x/y coordinates on this canvas, translates them - into the real world x/y/z coordinates of the displayed slice. - What order should the returned coordinates be in? - """ - pass + Overwrites SliceCanvas.canvasToWorld. Given pixel x/y + coordinates on this canvas, translates them into the + real world x/y/z coordinates of the displayed slice. + Returns a 3-tuple containing the (x, y, z) coordinates + (in the dimension order of the image list space). If the + given canvas position is out of the image range, None + is returned. + """ + + nrows = self._nrows + ncols = self.ncols + + screenx, screeny = slicecanvas.SliceCanvas.canvasToWorld( + self, xpos, ypos) + + xmin = self.imageList.bounds.getLo( self.xax) + ymin = self.imageList.bounds.getLo( self.yax) + xlen = self.imageList.bounds.getLen(self.xax) + ylen = self.imageList.bounds.getLen(self.yax) + + xmax = xmin + ncols * xlen + ymax = ymin + nrows * ylen + + col = int(np.floor((screenx - xmin) / xlen)) + row = nrows - int(np.floor((screeny - ymin) / ylen)) - 1 + sliceno = row * ncols + col + + if screenx < xmin or \ + screenx > xmax or \ + screeny < ymin or \ + screeny > ymax or \ + sliceno < 0 or \ + sliceno >= self._nslices: + return None + + xpos = screenx - col * xlen + ypos = screeny - (nrows - row - 1) * ylen + zpos = self.zmin + (sliceno + 0.5) * self.sliceSpacing + + pos = [0, 0, 0] + pos[self.xax] = xpos + pos[self.yax] = ypos + pos[self.zax] = zpos + + return tuple(pos) def __init__(self, @@ -175,13 +239,20 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): (which is different from the former if the canvas has a scroll bar). """ - zlen = self.zmax - self.zmin - self._nslices = int(np.floor(zlen / self.sliceSpacing)) - self._nrows = int(np.ceil(self._nslices / float(self.ncols))) - xlen = self.imageList.bounds.getLen(self.xax) ylen = self.imageList.bounds.getLen(self.yax) + zlen = self.zmax - self.zmin + width, height = self.GetClientSize().Get() + + if xlen == 0 or \ + ylen == 0 or \ + width == 0 or \ + height == 0: + return + + self._nslices = int(np.floor(zlen / self.sliceSpacing)) + self._nrows = int(np.ceil(self._nslices / float(self.ncols))) # no scrollbar -> display all rows if self._scrollbar is None: @@ -189,9 +260,12 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): # scrollbar -> display a selection of rows else: - width, height = self.GetClientSize().Get() - sliceWidth = width / self.ncols - sliceHeight = sliceWidth * (ylen / xlen) + + sliceWidth = width / self.ncols + sliceHeight = sliceWidth * (ylen / xlen) + + if sliceWidth == 0 or sliceHeight == 0: + return self._rowsOnScreen = int(np.ceil(height / sliceHeight)) @@ -374,6 +448,40 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): rowsOnScreen, True) + + def _drawCursor(self): + """ + Draws a cursor at the current canvas position (the SliceCanvas.pos + property). + """ + + xpos, ypos = self.worldToCanvas(*self.pos.xyz) + + xmin, xmax = self.imageList.bounds.getRange(self.xax) + ymin, ymax = self.imageList.bounds.getRange(self.yax) + + xverts = np.zeros((2, 3)) + yverts = np.zeros((2, 3)) + + xverts[:, self.xax] = xpos + xverts[0, self.yax] = ypos - 5 + xverts[1, self.yax] = ypos + 5 + xverts[:, self.zax] = self.pos.z + 1 + + yverts[:, self.yax] = ypos + yverts[:, self.xax] = [xpos - 5, xpos + 5] + yverts[:, self.zax] = self.pos.z + 1 + + log.debug('Drawing cursor at {} - {}'.format(xpos, ypos)) + + gl.glBegin(gl.GL_LINES) + gl.glColor3f(0, 1, 0) + gl.glVertex3f(*xverts[0]) + gl.glVertex3f(*xverts[1]) + gl.glVertex3f(*yverts[0]) + gl.glVertex3f(*yverts[1]) + gl.glEnd() + def _draw(self, ev): """ @@ -426,60 +534,11 @@ class LightBoxCanvas(slicecanvas.SliceCanvas): for zi in range(startSlice, endSlice): self._drawSlice(image, self._sliceIdxs[ i][zi], - self._transforms[i][zi]) - - gl.glUseProgram(0) - - self.SwapBuffers() - - -class LightBoxPanel(wx.Panel): - """ - Convenience Panel which contains a a LightBoxCanvas and a scrollbar, - and sets up mouse-scrolling behaviour. - """ - - def __init__(self, parent, *args, **kwargs): - """ - Accepts the same parameters as the LightBoxCanvas constructor, - although if you pass in a scrollbar, it will be ignored. - """ - - wx.Panel.__init__(self, parent) - - self.scrollbar = wx.ScrollBar(self, style=wx.SB_VERTICAL) - - kwargs['scrollbar'] = self.scrollbar - - self.canvas = LightBoxCanvas(self, *args, **kwargs) - - self.sizer = wx.BoxSizer(wx.HORIZONTAL) - self.SetSizer(self.sizer) + self._transforms[i][zi]) - self.sizer.Add(self.canvas, flag=wx.EXPAND, proportion=1) - self.sizer.Add(self.scrollbar, flag=wx.EXPAND) + gl.glUseProgram(0) - def scrollOnMouse(ev): + if self.showCursor: + self._drawCursor() - wheelDir = ev.GetWheelRotation() - - if wheelDir > 0: wheelDir = -1 - elif wheelDir < 0: wheelDir = 1 - - curPos = self.scrollbar.GetThumbPosition() - newPos = curPos + wheelDir - sbRange = self.scrollbar.GetRange() - rowsOnScreen = self.scrollbar.GetPageSize() - - if self.scrollbar.GetPageSize() >= self.scrollbar.GetRange(): - return - if newPos < 0 or newPos + rowsOnScreen > sbRange: - return - - self.scrollbar.SetThumbPosition(curPos + wheelDir) - self.canvas._updateDisplayBounds() - self.canvas.Refresh() - - self.Bind(wx.EVT_MOUSEWHEEL, scrollOnMouse) - - self.Layout() + self.SwapBuffers() diff --git a/fsl/fslview/lightboxpanel.py b/fsl/fslview/lightboxpanel.py new file mode 100644 index 0000000000000000000000000000000000000000..53adad309f49a1d12bbf42cf99ba102dee6281c4 --- /dev/null +++ b/fsl/fslview/lightboxpanel.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# +# lightboxpanel.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging +log = logging.getLogger(__name__) + +import wx + +import fsl.props as props +import fsl.fslview.lightboxcanvas as lightboxcanvas + +class LightBoxPanel(wx.Panel, props.HasProperties): + """ + Convenience Panel which contains a a LightBoxCanvas and a scrollbar, + and sets up mouse-scrolling behaviour. + """ + + def __init__(self, parent, *args, **kwargs): + """ + Accepts the same parameters as the LightBoxCanvas constructor, + although if you pass in a scrollbar, it will be ignored. + """ + + wx.Panel.__init__(self, parent) + self.name = 'LightBoxPanel_{}'.format(id(self)) + + self.scrollbar = wx.ScrollBar(self, style=wx.SB_VERTICAL) + + kwargs['scrollbar'] = self.scrollbar + + self.canvas = lightboxcanvas.LightBoxCanvas(self, *args, **kwargs) + + self.imageList = self.canvas.imageList + + self.sizer = wx.BoxSizer(wx.HORIZONTAL) + self.SetSizer(self.sizer) + + self.sizer.Add(self.canvas, flag=wx.EXPAND, proportion=1) + self.sizer.Add(self.scrollbar, flag=wx.EXPAND) + + self.canvas.Bind(wx.EVT_LEFT_DOWN, self.onMouseEvent) + self.canvas.Bind(wx.EVT_MOTION, self.onMouseEvent) + + self.Bind(wx.EVT_MOUSEWHEEL, self.onMouseScroll) + + def move(*a): + xpos = self.imageList.location.getPos(self.canvas.xax) + ypos = self.imageList.location.getPos(self.canvas.yax) + zpos = self.imageList.location.getPos(self.canvas.zax) + self.canvas.pos.xyz = (xpos, ypos, zpos) + + self.imageList.addListener('location', self.name, move) + + def onDestroy(ev): + self.imageList.removeListener('location', self.name) + ev.Skip() + + self.Layout() + + + def onMouseEvent(self, ev): + + if not ev.LeftIsDown(): return + if len(self.imageList) == 0: return + + mx, my = ev.GetPositionTuple() + w, h = self.canvas.GetClientSize() + + my = h - my + + clickPos = self.canvas.canvasToWorld(mx, my) + + if clickPos is None: + return + + xpos, ypos, zpos = clickPos + + log.debug('Mouse click on {}: ' + '({}, {} -> {: 5.2f}, {: 5.2f}, {: 5.2f})'.format( + self.canvas.name, mx, my, xpos, ypos, zpos)) + + self.imageList.location.xyz = xpos, ypos, zpos + + + def onMouseScroll(self, ev): + + wheelDir = ev.GetWheelRotation() + + if wheelDir > 0: wheelDir = -1 + elif wheelDir < 0: wheelDir = 1 + + curPos = self.scrollbar.GetThumbPosition() + newPos = curPos + wheelDir + sbRange = self.scrollbar.GetRange() + rowsOnScreen = self.scrollbar.GetPageSize() + + if self.scrollbar.GetPageSize() >= self.scrollbar.GetRange(): + return + if newPos < 0 or newPos + rowsOnScreen > sbRange: + return + + self.scrollbar.SetThumbPosition(curPos + wheelDir) + self.canvas._updateDisplayBounds() + self.canvas.Refresh() diff --git a/fsl/props/properties_types.py b/fsl/props/properties_types.py index 7521f1407244e60ef28cd28e9478c5bc65a84e2e..429a7f3aafb4cba50d59d5ea29b66ba57ac5d895 100644 --- a/fsl/props/properties_types.py +++ b/fsl/props/properties_types.py @@ -468,6 +468,12 @@ class BoundsValueList(propvals.PropertyValueList): elif lname == 'xlen': return self.getLen( 0) elif lname == 'ylen': return self.getLen( 1) elif lname == 'zlen': return self.getLen( 2) + elif lname == 'xmin': return self.getMin( 0) + elif lname == 'ymin': return self.getMin( 1) + elif lname == 'zmin': return self.getMin( 2) + elif lname == 'xmax': return self.getMax( 0) + elif lname == 'ymax': return self.getMax( 1) + elif lname == 'zmax': return self.getMax( 2) elif lname == 'all': return self[:] raise AttributeError('{} has no attribute called {}'.format( diff --git a/fsl/tools/fslview.py b/fsl/tools/fslview.py index a7fac4a7965f0be8b19542bebbdcc5a9d6acff02..b1ef7ef3902ee49f3d924502527e48c7e0c13b57 100644 --- a/fsl/tools/fslview.py +++ b/fsl/tools/fslview.py @@ -12,9 +12,9 @@ import argparse import wx import fsl.fslview.orthopanel as orthopanel +import fsl.fslview.lightboxpanel as lightboxpanel import fsl.fslview.locationpanel as locationpanel import fsl.fslview.imagelistpanel as imagelistpanel -import fsl.fslview.lightboxcanvas as lightboxcanvas import fsl.data.fslimage as fslimage import fsl.props as props @@ -92,11 +92,11 @@ class FslViewPanel(wx.Panel): def showLightBox(self): - if isinstance(self.mainPanel, lightboxcanvas.LightBoxPanel): + if isinstance(self.mainPanel, lightboxpanel.LightBoxPanel): return - mainPanel = lightboxcanvas.LightBoxPanel(self, self.imageList, - glContext=self.glContext) + mainPanel = lightboxpanel.LightBoxPanel(self, self.imageList, + glContext=self.glContext) ctrlPanel = props.buildGUI(self, mainPanel.canvas)