File size: 18,305 Bytes
663494c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
from typing import Dict, List, Optional, Tuple, Union

import cv2
import utils.transform as box_transform
import utils.calibration as calib_utils
import utils.bev as bev_vis
import utils.canvas_3d as vis_3d
import utils.color as color_vis
import matplotlib.pyplot as plt
plt.switch_backend('agg')
import numpy as np
import scipy
from PIL import Image
from pathlib import Path
import os


def get_color_object(obj_id: int, color_list: List) -> np.array:
    """Get color for each object for vis, w.r.t. the ID"""

    color_float: Tuple[float, float, float] = color_list[obj_id % len(color_list)]
    color = np.array(
        [
            int(color_float[0] * 255),
            int(color_float[1] * 255),
            int(color_float[2] * 255),
        ]
    )

    return color


def vis_box_on_lidar(
    lidar: Union[str, np.ndarray],
    save_path: str,
    bbox_det_in_ego_list: Optional[List[np.ndarray]] = None,
    bbox_gt_in_ego_list: Optional[List[np.ndarray]] = None,
    id_list: Optional[List[int]] = None,
    state_list: Optional[List[np.ndarray]] = None,
    fut_traj: Optional[List[np.ndarray]] = None,    # xy coordinate
    fut_traj_mask: Optional[List[np.ndarray]] = None,
    pre_traj: Optional[List[np.ndarray]] = None,
    pre_traj_mask: Optional[List[np.ndarray]] = None,
    ego2lidar: Optional[np.ndarray] = None,
    bev: bool = True,
    box_order_nuscenes: bool = False,
    left_hand: bool = True,     # carla
    vis_text: bool = True,
    command: Optional[int] = 2,
    command_dict: Optional[Dict[int, str]] = {0: 'TURN RIGHT', 1: 'TURN LEFT', 2: 'KEEP FORWARD'},
    bev_map: Optional[np.ndarray] = None,   # H x W x 3
) -> None:
    """Visualize detection/GT boxes on the LiDAR point cloud

    Args:
        lidar: str -> path to the data, np array -> N x 3 data already loaded
        state_list: a list of array including velocity/acceleration/angular_velocity
        fut_traj: N x 12 x 4 or N, 12 x 4
        past_traj: N x 8 x 2 or N, 8 x 2

    Note that carla lidar coordinate is
        +x -> left
        +y -> front
        +z -> up
    """

    # load lidar
    if isinstance(lidar, str):
        # try:
        # lidar = np.load(lidar, allow_pickle=True)[..., :3]  # XYZI, N x 3 now
        # except:
        lidar = np.fromfile(lidar, dtype=np.float32)
        lidar = lidar.reshape(-1, 4)  # N x 4
        lidar = lidar[:, :3]  # N x 3 now
    else:
        assert isinstance(lidar, np.ndarray), "the detection is either str or dict"

    # pre-load color for each object when visualizing tracklets
    # only when id_list is provided and visualizing either GT/tracklets
    if id_list is not None and (
        bbox_det_in_ego_list is None or bbox_gt_in_ego_list is None
    ):
        # get the list of colors to choose for each tracklet/prediction
        color_list: List[Tuple[float, float, float]] = color_vis.random_colors()

        if bbox_gt_in_ego_list is None:
            num_obj = len(bbox_det_in_ego_list)
        else:
            num_obj = len(bbox_gt_in_ego_list)

        colors = np.zeros((num_obj, 3), dtype=np.uint8)  # N x 3
        assert len(id_list) == num_obj, "number of objects is wrong"

        for obj_index, obj_id in enumerate(id_list):
            color: np.ndarray = get_color_object(obj_id, color_list)  # (3, )
            colors[obj_index, :] = color
    else:
        colors = None

    # convert the ID_list to list of strings to visualize
    # only if id_list is provided
    text_corner = 1
    if id_list is not None and vis_text:
        texts = [str(obj_id) for obj_id in id_list]

        # add the list of states to the text
        if state_list is not None:
            for index in range(len(state_list)):
                states: Union[np.ndarray, List] = state_list[index]
                state_str: str = " ".join(["%.2f" % state for state in states])
                texts[index] = texts[index] + " " + state_str
    else:
        texts = None

    # visualize lidar points only, default color ORANGE
    if bev:
        canvas = bev_vis.Canvas_BEV()
        canvas_xy, valid_mask = canvas.get_canvas_coords(lidar)  # Get Canvas coords
        canvas.draw_canvas_points(canvas_xy[valid_mask])  # Only draw valid points
    else:  # 3D
        canvas = vis_3d.Canvas_3D()
        canvas_xyz, valid_mask = canvas.get_canvas_coords(lidar)  # Get Canvas coords
        color_based_on_intensity = (
            (lidar[valid_mask, -1] * 255).astype("uint8").reshape((-1, 1))
        )  # N x 1
        color_based_on_intensity = np.repeat(
            color_based_on_intensity, 3, axis=1
        )  # N x 3
        canvas.draw_canvas_points(
            canvas_xyz[valid_mask], colors=color_based_on_intensity
        )  # Only draw valid points

    # extract height from the bbox
    if (bbox_det_in_ego_list is not None) or (bbox_gt_in_ego_list is not None):
        if bbox_det_in_ego_list is not None:
            num_obj = len(bbox_det_in_ego_list)
        else:
            num_obj = len(bbox_gt_in_ego_list)
        height_all_lidar = np.zeros((num_obj), dtype='float32')

    # visualize detection bbox on lidar, default color WHITE
    if bbox_det_in_ego_list is not None:
        bbox_det_in_lidar_list: List[np.ndarray] = list()
        count = 0
        box_in_ego: np.ndarray
        for box_in_ego in bbox_det_in_ego_list:

            # convert box in ego to lidar coordinate
            box_in_lidar: np.ndarray = box_transform.box_in_ego_to_lidar(
                box_in_ego,
                ego2lidar=ego2lidar,
            )  # 8 x 4
            height_all_lidar[count] = np.average(box_in_lidar, axis=0)[2]

            if bev:
                if box_order_nuscenes:
                    box_in_lidar = box_in_lidar[
                        [1, 2, 6, 5], :2
                    ]  # 4 x 2, nuScenes boxinstances
                else:
                    box_in_lidar = box_in_lidar[:4, :2]  # 4 x 2, take only BEV corners                
            else:
                box_in_lidar = box_in_lidar[:, :3]  # 8 x 3, take only xyz
            bbox_det_in_lidar_list.append(np.array(box_in_lidar))
            count += 1

        if len(bbox_det_in_lidar_list) > 0:
            bbox_det_in_lidar = np.stack(bbox_det_in_lidar_list, axis=0)  # N x 4 x 2
            if colors is None:
                colors_det = np.full(
                    (len(bbox_det_in_lidar), 3), fill_value=0, dtype=np.uint8
                )
                colors_det[:, :2] = 255  # (N x 3)
            else:
                colors_det = colors
            canvas.draw_boxes(
                corners=bbox_det_in_lidar,
                colors=colors_det,
                texts=texts,
                text_corner=text_corner,
            )  # Draw Boxes

    # visualize GT bbox on lidar, default color BLUE
    if bbox_gt_in_ego_list is not None:
        bbox_gt_in_lidar_list: List[np.ndarray] = list()
        box_in_ego: np.ndarray
        count = 0
        for box_in_ego in bbox_gt_in_ego_list:

            # convert box in ego to lidar coordinate
            box_in_lidar: np.ndarray = box_transform.box_in_ego_to_lidar(
                box_in_ego,
                ego2lidar=ego2lidar,
            )  # 8 x 4
            height_all_lidar[count] = np.average(box_in_lidar, axis=0)[2]

            # convert to BEV
            if bev:
                if box_order_nuscenes:
                    box_in_lidar = box_in_lidar[
                        [1, 2, 6, 5], :2
                    ]  # 4 x 2, nuScenes boxinstances
                else:
                    box_in_lidar = box_in_lidar[:4, :2]  # 4 x 2, take only BEV corners
                # box_in_lidar = box_in_lidar[[0, 3, 4, 7], :2]  # 4 x 2, take left side corners
                # box_in_lidar = box_in_lidar[[0, 1, 4, 5], :2]  # 4 x 2, take front side corners
            else:
                box_in_lidar = box_in_lidar[:, :3]

            # if count >= 10 and count < 13:
            #     print("box_in_lidar", box_in_lidar)

            bbox_gt_in_lidar_list.append(np.array(box_in_lidar))
            count += 1

        if len(bbox_gt_in_lidar_list) > 0:
            bbox_gt_in_lidar = np.stack(bbox_gt_in_lidar_list, axis=0)  # N x 4 x 2
            if colors is None:
                colors_gt = np.full(
                    (len(bbox_gt_in_lidar), 3), fill_value=0, dtype=np.uint8
                )
                colors_gt[:, 2] = 255  # N x 3
            else:
                colors_gt = colors

            canvas.draw_boxes(
                corners=bbox_gt_in_lidar,
                colors=colors_gt,
                texts=texts,
                text_corner=text_corner,
            )  # Draw Boxes

    # draw points for past/future trajectories in lidar coordinate
    if fut_traj is not None:
        fut_traj_obj: np.ndarray  # F x 4
        obj_index = 0
        for fut_traj_obj in fut_traj:

            # add height to the trajectory in the 3D vis cases
            if fut_traj_obj.shape[1] == 2 and not bev:
                fut_traj_obj_addh = np.concatenate([fut_traj_obj, np.zeros((fut_traj_obj.shape[0], 1))], axis=1)
                fut_traj_obj_addh[:, -1] = height_all_lidar[obj_index]
                fut_traj_obj = fut_traj_obj_addh

            # Get Canvas coords and mask within the visualization range
            canvas_xy, valid_mask = canvas.get_canvas_coords(
                fut_traj_obj
            )  # canvas_xy: N x 2, valid_mask: N
            valid_mask = valid_mask.astype("bool")

            # get valid mask for every timestamp
            fut_traj_mask_obj: np.ndarray = fut_traj_mask[obj_index, :, 0].astype(
                "bool"
            )  # N
            mask_combined = fut_traj_mask_obj & valid_mask

            # visualize
            canvas.draw_canvas_points(
                canvas_xy[mask_combined], colors=tuple(colors[obj_index]), radius=2
            )  # Only draw valid points
            obj_index += 1

    if pre_traj is not None:
        pre_traj_obj: np.ndarray  # F x 4
        obj_index = 0
        for pre_traj_obj in pre_traj:

            # add height to the trajectory in the 3D vis cases
            if pre_traj_obj.shape[1] == 2 and not bev:
                pre_traj_obj_addh = np.concatenate([pre_traj_obj, np.zeros((pre_traj_obj.shape[0], 1))], axis=1)
                pre_traj_obj_addh[:, -1] = height_all_lidar[obj_index]
                pre_traj_obj = pre_traj_obj_addh

            canvas_xy, valid_mask = canvas.get_canvas_coords(
                pre_traj_obj
            )  # Get Canvas coords
            valid_mask = valid_mask.astype("bool")

            # get valid mask for every timestamp
            pre_traj_mask_obj: np.ndarray = pre_traj_mask[obj_index, :, 0].astype(
                "bool"
            )  # N
            mask_combined = pre_traj_mask_obj & valid_mask
            canvas.draw_canvas_points(
                canvas_xy[mask_combined], colors=tuple(colors[obj_index]), radius=2
            )  # Only draw valid points
            obj_index += 1

    # rotate to make the view consistent with the image
    # now +x -> left (ego), +y -> front (ego)
    if bev:
        canvas.canvas = np.flip(canvas.canvas, axis=(0))
        # now +x -> right (ego), +y -> front (ego)
    if left_hand:
        canvas.canvas = np.flip(canvas.canvas, axis=(1))
        # now +x -> right (ego), +y -> back (ego)

    # draw figure
    plt.figure(figsize=(20, 20))
    plt.axis("off")
    plt.imshow(canvas.canvas)
    plt.tight_layout()

    # add text for commands
    plt.text(30, 50, command_dict[int(command)], fontsize=45, color=(1, 1, 1))

    # save figure
    save_dir = Path(save_path).resolve().parent
    if not os.path.exists(save_dir):
        os.makedirs(save_dir)    
    plt.savefig(save_path, transparent=False)
    plt.close()

    # overlay visualization with map
    if bev_map is not None and bev:
        traj_vis = cv2.imread(save_path)

        # convert the bev map to match with the higher-resolution visualization image
        bev_map = cv2.resize(bev_map, traj_vis.shape[1::-1])    # 2000 x 2000
        bev_map = (bev_map * 255).astype('uint8')

        # make up the third dimension if only one channel is given
        if len(bev_map.shape) == 2:
            bev_map = np.expand_dims(bev_map, axis=-1)  # 2000 x 2000 x 1
            bev_map = np.repeat(bev_map, 3, axis=-1)    # 2000 x 2000 x 3
        
        # synchronize with the lidar visualization
        if bev:        
            bev_map = np.flip(bev_map, axis=0)  # H x W x 3
        if left_hand:
            bev_map = np.flip(bev_map, axis=1)  # H x W x 3

        # blend & save
        dst = cv2.addWeighted(traj_vis, 0.7, bev_map, 0.3, 0)
        cv2.imwrite(save_path, dst)

