Skip to content

Commit

Permalink
Fix the wrong rotation of the image views, #70 . Fixes labeling on x,y.
Browse files Browse the repository at this point in the history
To do this, introduce 'data coordinates'. These are the t,x,y,z,c
coordinates. In imageView2D, we use these data coordinates and know
how to convert back and forth from scene coordinates using a QTransform.

This also fixes the labeling on the x and y slices.

To close #70, the coordinate systems need to be formally defined,
documented and the code in slicingpositionmarker adapted. Furhtermore,
it would be good if we can get different scene transformations to work.
  • Loading branch information
Thorben Kroeger committed Aug 22, 2011
1 parent 3e2c7d9 commit 3ac0d70
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 83 deletions.
7 changes: 2 additions & 5 deletions volumeeditor/brushingcontroler.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,13 @@ def __init__(self, brushingModel, positionModel, dataSink):
self._positionModel = positionModel

def _writeIntoSink(self, brushStrokeOffset, labels):
#print "BrushingControler._writeIntoSink(%r, %r)" % (brushStrokeOffset, brushStrokeImage)

activeView = self._positionModel.activeView
slicingPos = self._positionModel.slicingPos
t, c = self._positionModel.time, self._positionModel.channel

slicing = [slice(brushStrokeOffset.y(), brushStrokeOffset.y()+labels.shape[0]), \
slice(brushStrokeOffset.x(), brushStrokeOffset.x()+labels.shape[1])]
slicing = [slice(brushStrokeOffset.x(), brushStrokeOffset.x()+labels.shape[0]), \
slice(brushStrokeOffset.y(), brushStrokeOffset.y()+labels.shape[1])]
slicing.insert(activeView, slicingPos[activeView])

slicing = (t,) + tuple(slicing) + (c,)

self._dataSink.put(slicing, labels)
Expand Down
24 changes: 12 additions & 12 deletions volumeeditor/brushingmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class BrushingModel(QObject):

def __init__(self):
QObject.__init__(self)
self.shape = None
self.sliceRect = None
self.bb = QRect() #bounding box enclosing the drawing
self.brushSize = self.defaultBrushSize
self.drawColor = self.defaultColor
Expand Down Expand Up @@ -116,9 +116,8 @@ def setBrushColor(self, color):
self.emit.brushColorChanged(self.drawColor)


def beginDrawing(self, pos, shape):
print "BrushingModel.beginDrawing(pos=%r, shape=%r)" % (pos, shape)
self.shape = shape
def beginDrawing(self, pos, sliceRect):
self.sliceRect = sliceRect
self.scene.clear()
self.bb = QRect()
if self.erasing:
Expand All @@ -130,24 +129,25 @@ def beginDrawing(self, pos, shape):
return line

def endDrawing(self, pos):
print "BrushingModel.endDrawing(pos=%r)" % (pos)

self.moveTo(pos)

tempi = QImage(QSize(self.bb.width(), self.bb.height()), QImage.Format_ARGB32_Premultiplied) #TODO: format
tempi.fill(0)
painter = QPainter(tempi)
self.scene.render(painter, target=QRectF(), source=QRectF(QPointF(self.bb.x(), self.bb.y()), QSizeF(self.bb.width(), self.bb.height())))
painter.end()

ndarr = qimage2ndarray.rgb_view(tempi)[:,:,0]

labels = numpy.where(ndarr>0,numpy.uint8(self.drawnNumber),numpy.uint8(0))
labels = labels.swapaxes(0,1)
assert labels.shape[0] == self.bb.width()
assert labels.shape[1] == self.bb.height()

self.brushStrokeAvailable.emit(QPointF(self.bb.x(), self.bb.y()), labels)

def dumpDraw(self, pos):
res = self.endDrawing(pos)
self.beginDrawing(pos, self.shape)
self.beginDrawing(pos, self.sliceRect)
return res

def moveTo(self, pos):
Expand All @@ -163,10 +163,10 @@ def moveTo(self, pos):
if not self.bb.isValid():
self.bb = QRect(QPoint(x,y), QSize(1,1))
#grow bounding box
self.bb.setLeft( min(self.bb.left(), max(0, x-self.brushSize/2-1) ) )
self.bb.setRight( max(self.bb.right(), min(self.shape[0], x+self.brushSize/2+1) ) )
self.bb.setTop( min(self.bb.top(), max(0, y-self.brushSize/2-1) ) )
self.bb.setBottom(max(self.bb.bottom(), min(self.shape[1], y+self.brushSize/2+1) ) )
self.bb.setLeft( min(self.bb.left(), max(0, x-self.brushSize/2-1) ) )
self.bb.setRight( max(self.bb.right(), min(self.sliceRect[0], x+self.brushSize/2+1) ) )
self.bb.setTop( min(self.bb.top(), max(0, y-self.brushSize/2-1) ) )
self.bb.setBottom(max(self.bb.bottom(), min(self.sliceRect[1], y+self.brushSize/2+1) ) )

#update/move position
self.pos = pos
56 changes: 44 additions & 12 deletions volumeeditor/imageScene2D.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
# or implied, of their employers.

from functools import partial
from PyQt4.QtCore import QRect, QRectF, QTimer, pyqtSignal, QMutex
from PyQt4.QtGui import QGraphicsScene, QImage
from PyQt4.QtCore import QRect, QRectF, QTimer, pyqtSignal, QMutex, QPointF
from PyQt4.QtGui import QGraphicsScene, QImage, QTransform, QPen, QColor, QBrush
from PyQt4.QtOpenGL import QGLWidget
from OpenGL.GL import GL_CLAMP_TO_EDGE, GL_COLOR_BUFFER_BIT, GL_DEPTH_TEST, \
GL_NEAREST, GL_QUADS, GL_TEXTURE_2D, \
Expand Down Expand Up @@ -141,17 +141,38 @@ def stackedImageSources(self, s):
s.stackChanged.connect(partial(self._invalidateRect, QRect()))

@property
def shape(self):
def sceneShape(self):
"""
The shape of the scene in QGraphicsView's coordinate system.
"""
return (self.sceneRect().width(), self.sceneRect().height())
@shape.setter
def shape(self, shape2D):
assert len(shape2D) == 2
self.setSceneRect(0,0, *shape2D)
@sceneShape.setter
def sceneShape(self, sceneShape):
"""
Set the size of the scene in QGraphicsView's coordinate system.
sceneShape -- (widthX, widthY),
where the origin of the coordinate system is in the upper left corner
of the screen and 'x' points right and 'y' points down
"""

assert len(sceneShape) == 2
self.setSceneRect(0,0, *sceneShape)
self.addRect(QRectF(0,0,*sceneShape), pen=QPen(QColor(255,0,0)))

#The scene shape is in Qt's QGraphicsScene coordinate system,
#that is the origin is in the top left of the screen, and the
#'x' axis points to the right and the 'y' axis down.

#The coordinate system of the data handles things differently.
#The x axis points down and the y axis points to the right.

r = self.scene2data.mapRect(QRect(0,0,sceneShape[0], sceneShape[1]))
sliceShape = (r.width(), r.height())

del self._renderThread
del self.imagePatches

self._patchAccessor = PatchAccessor(self.shape[1], self.shape[0], blockSize=self.blockSize)
self._patchAccessor = PatchAccessor(sliceShape[0], sliceShape[1], blockSize=self.blockSize)
self.imagePatches = [[] for i in range(self._patchAccessor.patchCount)]

self._renderThread = ImageSceneRenderThread(self.imagePatches, self.stackedImageSources, parent=self)
Expand All @@ -172,6 +193,10 @@ def __init__( self ):
self._stackedImageSources = None
self._numLayers = 0 #current number of 'layers'

self.data2scene = QTransform(0,1,1,0,0,0)

self.scene2data = self.data2scene.transposed()

def cleanup():
self._renderThread.stop()
self.destroyed.connect(cleanup)
Expand All @@ -190,18 +215,23 @@ def deactivateOpenGL( self ):
self._glWidget = None

def _initializePatches(self):
if self.stackedImageSources is None or self.shape == (0.0, 0.0):
if self.stackedImageSources is None or self.sceneShape == (0.0, 0.0):
return

