|
|
""" |
|
|
Written by Jinhyung Park |
|
|
|
|
|
Simple BEV visualization for 3D points & boxes. |
|
|
""" |
|
|
|
|
|
import cv2 |
|
|
import matplotlib |
|
|
import numpy as np |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Canvas_BEV(object): |
|
|
def __init__( |
|
|
self, |
|
|
canvas_shape=(2000, 2000), |
|
|
canvas_x_range=(-50.0, 50.0), |
|
|
canvas_y_range=(-50.0, 50.0), |
|
|
canvas_bg_color=(0, 0, 0), |
|
|
): |
|
|
""" |
|
|
Args: |
|
|
canvas_shape (Tuple[int]): Shape of BEV Canvas image. First element |
|
|
corresponds to X range, the second element to Y range. |
|
|
canvas_x_range (Tuple[int]): Range of X-coords to visualize. X is |
|
|
vertical: negative ~ positive is top ~ down. |
|
|
canvas_y_range (Tuple[int]): Range of Y-coords to visualize. Y is |
|
|
horizontal: negative ~ positive is left ~ right. |
|
|
canvas_bg_color (Tuple[int]): RGB (0 ~ 255) of Canvas background |
|
|
color. |
|
|
""" |
|
|
|
|
|
|
|
|
if (canvas_shape[0] / canvas_shape[1]) != ( |
|
|
(canvas_x_range[0] - canvas_x_range[1]) |
|
|
/ (canvas_y_range[0] - canvas_y_range[1]) |
|
|
): |
|
|
|
|
|
print( |
|
|
"Not an error, but the x & y ranges are not " |
|
|
"proportional to canvas height & width." |
|
|
) |
|
|
|
|
|
self.canvas_shape = canvas_shape |
|
|
self.canvas_x_range = canvas_x_range |
|
|
self.canvas_y_range = canvas_y_range |
|
|
self.canvas_bg_color = canvas_bg_color |
|
|
|
|
|
self.clear_canvas() |
|
|
|
|
|
def get_canvas(self): |
|
|
return self.canvas |
|
|
|
|
|
def clear_canvas(self): |
|
|
self.canvas = np.zeros((*self.canvas_shape, 3), dtype=np.uint8) |
|
|
self.canvas[..., :] = self.canvas_bg_color |
|
|
|
|
|
def get_canvas_coords(self, xy): |
|
|
""" |
|
|
Args: |
|
|
xy (ndarray): (N, 2+) array of coordinates. Additional columns |
|
|
beyond the first two are ignored. |
|
|
|
|
|
Returns: |
|
|
canvas_xy (ndarray): (N, 2) array of xy scaled into canvas |
|
|
coordinates. Invalid locations of canvas_xy are clipped into |
|
|
range. "x" is dim0, "y" is dim1 of canvas. |
|
|
valid_mask (ndarray): (N,) boolean mask indicating which of |
|
|
canvas_xy fits into canvas. |
|
|
""" |
|
|
xy = np.copy(xy) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
x = xy[:, 0] |
|
|
y = xy[:, 1] |
|
|
|
|
|
|
|
|
valid_mask = ( |
|
|
(x > self.canvas_x_range[0]) |
|
|
& (x < self.canvas_x_range[1]) |
|
|
& (y > self.canvas_y_range[0]) |
|
|
& (y < self.canvas_y_range[1]) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
x = (x - self.canvas_x_range[0]) / ( |
|
|
self.canvas_x_range[1] - self.canvas_x_range[0] |
|
|
) |
|
|
|
|
|
x = x * self.canvas_shape[0] |
|
|
|
|
|
x = np.clip(np.around(x), 0, self.canvas_shape[0] - 1).astype(np.int32) |
|
|
|
|
|
|
|
|
y = (y - self.canvas_y_range[0]) / ( |
|
|
self.canvas_y_range[1] - self.canvas_y_range[0] |
|
|
) |
|
|
y = y * self.canvas_shape[1] |
|
|
y = np.clip(np.around(y), 0, self.canvas_shape[1] - 1).astype(np.int32) |
|
|
|
|
|
|
|
|
canvas_xy = np.stack([x, y], axis=1) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return canvas_xy, valid_mask |
|
|
|
|
|
def draw_canvas_points( |
|
|
self, canvas_xy, radius=-1, colors=None, colors_operand=None |
|
|
): |
|
|
""" |
|
|
Draws canvas_xy onto self.canvas. |
|
|
|
|
|
Args: |
|
|
canvas_xy (ndarray): (N, 2) array of *valid* canvas coordinates. |
|
|
"x" is dim0, "y" is dim1 of canvas. |
|
|
radius (Int): |
|
|
-1: Each point is visualized as a single pixel. |
|
|
r: Each point is visualized as a circle with radius r. |
|
|
colors: |
|
|
None: colors all points white. |
|
|
Tuple: RGB (0 ~ 255), indicating a single color for all points. |
|
|
ndarray: (N, 3) array of RGB values for each point. |
|
|
String: Such as "Spectral", uses a matplotlib cmap, with the |
|
|
operand (the value cmap is called on for each point) being |
|
|
colors_operand. If colors_operand is None, uses normalized |
|
|
distance from (0, 0) of XY point coords. |
|
|
colors_operand (ndarray | None): (N,) array of values cooresponding |
|
|
to canvas_xy, to be used only if colors is a cmap. |
|
|
""" |
|
|
if len(canvas_xy) == 0: |
|
|
return |
|
|
|
|
|
if colors is None: |
|
|
colors = np.full((len(canvas_xy), 3), fill_value=255, dtype=np.uint8) |
|
|
elif isinstance(colors, tuple): |
|
|
assert len(colors) == 3 |
|
|
colors_tmp = np.zeros((len(canvas_xy), 3), dtype=np.uint8) |
|
|
colors_tmp[..., :] = np.array(colors) |
|
|
colors = colors_tmp |
|
|
elif isinstance(colors, np.ndarray): |
|
|
assert len(colors) == len(canvas_xy) |
|
|
colors = colors.astype(np.uint8) |
|
|
elif isinstance(colors, str): |
|
|
colors = matplotlib.cm.get_cmap(colors) |
|
|
if colors_operand is None: |
|
|
|
|
|
origin_center = self.get_canvas_coords(np.zeros((1, 2)))[0][0] |
|
|
colors_operand = np.sqrt(((canvas_xy - origin_center) ** 2).sum(axis=1)) |
|
|
|
|
|
|
|
|
colors_operand = colors_operand - colors_operand.min() |
|
|
colors_operand = colors_operand / colors_operand.max() |
|
|
|
|
|
|
|
|
|
|
|
colors = (colors(colors_operand)[:, :3] * 255).astype(np.uint8) |
|
|
else: |
|
|
raise Exception( |
|
|
"colors type {} was not an expected type".format(type(colors)) |
|
|
) |
|
|
|
|
|
|
|
|
if radius == -1: |
|
|
|
|
|
self.canvas[canvas_xy[:, 1], canvas_xy[:, 0], :] = colors |
|
|
|
|
|
|
|
|
|
|
|
else: |
|
|
for color, (x, y) in zip(colors.tolist(), canvas_xy.tolist()): |
|
|
|
|
|
|
|
|
|
|
|
self.canvas = cv2.circle( |
|
|
self.canvas, (x, y), radius, color, -1, lineType=cv2.LINE_AA |
|
|
) |
|
|
|
|
|
def draw_boxes( |
|
|
self, |
|
|
boxes=None, |
|
|
corners=None, |
|
|
colors=None, |
|
|
texts=None, |
|
|
box_line_thickness=2, |
|
|
box_text_size=0.5, |
|
|
text_corner=0, |
|
|
): |
|
|
""" |
|
|
Draws a set of boxes onto the canvas. |
|
|
Args: |
|
|
boxes (ndarray): Can either be of shape: |
|
|
(N, 7): Then, assumes (x, y, z, x_size, y_size, z_size, yaw) |
|
|
(N, 5): Then, assumes (x, y, x_size, y_size, yaw) |
|
|
Everything is in the same coordinate system as points |
|
|
(not canvas coordinates) |
|
|
colors: |
|
|
None: colors all points white. |
|
|
Tuple: RGB (0 ~ 255), indicating a single color for all points. |
|
|
ndarray: (N, 3) array of RGB values for each point. |
|
|
texts (List[String]): Length N; text to write next to boxes. |
|
|
box_line_thickness (int): cv2 line/text thickness |
|
|
box_text_size (float): cv2 putText size |
|
|
text_corner (int): 0 ~ 3. Which corner of 3D box to write text at. |
|
|
""" |
|
|
num_boxes = len(boxes) if boxes is not None else len(corners) |
|
|
|
|
|
|
|
|
if colors is None: |
|
|
colors = np.full((num_boxes, 3), fill_value=255, dtype=np.uint8) |
|
|
elif isinstance(colors, tuple): |
|
|
assert len(colors) == 3 |
|
|
colors_tmp = np.zeros((num_boxes, 3), dtype=np.uint8) |
|
|
colors_tmp[..., : len(colors)] = np.array(colors) |
|
|
colors = colors_tmp |
|
|
elif isinstance(colors, np.ndarray): |
|
|
assert len(colors) == num_boxes |
|
|
colors = colors.astype(np.uint8) |
|
|
else: |
|
|
raise Exception( |
|
|
"colors type {} was not an expected type".format(type(colors)) |
|
|
) |
|
|
|
|
|
if boxes is not None: |
|
|
boxes = np.copy(boxes) |
|
|
assert len(boxes.shape) == 2 |
|
|
|
|
|
if boxes.shape[-1] == 7: |
|
|
boxes = boxes[:, [0, 1, 3, 4, 6]] |
|
|
else: |
|
|
assert boxes.shape[-1] == 5 |
|
|
|
|
|
|
|
|
|
|
|
bev_corners = ( |
|
|
np.array([[[0.5, 0.5], [-0.5, 0.5], [-0.5, -0.5], [0.5, -0.5]]]) |
|
|
* boxes[:, None, [2, 3]] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
rot_sin = np.sin(boxes[:, -1]) |
|
|
rot_cos = np.cos(boxes[:, -1]) |
|
|
rot_matrix = np.stack( |
|
|
[[rot_cos, -rot_sin], [rot_sin, rot_cos]] |
|
|
) |
|
|
|
|
|
|
|
|
bev_corners = np.einsum("aij,jka->aik", bev_corners, rot_matrix) |
|
|
|
|
|
|
|
|
|
|
|
bev_corners = bev_corners + boxes[:, None, [0, 1]] |
|
|
elif corners is not None: |
|
|
bev_corners = corners |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bev_corners_canvas, valid_mask = self.get_canvas_coords( |
|
|
bev_corners.reshape(-1, 2) |
|
|
) |
|
|
|
|
|
|
|
|
bev_corners_canvas = bev_corners_canvas.reshape(*bev_corners.shape) |
|
|
valid_mask = valid_mask.reshape(*bev_corners.shape[:-1]) |
|
|
|
|
|
|
|
|
|
|
|
valid_mask = valid_mask.sum(axis=1) > 0 |
|
|
bev_corners_canvas = bev_corners_canvas[valid_mask] |
|
|
if texts is not None: |
|
|
texts = np.array(texts)[valid_mask] |
|
|
if colors is not None: |
|
|
colors = np.array(colors)[valid_mask] |
|
|
|
|
|
|
|
|
|
|
|
idx_draw_pairs = [(0, 1), (1, 2), (2, 3), (3, 0)] |
|
|
for i, (color, curr_box_corners) in enumerate( |
|
|
zip(colors.tolist(), bev_corners_canvas) |
|
|
): |
|
|
curr_box_corners = curr_box_corners.astype(np.int32) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for start, end in idx_draw_pairs: |
|
|
self.canvas = cv2.line( |
|
|
self.canvas, |
|
|
tuple(curr_box_corners[start].tolist()), |
|
|
tuple(curr_box_corners[end].tolist()), |
|
|
|
|
|
|
|
|
color=color, |
|
|
thickness=box_line_thickness, |
|
|
) |
|
|
if texts is not None: |
|
|
self.canvas = cv2.putText( |
|
|
self.canvas, |
|
|
str(texts[i]), |
|
|
|
|
|
tuple(curr_box_corners[text_corner].tolist()), |
|
|
cv2.FONT_HERSHEY_SIMPLEX, |
|
|
box_text_size, |
|
|
color=color, |
|
|
thickness=box_line_thickness, |
|
|
) |
|
|
|