def vis_box_on_camera(
    cameras: Dict[str, np.ndarray],
    save_path: str,
    cam_param_all: Dict[str, Dict],
    bbox_det_in_ego_list: Optional[List[np.ndarray]] = None,
    bbox_gt_in_ego_list: Optional[List[np.ndarray]] = None,
    left_hand_system: bool = True,
) -> None:
    """Visualize detection/GT boxes on the multiple cameras

    Args:
        camera: keys can be ["left", "front", "right", "back"]
        values are H x W x 3

    """

    # process each camera
    cam_name: str
    cam_param: Dict[str, float]
    for cam_name, cam_param in cam_param_all.items():

        # if we already have ego2cam for visualization, no need to construct the transform
        if "intrinsics" not in cam_param:
            intrinsics: np.ndarray = calib_utils.get_camera_intrinsics(cam_param, dim=4)
        else:
            intrinsics: np.ndarray = cam_param["intrinsics"]

        # visualize each box for detection, default color ORANGE
        if bbox_det_in_ego_list is not None:
            box_in_ego: np.ndarray
            for box_in_ego in bbox_det_in_ego_list:

                # first convert to the camera coordinate
                # and then 3D -> 2D projection to the image coordinate
                box_in_camera: np.ndarray = box_transform.box_in_ego_to_camera(
                    box_in_ego, cam_param
                )  # 8 x 4

                # camera projection
                box_2d_in_image: np.ndarray = box_transform.box_projection_to_image(
                    box_in_camera,
                    intrinsics,
                    left_hand_system=left_hand_system,
                )  # 8 x 2

                # check if box is in front of the camera for visualization
                if box_2d_in_image is not None:
                    cameras[cam_name], _ = vis_box_on_single_image(
                        cameras[cam_name],
                        box_2d_in_image,
                        img_size=(cam_param["height"], cam_param["width"]),
                        color=(255, 255, 0),
                        thickness=2,
                    )

        # visualize each box for GT, default color BLUE
        if bbox_gt_in_ego_list is not None:
            box_in_ego: np.ndarray
            for box_in_ego in bbox_gt_in_ego_list:

                # first convert to the camera coordinate
                # and then 3D -> 2D projection to the image coordinate
                box_in_camera: np.ndarray = box_transform.box_in_ego_to_camera(
                    box_in_ego, cam_param
                )  # 8 x 4

                # camera projection
                box_2d_in_image: np.ndarray = box_transform.box_projection_to_image(
                    box_in_camera,
                    intrinsics,
                    left_hand_system=left_hand_system,
                )  # 8 x 2

                # check if box is in front of the camera for visualization
                if box_2d_in_image is not None:
                    cameras[cam_name], _ = vis_box_on_single_image(
                        cameras[cam_name],
                        box_2d_in_image,
                        img_size=(cam_param["height"], cam_param["width"]),
                        color=(0, 0, 255),
                        thickness=2,
                    )

    # visualize all images together
    # TODO (Xinshuo): remove this assumption
    # assume all cameras have the same width and height
    image = np.concatenate(list(cameras.values()), axis=1)  # H x 3W x 3
    image = image[:, :, ::-1]  # convert bgr to rgb
    image = Image.fromarray(image)
    image = image.resize(
        (cam_param["width"] * len(cameras.keys()), cam_param["height"])
    )
    save_dir = Path(save_path).resolve().parent
    if not os.path.exists(save_dir):
        os.makedirs(save_dir)    
    image.save(save_path)


