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

[Feature] Full angle range #1061

Merged
merged 4 commits into from
Sep 13, 2024
Merged
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
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()
Copy link
Collaborator

Choose a reason for hiding this comment

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

This new line of code will produce incorrect image flips, especially for the original centered representation. Please check it and fix it.

elif direction == 'vertical':
flipped[:, 1] = img_shape[0] - bboxes[:, 1] - 1
flipped[:4] = flipped[[1, 0, 3, 2]].copy()
Copy link
Collaborator

Choose a reason for hiding this comment

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

This new line of code will produce incorrect image flips, especially for the original centered representation. Please check it and fix it.

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
Loading