Skip to content

Commit

Permalink
[Feature] Full angle range (#1061)
Browse files Browse the repository at this point in the history
* full range added

* precommit hook

* test update

* chirality preserving flip
  • Loading branch information
calcoloergosum authored Sep 13, 2024
1 parent 9ea1aee commit 95123b9
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 8 deletions.
12 changes: 9 additions & 3 deletions mmrotate/core/bbox/coder/angle_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ class CSLCoder(BaseBBoxCoder):
def __init__(self, angle_version, omega=1, window='gaussian', radius=6):
super().__init__()
self.angle_version = angle_version
assert angle_version in ['oc', 'le90', 'le135']
assert angle_version in ['oc', 'le90', 'le135', 'full360']
assert window in ['gaussian', 'triangle', 'rect', 'pulse']
self.angle_range = 90 if angle_version == 'oc' else 180
self.angle_offset_dict = {'oc': 0, 'le90': 90, 'le135': 45}
self.angle_range = 90 if angle_version == 'oc' else \
(360 if angle_version == 'full360' else 180)
self.angle_offset_dict = {
'oc': 0,
'le90': 90,
'le135': 45,
'full360': 180
}
self.angle_offset = self.angle_offset_dict[angle_version]
self.omega = omega
self.window = window
Expand Down
4 changes: 2 additions & 2 deletions mmrotate/core/bbox/coder/delta_xywha_hbbox_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def encode(self, bboxes, gt_bboxes):
assert bboxes.size(0) == gt_bboxes.size(0)
assert bboxes.size(-1) == 4
assert gt_bboxes.size(-1) == 5
if self.angle_range in ['oc', 'le135', 'le90']:
if self.angle_range in ['oc', 'le135', 'le90', 'full360']:
return bbox2delta(bboxes, gt_bboxes, self.means, self.stds,
self.angle_range, self.norm_factor,
self.edge_swap)
Expand Down Expand Up @@ -104,7 +104,7 @@ def decode(self,
assert pred_bboxes.size(1) == bboxes.size(1)
assert bboxes.size(-1) == 4
assert pred_bboxes.size(-1) == 5
if self.angle_range in ['oc', 'le135', 'le90']:
if self.angle_range in ['oc', 'le135', 'le90', 'full360']:
return delta2bbox(bboxes, pred_bboxes, self.means, self.stds,
wh_ratio_clip, self.add_ctr_clamp,
self.ctr_clamp, self.angle_range,
Expand Down
4 changes: 2 additions & 2 deletions mmrotate/core/bbox/coder/delta_xywha_rbbox_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def encode(self, bboxes, gt_bboxes):
assert bboxes.size(0) == gt_bboxes.size(0)
assert bboxes.size(-1) == 5
assert gt_bboxes.size(-1) == 5
if self.angle_range in ['oc', 'le135', 'le90']:
if self.angle_range in ['oc', 'le135', 'le90', 'full360']:
return bbox2delta(bboxes, gt_bboxes, self.means, self.stds,
self.angle_range, self.norm_factor,
self.edge_swap, self.proj_xy)
Expand Down Expand Up @@ -99,7 +99,7 @@ def decode(self,
torch.Tensor: Decoded boxes.
"""
assert pred_bboxes.size(0) == bboxes.size(0)
if self.angle_range in ['oc', 'le135', 'le90']:
if self.angle_range in ['oc', 'le135', 'le90', 'full360']:
return delta2bbox(bboxes, pred_bboxes, self.means, self.stds,
max_shape, wh_ratio_clip, self.add_ctr_clamp,
self.ctr_clamp, self.angle_range,
Expand Down
115 changes: 115 additions & 0 deletions mmrotate/core/bbox/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def poly2obb(polys, version='oc'):
results = poly2obb_le135(polys)
elif version == 'le90':
results = poly2obb_le90(polys)
elif version == 'full360':
results = poly2obb_full360(polys)
else:
raise NotImplementedError
return results
Expand All @@ -129,6 +131,8 @@ def poly2obb_np(polys, version='oc'):
results = poly2obb_np_le135(polys)
elif version == 'le90':
results = poly2obb_np_le90(polys)
elif version == 'full360':
results = poly2obb_np_full360(polys)
else:
raise NotImplementedError
return results
Expand All @@ -150,6 +154,9 @@ def obb2hbb(rbboxes, version='oc'):
results = obb2hbb_le135(rbboxes)
elif version == 'le90':
results = obb2hbb_le90(rbboxes)
elif version == 'full360':
# NOTE: same as 90
results = obb2hbb_le90(rbboxes)
else:
raise NotImplementedError
return results
Expand All @@ -171,6 +178,9 @@ def obb2poly(rbboxes, version='oc'):
results = obb2poly_le135(rbboxes)
elif version == 'le90':
results = obb2poly_le90(rbboxes)
elif version == 'full360':
# NOTE: same as 90
results = obb2poly_le90(rbboxes)
else:
raise NotImplementedError
return results
Expand All @@ -192,6 +202,8 @@ def obb2poly_np(rbboxes, version='oc'):
results = obb2poly_np_le135(rbboxes)
elif version == 'le90':
results = obb2poly_np_le90(rbboxes)
elif version == 'full360':
results = obb2poly_np_full360(rbboxes)
else:
raise NotImplementedError
return results
Expand All @@ -213,6 +225,9 @@ def obb2xyxy(rbboxes, version='oc'):
results = obb2xyxy_le135(rbboxes)
elif version == 'le90':
results = obb2xyxy_le90(rbboxes)
elif version == 'full360':
# NOTE: same as 90
results = obb2xyxy_le90(rbboxes)
else:
raise NotImplementedError
return results
Expand All @@ -235,6 +250,7 @@ def hbb2obb(hbboxes, version='oc'):
elif version == 'le90':
results = hbb2obb_le90(hbboxes)
else:
# NOTE: not well defined for full360. Leave it unimplemented
raise NotImplementedError
return results

Expand Down Expand Up @@ -298,6 +314,31 @@ def poly2obb_le135(polys):
return torch.stack([x_ctr, y_ctr, width, height, angles], 1)


def poly2obb_full360(polys):
"""Convert polygons to oriented bounding boxes.
Args:
polys (torch.Tensor): [x0,y0,x1,y1,x2,y2,x3,y3]
Returns:
obbs (torch.Tensor): [x_ctr,y_ctr,w,h,angle]
"""
polys = torch.reshape(polys, [-1, 8])
pt1, pt2, pt3, _ = polys[..., :8].chunk(4, 1)
width = torch.sqrt(
torch.pow(pt1[..., 0] - pt2[..., 0], 2) +
torch.pow(pt1[..., 1] - pt2[..., 1], 2))
height = torch.sqrt(
torch.pow(pt2[..., 0] - pt3[..., 0], 2) +
torch.pow(pt2[..., 1] - pt3[..., 1], 2))
angles = torch.atan2((pt1[..., 1] - pt2[..., 1]),
(pt1[..., 0] - pt2[..., 0]))
angles = norm_angle(angles, 'full360')
x_ctr = (pt1[..., 0] + pt3[..., 0]) / 2.0
y_ctr = (pt1[..., 1] + pt3[..., 1]) / 2.0
return torch.stack([x_ctr, y_ctr, width, height, angles], 1)


def poly2obb_le90(polys):
"""Convert polygons to oriented bounding boxes.
Expand Down Expand Up @@ -418,6 +459,33 @@ def poly2obb_np_le90(poly):
return x, y, w, h, a


def poly2obb_np_full360(poly):
"""Convert polygons to oriented bounding boxes. Assumes head points then
tail points.
Args:
polys (ndarray): [x0,y0,x1,y1,x2,y2,x3,y3]
Returns:
obbs (ndarray): [x_ctr,y_ctr,w,h,angle]
"""
pt1, pt2, pt3, pt4 = np.array(poly).reshape((4, 2))
x, y = (pt1 + pt2 + pt3 + pt4) / 4.0
dx, dy = pt2 - pt1
a = np.arctan2(dy, dx)
w = np.linalg.norm(pt2 - pt1)
h = np.linalg.norm(pt3 - pt2)
if w < 2 or h < 2:
return
while not np.pi > a >= -np.pi:
if a >= np.pi:
a -= np.pi
else:
a += np.pi
assert np.pi > a >= -np.pi
return x, y, w, h, a


def obb2poly_oc(rboxes):
"""Convert oriented bounding boxes to polygons.
Expand Down Expand Up @@ -634,6 +702,26 @@ def hbb2obb_le90(hbboxes):
return obboxes


def hbb2obb_full360(hbboxes):
"""Convert horizontal bounding boxes to oriented bounding boxes.
Args:
hbbs (torch.Tensor): [x_lt,y_lt,x_rb,y_rb]
Returns:
obbs (torch.Tensor): [x_ctr,y_ctr,w,h,angle]
"""
x = (hbboxes[..., 0] + hbboxes[..., 2]) * 0.5
y = (hbboxes[..., 1] + hbboxes[..., 3]) * 0.5
w = hbboxes[..., 2] - hbboxes[..., 0]
h = hbboxes[..., 3] - hbboxes[..., 1]
theta = x.new_zeros(*x.shape)
obboxes1 = torch.stack([x, y, w, h, theta], dim=-1)
obboxes2 = torch.stack([x, y, h, w, theta - np.pi / 2], dim=-1)
obboxes = torch.where((w >= h)[..., None], obboxes1, obboxes2)
return obboxes


def obb2xyxy_oc(rbboxes):
"""Convert oriented bounding boxes to horizontal bounding boxes.
Expand Down Expand Up @@ -783,6 +871,31 @@ def obb2poly_np_le90(obboxes):
return polys


def obb2poly_np_full360(obboxes):
"""Convert oriented bounding boxes to polygons.
Args:
obbs (ndarray): [x_ctr,y_ctr,w,h,angle,score]
Returns:
polys (ndarray): [x0,y0,x1,y1,x2,y2,x3,y3,score]
"""
try:
center, w, h, theta, score = np.split(obboxes, (2, 3, 4, 5), axis=-1)
except: # noqa: E722
results = np.stack([0., 0., 0., 0., 0., 0., 0., 0., 0.], axis=-1)
return results.reshape(1, -1)
Cos, Sin = np.cos(theta), np.sin(theta)
vector1 = np.concatenate([w / 2 * Cos, w / 2 * Sin], axis=-1)
vector2 = np.concatenate([-h / 2 * Sin, h / 2 * Cos], axis=-1)
point1 = center - vector1 - vector2
point2 = center + vector1 - vector2
point3 = center + vector1 + vector2
point4 = center - vector1 + vector2
polys = np.concatenate([point1, point2, point3, point4, score], axis=-1)
return polys


def cal_line_length(point1, point2):
"""Calculate the length of line.
Expand Down Expand Up @@ -863,6 +976,8 @@ def norm_angle(angle, angle_range):
return (angle + np.pi / 4) % np.pi - np.pi / 4
elif angle_range == 'le90':
return (angle + np.pi / 2) % np.pi - np.pi / 2
elif angle_range == 'full360':
return angle % (2 * np.pi) - np.pi
else:
print('Not yet implemented.')

Expand Down
3 changes: 2 additions & 1 deletion mmrotate/datasets/pipelines/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ def bbox_flip(self, bboxes, img_shape, direction):
flipped = bboxes.copy()
if direction == 'horizontal':
flipped[:, 0] = img_shape[1] - bboxes[:, 0] - 1
flipped[:4] = flipped[[1, 0, 3, 2]].copy()
elif direction == 'vertical':
flipped[:, 1] = img_shape[0] - bboxes[:, 1] - 1
flipped[:4] = flipped[[1, 0, 3, 2]].copy()
elif direction == 'diagonal':
flipped[:, 0] = img_shape[1] - bboxes[:, 0] - 1
flipped[:, 1] = img_shape[0] - bboxes[:, 1] - 1
Expand Down Expand Up @@ -271,7 +273,6 @@ def __call__(self, results):
def __repr__(self):
repr_str = self.__class__.__name__
repr_str += f'(rotate_ratio={self.rotate_ratio}, ' \
f'base_angles={self.base_angles}, ' \
f'angles_range={self.angles_range}, ' \
f'auto_bound={self.auto_bound})'
return repr_str
Expand Down
20 changes: 20 additions & 0 deletions tests/test_utils/test_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,23 @@ def test_transforms():
obboxes3 = rtf.hbb2obb(hbboxes, 'le90')
assert not np.allclose(obboxes1.numpy(), obboxes2)
assert np.allclose(obboxes2.numpy(), obboxes3)

# test full360
# Check obb2poly and poly2obb is inverse function in full360 rotation
for angle in np.linspace(-.9 * np.pi, .9 * np.pi, 4):
# numpy version
box_np = np.array((100, 100, 80, 50, angle), dtype=np.float32)
pts_np = rtf.obb2poly_np(box_np[None], version='full360')[0]
box2_np = rtf.poly2obb_np(pts_np, version='full360')
np.testing.assert_almost_equal(box_np, box2_np, decimal=4)

# torch version
box_torch = torch.tensor((100, 100, 80, 50, angle),
dtype=torch.float32)
pts_torch = rtf.obb2poly(box_torch[None], version='full360')[0]
box2_torch = rtf.poly2obb(pts_torch, version='full360')[0]
torch.norm(box_torch - box2_torch) < 1e-4

# compatibility between numpy and torch implementations
torch.norm(box_torch - torch.from_numpy(box_np)) < 1e-4
torch.norm(pts_torch - torch.from_numpy(pts_np)) < 1e-4

1 comment on commit 95123b9

@yangxue0827
Copy link
Collaborator

@yangxue0827 yangxue0827 commented on 95123b9 Sep 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@calcoloergosum
Line 83 and line 86 will produce incorrect image flips, especially for the original centered representation. Please check it and fix it.

Please sign in to comment.