if len(self.stackedImageSources) != self._numLayers:
self._numLayers = len(self.stackedImageSources)
#add an additional layer for the final composited image patch
for i in range(self._patchAccessor.patchCount):
r = self._patchAccessor.patchRectF(i, self.overlap)
patches = [ImagePatch(r) for j in range(self._numLayers+1)]
rect = self._patchAccessor.patchRectF(i, self.overlap)
sceneRect = self.data2scene.mapRect(rect)
#the patch accessor uses the data coordinate system
#
#because the patch is drawn on the screen, its holds coordinates
#corresponding to Qt's QGraphicsScene's system
#convert to scene coordinates
patches = [ImagePatch(sceneRect) for j in range(self._numLayers+1)]
self.imagePatches[i] = patches


def _invalidateRect(self, rect = QRect()):
if not rect.isValid():
#everything is invalidated
Expand Down Expand Up @@ -235,6 +265,8 @@ def drawBackgroundSoftware(self, painter, rect):
painter.drawImage(patch.rectF.topLeft(), patch.image)
patch.mutex.unlock()
drawnTiles +=1
r = self.scene2data.mapRect(QRect(0,0,self.sceneShape[0], self.sceneShape[1]))
sliceShape = (r.width(), r.height())
#print "ImageView2D.drawBackgroundSoftware: drew %d of %d tiles" % (drawnTiles, len(self.imagePatches))

def drawBackgroundGL(self, painter, rect):
Expand Down
62 changes: 30 additions & 32 deletions volumeeditor/imageView2D.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,33 +59,35 @@ class ImageView2D(QGraphicsView):
#that is requested
changeSliceDelta = pyqtSignal(int)

#all the following signals refer to data coordinates
drawing = pyqtSignal(QPointF)
beginDraw = pyqtSignal(QPointF, object)
endDraw = pyqtSignal(QPointF)

erasingToggled = pyqtSignal(bool)

#notifies that the mouse has moved to 2D coordinate x,y
#notifies that the mouse has moved to 2D data coordinate x,y
mouseMoved = pyqtSignal(int, int)
#notifies that the user has double clicked on the 2D coordinate x,y
#notifies that the user has double clicked on the 2D data coordinate x,y
mouseDoubleClicked = pyqtSignal(int, int)

drawUpdateInterval = 300 #ms

@property
def shape(self):
def sliceShape(self):
"""
(width, height) of the scene.
Specifying the shape is necessary to allow for correct
scrollbars
"""
return self._shape
@shape.setter
def shape(self, s):
self._shape = s
self.scene().shape = s
self._crossHairCursor.shape = (s[1], s[0])
self._sliceIntersectionMarker.shape = (s[1], s[0])
return self._sliceShape
@sliceShape.setter
def sliceShape(self, s):
self._sliceShape = s
sceneShape = (s[1], s[0])
self.scene().sceneShape = sceneShape
self._crossHairCursor.shape = sceneShape
self._sliceIntersectionMarker.shape = sceneShape

#FIXME unused?
@property
Expand Down Expand Up @@ -133,7 +135,7 @@ def __init__(self, imagescene2d, useGL=False):
self.setScene(imagescene2d)

#these attributes are exposed as public properties above
self._shape = None #2D shape of this view's shown image
self._sliceShape = None #2D shape of this view's shown image
self._slices = None #number of slices that are stacked
self._name = ''
self._hud = None
Expand Down Expand Up @@ -221,15 +223,6 @@ def __init__(self, imagescene2d, useGL=False):

self._tempErase = False

def swapAxes(self):
"""
Displays this image as if the x and y axes were swapped.
"""
#FIXME: This is needed for the current arrangements of the three
# 3D slice views. Can this be made more elegant
self.rotate(90.0)
self.scale(1.0,-1.0)

def _cleanUp(self):
self._ticker.stop()
self._drawTimer.stop()
Expand Down Expand Up @@ -262,6 +255,9 @@ def saveSlice(self, filename):
result_image = result_image.transformed(transform)
result_image.save(QString(filename))

def mapScene2Data(self, pos):
return self.scene().scene2data.map(pos)

def notifyDrawing(self):
print "ImageView2D.notifyDrawing"
#FIXME: resurrect
Expand All @@ -270,7 +266,7 @@ def notifyDrawing(self):
def beginDrawing(self, pos):
self.mousePos = pos
self._isDrawing = True
self.beginDraw.emit(pos, self.shape)
self.beginDraw.emit(pos, self.sliceShape)

