From 95123b9340469cea721dd2cdf840dbd40a4708cd Mon Sep 17 00:00:00 2001 From: Han Jaeseung Date: Fri, 13 Sep 2024 12:48:14 +0900 Subject: [PATCH] [Feature] Full angle range (#1061) * full range added * precommit hook * test update * chirality preserving flip --- mmrotate/core/bbox/coder/angle_coder.py | 12 +- .../bbox/coder/delta_xywha_hbbox_coder.py | 4 +- .../bbox/coder/delta_xywha_rbbox_coder.py | 4 +- mmrotate/core/bbox/transforms.py | 115 ++++++++++++++++++ mmrotate/datasets/pipelines/transforms.py | 3 +- tests/test_utils/test_transformer.py | 20 +++ 6 files changed, 150 insertions(+), 8 deletions(-) diff --git a/mmrotate/core/bbox/coder/angle_coder.py b/mmrotate/core/bbox/coder/angle_coder.py index b84000a5e..760065d1b 100644 --- a/mmrotate/core/bbox/coder/angle_coder.py +++ b/mmrotate/core/bbox/coder/angle_coder.py @@ -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 diff --git a/mmrotate/core/bbox/coder/delta_xywha_hbbox_coder.py b/mmrotate/core/bbox/coder/delta_xywha_hbbox_coder.py index 0d99a4f22..09c692e89 100644 --- a/mmrotate/core/bbox/coder/delta_xywha_hbbox_coder.py +++ b/mmrotate/core/bbox/coder/delta_xywha_hbbox_coder.py @@ -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) @@ -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, diff --git a/mmrotate/core/bbox/coder/delta_xywha_rbbox_coder.py b/mmrotate/core/bbox/coder/delta_xywha_rbbox_coder.py index e14ced676..1b2216eb3 100644 --- a/mmrotate/core/bbox/coder/delta_xywha_rbbox_coder.py +++ b/mmrotate/core/bbox/coder/delta_xywha_rbbox_coder.py @@ -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) @@ -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, diff --git a/mmrotate/core/bbox/transforms.py b/mmrotate/core/bbox/transforms.py index 01d51651e..0153327c2 100644 --- a/mmrotate/core/bbox/transforms.py +++ b/mmrotate/core/bbox/transforms.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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. @@ -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. @@ -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. @@ -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. @@ -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.') diff --git a/mmrotate/datasets/pipelines/transforms.py b/mmrotate/datasets/pipelines/transforms.py index 63e832182..6fa523c2f 100644 --- a/mmrotate/datasets/pipelines/transforms.py +++ b/mmrotate/datasets/pipelines/transforms.py @@ -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 @@ -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 diff --git a/tests/test_utils/test_transformer.py b/tests/test_utils/test_transformer.py index f33bf9205..173e9ffd2 100644 --- a/tests/test_utils/test_transformer.py +++ b/tests/test_utils/test_transformer.py @@ -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