Browse Source

Scale and center painter & image

Draw the image in the same painter as the shapes.

Center the image inside the widget and transform the painter so that any
drawing is done relative to the pixmap, and any scaling is applied to both.
Michael Pitidis 13 years ago
parent
commit
29d67ad811
3 changed files with 72 additions and 19 deletions
  1. 60 13
      canvas.py
  2. 6 3
      labelme.py
  3. 6 3
      shape.py

+ 60 - 13
canvas.py

@@ -6,7 +6,7 @@ from PyQt4.QtCore import *
 
 
 from shape import Shape
 from shape import Shape
 
 
-class Canvas(QLabel):
+class Canvas(QWidget):
     epsilon = 9.0 # TODO: Tune value
     epsilon = 9.0 # TODO: Tune value
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -15,15 +15,27 @@ class Canvas(QLabel):
         self.current = None
         self.current = None
         self.line_color = QColor(0, 0, 255)
         self.line_color = QColor(0, 0, 255)
         self.line = Shape(line_color=self.line_color)
         self.line = Shape(line_color=self.line_color)
+        self.scale = 1.0
+        self.pixmap = None
+
+        self.setFocusPolicy(Qt.WheelFocus)
 
 
     def mouseMoveEvent(self, ev):
     def mouseMoveEvent(self, ev):
         """Update line with last point and current coordinates."""
         """Update line with last point and current coordinates."""
+        # Don't allow the user to draw outside the image area.
+        # FIXME: When making fast mouse movements, there is not enough
+        # spatial resolution to leave the cursor at the edge of the
+        # picture. We should probably place the line at the projected
+        # position here...
+        pos = self.transformPos(ev.posF())
+        if self.outOfPixmap(pos):
+            return ev.ignore()
         if self.current:
         if self.current:
-            if len(self.current) > 1 and self.closeEnough(ev.pos(), self.current[0]):
+            if len(self.current) > 1 and self.closeEnough(pos, self.current[0]):
                 self.line[1] = self.current[0]
                 self.line[1] = self.current[0]
                 self.line.line_color = self.current.line_color
                 self.line.line_color = self.current.line_color
             else:
             else:
-                self.line[1] = ev.pos()
+                self.line[1] = pos
                 self.line.line_color = self.line_color
                 self.line.line_color = self.line_color
             self.repaint()
             self.repaint()
 
 
@@ -36,9 +48,10 @@ class Canvas(QLabel):
                     self.finalise()
                     self.finalise()
                 self.repaint()
                 self.repaint()
             else:
             else:
+                pos = self.transformPos(ev.posF())
                 self.current = Shape()
                 self.current = Shape()
-                self.line.points = [ev.pos(), ev.pos()]
-                self.current.addPoint(ev.pos())
+                self.line.points = [pos, pos]
+                self.current.addPoint(pos)
                 self.setMouseTracking(True)
                 self.setMouseTracking(True)
 
 
     def mouseDoubleClickEvent(self, ev):
     def mouseDoubleClickEvent(self, ev):
@@ -49,16 +62,41 @@ class Canvas(QLabel):
             self.finalise()
             self.finalise()
 
 
     def paintEvent(self, event):
     def paintEvent(self, event):
-        super(Canvas, self).paintEvent(event)
-        qp = QPainter()
-        qp.begin(self)
-        qp.setRenderHint(QPainter.Antialiasing)
+        if not self.pixmap:
+            return super(Canvas, self).paintEvent(event)
+
+        p = QPainter()
+        p.begin(self)
+        p.setRenderHint(QPainter.Antialiasing)
+
+        p.scale(self.scale, self.scale)
+        p.translate(self.offsetToCenter())
+
+        p.drawPixmap(0, 0, self.pixmap)
         for shape in self.shapes:
         for shape in self.shapes:
-            shape.paint(qp)
+            shape.paint(p)
         if self.current:
         if self.current:
-            self.current.paint(qp)
-            self.line.paint(qp)
-        qp.end()
+            self.current.paint(p)
+            self.line.paint(p)
+
+        p.end()
+
+    def transformPos(self, point):
+        """Convert from widget-logical coordinates to painter-logical coordinates."""
+        return point / self.scale - self.offsetToCenter()
+
+    def offsetToCenter(self):
+        s = self.scale
+        area = super(Canvas, self).size()
+        w, h = self.pixmap.width() * s, self.pixmap.height() * s
+        aw, ah = area.width(), area.height()
+        x = (aw-w)/(2*s) if aw > w else 0
+        y = (ah-h)/(2*s) if ah > h else 0
+        return QPointF(x, y)
+
+    def outOfPixmap(self, p):
+        w, h = self.pixmap.width(), self.pixmap.height()
+        return not (0 <= p.x() <= w and 0 <= p.y() <= h)
 
 
 
 
     def finalise(self):
     def finalise(self):
