浏览代码

Merge branch 'features/scaling,controls'

Merge manual labeling/selection code into scale/control branch.

Manually handle conflicts in canvas.py.
Michael Pitidis 13 年之前
父节点
当前提交
83899de186
共有 3 个文件被更改,包括 114 次插入20 次删除
  1. 82 14
      canvas.py
  2. 26 3
      labelme.py
  3. 6 3
      shape.py

+ 82 - 14
canvas.py

@@ -6,7 +6,10 @@ from PyQt4.QtCore import *
 
 from shape import Shape
 
-class Canvas(QLabel):
+class Canvas(QWidget):
+    zoomRequest = pyqtSignal(int)
+    scrollRequest = pyqtSignal(int, int)
+
     epsilon = 9.0 # TODO: Tune value
 
     def __init__(self, *args, **kwargs):
@@ -17,15 +20,25 @@ class Canvas(QLabel):
         self.selectedShape=None # save the selected shape here
         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."""
-        if self.current and self.startLabeling:
-            if len(self.current) > 1 and self.closeEnough(ev.pos(), self.current[0]):
+        if self.startLabeling and self.current:
+            pos = self.transformPos(ev.posF())
+            # Don't allow the user to draw outside of the pixmap area.
+            # FIXME: Project point to pixmap's edge when getting out too fast.
+            if self.outOfPixmap(pos):
+                return ev.ignore()
+            if len(self.current) > 1 and self.closeEnough(pos, self.current[0]):
+                # Attract line to starting point and colorise to alert the user:
                 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()
 
@@ -39,9 +52,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)
             else: # not in adding new label mode
                 self.selectShape(ev.pos())
@@ -68,16 +82,41 @@ class Canvas(QLabel):
             self.repaint()
 
     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):
@@ -97,6 +136,35 @@ 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 wheelEvent(self, ev):
+        if ev.orientation() == Qt.Vertical:
+            mods = ev.modifiers()
+            if Qt.ControlModifier == int(mods):
+                self.zoomRequest.emit(ev.delta())
+            else:
+                self.scrollRequest.emit(ev.delta(),
+                        Qt.Horizontal if (Qt.ShiftModifier == int(mods))\
+                                      else Qt.Vertical)
+        else:
+            self.scrollRequest.emit(ev.delta(), Qt.Horizontal)
+        ev.accept()
+
+    def keyPressEvent(self, ev):
+        if ev.key() == Qt.Key_Escape and self.current:
+            self.current = None
+            self.setMouseTracking(False)
+            self.repaint()
+
 
 def distance(p):
     return sqrt(p.x() * p.x() + p.y() * p.y())

+ 26 - 3
labelme.py

@@ -17,6 +17,8 @@ from zoomwidget import ZoomWidget
 
 __appname__ = 'labelme'
 
+# TODO:
+# - Zoom is too "steppy".
 
 ### Utility functions and classes.
 
@@ -80,12 +82,18 @@ 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)
+        self.canvas.zoomRequest.connect(self.zoomRequest)
 
         scroll = QScrollArea()
         scroll.setWidget(self.canvas)
         scroll.setWidgetResizable(True)
+        self.scrollBars = {
+            Qt.Vertical: scroll.verticalScrollBar(),
+            Qt.Horizontal: scroll.horizontalScrollBar()
+            }
+        self.canvas.scrollRequest.connect(self.scrollRequest)
 
         self.setCentralWidget(scroll)
         self.addDockWidget(Qt.BottomDockWidgetArea, self.dock)
@@ -159,6 +167,18 @@ class MainWindow(QMainWindow, WindowMixin):
 
 
     ## Callback functions:
+    def scrollRequest(self, delta, orientation):
+        units = - delta / (8 * 15)
+        bar = self.scrollBars[orientation]
+        bar.setValue(bar.value() + bar.singleStep() * units)
+
+    def zoomRequest(self, delta):
+        if not self.fit_window:
+            units = delta / (8 * 15)
+            scale = 10
+            self.zoom_widget.setValue(self.zoom_widget.value() + scale * units)
+            self.zoom_widget.editingFinished.emit()
+
     def setFitWindow(self, value=True):
         self.zoom_widget.setEnabled(not value)
         self.fit_window = value
@@ -193,17 +213,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)
 
@@ -47,11 +50,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)
@@ -65,7 +68,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 containsPoint(self, point):
         path = QPainterPath(QPointF(self.points[0]))