Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Features: Toggling shape visibility on right click; Support constrained shape rotating #1324

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 107 additions & 102 deletions examples/semantic_segmentation/data_annotated/2011_000003.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions labelme/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def __init__(

self.canvas.newShape.connect(self.newShape)
self.canvas.shapeMoved.connect(self.setDirty)
self.canvas.shapeRotated.connect(self.setDirty)
self.canvas.selectionChanged.connect(self.shapeSelectionChanged)
self.canvas.drawingPolygon.connect(self.toggleDrawingSensitive)

Expand Down Expand Up @@ -457,6 +458,13 @@ def __init__(
tip=self.tr("Show all polygons"),
enabled=False,
)
hideOrShow = action(
self.tr("&Show or Hide\n Polygons"),
self.reversePolygons,
icon="eye",
tip=self.tr("Hide or show all polygons"),
enabled=False,
)

help = action(
self.tr("&Tutorial"),
Expand Down Expand Up @@ -656,6 +664,7 @@ def __init__(
createPointMode,
createLineStripMode,
createAiPolygonMode,
hideOrShow,
editMode,
edit,
duplicate,
Expand All @@ -675,6 +684,7 @@ def __init__(
createPointMode,
createLineStripMode,
createAiPolygonMode,
hideOrShow,
editMode,
brightnessContrast,
),
Expand Down Expand Up @@ -1539,6 +1549,10 @@ def togglePolygons(self, value):
for item in self.labelList:
item.setCheckState(Qt.Checked if value else Qt.Unchecked)

def reversePolygons(self):
for item in self.labelList:
item.setCheckState(Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked)

def loadFile(self, filename=None):
"""Load the specified file, or the last opened file if None."""
# changing fileListWidget loads file
Expand Down
109 changes: 98 additions & 11 deletions labelme/shape.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import copy
import math

from functools import reduce
from contextlib import contextmanager

from qtpy import QtCore
from qtpy import QtGui

Expand Down Expand Up @@ -31,7 +34,7 @@ class Shape(object):
# Flag for the handles we would move if dragging
MOVE_VERTEX = 0

# Flag for all other handles on the curent shape
# Flag for all other handles on the current shape
NEAR_VERTEX = 1

# The following class variables influence the drawing of all shape objects.
Expand All @@ -45,6 +48,8 @@ class Shape(object):
point_size = 8
scale = 1.0

center_scale = 1.5

def __init__(
self,
label=None,
Expand All @@ -57,6 +62,7 @@ def __init__(
self.label = label
self.group_id = group_id
self.points = []
self.new_points = []
self.point_labels = []
self.shape_type = shape_type
self._shape_raw = None
Expand Down Expand Up @@ -84,6 +90,9 @@ def __init__(
# is used for drawing the pending line a different color.
self.line_color = line_color

# check whether the shape is being rotated
self._is_rotating = False

def setShapeRefined(self, points, point_labels, shape_type):
self._shape_raw = (self.points, self.point_labels, self.shape_type)
self.points = points
Expand Down Expand Up @@ -116,6 +125,66 @@ def shape_type(self, value):
raise ValueError("Unexpected shape_type: {}".format(value))
self._shape_type = value

@property
def center(self):
if len(self.points) == 0:
return None

return reduce(lambda x, y : x+y, self.points) / len(self.points)

@contextmanager
def rotatingRenderScope(self):

if not self.isRotating() or len(self.new_points) == 0:
yield
else:
raw_points = self.points
self.points = self.new_points
yield
self.points = raw_points

def isRotating(self):

return self._is_rotating

def setRotating(self, value):

# rotating is only implemented for polygon
if self.shape_type != "polygon":
return

self._is_rotating = value

if not value:
self.new_points = []

def rotate(self, degree):

"""
Rotate the shape by certain degree
"""
center : QtCore.QPointF = self.center
if center is None or not self.isRotating():
return

radius = degree / 180.0 * math.pi
cos = math.cos(radius)
sin = math.sin(radius)
cx, cy = center.x(), center.y()
new_points = [
QtCore.QPointF(
cos * (point.x() - cx) - sin * (point.y() - cy) + cx,
sin * (point.x() - cx) + cos * (point.y() - cy) + cy
) for point in self.points
]

self.new_points = new_points

def applyRotate(self):

self.points = self.new_points
self.setRotating(False)

def close(self):
self._closed = True

Expand Down Expand Up @@ -221,17 +290,22 @@ def paint(self, painter):
else:
self.drawVertex(negative_vrtx_path, i)
else:
line_path.moveTo(self.points[0])
# Uncommenting the following line will draw 2 paths
# for the 1st vertex, and make it non-filled, which
# may be desirable.
# self.drawVertex(vrtx_path, 0)
with self.rotatingRenderScope():

for i, p in enumerate(self.points):
line_path.lineTo(p)
self.drawVertex(vrtx_path, i)
if self.isClosed():
line_path.lineTo(self.points[0])
line_path.moveTo(self.points[0])
# Uncommenting the following line will draw 2 paths
# for the 1st vertex, and make it non-filled, which
# may be desirable.
# self.drawVertex(vrtx_path, 0)

for i, p in enumerate(self.points):
line_path.lineTo(p)
self.drawVertex(vrtx_path, i)
if self.isClosed():
line_path.lineTo(self.points[0])

if self.isRotating():
self.drawCenter(vrtx_path)

painter.drawPath(line_path)
painter.drawPath(vrtx_path)
Expand All @@ -249,6 +323,19 @@ def paint(self, painter):
painter.drawPath(negative_vrtx_path)
painter.fillPath(negative_vrtx_path, QtGui.QColor(255, 0, 0, 255))

def drawCenter(self, path):

d = self.point_size / self.scale * self.center_scale
center = self.center
shape = self.point_type
self._vertex_fill_color = self.vertex_fill_color
if shape == self.P_SQUARE:
path.addRect(center.x() - d / 2, center.y() - d / 2, d, d)
elif shape == self.P_ROUND:
path.addEllipse(center, d / 2.0, d / 2.0)
else:
assert False, "unsupported vertex shape"

def drawVertex(self, path, i):
d = self.point_size / self.scale
shape = self.point_type
Expand Down
74 changes: 72 additions & 2 deletions labelme/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
CURSOR_DRAW = QtCore.Qt.CrossCursor
CURSOR_MOVE = QtCore.Qt.ClosedHandCursor
CURSOR_GRAB = QtCore.Qt.OpenHandCursor
CURSOR_ROTATE = QtCore.Qt.SizeAllCursor

MOVE_SPEED = 5.0

# change the value to adjust sensitivity to mouse movement
ANGLES_PER_PIXEL = 6.0
ANGLES_PER_PIXEL_LOW_SPEED = 3.0

class Canvas(QtWidgets.QWidget):

Expand All @@ -30,6 +34,7 @@ class Canvas(QtWidgets.QWidget):
newShape = QtCore.Signal()
selectionChanged = QtCore.Signal(list)
shapeMoved = QtCore.Signal()
shapeRotated = QtCore.Signal()
drawingPolygon = QtCore.Signal(bool)
vertexSelected = QtCore.Signal(bool)

Expand Down Expand Up @@ -95,6 +100,8 @@ def __init__(self, *args, **kwargs):
self.hShapeIsSelected = False
self._painter = QtGui.QPainter()
self._cursor = CURSOR_DEFAULT

self._rotate_anchor_point = None
# Menus:
# 0: right-click without selection and dragging of shapes
# 1: right-click with selection and dragging of shapes
Expand Down Expand Up @@ -305,7 +312,7 @@ def mouseMoveEvent(self, ev):
return

# Polygon copy moving.
if QtCore.Qt.RightButton & ev.buttons():
if QtCore.Qt.RightButton & ev.buttons() and not self.isRotatingShapes():
if self.selectedShapesCopy and self.prevPoint:
self.overrideCursor(CURSOR_MOVE)
self.boundedMoveShapes(self.selectedShapesCopy, pos)
Expand All @@ -318,7 +325,7 @@ def mouseMoveEvent(self, ev):
return

# Polygon/Vertex moving.
if QtCore.Qt.LeftButton & ev.buttons():
if QtCore.Qt.LeftButton & ev.buttons() and not self.isRotatingShapes():
if self.selectedVertex():
self.boundedMoveVertex(pos)
self.repaint()
Expand All @@ -330,6 +337,26 @@ def mouseMoveEvent(self, ev):
self.movingShape = True
return

# logic to process the rotating (no clicking)
# in rotation mode, the hovering/highlighting action is forbidden
if self.isRotatingShapes() and self._rotate_anchor_point is not None:
angle_speed = ANGLES_PER_PIXEL
if ev.modifiers() == QtCore.Qt.ShiftModifier:
angle_speed = ANGLES_PER_PIXEL_LOW_SPEED

delta_angle = (self._rotate_anchor_point.y() - pos.y()) * angle_speed

for shape in self.selectedShapes:
last_rotate_points = shape.new_points
shape.rotate(degree=delta_angle)
# bounding constraint for rotation
if any(self.outOfPixmap(p) for p in shape.new_points):
shape.new_points = last_rotate_points

self.repaint()

return

# Just hovering over the canvas, 2 possibilities:
# - Highlight shapes
# - Highlight vertex
Expand Down Expand Up @@ -477,6 +504,9 @@ def mousePressEvent(self, ev):
self.drawingPolygon.emit(True)
self.update()
elif self.editing():
if self.isRotatingShapes():
return

if self.selectedEdge():
self.addPointToEdge()
elif (
Expand All @@ -491,6 +521,9 @@ def mousePressEvent(self, ev):
self.prevPoint = pos
self.repaint()
elif ev.button() == QtCore.Qt.RightButton and self.editing():
if self.isRotatingShapes():
return

group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier
if not self.selectedShapes or (
self.hShape is not None
Expand All @@ -502,6 +535,9 @@ def mousePressEvent(self, ev):

def mouseReleaseEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton:
if self.isRotatingShapes():
return

menu = self.menus[len(self.selectedShapesCopy) > 0]
self.restoreCursor()
if (
Expand Down Expand Up @@ -533,6 +569,12 @@ def mouseReleaseEvent(self, ev):

self.movingShape = False

if self.isRotatingShapes():
for s in filter(lambda shape : shape.isRotating(), self.selectedShapes):
s.applyRotate()

self.shapeRotated.emit()

def endMove(self, copy):
assert self.selectedShapes and self.selectedShapesCopy
assert len(self.selectedShapesCopy) == len(self.selectedShapes)
Expand Down Expand Up @@ -942,6 +984,20 @@ def moveByKeyboard(self, offset):
self.repaint()
self.movingShape = True

def isRotatingShapes(self):

return any(shape.isRotating() for shape in self.selectedShapes)

def rotateSelectedShapes(self, value):

if value:
self.overrideCursor(CURSOR_ROTATE)
else:
self.overrideCursor(CURSOR_DEFAULT)

for shape in self.selectedShapes:
shape.setRotating(value)

def keyPressEvent(self, ev):
modifiers = ev.modifiers()
key = ev.key()
Expand All @@ -963,6 +1019,20 @@ def keyPressEvent(self, ev):
self.moveByKeyboard(QtCore.QPointF(-MOVE_SPEED, 0.0))
elif key == QtCore.Qt.Key_Right:
self.moveByKeyboard(QtCore.QPointF(MOVE_SPEED, 0.0))
elif key == QtCore.Qt.Key_R:
if not self.isRotatingShapes() and len(self.selectedShapes) > 0:
self.storeShapes()
self.rotateSelectedShapes(True)
current_cursor = QtGui.QCursor().pos()
self._rotate_anchor_point = self.transformPos(self.mapFromGlobal(current_cursor))
elif self.isRotatingShapes():
self.rotateSelectedShapes(False)
self._rotate_anchor_point = None
self.repaint()
elif key == QtCore.Qt.Key_Escape and self.isRotatingShapes():
self.rotateSelectedShapes(False)
self._rotate_anchor_point = None
self.repaint()

def keyReleaseEvent(self, ev):
modifiers = ev.modifiers()
Expand Down