def check_outside_image(x, y, height, width):
    if x < 0 or x >= width:
        return True
    if y < 0 or y >= height:
        return True


def vis_box_on_single_image(
    image, qs, img_size=(900, 1600), color=(255, 255, 255), thickness=4
):
    """Draw 3d bounding box in image
    qs: (8,2) array of vertices for the 3d box in following order:
        1 -------- 0
       /|         /|
      2 -------- 3 .
      | |        | |
      . 5 -------- 4
      |/         |/
      6 -------- 7
    """

    # if 6 points of the box are outside the image, then do not draw
    pts_outside = 0
    for index in range(8):
        check = check_outside_image(
            qs[index, 0], qs[index, 1], img_size[0], img_size[1]
        )
        if check:
            pts_outside += 1
    if pts_outside >= 6:
        return image, False

    # actually draw
    if qs is not None:
        qs = qs.astype(np.int32)
        for k in range(0, 4):
            i, j = k, (k + 1) % 4
            cv2.line(
                image,
                (qs[i, 0], qs[i, 1]),
                (qs[j, 0], qs[j, 1]),
                color,
                thickness,
                cv2.LINE_AA,
            )  # use LINE_AA for opencv3

            i, j = k + 4, (k + 1) % 4 + 4
            cv2.line(
                image,
                (qs[i, 0], qs[i, 1]),
                (qs[j, 0], qs[j, 1]),
                color,
                thickness,
                cv2.LINE_AA,
            )

            i, j = k, k + 4
            cv2.line(
                image,
                (qs[i, 0], qs[i, 1]),
                (qs[j, 0], qs[j, 1]),
                color,
                thickness,
                cv2.LINE_AA,
            )

    return image, True