@@ -77,6 +115,15 @@ class Canvas(QLabel):
         return distance(p1 - p2) < self.epsilon
         return distance(p1 - p2) < self.epsilon
 
 
 
 
+    # These two, along with a call to adjustSize are required for the
+    # scroll area.
+    def sizeHint(self):
+        return self.minimumSizeHint()
+
+    def minimumSizeHint(self):
+        if self.pixmap:
+            return self.scale * self.pixmap.size()
+        return super(Canvas, self).minimumSizeHint()
 
 
 def distance(p):
 def distance(p):
     return sqrt(p.x() * p.x() + p.y() * p.y())
     return sqrt(p.x() * p.x() + p.y() * p.y())

+ 6 - 3
labelme.py

@@ -80,7 +80,7 @@ class MainWindow(QMainWindow, WindowMixin):
         #self.dock.setFeatures(QDockWidget.DockWidgetMovable|QDockWidget.DockWidgetFloatable)
         #self.dock.setFeatures(QDockWidget.DockWidgetMovable|QDockWidget.DockWidgetFloatable)
 
 
         self.canvas = Canvas()
         self.canvas = Canvas()
-        self.canvas.setAlignment(Qt.AlignCenter)
+        #self.canvas.setAlignment(Qt.AlignCenter)
         self.canvas.setContextMenuPolicy(Qt.ActionsContextMenu)
         self.canvas.setContextMenuPolicy(Qt.ActionsContextMenu)
 
 
         scroll = QScrollArea()
         scroll = QScrollArea()
@@ -191,17 +191,20 @@ class MainWindow(QMainWindow, WindowMixin):
         if self.image.isNull():
         if self.image.isNull():
             return
             return
         size = self.imageSize()
         size = self.imageSize()
-        self.canvas.setPixmap(QPixmap.fromImage(self.image.scaled(
-                size, Qt.KeepAspectRatio, Qt.SmoothTransformation)))
+        self.canvas.pixmap = QPixmap.fromImage(self.image)
+        self.canvas.adjustSize()
+        self.canvas.repaint()
         self.canvas.show()
         self.canvas.show()
 
 
     def imageSize(self):
     def imageSize(self):
         """Calculate the size of the image based on current settings."""
         """Calculate the size of the image based on current settings."""
         if self.fit_window:
         if self.fit_window:
             width, height = self.centralWidget().width()-2, self.centralWidget().height()-2
             width, height = self.centralWidget().width()-2, self.centralWidget().height()-2
+            self.canvas.scale = 1.0
         else: # Follow zoom:
         else: # Follow zoom:
             s = self.zoom_widget.value() / 100.0
             s = self.zoom_widget.value() / 100.0
             width, height = s * self.image.width(), s * self.image.height()
             width, height = s * self.image.width(), s * self.image.height()
+            self.canvas.scale = s
         return QSize(width, height)
         return QSize(width, height)
 
 
     def closeEvent(self, event):
     def closeEvent(self, event):

+ 6 - 3
shape.py

@@ -4,6 +4,9 @@
 from PyQt4.QtGui import *
 from PyQt4.QtGui import *
 from PyQt4.QtCore import *
 from PyQt4.QtCore import *
 
 
+# FIXME:
+# - Don't scale vertices.
+
 class Shape(object):
 class Shape(object):
     P_SQUARE, P_ROUND = range(2)
     P_SQUARE, P_ROUND = range(2)
 
 
@@ -45,11 +48,11 @@ class Shape(object):
             line_path = QPainterPath()
             line_path = QPainterPath()
             vrtx_path = QPainterPath()
             vrtx_path = QPainterPath()
 
 
-            line_path.moveTo(QPointF(self.points[0]))
+            line_path.moveTo(self.points[0])
             self.drawVertex(vrtx_path, self.points[0])
             self.drawVertex(vrtx_path, self.points[0])
 
 
             for p in self.points[1:]:
             for p in self.points[1:]:
-                line_path.lineTo(QPointF(p))
+                line_path.lineTo(p)
                 # Skip last element, otherwise its vertex is not filled.
                 # Skip last element, otherwise its vertex is not filled.
                 if p != self.points[0]:
                 if p != self.points[0]:
                     self.drawVertex(vrtx_path, p)
                     self.drawVertex(vrtx_path, p)
@@ -63,7 +66,7 @@ class Shape(object):
         if self.point_type == self.P_SQUARE:
         if self.point_type == self.P_SQUARE:
             path.addRect(point.x() - d/2, point.y() - d/2, d, d)
             path.addRect(point.x() - d/2, point.y() - d/2, d, d)
         else:
         else:
-            path.addEllipse(QPointF(point), d/2.0, d/2.0)
+            path.addEllipse(point, d/2.0, d/2.0)
 
 
     def __len__(self):
     def __len__(self):
         return len(self.points)
         return len(self.points)