Pārlūkot izejas kodu

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 gadi atpakaļ
vecāks
revīzija
29d67ad811
3 mainītis faili ar 72 papildinājumiem un 19 dzēšanām
  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
 
-class Canvas(QLabel):
+class Canvas(QWidget):
     epsilon = 9.0 # TODO: Tune value
 
     def __init__(self, *args, **kwargs):
@@ -15,15 +15,27 @@ class Canvas(QLabel):
         self.current = None
         self.line_color = QColor(0, 0, 255)
         self.line = Shape(line_color=self.line_color)
+        self.scale = 1.0
+        self.pixmap = None
+
+        self.setFocusPolicy(Qt.WheelFocus)
 
     def mouseMoveEvent(self, ev):
         """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 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.line_color = self.current.line_color
             else:
-                self.line[1] = ev.pos()
+                self.line[1] = pos
                 self.line.line_color = self.line_color
             self.repaint()
 
@@ -36,9 +48,10 @@ class Canvas(QLabel):
                     self.finalise()
                 self.repaint()
             else:
+                pos = self.transformPos(ev.posF())
                 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)
 
     def mouseDoubleClickEvent(self, ev):
@@ -49,16 +62,41 @@ class Canvas(QLabel):
             self.finalise()
 
     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:
-            shape.paint(qp)
+            shape.paint(p)
         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):
@@ -77,6 +115,15 @@ class Canvas(QLabel):
         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):
     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.canvas = Canvas()
-        self.canvas.setAlignment(Qt.AlignCenter)
+        #self.canvas.setAlignment(Qt.AlignCenter)
         self.canvas.setContextMenuPolicy(Qt.ActionsContextMenu)
 
         scroll = QScrollArea()
@@ -191,17 +191,20 @@ class MainWindow(QMainWindow, WindowMixin):
         if self.image.isNull():
             return
         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()
 
     def imageSize(self):
         """Calculate the size of the image based on current settings."""
         if self.fit_window:
             width, height = self.centralWidget().width()-2, self.centralWidget().height()-2
+            self.canvas.scale = 1.0
         else: # Follow zoom:
             s = self.zoom_widget.value() / 100.0
             width, height = s * self.image.width(), s * self.image.height()
+            self.canvas.scale = s
         return QSize(width, height)
 
     def closeEvent(self, event):

+ 6 - 3
shape.py

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