From 3358d6d579b6df171ee438b815ab3e5853bb7197 Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 19:05:39 +0200 Subject: [PATCH 1/9] Add the missing PolygonItem Add the functionality to add labels of polygon shape. That means: - Fix the non-working PolygonItemInserter class - Add the missing PolygonItem class - Add the "polygon" class to the default configuration --- sloth/conf/default_config.py | 9 +++++ sloth/items/inserters.py | 52 +++++++++++++++++++------ sloth/items/items.py | 74 ++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 11 deletions(-) diff --git a/sloth/conf/default_config.py b/sloth/conf/default_config.py index 52d939f..45b999c 100644 --- a/sloth/conf/default_config.py +++ b/sloth/conf/default_config.py @@ -61,6 +61,15 @@ LABELS = ( 'hotkey': 'p', 'text': 'Point', }, + { + 'attributes': { + 'class': 'polygon', + }, + 'inserter': 'sloth.items.PolygonItemInserter', + 'item': 'sloth.items.PolygonItem', + 'hotkey': 'o', + 'text': 'Polygon', + }, ) # HOTKEYS diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index 93fbd9a..35e0bc4 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -321,31 +321,61 @@ class NPointFaceInserter(SequenceItemInserter): "Now at: " + self.inserters[self._state][2]) -# TODO class PolygonItemInserter(ItemInserter): - def __init__(self, scene, mode=None): - ItemInserter.__init__(self, scene, mode) - self._current_item = None + def __init__(self, labeltool, scene, default_properties=None, + prefix="", commit=True): + ItemInserter.__init__(self, labeltool, scene, default_properties, + prefix, commit) + self._item = None def mousePressEvent(self, event, image_item): pos = event.scenePos() - if self._current_item is None: + if self._item is None: item = QGraphicsPolygonItem(QPolygonF([pos])) - self._current_item = item + self._item = item + self._item.setPen(self.pen()) self._scene.addItem(item) + self._current_image_item = image_item else: - polygon = self._current_item.polygon() + polygon = self._item.polygon() polygon.append(pos) - self._current_item.setPolygon(polygon) + self._item.setPolygon(polygon) event.accept() def mouseMoveEvent(self, event, image_item): - if self._current_item is not None: + if self._item is not None: pos = event.scenePos() - polygon = self._current_item.polygon() + polygon = self._item.polygon() assert polygon.size() > 0 polygon[-1] = pos - self._current_item.setPolygon(polygon) + self._item.setPolygon(polygon) event.accept() + + def abort(self): + # XXX Is it abuse to handle the end of inserting here in abort()? + if self._item is not None: + polygon = self._item.polygon() + assert polygon.size() > 0 + + # The last point of the polygon is the point the user would add + # to the polygon when pressing the mouse button. At this point, + # we want to throw it away. + polygon.remove(polygon.size()-1) + assert polygon.size() > 0 + + self._ann.update({self._prefix + 'xn': + ";".join([str(p.x()) for p in polygon]), + self._prefix + 'yn': + ";".join([str(p.y()) for p in polygon])}) + self._ann.update(self._default_properties) + if self._commit: + self._current_image_item.addAnnotation(self._ann) + self._scene.removeItem(self._item) + self.annotationFinished.emit() + self._init_pos = None + self._item = None + self._current_image_item = None + + self.inserterFinished.emit() diff --git a/sloth/items/items.py b/sloth/items/items.py index 64fcbf0..d6479ae 100644 --- a/sloth/items/items.py +++ b/sloth/items/items.py @@ -757,3 +757,77 @@ class NPointFaceItem(GroupItem): pen.setStyle(Qt.DashLine) painter.setPen(pen) painter.drawRect(self.boundingRect()) + +class PolygonItem(BaseItem): + # XXX prefix="pointlist" + def __init__(self, model_item=None, prefix="pointlist", parent=None): + BaseItem.__init__(self, model_item, prefix, parent) + + # Make it non-movable for now + self.setFlags(QGraphicsItem.ItemIsSelectable | + QGraphicsItem.ItemSendsGeometryChanges | + QGraphicsItem.ItemSendsScenePositionChanges) + self._polygon = None + + self._updatePolygon(self._dataToPolygon(self._model_item)) + LOG.debug("Constructed polygon %s for model item %s" % + (self._polygon, model_item)) + + def __call__(self, model_item=None, parent=None): + item = PolygonItem(model_item, parent) + item.setPen(self.pen()) + item.setBrush(self.brush()) + return item + + def _dataToPolygon(self, model_item): + if model_item is None: + return QPolygonF() + + try: + polygon = QPolygonF() + xn = [float(x) for x in model_item["xn"].split(";")] + yn = [float(y) for y in model_item["yn"].split(";")] + for x, y in zip(xn, yn): + polygon.append(QPointF(x, y)) + + return polygon + + except KeyError as e: + LOG.debug("PolygonItem: Could not find expected key in item: " + + str(e) + ". Check your config!") + self.setValid(False) + return QPolygonF() + + def _updatePolygon(self, polygon): + if polygon == self._polygon: + return + + self.prepareGeometryChange() + self._polygon = polygon + self.setPos(QPointF(0, 0)) + + def boundingRect(self): + xn = [p.x() for p in self._polygon] + yn = [p.y() for p in self._polygon] + xmin = min(xn) + xmax = max(xn) + ymin = min(yn) + ymax = max(yn) + return QRectF(xmin, ymin, xmax - xmin, ymax - ymin) + + def paint(self, painter, option, widget=None): + BaseItem.paint(self, painter, option, widget) + + pen = self.pen() + if self.isSelected(): + pen.setStyle(Qt.DashLine) + painter.setPen(pen) + + for k in range(-1, len(self._polygon)-1): + p1 = self._polygon[k] + p2 = self._polygon[k+1] + painter.drawLine(p1, p2) + + def dataChange(self): + polygon = self._dataToPolygon(self._model_item) + self._updatePolygon(polygon) From e0fa0534d5f039428f44dad76ae29e0121fdb07c Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 19:30:44 +0200 Subject: [PATCH 2/9] Use the Enter key to finish the polygon Using abort() (aka Escape) is not the right way to finish the polygon. Use the Enter key instead. It works this way in Inkscape. Display a message to instruct the user to use the Enter key to finish the polygon. --- sloth/items/inserters.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index 35e0bc4..523c9dd 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -336,6 +336,8 @@ class PolygonItemInserter(ItemInserter): self._item.setPen(self.pen()) self._scene.addItem(item) self._current_image_item = image_item + + self._scene.setMessage("Press Enter to finish the polygon.") else: polygon = self._item.polygon() polygon.append(pos) @@ -353,9 +355,11 @@ class PolygonItemInserter(ItemInserter): event.accept() - def abort(self): - # XXX Is it abuse to handle the end of inserting here in abort()? - if self._item is not None: + def keyPressEvent(self, event, image_item): + """ + When the user presses Enter, the polygon is finished. + """ + if event.key() == Qt.Key_Return and self._item is not None: polygon = self._item.polygon() assert polygon.size() > 0 @@ -377,5 +381,6 @@ class PolygonItemInserter(ItemInserter): self._init_pos = None self._item = None self._current_image_item = None + self._scene.clearMessage() self.inserterFinished.emit() From 2579c4f7b0911271e20c3f808ef5f758c4fec6a5 Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 21:37:51 +0200 Subject: [PATCH 3/9] Fix the start point of the polygon --- sloth/items/inserters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index 523c9dd..ecc5a3b 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -338,10 +338,10 @@ class PolygonItemInserter(ItemInserter): self._current_image_item = image_item self._scene.setMessage("Press Enter to finish the polygon.") - else: - polygon = self._item.polygon() - polygon.append(pos) - self._item.setPolygon(polygon) + + polygon = self._item.polygon() + polygon.append(pos) + self._item.setPolygon(polygon) event.accept() From 32a4bbce76b934e3361ef6778a4b7cf335875e66 Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 21:40:43 +0200 Subject: [PATCH 4/9] No need for _current_image_item anymore The insertion does not happen in abort() anymore, so PolygonItemInserter does not need to save the current image item anymore for later use. --- sloth/items/inserters.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index ecc5a3b..9616185 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -330,12 +330,12 @@ class PolygonItemInserter(ItemInserter): def mousePressEvent(self, event, image_item): pos = event.scenePos() + if self._item is None: item = QGraphicsPolygonItem(QPolygonF([pos])) self._item = item self._item.setPen(self.pen()) self._scene.addItem(item) - self._current_image_item = image_item self._scene.setMessage("Press Enter to finish the polygon.") @@ -375,12 +375,11 @@ class PolygonItemInserter(ItemInserter): ";".join([str(p.y()) for p in polygon])}) self._ann.update(self._default_properties) if self._commit: - self._current_image_item.addAnnotation(self._ann) + image_item.addAnnotation(self._ann) self._scene.removeItem(self._item) self.annotationFinished.emit() self._init_pos = None self._item = None - self._current_image_item = None self._scene.clearMessage() self.inserterFinished.emit() From 8fbb44218d05ccbfd50bf5ac08b8fe89e602e84b Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 21:41:45 +0200 Subject: [PATCH 5/9] Remove unused variable _init_pos --- sloth/items/inserters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index 9616185..6307707 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -378,7 +378,6 @@ class PolygonItemInserter(ItemInserter): image_item.addAnnotation(self._ann) self._scene.removeItem(self._item) self.annotationFinished.emit() - self._init_pos = None self._item = None self._scene.clearMessage() From e2d61febc3bea1ff2919e565a5073bc9f8916d55 Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 21:46:34 +0200 Subject: [PATCH 6/9] Allow aborting when inserting a polygon --- sloth/items/inserters.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index 6307707..5ef7188 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -382,3 +382,10 @@ class PolygonItemInserter(ItemInserter): self._scene.clearMessage() self.inserterFinished.emit() + + def abort(self): + if self._item is not None: + self._scene.removeItem(self._item) + self._item = None + self._scene.clearMessage() + ItemInserter.abort(self) From e810cd09afba116c1468f92242a87df53f4f6385 Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 23:14:10 +0200 Subject: [PATCH 7/9] Allow finishing a polygon using a double click --- sloth/gui/annotationscene.py | 13 +++++++++++ sloth/items/inserters.py | 44 ++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/sloth/gui/annotationscene.py b/sloth/gui/annotationscene.py index f7ab14d..69fc47d 100644 --- a/sloth/gui/annotationscene.py +++ b/sloth/gui/annotationscene.py @@ -185,6 +185,19 @@ class AnnotationScene(QGraphicsScene): # selection mode QGraphicsScene.mousePressEvent(self, event) + def mouseDoubleClickEvent(self, event): + LOG.debug("mouseDoubleClickEvent %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(): + # ignore events outside the scene rect + return + # insert mode + self._inserter.mouseDoubleClickEvent(event, self._image_item) + else: + # selection mode + QGraphicsScene.mouseDoubleClickEvent(self, event) + def mouseReleaseEvent(self, event): LOG.debug("mouseReleaseEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos())) if self._inserter is not None: diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index 5ef7188..da6d34d 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -38,6 +38,9 @@ class ItemInserter(QObject): def mousePressEvent(self, event, image_item): event.accept() + def mouseDoubleClickEvent(self, event, image_item): + event.accept() + def mouseReleaseEvent(self, event, image_item): event.accept() @@ -345,6 +348,32 @@ class PolygonItemInserter(ItemInserter): event.accept() + def mouseDoubleClickEvent(self, event, image_item): + """Finish the polygon when the user double clicks.""" + + # No need to add the position of the click, as a single mouse + # press event added the point already. + # Even then, the last point of the polygon is duplicate as it would be + # shortly after a single mouse press. At this point, we want to throw it + # away. + polygon = self._item.polygon() + polygon.remove(polygon.size()-1) + assert polygon.size() > 0 + self._item.setPolygon(polygon) + + self._updateAnnotation() + if self._commit: + image_item.addAnnotation(self._ann) + self._scene.removeItem(self._item) + self.annotationFinished.emit() + self._item = None + self._scene.clearMessage() + + self.inserterFinished.emit() + + event.accept() + + def mouseMoveEvent(self, event, image_item): if self._item is not None: pos = event.scenePos() @@ -368,12 +397,9 @@ class PolygonItemInserter(ItemInserter): # we want to throw it away. polygon.remove(polygon.size()-1) assert polygon.size() > 0 + self._item.setPolygon(polygon) - self._ann.update({self._prefix + 'xn': - ";".join([str(p.x()) for p in polygon]), - self._prefix + 'yn': - ";".join([str(p.y()) for p in polygon])}) - self._ann.update(self._default_properties) + self._updateAnnotation() if self._commit: image_item.addAnnotation(self._ann) self._scene.removeItem(self._item) @@ -389,3 +415,11 @@ class PolygonItemInserter(ItemInserter): self._item = None self._scene.clearMessage() ItemInserter.abort(self) + + def _updateAnnotation(self): + polygon = self._item.polygon() + self._ann.update({self._prefix + 'xn': + ";".join([str(p.x()) for p in polygon]), + self._prefix + 'yn': + ";".join([str(p.y()) for p in polygon])}) + self._ann.update(self._default_properties) From 4feb87c72bd09295f942d9ef3d549e20f18e57c0 Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 23:15:29 +0200 Subject: [PATCH 8/9] When handling keys, only pressing Return finishes the polygon --- sloth/items/inserters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index da6d34d..3b573b3 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -407,7 +407,7 @@ class PolygonItemInserter(ItemInserter): self._item = None self._scene.clearMessage() - self.inserterFinished.emit() + self.inserterFinished.emit() def abort(self): if self._item is not None: From 26543b7a6d6e4979877d5b49793f832439b55f33 Mon Sep 17 00:00:00 2001 From: Mike Gerber Date: Thu, 10 Apr 2014 23:36:06 +0200 Subject: [PATCH 9/9] Remove default prefix --- sloth/items/items.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sloth/items/items.py b/sloth/items/items.py index d6479ae..2b41bc3 100644 --- a/sloth/items/items.py +++ b/sloth/items/items.py @@ -759,8 +759,7 @@ class NPointFaceItem(GroupItem): painter.drawRect(self.boundingRect()) class PolygonItem(BaseItem): - # XXX prefix="pointlist" - def __init__(self, model_item=None, prefix="pointlist", parent=None): + def __init__(self, model_item=None, prefix="", parent=None): BaseItem.__init__(self, model_item, prefix, parent) # Make it non-movable for now