def endDrawing(self, pos):
InteractionLogger.log("%f: endDrawing()" % (time.clock()))
Expand All @@ -283,7 +279,9 @@ def wheelEvent(self, event):
k_alt = (keys == Qt.AltModifier)
k_ctrl = (keys == Qt.ControlModifier)

self.mousePos = self.mapToScene(event.pos())
self.mousePos = self.mapScene2Data(self.mapToScene(event.pos()))

sceneMousePos = self.mapToScene(event.pos())
grviewCenter = self.mapToScene(self.viewport().rect().center())

if event.delta() > 0:
Expand All @@ -304,7 +302,7 @@ def wheelEvent(self, event):
self.changeSlice(-1)
if k_ctrl:
mousePosAfterScale = self.mapToScene(event.pos())
offset = self.mousePos - mousePosAfterScale
offset = sceneMousePos - mousePosAfterScale
newGrviewCenter = grviewCenter + offset
self.centerOn(newGrviewCenter)
self.mouseMoveEvent(event)
Expand Down Expand Up @@ -336,8 +334,8 @@ def mousePressEvent(self, event):
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
self.erasingToggled.emit(True)
self._tempErase = True
mousePos = self.mapToScene(event.pos())
self.beginDrawing(mousePos)
self.mousePos = self.mapScene2Data(self.mapToScene(event.pos()))
self.beginDrawing(self.mousePos)

def mouseMoveEvent(self,event):
if self._dragMode == True:
Expand All @@ -352,25 +350,25 @@ def mouseMoveEvent(self,event):
#do nothing until it comes to a complete stop
return

self.mousePos = mousePos = self.mapToScene(event.pos())
self.mousePos = mousePos = self.mapScene2Data(self.mapToScene(event.pos()))
x = self.x = mousePos.x()
y = self.y = mousePos.y()

self.mouseMoved.emit(x,y)

if self._isDrawing:
self.drawing.emit(mousePos)

def mouseReleaseEvent(self, event):
self.mousePos = self.mapScene2Data(self.mapToScene(event.pos()))

if event.button() == Qt.MidButton:
self.setCursor(QCursor())
releasePoint = event.pos()
self._lastPanPoint = releasePoint
self._dragMode = False
self._ticker.start(20)
if self._isDrawing:
mousePos = self.mapToScene(event.pos())
self.endDrawing(mousePos)
self.endDrawing(self.mousePos)
if self._tempErase:
self.erasingToggled.emit(False)
self._tempErase = False
Expand Down Expand Up @@ -432,10 +430,10 @@ def _tickerEvent(self):
self._panning()

def mouseDoubleClickEvent(self, event):
mousePos = self.mapToScene(event.pos())
self.mouseDoubleClicked.emit(mousePos.x(), mousePos.y())
self.mousePos = self.mapScene2Data(self.mapToScene(event.pos()))
self.mouseDoubleClicked.emit(self.mousePos.x(), self.mousePos.y())

def changeSlice(self, delta):
def changeSlice(self, delta):
if self._isDrawing:
self.endDrawing(self.mousePos)
self._isDrawing = True
Expand Down
12 changes: 2 additions & 10 deletions volumeeditor/navigationControler.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,8 @@ def positionCursor(self, x, y, axis):
#set this view as active
self._model.activeView = axis

newPos = copy.copy(self._model.cursorPos)
if axis == 0:
newPos[1] = y
newPos[2] = x
if axis == 1:
newPos[0] = y
newPos[2] = x
if axis == 2:
newPos[0] = y
newPos[1] = x
newPos = [x,y]
newPos.insert(axis, self._model.cursorPos[axis])

if newPos == self._model.cursorPos:
return
Expand Down
2 changes: 1 addition & 1 deletion volumeeditor/volumeEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def __init__( self, shape, layerStackModel, labelsink=None, useGL = False):
def _initConnects(self):
for i, v in enumerate(self.imageViews):
#connect interpreter
v.shape = self.posModel.sliceShape(axis=i)
v.sliceShape = self.posModel.sliceShape(axis=i)
v.mouseMoved.connect(partial(self.navInterpret.positionCursor, axis=i))
v.mouseDoubleClicked.connect(partial(self.navInterpret.positionSlice, axis=i))
v.changeSliceDelta.connect(partial(self.navInterpret.changeSliceRelative, axis=i))
Expand Down
Loading

0 comments on commit 3ac0d70

Please sign in to comment.