Source code for magbotsim.utils.geometry_2D_utils

##########################################################
# Copyright (c) 2024 Lara Bergmann, Bielefeld University #
##########################################################

import numpy as np
from scipy.spatial.transform import Rotation as R


[docs] def check_line_segments_intersect(p1: np.ndarray, p2: np.ndarray, q1: np.ndarray, q2: np.ndarray) -> np.ndarray: """Check whether two line segments p and q intersect by considering the orientation of ordered points, as explained here: https://www.dcs.gla.ac.uk/~pat/52233/slides/Geometry1x1.pdf. The two line segments are each defined by two points in the (x,y)-plane (p1 and p2; q1 and q2). This function is vectorized and can perform multiple checks. In the case of multiple tests, the line segments given by the points ``p1[i,:]``, ``p2[i,:]``, ``q1[i,:]``, ``q2[i,:]`` are tested. :param p1: a numpy array of shape (num_checks,2) specifying the first points belonging to p :param p2: a numpy array of shape (num_checks,2) specifying the second points belonging to p :param q1: a numpy array of shape (num_checks,2) specifying the first points belonging to q :param q2: a numpy array of shape (num_checks,2) specifying the second points belonging to q :return: a numpy array of shape (num_checks,), where an entry is True if the two line segments intersect, False otherwise """ num_checks = p1.shape[0] assert p1.shape == (num_checks, 2) assert p2.shape == (num_checks, 2) assert q1.shape == (num_checks, 2) assert q2.shape == (num_checks, 2) ls_intersect = np.zeros(num_checks).astype(bool) mask_points_equal = ( (np.sum((np.abs(p1 - q1) < 1e-7), axis=1) == 2) + (np.sum((np.abs(p1 - q2) < 1e-7), axis=1) == 2) + (np.sum((np.abs(p2 - q1) < 1e-7), axis=1) == 2) + (np.sum((np.abs(p2 - q2) < 1e-7), axis=1) == 2) ) min_xy_p = np.minimum(p1, p2) min_xy_q = np.minimum(q1, q2) max_xy_p = np.maximum(p1, p2) max_xy_q = np.maximum(q1, q2) mask_pq = max_xy_p < min_xy_q mask_qp = max_xy_q < min_xy_p mask_minmax = mask_pq * (1 - (np.abs(max_xy_p - min_xy_q) < 1e-7)) + mask_qp * (1 - (np.abs(max_xy_q - min_xy_p) < 1e-7)) mask_minmax = np.sum(mask_minmax, axis=1) >= 1 p11 = np.pad(p1, ((0, 0), (0, 1)), mode='constant', constant_values=1) p21 = np.pad(p2, ((0, 0), (0, 1)), mode='constant', constant_values=1) q11 = np.pad(q1, ((0, 0), (0, 1)), mode='constant', constant_values=1) q21 = np.pad(q2, ((0, 0), (0, 1)), mode='constant', constant_values=1) mat_p11p21q11 = np.swapaxes(np.swapaxes(np.array([p11, p21, q11]), 0, 1), 1, 2) mat_p11p21q21 = np.swapaxes(np.swapaxes(np.array([p11, p21, q21]), 0, 1), 1, 2) mat_q11q21p11 = np.swapaxes(np.swapaxes(np.array([q11, q21, p11]), 0, 1), 1, 2) mat_q11q21p21 = np.swapaxes(np.swapaxes(np.array([q11, q21, p21]), 0, 1), 1, 2) det_p11p21q11 = np.linalg.det(mat_p11p21q11) det_p11p21q21 = np.linalg.det(mat_p11p21q21) det_q11q21p11 = np.linalg.det(mat_q11q21p11) det_q11q21p21 = np.linalg.det(mat_q11q21p21) mask_orientation = ((np.sign(det_p11p21q11 * det_p11p21q21) <= 0) + (np.abs(det_p11p21q11 * det_p11p21q21) < 1e-7)) * ( (np.sign(det_q11q21p11 * det_q11q21p21) <= 0) + (np.abs(det_q11q21p11 * det_q11q21p21) < 1e-7) ) ls_intersect[mask_orientation] = True ls_intersect[mask_minmax] = False ls_intersect[mask_points_equal] = True return ls_intersect
[docs] def get_2D_rect_vertices(qpos: np.ndarray, size: np.ndarray) -> np.ndarray: """Get the (x,y) coordinates of the vertices of rectangles w.r.t. the base frame. This function is vectorized and can calculate the vertices of multiple rectangles. :param qpos: qpos (position and orientation) of the rectangles specified as a numpy array of shape (num_rectangles,7) (x_p,y_p,z_p,w_o,x_o,y_o,z_o) :param size: length and width (half-size) of the rectangles specified as a numpy array of shape (num_rectangles,2) :return: the (x,y) coordinates of the vertices (numpy array of shape (num_rectangles,2,4)) """ num_rectangles = qpos.shape[0] assert qpos.shape == (num_rectangles, 7) assert size.shape == (num_rectangles, 2) quats = qpos[:, -4:].copy() r_quats = R.from_quat(quats, scalar_first=True) # quaternions are automatically normalized before initialization rot_mats = r_quats.as_matrix() assert rot_mats.shape == (num_rectangles, 3, 3) # vertices w.r.t. local frame of each rectangle vertices_l = np.array( [ [-size[:, 0], -size[:, 0], size[:, 0], size[:, 0]], [-size[:, 1], size[:, 1], size[:, 1], -size[:, 1]], np.zeros((4, num_rectangles)), ] ) vertices_l = np.swapaxes(vertices_l, 0, 2) vertices_l = np.swapaxes(vertices_l, 1, 2) # vertices w.r.t. base frame vertices_b = (rot_mats @ vertices_l)[:, :2, :] + np.repeat(qpos[:, :2].reshape((num_rectangles, 2, -1)), 4, axis=2) return vertices_b
[docs] def check_rectangles_intersect( qpos_r1: np.ndarray, qpos_r2: np.ndarray, size_r1: np.ndarray, size_r2: np.ndarray, eps: float = 1e-8 ) -> np.ndarray: """Check whether two rectangles of any orientation intersect. This function is vectorized and can perform multiple checks, i.e. it is checked whether the rectangles with qpos ``qpos_r1[i,:]`` and ``qpos_r2[i,:]`` and sizes ``size_r1[i,:]`` and ``size_r2[i,:]`` intersect. :param qpos_r1: qpos (position and orientation) of the first rectangles specified as a numpy array of shape (num_checks,7) (x_p,y_p,z_p,w_o,x_o,y_o,z_o) :param qpos_r2: qpos (position and orientation) of the second rectangles specified as a numpy array of shape (num_checks,7) (x_p,y_p,z_p,w_o,x_o,y_o,z_o) :param size_r1: length and width (half-size) of the first rectangles specified as a numpy array of shape (num_checks,2) :param size_r2: length and width (half-size) of the second rectangles specified as a numpy array of shape (num_checks,2) :return: a numpy array of shape (num_checks,), where an entry is True if the rectangles intersect, False otherwise """ num_checks = qpos_r1.shape[0] assert qpos_r1.shape == (num_checks, 7) assert qpos_r2.shape == (num_checks, 7) assert size_r1.shape == (num_checks, 2) assert size_r2.shape == (num_checks, 2) pos1 = qpos_r1[:, :2] pos2 = qpos_r2[:, :2] w1, x1, y1, z1 = qpos_r1[:, 3:].T w2, x2, y2, z2 = qpos_r2[:, 3:].T yaw1 = np.arctan2(2 * (w1 * z1 + x1 * y1), 1 - 2 * (y1 * y1 + z1 * z1)) yaw2 = np.arctan2(2 * (w2 * z2 + x2 * y2), 1 - 2 * (y2 * y2 + z2 * z2)) c1, s1 = np.cos(yaw1), np.sin(yaw1) c2, s2 = np.cos(yaw2), np.sin(yaw2) R1 = np.stack((np.stack((c1, -s1), axis=1), np.stack((s1, c1), axis=1)), axis=1) R2 = np.stack((np.stack((c2, -s2), axis=1), np.stack((s2, c2), axis=1)), axis=1) half10, half11 = size_r1[:, 0], size_r1[:, 1] half20, half21 = size_r2[:, 0], size_r2[:, 1] axes = np.stack([R1[:, :, 0], R1[:, :, 1], R2[:, :, 0], R2[:, :, 1]], axis=1) axis_norm = axes / np.sqrt(np.sum(axes**2, axis=2, keepdims=True)) center_diff = (pos2 - pos1)[:, None, :] proj_center_diff = np.abs(np.sum(center_diff * axis_norm, axis=2)) proj_r1 = np.abs(half10[:, None] * np.sum(R1[:, None, :, 0] * axis_norm, axis=2)) + np.abs( half11[:, None] * np.sum(R1[:, None, :, 1] * axis_norm, axis=2) ) proj_r2 = np.abs(half20[:, None] * np.sum(R2[:, None, :, 0] * axis_norm, axis=2)) + np.abs( half21[:, None] * np.sum(R2[:, None, :, 1] * axis_norm, axis=2) ) return ~np.any(proj_center_diff > (proj_r1 + proj_r2 + eps), axis=1)