From 20ad715411df37e16e282c04e817ba07f2f18208 Mon Sep 17 00:00:00 2001 From: Mika Fischer Date: Tue, 28 Jun 2011 10:48:38 +0200 Subject: [PATCH] Add new PropertyEditor to replace ButtonArea Basic functionality working, some things are left to do. This also fixes the Layout issues with the ButtonArea --- examples/example1_labels.json | 6 +- sloth/annotations/model.py | 10 +-- sloth/core/labeltool.py | 15 +++- sloth/gui/annotationscene.py | 137 ++++++++++++++++------------------ sloth/gui/floatinglayout.py | 111 ++++++++++++++------------- sloth/gui/labeltool.py | 31 ++++---- sloth/items/factory.py | 3 +- 7 files changed, 163 insertions(+), 150 deletions(-) diff --git a/examples/example1_labels.json b/examples/example1_labels.json index 47c9fb9..9d786ae 100644 --- a/examples/example1_labels.json +++ b/examples/example1_labels.json @@ -7,14 +7,16 @@ "width": 46.0, "y": 105.0, "x": 346.0, - "type": "rect" + "type": "rect", + "class": "Face" }, { "height": 58.0, "width": 56.0, "y": 119.0, "x": 636.0, - "type": "rect" + "type": "rect", + "class": "Face" } ], "filename": "image1.jpg" diff --git a/sloth/annotations/model.py b/sloth/annotations/model.py index 4527f6f..220ea02 100644 --- a/sloth/annotations/model.py +++ b/sloth/annotations/model.py @@ -10,7 +10,7 @@ import time import logging LOG = logging.getLogger(__name__) -ItemRole, TypeRole, DataRole, ImageRole = [Qt.UserRole + i + 1 for i in range(4)] +ItemRole, DataRole, ImageRole = [Qt.UserRole + i + 1 for i in range(3)] class ModelItem: def __init__(self): @@ -44,21 +44,21 @@ class ModelItem: def setData(self, value, role=Qt.DisplayRole, column=0): return False - def getChildAt(self, pos): + def childAt(self, pos): return self._children[pos] def getPreviousSibling(self): p = self.parent() if p is not None: if self._row > 0: - return p.getChildAt(self._row-1) + return p.childAt(self._row-1) return None def getNextSibling(self): p = self.parent() if p is not None: if self._row < len(p.children()) - 1: - return p.getChildAt(self._row+1) + return p.childAt(self._row+1) return None def _attachToModel(self, model): @@ -348,8 +348,6 @@ class AnnotationModelItem(KeyValueModelItem): return self['type'] else: return "" - elif role == TypeRole: - return self['type'] elif role == DataRole: return self._annotation return ModelItem.data(self, role, column) diff --git a/sloth/core/labeltool.py b/sloth/core/labeltool.py index 05c8303..12c9245 100755 --- a/sloth/core/labeltool.py +++ b/sloth/core/labeltool.py @@ -46,7 +46,7 @@ class LabelTool(QObject): # This still emits a QModelIndex, because Qt cannot handle emiting # a derived class instead of a base class, i.e. ImageFileModelItem # instead of ModelItem - currentImageChanged = pyqtSignal(QModelIndex) + currentImageChanged = pyqtSignal() # TODO clean up --> prefix all members with _ def __init__(self, parent=None): @@ -299,7 +299,7 @@ class LabelTool(QObject): raise RuntimeError("Tried to set current image to item that has no Image or Frame as parent!") if image != self._current_image: self._current_image = image - self.currentImageChanged.emit(self._current_image.index()) + self.currentImageChanged.emit() def getImage(self, item): # TODO: Also handle video frames @@ -339,6 +339,15 @@ class LabelTool(QObject): self._model._root.appendFileItem(fileitem) + ### + ### PropertyEditor functions + ###___________________________________________________________________________________________ + def propertyeditor(self): + if self._mainwindow is None: + return None + else: + return self._mainwindow.property_editor + ### ### Scene functions ###___________________________________________________________________________________________ @@ -363,7 +372,7 @@ class LabelTool(QObject): def exitInsertMode(self): if self._mainwindow is not None: - return self._mainwindow.buttonarea.exitInsertMode() + return self._mainwindow.property_editor.endInsertionMode() ### ### TreeView functions diff --git a/sloth/gui/annotationscene.py b/sloth/gui/annotationscene.py index 2e87ff9..e7fb3f4 100644 --- a/sloth/gui/annotationscene.py +++ b/sloth/gui/annotationscene.py @@ -2,42 +2,30 @@ from PyQt4.QtGui import * from PyQt4.QtCore import * from sloth.items import * -from sloth.annotations.model import TypeRole from sloth.core.exceptions import InvalidArgumentException import okapy import logging LOG = logging.getLogger(__name__) class AnnotationScene(QGraphicsScene): - """Dies ist ein Test""" - - # TODO signal itemadded - def __init__(self, labeltool, items=None, inserters=None, parent=None): super(AnnotationScene, self).__init__(parent) - self.model_ = None - self.mode_ = None - self.inserter_ = None - self.debug_ = True - self.message_ = "" - self.last_key_ = None - self.labeltool_ = labeltool + self.model_ = None + self.image_item_ = None + self.inserter_ = None + self.message_ = "" + self.labeltool_ = labeltool self.itemfactory_ = Factory(items) self.inserterfactory_ = Factory(inserters) self.setBackgroundBrush(Qt.darkGray) - - self.setMode(None) self.reset() # # getters/setters #______________________________________________________________________________________________________ - def model(self): - return self.model_ - def setModel(self, model): if model == self.model_: # same model as the current one @@ -66,79 +54,78 @@ class AnnotationScene(QGraphicsScene): # reset caches, invalidate root self.reset() - def root(self): - return self.root_ - - def setRoot(self, root): + def setCurrentImage(self, current_image): """ Set the index of the model which denotes the current image to be displayed by the scene. This can be either the index to a frame in a video, or to an image. """ - self.image_item_ = None - self.image_ = None - self.pixmap_ = None - - self.root_ = root - self.clear() - if not root.isValid(): + if current_image == self.image_item_: return + elif current_image is None: + self.clear() + self.image_item_ = None + self.image_ = None + self.pixmap_ = None + else: + self.clear() + self.image_item_ = current_image + assert self.image_item_.model() == self.model_ + self.image_ = self.labeltool_.getImage(self.image_item_) + self.pixmap_ = QPixmap(okapy.guiqt.toQImage(self.image_)) + item = QGraphicsPixmapItem(self.pixmap_) + item.setZValue(-1) + self.setSceneRect(0, 0, self.pixmap_.width(), self.pixmap_.height()) + self.addItem(item) - assert self.root_.model() == self.model_ - self.image_item_ = self.model_.itemFromIndex(root) - self.image_ = self.labeltool_.getImage(self.image_item_) - self.pixmap_ = QPixmap(okapy.guiqt.toQImage(self.image_)) - item = QGraphicsPixmapItem(self.pixmap_) - item.setZValue(-1) - self.setSceneRect(0, 0, self.pixmap_.width(), self.pixmap_.height()) - self.addItem(item) - - num_items = self.model_.rowCount(self.root_) - self.insertItems(0, num_items) - self.update() + self.insertItems(0, len(self.image_item_.children())-1) + self.update() def insertItems(self, first, last): - if not self.root_.isValid(): + if self.image_item_ is None: return assert self.model_ is not None # create a graphics item for each model index for row in range(first, last+1): - child = self.root_.child(row, 0) # get index - t = child.data(TypeRole) # get type from index - if isinstance(t, QVariant): - t = t.toPyObject() - _type = str(t) - item = self.itemfactory_.create(_type, self.model_.itemFromIndex(child)) # create graphics item from factory + child = self.image_item_.childAt(row) + label_class = child['class'] + item = self.itemfactory_.create(label_class, child) if item is not None: self.addItem(item) + else: + LOG.warn("Could not find item for annotation with class '%s'" % label_class) def onInserterFinished(self): self.sender().inserterFinished.disconnect(self.onInserterFinished) self.inserter_ = None - def setMode(self, mode): - LOG.debug("setMode : %s" % mode) - + def onInsertionModeStarted(self, label_class): # Abort current inserter if self.inserter_ is not None: self.inserter_.abort() + self.deselectAllItems() + # Add new inserter - if mode is not None: - inserter = self.inserterfactory_.create(mode['type'], self.labeltool_, self, mode) - if inserter is None: - raise InvalidArgumentException("Invalid mode") - inserter.inserterFinished.connect(self.onInserterFinished) - self.inserter_ = inserter + default_properties = self.labeltool_.propertyeditor().currentEditorProperties() + inserter = self.inserterfactory_.create(label_class, self.labeltool_, self, default_properties) + if inserter is None: + raise InvalidArgumentException("Invalid mode") + inserter.inserterFinished.connect(self.onInserterFinished) + self.inserter_ = inserter + + def onInsertionModeEnded(self): + if self.inserter_ is not None: + self.inserter_.abort() # # common methods #______________________________________________________________________________________________________ def reset(self): self.clear() - self.setRoot(QModelIndex()) + self.setCurrentImage(None) self.clearMessage() def addItem(self, item): @@ -149,8 +136,7 @@ class AnnotationScene(QGraphicsScene): # mouse event handlers #______________________________________________________________________________________________________ def mousePressEvent(self, event): - if self.debug_: - LOG.debug("mousePressEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos())) + LOG.debug("mousePressEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos())) if self.inserter_ is not None: if not self.sceneRect().contains(event.scenePos()) and \ not self.inserter_.allowOutOfSceneEvents(): @@ -163,8 +149,7 @@ class AnnotationScene(QGraphicsScene): QGraphicsScene.mousePressEvent(self, event) def mouseReleaseEvent(self, event): - if self.debug_: - LOG.debug("mouseReleaseEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos())) + LOG.debug("mouseReleaseEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos())) if self.inserter_ is not None: # insert mode self.inserter_.mouseReleaseEvent(event, self.image_item_) @@ -173,8 +158,7 @@ class AnnotationScene(QGraphicsScene): QGraphicsScene.mouseReleaseEvent(self, event) def mouseMoveEvent(self, event): - #if self.debug_: - # print "mouseMoveEvent", self.sceneRect().contains(event.scenePos()), event.scenePos() + # print "mouseMoveEvent", self.sceneRect().contains(event.scenePos()), event.scenePos() if self.inserter_ is not None: # insert mode self.inserter_.mouseMoveEvent(event, self.image_item_) @@ -182,10 +166,14 @@ class AnnotationScene(QGraphicsScene): # selection mode QGraphicsScene.mouseMoveEvent(self, event) + def deselectAllItems(self): + for item in self.items(): + item.setSelected(False) def onSelectionChanged(self): model_items = [item.modelItem() for item in self.selectedItems()] self.labeltool_.treeview().setSelectedItems(model_items) + self.editSelectedItems() def onSelectionChangedInTreeView(self, items): block = self.blockSignals(True) @@ -196,6 +184,13 @@ class AnnotationScene(QGraphicsScene): if item is not None: item.setSelected(True) self.blockSignals(block) + self.editSelectedItems() + + def editSelectedItems(self): + scene_items = self.selectedItems() + if self.inserter_ is None or len(scene_items) > 0: + items = [item.modelItem() for item in scene_items] + self.labeltool_.propertyeditor().startEditMode(items) # # key event handlers @@ -232,10 +227,9 @@ class AnnotationScene(QGraphicsScene): break def keyPressEvent(self, event): - if self.debug_: - LOG.debug("keyPressEvent %s" % event) + LOG.debug("keyPressEvent %s" % event) - if self.model_ is None or not self.root_.isValid(): + if self.model_ is None or self.image_item_ is None: event.ignore() return @@ -266,12 +260,12 @@ class AnnotationScene(QGraphicsScene): # this is the implemenation of the scene as a view of the model #______________________________________________________________________________________________________ def dataChanged(self, indexFrom, indexTo): - if not self.root_.isValid(): + if self.image_item_ is None: return - annotation_item_index= indexFrom.parent() + annotation_item_index = indexFrom.parent() - if self.root_ != annotation_item_index.parent(): + if self.image_item_.index() != annotation_item_index.parent(): return item = self.itemFromIndex(annotation_item_index) @@ -279,14 +273,13 @@ class AnnotationScene(QGraphicsScene): item.dataChanged() def rowsInserted(self, index, first, last): - if self.root_ != index: + if self.image_item_.index() != index: return self.insertItems(first, last) - def rowsAboutToBeRemoved(self, index, first, last): - if self.root_ != index: + if self.image_item_.index() != index: return for row in range(first, last+1): diff --git a/sloth/gui/floatinglayout.py b/sloth/gui/floatinglayout.py index bd1184d..b80c313 100644 --- a/sloth/gui/floatinglayout.py +++ b/sloth/gui/floatinglayout.py @@ -1,72 +1,35 @@ -from PyQt4.QtGui import * -from PyQt4.QtCore import * +from PyQt4.QtCore import Qt, QRect, QSize, QPoint +from PyQt4.QtGui import QLayout, QSizePolicy class FloatingLayout(QLayout): def __init__(self, parent=None): QLayout.__init__(self, parent) self._items = [] - self._last_min_size = self.minimumSize() + self._updateMinimumSize() - def addItem(self, item): - self._items.append(item) - - def count(self): - return len(self._items) - - def itemAt(self, index): - if index < 0 or index >= len(self._items): - return None - return self._items[index] - - def takeAt(self, index): - if index < 0 or index >= len(self._items): - return None - else: - item = self._items[index] - del self._items[index] - return item - - def sizeHint(self): - return self.minimumSize() - - def setGeometry(self, r): - QLayout.setGeometry(self, r) - self.layoutChildren(r) - min_size = self.minimumSize() - if self._last_min_size != min_size: - self._last_min_size = min_size - self.parentWidget().updateGeometry() - - def minimumSize(self): - w = 0 - h = 0 + def _updateMinimumSize(self, height=None): + w, h = 0, 0 for item in self._items: w = max(w, item.minimumSize().width()) h = max(h, item.minimumSize().height()) left, top, right, bottom = self.getContentsMargins() - current_width = self.contentsRect().width() - left - right - if current_width > 0: - h = self.heightForWidth(current_width) - w += left + right h += top + bottom - return QSize(w, h) + if height is None: + current_width = self.contentsRect().width() + if current_width > 0: + height = self.heightForWidth(current_width + left + right) + if height is not None: + h = max(h, height) - def hasHeightForWidth(self): - return True + self._min_w, self._min_h = w, h - def heightForWidth(self, width): - height = self.layoutChildren(QRect(0, 0, width, 0), False) - left, top, right, bottom = self.getContentsMargins() - return height + top + bottom - - def layoutChildren(self, rect, appl=True): + def _layoutChildren(self, rect, appl=True): left, top, right, bottom = self.getContentsMargins() r = rect.adjusted(+left, +top, -right, -bottom) - x = r.x(); - y = r.y(); + x, y = r.x(), r.y() lineHeight = 0 for item in self._items: @@ -86,4 +49,48 @@ class FloatingLayout(QLayout): x += sz_hint.width() + spaceX lineHeight = max(lineHeight, sz_hint.height()) - return y + lineHeight - r.y() + return y + lineHeight - r.y() + top + bottom + + def heightForWidth(self, width): + return self._layoutChildren(QRect(0, 0, width, 0), False) + + def setGeometry(self, r): + QLayout.setGeometry(self, r) + new_height = self._layoutChildren(r) + if new_height != self._min_h: + self._updateMinimumSize(new_height) + i = 0 + wid = self.parentWidget() + while wid is not None: + wid.updateGeometry() + wid = wid.parentWidget() + i += 1 + + def addItem(self, item): + self._items.append(item) + + def count(self): + return len(self._items) + + def hasHeightForWidth(self): + return True + + def itemAt(self, index): + if index < 0 or index >= len(self._items): + return None + return self._items[index] + + def minimumSize(self): + return QSize(self._min_w, self._min_h) + + def takeAt(self, index): + if index < 0 or index >= len(self._items): + return None + else: + item = self._items[index] + del self._items[index] + return item + + def sizeHint(self): + return self.minimumSize() + diff --git a/sloth/gui/labeltool.py b/sloth/gui/labeltool.py index e5ba132..f13c12d 100755 --- a/sloth/gui/labeltool.py +++ b/sloth/gui/labeltool.py @@ -6,7 +6,6 @@ from PyQt4.QtGui import QMainWindow, QSizePolicy, QWidget, QVBoxLayout, QAction, QKeySequence, QLabel, QItemSelectionModel, QMessageBox, QFileDialog from PyQt4.QtCore import SIGNAL, QSettings, QSize, QPoint, QVariant, QFileInfo import PyQt4.uic as uic -from sloth.gui.buttonarea import ButtonArea from sloth.gui.propertyeditor import PropertyEditor from sloth.gui.annotationscene import AnnotationScene from sloth.gui.frameviewer import GraphicsView @@ -54,10 +53,10 @@ class MainWindow(QMainWindow): self.treeview.setSelectionModel(self.selectionmodel) self.treeview.selectionModel().currentChanged.connect(self.labeltool.setCurrentImage) - def onCurrentImageChanged(self, index): - self.scene.setRoot(index) - + def onCurrentImageChanged(self): new_image = self.labeltool.currentImage() + self.scene.setCurrentImage(new_image) + img = self.labeltool.getImage(new_image) h = img.shape[0] @@ -70,7 +69,7 @@ class MainWindow(QMainWindow): elif isinstance(new_image, ImageFileModelItem): self.controls.setFilename(os.path.basename(new_image['filename'])) - self.selectionmodel.setCurrentIndex(index, QItemSelectionModel.ClearAndSelect|QItemSelectionModel.Rows) + self.selectionmodel.setCurrentIndex(new_image.index(), QItemSelectionModel.ClearAndSelect|QItemSelectionModel.Rows) def onScaleChanged(self, scale): self.zoominfo.setText("%.2f%%" % (100 * scale, )) @@ -103,17 +102,27 @@ class MainWindow(QMainWindow): # get inserters and items from labels # FIXME for handling the new-style config correctly - inserters = dict([(label['attributes']['type'], label['inserter']) + inserters = dict([(label['attributes']['class'], label['inserter']) for label in config.LABELS - if 'type' in label.get('attributes', {}) and 'inserter' in label]) - items = dict([(label['attributes']['type'], label['item']) + if 'class' in label.get('attributes', {}) and 'inserter' in label]) + items = dict([(label['attributes']['class'], label['item']) for label in config.LABELS - if 'type' in label.get('attributes', {}) and 'item' in label]) + if 'class' in label.get('attributes', {}) and 'item' in label]) + # Property Editor + self.property_editor = PropertyEditor(config.LABELS) + self.ui.dockAnnotationButtons.setWidget(self.property_editor) + + # Scene self.scene = AnnotationScene(self.labeltool, items=items, inserters=inserters) + self.property_editor.insertionModeStarted.connect(self.scene.onInsertionModeStarted) + self.property_editor.insertionModeEnded.connect(self.scene.onInsertionModeEnded) + + # SceneView self.view = GraphicsView(self) self.view.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.view.setScene(self.scene) + self.central_widget = QWidget() self.central_layout = QVBoxLayout() self.controls = ControlButtonWidget() @@ -127,10 +136,6 @@ class MainWindow(QMainWindow): self.initShortcuts(config.HOTKEYS) - self.buttonarea = ButtonArea(config.LABELS) - self.ui.dockAnnotationButtons.setWidget(self.buttonarea) - self.buttonarea.stateChanged.connect(self.scene.setMode) - self.treeview = AnnotationTreeView() self.treeview.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) self.ui.dockInformation.setWidget(self.treeview) diff --git a/sloth/items/factory.py b/sloth/items/factory.py index 4c5f530..0043be9 100644 --- a/sloth/items/factory.py +++ b/sloth/items/factory.py @@ -1,4 +1,3 @@ -from sloth.core import exceptions from sloth.core.utils import import_callable class Factory: @@ -77,7 +76,7 @@ class Factory: Newly created object. If for the given type no mapping exists, this function returns ``None``. """ - _type = _type.lower() + _type = str(_type).lower() if _type not in self.items_: return None