Skip to content

Add Boundary Clipping for Rotation Shapes (Similar to Rectangle) #1268

@poearrow

Description

@poearrow

Search before asking

  • I have searched the X-AnyLabeling issues and found no similar feature requests.

Description

Currently, X-AnyLabeling has excellent boundary clipping functionality for rectangle shapes - when a rectangle's points go
outside the image boundaries, the software automatically calculates and adjusts them to stay within bounds. However, this
same functionality is missing for rotation shapes, which can lead to inconsistent user experience.

🔄 Background

The rectangle boundary clipping feature (implemented in clip_rectangle_to_pixmap() method) was previously added based on user
feedback and works perfectly. When drawing rectangles:

  • If any corner goes outside image bounds, it's automatically clipped to the edge
  • The rectangle maintains its aspect ratio as much as possible
  • Users get a smooth, predictable experience

Rotation shapes currently lack this same boundary management, making them behave differently from other shape types.

🎯 Proposed Feature

Implement automatic boundary clipping for rotation shapes similar to the existing rectangle functionality, ensuring that:

  1. When rotation shape vertices go outside image boundaries, they are automatically adjusted
  2. The rotation angle and proportions are preserved as much as possible
  3. The shape remains fully contained within the image bounds

🔍 Technical Analysis

Current Rectangle Implementation (✅ Working)

Location: anylabeling/views/labeling/widgets/canvas.py:273-315

def clip_rectangle_to_pixmap(self, shape):
"""Clip rectangle shape to pixmap boundaries"""
if self.pixmap is None or shape.shape_type != "rectangle":
return True

  w, h = self.pixmap.width(), self.pixmap.height()
  points = shape.points

  # Calculate bounds and clip to [0, w] and [0, h]
  x_coords = [p.x() for p in points]
  y_coords = [p.y() for p in points]
  min_x, max_x = min(x_coords), max(x_coords)
  min_y, max_y = min(y_coords), max(y_coords)

  # Clipping logic...

Called from: canvas.py:1992 when finishing rectangle drawing.

Current Rotation Implementation (❌ Missing Boundary Check)

Location: anylabeling/views/labeling/widgets/canvas.py:1097-1115

if shape.shape_type == "rotation":
sindex = (index + 2) % 4
# Get the other 3 points after transformed
p2, p3, p4 = self.get_adjoint_points(
shape.direction, shape[sindex], pos, index
)
# NOTE: No boundary clipping logic here

Rotation Shape Structure

  • Rotation shapes use 4 points representing the corners of a rotated rectangle
  • They store a direction property for the rotation angle
  • Points are stored in shape.points[0] through shape.points[3]

💡 Suggested Implementation

  1. Create clip_rotation_to_pixmap() method

def clip_rotation_to_pixmap(self, shape):
"""Clip rotation shape to pixmap boundaries"""
if self.pixmap is None or shape.shape_type != "rotation":
return True

  w, h = self.pixmap.width(), self.pixmap.height()
  points = shape.points

  if len(points) != 4:
      return True

  # Check if any point is out of bounds
  out_of_bounds = False
  for point in points:
      if point.x() < 0 or point.x() > w or point.y() < 0 or point.y() > h:
          out_of_bounds = True
          break

  if not out_of_bounds:
      return True

  # Calculate center and adjust
  center_x = sum(p.x() for p in points) / 4
  center_y = sum(p.y() for p in points) / 4

  # Clip each point to bounds while preserving rotation
  clipped_points = []
  for point in points:
      clipped_x = max(0, min(w, point.x()))
      clipped_y = max(0, min(h, point.y()))
      clipped_points.append(QtCore.QPointF(clipped_x, clipped_y))

  # Update shape points
  for i, clipped_point in enumerate(clipped_points):
      shape.points[i] = clipped_point

  return True
  1. Add boundary check in mouse release event

Modify canvas.py:1991-1997 to include rotation shapes:

if self.current.shape_type == "rectangle":
if not self.clip_rectangle_to_pixmap(self.current):
self.current = None
# ... cleanup code
elif self.current.shape_type == "rotation":
if not self.clip_rotation_to_pixmap(self.current):
self.current = None
# ... similar cleanup code

Use case

No response

Additional

No response

Are you willing to submit a PR?

  • Yes I'd like to help by submitting a PR!

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions