# Copyright 2022 The KerasCV Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import numpy as np import tensorflow as tf from absl.testing import parameterized from keras_cv import ops class Boxes3DTestCase(tf.test.TestCase, parameterized.TestCase): def test_convert_center_to_corners(self): boxes = tf.constant( [ [[1, 2, 3, 4, 3, 6, 0], [1, 2, 3, 4, 3, 6, 0]], [[1, 2, 3, 4, 3, 6, np.pi / 2.0], [1, 2, 3, 4, 3, 6, np.pi / 2.0]], ] ) corners = ops._center_xyzWHD_to_corner_xyz(boxes) self.assertEqual((2, 2, 8, 3), corners.shape) for i in [0, 1]: self.assertAllClose(-1, np.min(corners[0, i, :, 0])) self.assertAllClose(3, np.max(corners[0, i, :, 0])) self.assertAllClose(0.5, np.min(corners[0, i, :, 1])) self.assertAllClose(3.5, np.max(corners[0, i, :, 1])) self.assertAllClose(0, np.min(corners[0, i, :, 2])) self.assertAllClose(6, np.max(corners[0, i, :, 2])) for i in [0, 1]: self.assertAllClose(-0.5, np.min(corners[1, i, :, 0])) self.assertAllClose(2.5, np.max(corners[1, i, :, 0])) self.assertAllClose(0.0, np.min(corners[1, i, :, 1])) self.assertAllClose(4.0, np.max(corners[1, i, :, 1])) self.assertAllClose(0, np.min(corners[1, i, :, 2])) self.assertAllClose(6, np.max(corners[1, i, :, 2])) def test_within_box2d(self): boxes = tf.constant( [[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]], dtype=tf.float32 ) points = tf.constant( [ [-0.5, -0.5], [0.5, -0.5], [1.5, -0.5], [1.5, 0.5], [1.5, 1.5], [0.5, 1.5], [-0.5, 1.5], [-0.5, 0.5], [1.0, 1.0], [0.5, 0.5], ], dtype=tf.float32, ) is_inside = ops.is_within_box2d(points, boxes) expected = [[False]] * 8 + [[True]] * 2 self.assertAllEqual(expected, is_inside) def test_within_zero_box2d(self): bbox = tf.constant( [[[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]]], dtype=tf.float32 ) points = tf.constant( [ [-0.5, -0.5], [0.5, -0.5], [1.5, -0.5], [1.5, 0.5], [1.5, 1.5], [0.5, 1.5], [-0.5, 1.5], [-0.5, 0.5], [1.0, 1.0], [0.5, 0.5], ], dtype=tf.float32, ) is_inside = ops.is_within_box2d(points, bbox) expected = [[False]] * 10 self.assertAllEqual(expected, is_inside) def test_is_on_lefthand_side(self): v1 = tf.constant([[0.0, 0.0]], dtype=tf.float32) v2 = tf.constant([[1.0, 0.0]], dtype=tf.float32) p = tf.constant([[0.5, 0.5], [-1.0, -3], [-1.0, 1.0]], dtype=tf.float32) res = ops._is_on_lefthand_side(p, v1, v2) self.assertAllEqual([[True, False, True]], res) res = ops._is_on_lefthand_side(v1, v1, v2) self.assertAllEqual([[True]], res) res = ops._is_on_lefthand_side(v2, v1, v2) self.assertAllEqual([[True]], res) @parameterized.named_parameters( ("without_rotation", 0.0), ("with_rotation_1_rad", 1.0), ("with_rotation_2_rad", 2.0), ("with_rotation_3_rad", 3.0), ) def test_box_area(self, angle): boxes = tf.constant( [ [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]], [[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]], ], dtype=tf.float32, ) expected = [[1.0], [2.0], [4.0]] def _rotate(bbox, theta): rotation_matrix = tf.reshape( [tf.cos(theta), -tf.sin(theta), tf.sin(theta), tf.cos(theta)], shape=(2, 2), ) return tf.matmul(bbox, rotation_matrix) rotated_bboxes = _rotate(boxes, angle) res = ops._box_area(rotated_bboxes) self.assertAllClose(expected, res) def test_within_box3d(self): num_points, num_boxes = 19, 4 # rotate the first box by pi / 2 so dim_x and dim_y are swapped. # The last box is a cube rotated by 45 degrees. bboxes = tf.constant( [ [1.0, 2.0, 3.0, 6.0, 0.4, 6.0, np.pi / 2], [4.0, 5.0, 6.0, 7.0, 0.8, 7.0, 0.0], [0.4, 0.3, 0.2, 0.1, 0.1, 0.2, 0.0], [-10.0, -10.0, -10.0, 3.0, 3.0, 3.0, np.pi / 4], ], dtype=tf.float32, ) points = tf.constant( [ [1.0, 2.0, 3.0], # box 0 (centroid) [0.8, 2.0, 3.0], # box 0 (below x) [1.1, 2.0, 3.0], # box 0 (above x) [1.3, 2.0, 3.0], # box 0 (too far x) [0.7, 2.0, 3.0], # box 0 (too far x) [4.0, 5.0, 6.0], # box 1 (centroid) [4.0, 4.6, 6.0], # box 1 (below y) [4.0, 5.4, 6.0], # box 1 (above y) [4.0, 4.5, 6.0], # box 1 (too far y) [4.0, 5.5, 6.0], # box 1 (too far y) [0.4, 0.3, 0.2], # box 2 (centroid) [0.4, 0.3, 0.1], # box 2 (below z) [0.4, 0.3, 0.3], # box 2 (above z) [0.4, 0.3, 0.0], # box 2 (too far z) [0.4, 0.3, 0.4], # box 2 (too far z) [5.0, 7.0, 8.0], # none [1.0, 5.0, 3.6], # box0, box1 [-11.6, -10.0, -10.0], # box3 (rotated corner point). [-11.4, -11.4, -10.0], # not in box3, would be if not rotated. ], dtype=tf.float32, ) expected_is_inside = np.array( [ [True, False, False, False], [True, False, False, False], [True, False, False, False], [False, False, False, False], [False, False, False, False], [False, True, False, False], [False, True, False, False], [False, True, False, False], [False, False, False, False], [False, False, False, False], [False, False, True, False], [False, False, True, False], [False, False, True, False], [False, False, False, False], [False, False, False, False], [False, False, False, False], [True, True, False, False], [False, False, False, True], [False, False, False, False], ] ) assert points.shape[0] == num_points assert bboxes.shape[0] == num_boxes assert expected_is_inside.shape[0] == num_points assert expected_is_inside.shape[1] == num_boxes is_inside = ops.is_within_box3d(points, bboxes) self.assertAllEqual([num_points, num_boxes], is_inside.shape) self.assertAllEqual(expected_is_inside, is_inside) # Add a batch dimension to the data and see that it still works # as expected. batch_size = 3 points = tf.tile(points[tf.newaxis, ...], [batch_size, 1, 1]) bboxes = tf.tile(bboxes[tf.newaxis, ...], [batch_size, 1, 1]) is_inside = ops.is_within_box3d(points, bboxes) self.assertAllEqual([batch_size, num_points, num_boxes], is_inside.shape) for batch_idx in range(batch_size): self.assertAllEqual(expected_is_inside, is_inside[batch_idx]) def testCoordinateTransform(self): # This is a validated test case from a real scene. # # A single point [1, 1, 3]. point = tf.constant( [[[5736.94580078, 1264.85168457, 45.0271225]]], dtype=tf.float32 ) # Replicate the point to test broadcasting behavior. replicated_points = tf.tile(point, [2, 4, 1]) # Pose of the car (x, y, z, yaw, roll, pitch). # # We negate the translations so that the coordinates are translated # such that the car is at the origin. pose = tf.constant( [ -5728.77148438, -1264.42236328, -45.06399918, -3.10496902, 0.03288471, 0.00115049, ], dtype=tf.float32, ) result = ops.coordinate_transform(replicated_points, pose) # We expect the point to be translated close to the car, and then rotated # mostly around the x-axis. # the result is device dependent, skip or ignore this test locally if it fails. expected = np.tile([[[-8.184512, -0.13086952, -0.04200769]]], [2, 4, 1]) self.assertAllClose(expected, result) def testSphericalCoordinatesTransform(self): np_xyz = np.random.randn(5, 6, 3) points = tf.constant(np_xyz, dtype=tf.float32) spherical_coordinates = ops.spherical_coordinate_transform(points) # Convert coordinates back to xyz to verify. dist = spherical_coordinates[..., 0] theta = spherical_coordinates[..., 1] phi = spherical_coordinates[..., 2] x = dist * np.sin(theta) * np.cos(phi) y = dist * np.sin(theta) * np.sin(phi) z = dist * np.cos(theta) self.assertAllClose(x, np_xyz[..., 0]) self.assertAllClose(y, np_xyz[..., 1]) self.assertAllClose(z, np_xyz[..., 2])