From fbd6f9519f7ecd0c47a09560caa004f55797a912 Mon Sep 17 00:00:00 2001 From: Christian Ertler Date: Thu, 31 Aug 2017 14:55:53 +0200 Subject: [PATCH 1/2] implement ExtremeClickingInserter This first implementation still only produces a standard RectImtem. A better implementation would come with a custom Item that also handles the extreme points. --- sloth/items/inserters.py | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index 371c2a4..6e2c69b 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -1,4 +1,5 @@ import math +import sys from PyQt4.QtGui import * from PyQt4.Qt import * @@ -78,6 +79,87 @@ def mousePressEvent(self, event, image_item): self.annotationFinished.emit() event.accept() +class ExtremeClickingInserter(ItemInserter): + def __init__(self, labeltool, scene, default_properties=None, + prefix="", commit=True): + ItemInserter.__init__(self, labeltool, scene, default_properties, + prefix, commit) + + self._helpLines = None + self._helpLinesPen = QPen(Qt.green, 2, Qt.DashLine) + + self._points = QGraphicsItemGroup() + self._scene.addItem(self._points) + self._pointPen = QPen(Qt.red) + + def mouseReleaseEvent(self, event, image_item): + pos = event.scenePos() + new_point = QGraphicsEllipseItem(QRectF(pos.x()-2, pos.y()-2, 5, 5)) + new_point.setPen(self._pointPen) + self._points.addToGroup(new_point) + + if len(self._points.childItems()) == 4: + x1 = sys.maxint + x2 = -sys.maxint + y1 = sys.maxint + y2 = -sys.maxint + for point in self._points.childItems(): + x1 = min(x1, point.rect().x()) + x2 = max(x2, point.rect().x()) + y1 = min(y1, point.rect().y()) + y2 = max(y2, point.rect().y()) + width = x2 - x1 + height = y2 - y1 + + if width > 1 and height > 1: + self._ann.update({self._prefix + 'x': x1, + self._prefix + 'y': y1, + self._prefix + 'width': width, + self._prefix + 'height': height}) + self._ann.update(self._default_properties) + if self._commit: + image_item.addAnnotation(self._ann) + self.annotationFinished.emit() + + self._scene.removeItem(self._points) + self._points = QGraphicsItemGroup() + self._scene.addItem(self._points) + + event.accept() + + def mouseMoveEvent(self, event, image_item): + if self._helpLines is not None: + self._scene.removeItem(self._helpLines) + + self._helpLines = QGraphicsItemGroup() + group = self._helpLines + + verticalHelpLine = QGraphicsLineItem(event.scenePos().x(), 0, event.scenePos().x(), self._scene.height()) + horizontalHelpLine = QGraphicsLineItem(0, event.scenePos().y(), self._scene.width(), event.scenePos().y()) + + horizontalHelpLine.setPen(self._helpLinesPen) + verticalHelpLine.setPen(self._helpLinesPen) + + group.addToGroup(verticalHelpLine) + group.addToGroup(horizontalHelpLine) + + self._scene.addItem(self._helpLines) + + event.accept() + + def allowOutOfSceneEvents(self): + return True + + def abort(self): + if self._helpLines is not None: + self._scene.removeItem(self._helpLines) + self._helpLines = None + + self._scene.removeItem(self._points) + self._points = QGraphicsItemGroup() + self._scene.addItem(self._points) + + ItemInserter.abort(self) class RectItemInserter(ItemInserter): def __init__(self, labeltool, scene, default_properties=None, From 5fce97f607f4327a9ae259cc76c064b7fc05726b Mon Sep 17 00:00:00 2001 From: Christian Ertler Date: Mon, 4 Sep 2017 17:12:12 +0200 Subject: [PATCH 2/2] implement custom ExtremeClicking item --- sloth/conf/default_config.py | 23 ++++-- sloth/items/inserters.py | 37 +++++---- sloth/items/items.py | 151 ++++++++++++++++++++++++++++++++++- 3 files changed, 188 insertions(+), 23 deletions(-) diff --git a/sloth/conf/default_config.py b/sloth/conf/default_config.py index 45b999c..e2f2010 100644 --- a/sloth/conf/default_config.py +++ b/sloth/conf/default_config.py @@ -1,7 +1,7 @@ # This is sloth's default configuration. # # The configuration file is a simple python module with module-level -# variables. This module contains the default values for sloth's +# variables. This module contains the default values for sloth's # configuration variables. # # In all cases in the configuration where a python callable (such as a @@ -16,16 +16,16 @@ # be one dictionary that contains the following keys: # # - 'item' : Visualization item for this label. This can be -# any python callable or a module path string +# any python callable or a module path string # implementing the visualization item interface. # # - 'inserter' : (optional) Item inserter for this label. # If the user selects to insert a new label of this class -# the inserter is responsible to actually +# the inserter is responsible to actually # capture the users mouse actions and insert # a new label into the annotation model. # -# - 'hotkey' : (optional) A keyboard shortcut starting +# - 'hotkey' : (optional) A keyboard shortcut starting # the insertion of a new label of this class. # # - 'attributes' : (optional) A dictionary that defines the @@ -52,6 +52,15 @@ 'hotkey': 'r', 'text': 'Rectangle', }, + { + 'attributes': { + 'class': 'extreme_clicks', + }, + 'inserter': 'sloth.items.ExtremeClickingInserter', + 'item': 'sloth.items.ExtremeClickingItem', + 'hotkey': 'e', + 'text': 'Extreme Clicks', + }, { 'attributes': { 'class': 'point', @@ -78,7 +87,7 @@ # with at least 2 entries, where the first entry is the hotkey (sequence), # and the second entry is the function that is called. The function # should expect a single parameter, the labeltool object. The optional -# third entry -- if present -- is expected to be a string describing the +# third entry -- if present -- is expected to be a string describing the # action. HOTKEYS = ( ('Space', [lambda lt: lt.currentImage().confirmAll(), @@ -115,9 +124,7 @@ # PLUGINS # # A list/tuple of classes implementing the sloth plugin interface. The -# classes can either be given directly or their module path be specified +# classes can either be given directly or their module path be specified # as string. PLUGINS = ( ) - - diff --git a/sloth/items/inserters.py b/sloth/items/inserters.py index 6e2c69b..40327c5 100644 --- a/sloth/items/inserters.py +++ b/sloth/items/inserters.py @@ -99,23 +99,32 @@ def mouseReleaseEvent(self, event, image_item): self._points.addToGroup(new_point) if len(self._points.childItems()) == 4: - x1 = sys.maxint - x2 = -sys.maxint - y1 = sys.maxint - y2 = -sys.maxint + left = QPointF(sys.maxint, 0) + right = QPointF(-sys.maxint, 0) + top = QPointF(0, sys.maxint) + bottom = QPointF(0, -sys.maxint) for point in self._points.childItems(): - x1 = min(x1, point.rect().x()) - x2 = max(x2, point.rect().x()) - y1 = min(y1, point.rect().y()) - y2 = max(y2, point.rect().y()) - width = x2 - x1 - height = y2 - y1 + if point.rect().x() < left.x(): + left = point.rect() + if point.rect().y() < top.y(): + top = point.rect() + if (point.rect().x() + point.rect().width()) > right.x(): + right = point.rect() + if (point.rect().y() + point.rect().height()) > bottom.y(): + bottom = point.rect() + + width = right.x() - left.x() + height = bottom.y() - top.y() if width > 1 and height > 1: - self._ann.update({self._prefix + 'x': x1, - self._prefix + 'y': y1, - self._prefix + 'width': width, - self._prefix + 'height': height}) + self._ann.update({self._prefix + 'left_x': left.x(), + self._prefix + 'left_y': left.y(), + self._prefix + 'right_x': right.x(), + self._prefix + 'right_y': right.y(), + self._prefix + 'top_x': top.x(), + self._prefix + 'top_y': top.y(), + self._prefix + 'bottom_x': bottom.x(), + self._prefix + 'bottom_y': bottom.y()}) self._ann.update(self._default_properties) if self._commit: image_item.addAnnotation(self._ann) diff --git a/sloth/items/items.py b/sloth/items/items.py index 2b41bc3..bd3e469 100644 --- a/sloth/items/items.py +++ b/sloth/items/items.py @@ -1,6 +1,7 @@ import logging from PyQt4.Qt import * - +import numpy as np +import sys LOG = logging.getLogger(__name__) @@ -346,6 +347,154 @@ def keyPressEvent(self, event): self.moveBy(*ds) event.accept() +class ExtremeClickingItem(BaseItem): + def __init__(self, model_item=None, prefix="", parent=None): + BaseItem.__init__(self, model_item, prefix, parent) + + self.setFlags(QGraphicsItem.ItemIsSelectable | \ + QGraphicsItem.ItemSendsGeometryChanges | \ + QGraphicsItem.ItemSendsScenePositionChanges) + + self._left_point = None + self._right_point = None + self._top_point = None + self._bottom_point = None + + self._move_point = None + self._move_x_key = None + self._move_y_key = None + self._move_limit_x = None + self._move_limit_y = None + + self._updatePoints(model_item) + + def __call__(self, model_item=None, parent=None): + item = ExtremeClickingItem(model_item, parent) + item.setPen(self.pen()) + item.setBrush(self.brush()) + return item + + + def _updatePoints(self, model_item): + self.prepareGeometryChange() + if model_item is not None: + try: + self._left_point = QPointF(float(model_item[self.prefix() + "left_x"]), + float(model_item[self.prefix() + "left_y"])) + self._right_point = QPointF(float(model_item[self.prefix() + "right_x"]), + float(model_item[self.prefix() + "right_y"])) + self._top_point = QPointF(float(model_item[self.prefix() + "top_x"]), + float(model_item[self.prefix() + "top_y"])) + self._bottom_point = QPointF(float(model_item[self.prefix() + "bottom_x"]), + float(model_item[self.prefix() + "bottom_y"])) + except KeyError as e: + LOG.debug("ExtremeClickItem: Could not find expected key in item: " + + str(e) + ". Check your config!") + self.setValid(False) + else: + self._left_point = QPointF() + self._right_point = QPointF() + self._top_point = QPointF() + self._bottom_point = QPointF() + self.setPos(QPointF(0, 0)) + + def _buildRect(self): + x_min = self._left_point.x() + y_min = self._top_point.y() + x_max = self._right_point.x() + y_max = self._bottom_point.y() + width = x_max - x_min + height = y_max - y_min + return QRectF(QPointF(x_min, y_min), QSizeF(width, height)) + + def boundingRect(self): + return self._buildRect() + + def _drawPoint(self, painter, point): + painter.drawEllipse(QRectF(point.x()-2, point.y()-2, 5, 5)) + + 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) + painter.drawRect(self.boundingRect()) + self._drawPoint(painter, self._left_point) + self._drawPoint(painter, self._right_point) + self._drawPoint(painter, self._top_point) + self._drawPoint(painter, self._bottom_point) + + def dataChange(self): + self._updatePoints(self._model_item) + + def mousePressEvent(self, event): + #if event.modifiers() & Qt.ControlModifier != 0: + if event.button() & Qt.RightButton != 0: + rect = self.boundingRect() + dist_left = abs(event.scenePos().x() - rect.x()) + dist_right = abs(event.scenePos().x() - (rect.x() + rect.width())) + dist_top = abs(event.scenePos().y() - rect.y()) + dist_bottom = abs(event.scenePos().y() - (rect.y() + rect.height())) + + # find nearest rect border + np_dists = np.array([dist_left, dist_right, dist_top, dist_bottom]) + argmin_dist = np_dists.argmin() + + np_points = np.array([[self._left_point.x(), self._left_point.y()], + [self._right_point.x(), self._right_point.y()], + [self._top_point.x(), self._top_point.y()], + [self._bottom_point.x(), self._bottom_point.y()]]) + np_other_points = np_points[[i != argmin_dist for i in range(4)], :] + + if argmin_dist == 0: + self._move_point = self._left_point + point_prefix = 'left' + self._move_limit_x = (-sys.maxint, np_other_points[:, 0].min()) + self._move_limit_y = (self._top_point.y(), self._bottom_point.y()) + elif argmin_dist == 1: + self._move_point = self._right_point + point_prefix = 'right' + self._move_limit_x = (np_other_points[:, 0].max(), sys.maxint) + self._move_limit_y = (self._top_point.y(), self._bottom_point.y()) + elif argmin_dist == 2: + self._move_point = self._top_point + point_prefix = 'top' + self._move_limit_x = (self._left_point.x(), self._right_point.x()) + self._move_limit_y = (-sys.maxint, np_other_points[:, 1].min()) + else: + self._move_point = self._bottom_point + point_prefix = 'bottom' + self._move_limit_x = (self._left_point.x(), self._right_point.x()) + self._move_limit_y = (np_other_points[:, 1].max(), sys.maxint) + + self._move_x_key = point_prefix + '_x' + self._move_y_key = point_prefix + '_y' + + event.accept() + else: + BaseItem.mousePressEvent(self, event) + + def mouseMoveEvent(self, event): + if self._move_point is not None: + new_x = min(max(event.scenePos().x(), self._move_limit_x[0]), self._move_limit_x[1]) + new_y = min(max(event.scenePos().y(), self._move_limit_y[0]), self._move_limit_y[1]) + self._move_point.setX(new_x) + self._move_point.setY(new_y) + self._model_item[self._move_x_key] = self._move_point.x() + self._model_item[self._move_y_key] = self._move_point.y() + self._updatePoints(self._model_item) + event.accept() + else: + BaseItem.mouseMoveEvent(self, event) + + def mouseReleaseEvent(self, event): + if self._move_point is not None: + self._move_point = None + event.accept() + else: + BaseItem.mouseReleaseEvent(self, event) class RectItem(BaseItem): def __init__(self, model_item=None, prefix="", parent=None):