Skip to content

Commit 95123b9

Browse files
[Feature] Full angle range (#1061)
* full range added * precommit hook * test update * chirality preserving flip
1 parent 9ea1aee commit 95123b9

6 files changed

Lines changed: 150 additions & 8 deletions

File tree

mmrotate/core/bbox/coder/angle_coder.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,16 @@ class CSLCoder(BaseBBoxCoder):
2727
def __init__(self, angle_version, omega=1, window='gaussian', radius=6):
2828
super().__init__()
2929
self.angle_version = angle_version
30-
assert angle_version in ['oc', 'le90', 'le135']
30+
assert angle_version in ['oc', 'le90', 'le135', 'full360']
3131
assert window in ['gaussian', 'triangle', 'rect', 'pulse']
32-
self.angle_range = 90 if angle_version == 'oc' else 180
33-
self.angle_offset_dict = {'oc': 0, 'le90': 90, 'le135': 45}
32+
self.angle_range = 90 if angle_version == 'oc' else \
33+
(360 if angle_version == 'full360' else 180)
34+
self.angle_offset_dict = {
35+
'oc': 0,
36+
'le90': 90,
37+
'le135': 45,
38+
'full360': 180
39+
}
3440
self.angle_offset = self.angle_offset_dict[angle_version]
3541
self.omega = omega
3642
self.window = window

mmrotate/core/bbox/coder/delta_xywha_hbbox_coder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def encode(self, bboxes, gt_bboxes):
6868
assert bboxes.size(0) == gt_bboxes.size(0)
6969
assert bboxes.size(-1) == 4
7070
assert gt_bboxes.size(-1) == 5
71-
if self.angle_range in ['oc', 'le135', 'le90']:
71+
if self.angle_range in ['oc', 'le135', 'le90', 'full360']:
7272
return bbox2delta(bboxes, gt_bboxes, self.means, self.stds,
7373
self.angle_range, self.norm_factor,
7474
self.edge_swap)
@@ -104,7 +104,7 @@ def decode(self,
104104
assert pred_bboxes.size(1) == bboxes.size(1)
105105
assert bboxes.size(-1) == 4
106106
assert pred_bboxes.size(-1) == 5
107-
if self.angle_range in ['oc', 'le135', 'le90']:
107+
if self.angle_range in ['oc', 'le135', 'le90', 'full360']:
108108
return delta2bbox(bboxes, pred_bboxes, self.means, self.stds,
109109
wh_ratio_clip, self.add_ctr_clamp,
110110
self.ctr_clamp, self.angle_range,

mmrotate/core/bbox/coder/delta_xywha_rbbox_coder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def encode(self, bboxes, gt_bboxes):
6767
assert bboxes.size(0) == gt_bboxes.size(0)
6868
assert bboxes.size(-1) == 5
6969
assert gt_bboxes.size(-1) == 5
70-
if self.angle_range in ['oc', 'le135', 'le90']:
70+
if self.angle_range in ['oc', 'le135', 'le90', 'full360']:
7171
return bbox2delta(bboxes, gt_bboxes, self.means, self.stds,
7272
self.angle_range, self.norm_factor,
7373
self.edge_swap, self.proj_xy)
@@ -99,7 +99,7 @@ def decode(self,
9999
torch.Tensor: Decoded boxes.
100100
"""
101101
assert pred_bboxes.size(0) == bboxes.size(0)
102-
if self.angle_range in ['oc', 'le135', 'le90']:
102+
if self.angle_range in ['oc', 'le135', 'le90', 'full360']:
103103
return delta2bbox(bboxes, pred_bboxes, self.means, self.stds,
104104
max_shape, wh_ratio_clip, self.add_ctr_clamp,
105105
self.ctr_clamp, self.angle_range,

mmrotate/core/bbox/transforms.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def poly2obb(polys, version='oc'):
108108
results = poly2obb_le135(polys)
109109
elif version == 'le90':
110110
results = poly2obb_le90(polys)
111+
elif version == 'full360':
112+
results = poly2obb_full360(polys)
111113
else:
112114
raise NotImplementedError
113115
return results
@@ -129,6 +131,8 @@ def poly2obb_np(polys, version='oc'):
129131
results = poly2obb_np_le135(polys)
130132
elif version == 'le90':
131133
results = poly2obb_np_le90(polys)
134+
elif version == 'full360':
135+
results = poly2obb_np_full360(polys)
132136
else:
133137
raise NotImplementedError
134138
return results
@@ -150,6 +154,9 @@ def obb2hbb(rbboxes, version='oc'):
150154
results = obb2hbb_le135(rbboxes)
151155
elif version == 'le90':
152156
results = obb2hbb_le90(rbboxes)
157+
elif version == 'full360':
158+
# NOTE: same as 90
159+
results = obb2hbb_le90(rbboxes)
153160
else:
154161
raise NotImplementedError
155162
return results
@@ -171,6 +178,9 @@ def obb2poly(rbboxes, version='oc'):
171178
results = obb2poly_le135(rbboxes)
172179
elif version == 'le90':
173180
results = obb2poly_le90(rbboxes)
181+
elif version == 'full360':
182+
# NOTE: same as 90
183+
results = obb2poly_le90(rbboxes)
174184
else:
175185
raise NotImplementedError
176186
return results
@@ -192,6 +202,8 @@ def obb2poly_np(rbboxes, version='oc'):
192202
results = obb2poly_np_le135(rbboxes)
193203
elif version == 'le90':
194204
results = obb2poly_np_le90(rbboxes)
205+
elif version == 'full360':
206+
results = obb2poly_np_full360(rbboxes)
195207
else:
196208
raise NotImplementedError
197209
return results
@@ -213,6 +225,9 @@ def obb2xyxy(rbboxes, version='oc'):
213225
results = obb2xyxy_le135(rbboxes)
214226
elif version == 'le90':
215227
results = obb2xyxy_le90(rbboxes)
228+
elif version == 'full360':
229+
# NOTE: same as 90
230+
results = obb2xyxy_le90(rbboxes)
216231
else:
217232
raise NotImplementedError
218233
return results
@@ -235,6 +250,7 @@ def hbb2obb(hbboxes, version='oc'):
235250
elif version == 'le90':
236251
results = hbb2obb_le90(hbboxes)
237252
else:
253+
# NOTE: not well defined for full360. Leave it unimplemented
238254
raise NotImplementedError
239255
return results
240256

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

300316

317+
def poly2obb_full360(polys):
318+
"""Convert polygons to oriented bounding boxes.
319+
320+
Args:
321+
polys (torch.Tensor): [x0,y0,x1,y1,x2,y2,x3,y3]
322+
323+
Returns:
324+
obbs (torch.Tensor): [x_ctr,y_ctr,w,h,angle]
325+
"""
326+
polys = torch.reshape(polys, [-1, 8])
327+
pt1, pt2, pt3, _ = polys[..., :8].chunk(4, 1)
328+
width = torch.sqrt(
329+
torch.pow(pt1[..., 0] - pt2[..., 0], 2) +
330+
torch.pow(pt1[..., 1] - pt2[..., 1], 2))
331+
height = torch.sqrt(
332+
torch.pow(pt2[..., 0] - pt3[..., 0], 2) +
333+
torch.pow(pt2[..., 1] - pt3[..., 1], 2))
334+
angles = torch.atan2((pt1[..., 1] - pt2[..., 1]),
335+
(pt1[..., 0] - pt2[..., 0]))
336+
angles = norm_angle(angles, 'full360')
337+
x_ctr = (pt1[..., 0] + pt3[..., 0]) / 2.0
338+
y_ctr = (pt1[..., 1] + pt3[..., 1]) / 2.0
339+
return torch.stack([x_ctr, y_ctr, width, height, angles], 1)
340+
341+
301342
def poly2obb_le90(polys):
302343
"""Convert polygons to oriented bounding boxes.
303344
@@ -418,6 +459,33 @@ def poly2obb_np_le90(poly):
418459
return x, y, w, h, a
419460

420461

462+
def poly2obb_np_full360(poly):
463+
"""Convert polygons to oriented bounding boxes. Assumes head points then
464+
tail points.
465+
466+
Args:
467+
polys (ndarray): [x0,y0,x1,y1,x2,y2,x3,y3]
468+
469+
Returns:
470+
obbs (ndarray): [x_ctr,y_ctr,w,h,angle]
471+
"""
472+
pt1, pt2, pt3, pt4 = np.array(poly).reshape((4, 2))
473+
x, y = (pt1 + pt2 + pt3 + pt4) / 4.0
474+
dx, dy = pt2 - pt1
475+
a = np.arctan2(dy, dx)
476+
w = np.linalg.norm(pt2 - pt1)
477+
h = np.linalg.norm(pt3 - pt2)
478+
if w < 2 or h < 2:
479+
return
480+
while not np.pi > a >= -np.pi:
481+
if a >= np.pi:
482+
a -= np.pi
483+
else:
484+
a += np.pi
485+
assert np.pi > a >= -np.pi
486+
return x, y, w, h, a
487+
488+
421489
def obb2poly_oc(rboxes):
422490
"""Convert oriented bounding boxes to polygons.
423491
@@ -634,6 +702,26 @@ def hbb2obb_le90(hbboxes):
634702
return obboxes
635703

636704

705+
def hbb2obb_full360(hbboxes):
706+
"""Convert horizontal bounding boxes to oriented bounding boxes.
707+
708+
Args:
709+
hbbs (torch.Tensor): [x_lt,y_lt,x_rb,y_rb]
710+
711+
Returns:
712+
obbs (torch.Tensor): [x_ctr,y_ctr,w,h,angle]
713+
"""
714+
x = (hbboxes[..., 0] + hbboxes[..., 2]) * 0.5
715+
y = (hbboxes[..., 1] + hbboxes[..., 3]) * 0.5
716+
w = hbboxes[..., 2] - hbboxes[..., 0]
717+
h = hbboxes[..., 3] - hbboxes[..., 1]
718+
theta = x.new_zeros(*x.shape)
719+
obboxes1 = torch.stack([x, y, w, h, theta], dim=-1)
720+
obboxes2 = torch.stack([x, y, h, w, theta - np.pi / 2], dim=-1)
721+
obboxes = torch.where((w >= h)[..., None], obboxes1, obboxes2)
722+
return obboxes
723+
724+
637725
def obb2xyxy_oc(rbboxes):
638726
"""Convert oriented bounding boxes to horizontal bounding boxes.
639727
@@ -783,6 +871,31 @@ def obb2poly_np_le90(obboxes):
783871
return polys
784872

785873

874+
def obb2poly_np_full360(obboxes):
875+
"""Convert oriented bounding boxes to polygons.
876+
877+
Args:
878+
obbs (ndarray): [x_ctr,y_ctr,w,h,angle,score]
879+
880+
Returns:
881+
polys (ndarray): [x0,y0,x1,y1,x2,y2,x3,y3,score]
882+
"""
883+
try:
884+
center, w, h, theta, score = np.split(obboxes, (2, 3, 4, 5), axis=-1)
885+
except: # noqa: E722
886+
results = np.stack([0., 0., 0., 0., 0., 0., 0., 0., 0.], axis=-1)
887+
return results.reshape(1, -1)
888+
Cos, Sin = np.cos(theta), np.sin(theta)
889+
vector1 = np.concatenate([w / 2 * Cos, w / 2 * Sin], axis=-1)
890+
vector2 = np.concatenate([-h / 2 * Sin, h / 2 * Cos], axis=-1)
891+
point1 = center - vector1 - vector2
892+
point2 = center + vector1 - vector2
893+
point3 = center + vector1 + vector2
894+
point4 = center - vector1 + vector2
895+
polys = np.concatenate([point1, point2, point3, point4, score], axis=-1)
896+
return polys
897+
898+
786899
def cal_line_length(point1, point2):
787900
"""Calculate the length of line.
788901
@@ -863,6 +976,8 @@ def norm_angle(angle, angle_range):
863976
return (angle + np.pi / 4) % np.pi - np.pi / 4
864977
elif angle_range == 'le90':
865978
return (angle + np.pi / 2) % np.pi - np.pi / 2
979+
elif angle_range == 'full360':
980+
return angle % (2 * np.pi) - np.pi
866981
else:
867982
print('Not yet implemented.')
868983

mmrotate/datasets/pipelines/transforms.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@ def bbox_flip(self, bboxes, img_shape, direction):
8080
flipped = bboxes.copy()
8181
if direction == 'horizontal':
8282
flipped[:, 0] = img_shape[1] - bboxes[:, 0] - 1
83+
flipped[:4] = flipped[[1, 0, 3, 2]].copy()
8384
elif direction == 'vertical':
8485
flipped[:, 1] = img_shape[0] - bboxes[:, 1] - 1
86+
flipped[:4] = flipped[[1, 0, 3, 2]].copy()
8587
elif direction == 'diagonal':
8688
flipped[:, 0] = img_shape[1] - bboxes[:, 0] - 1
8789
flipped[:, 1] = img_shape[0] - bboxes[:, 1] - 1
@@ -271,7 +273,6 @@ def __call__(self, results):
271273
def __repr__(self):
272274
repr_str = self.__class__.__name__
273275
repr_str += f'(rotate_ratio={self.rotate_ratio}, ' \
274-
f'base_angles={self.base_angles}, ' \
275276
f'angles_range={self.angles_range}, ' \
276277
f'auto_bound={self.auto_bound})'
277278
return repr_str

tests/test_utils/test_transformer.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,23 @@ def test_transforms():
2828
obboxes3 = rtf.hbb2obb(hbboxes, 'le90')
2929
assert not np.allclose(obboxes1.numpy(), obboxes2)
3030
assert np.allclose(obboxes2.numpy(), obboxes3)
31+
32+
# test full360
33+
# Check obb2poly and poly2obb is inverse function in full360 rotation
34+
for angle in np.linspace(-.9 * np.pi, .9 * np.pi, 4):
35+
# numpy version
36+
box_np = np.array((100, 100, 80, 50, angle), dtype=np.float32)
37+
pts_np = rtf.obb2poly_np(box_np[None], version='full360')[0]
38+
box2_np = rtf.poly2obb_np(pts_np, version='full360')
39+
np.testing.assert_almost_equal(box_np, box2_np, decimal=4)
40+
41+
# torch version
42+
box_torch = torch.tensor((100, 100, 80, 50, angle),
43+
dtype=torch.float32)
44+
pts_torch = rtf.obb2poly(box_torch[None], version='full360')[0]
45+
box2_torch = rtf.poly2obb(pts_torch, version='full360')[0]
46+
torch.norm(box_torch - box2_torch) < 1e-4
47+
48+
# compatibility between numpy and torch implementations
49+
torch.norm(box_torch - torch.from_numpy(box_np)) < 1e-4
50+
torch.norm(pts_torch - torch.from_numpy(pts_np)) < 1e-4

0 commit comments

Comments
 (0)