diff --git a/README.md b/README.md index ad357a0..e50ca0b 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ If you prefer to build the Docker image by yourself, Check [build-docker-image]( ## 1. Data Preparation -Refer to [dataprocess/README.md](dataprocess/README.md) for dataset download instructions. Currently, we support **Argoverse 2**, **Waymo**, **nuScenes**, **ZOD** and **custom datasets** (more datasets will be added in the future). +Refer to [dataprocess/README.md](dataprocess/README.md) for dataset download instructions. Currently, we support **Argoverse 2**, **Waymo**, **nuScenes**, [**MAN-TruckScene**](https://github.com/TUMFTM/truckscenes-devkit), [**ZOD**](https://github.com/zenseact/zod) and **custom datasets** (more datasets will be added in the future). After downloading, convert the raw data to `.h5` format for easy training, evaluation, and visualization. Follow the steps in [dataprocess/README.md#process](dataprocess/README.md#process). @@ -337,6 +337,12 @@ If you find it useful, please cite our works: year={2025}, journal={arXiv preprint arXiv:2503.00803}, } +@article{zhang2025deltaflow, + title={{DeltaFlow}: An Efficient Multi-frame Scene Flow Estimation Method}, + author={Zhang, Qingwen and Zhu, Xiaomeng and Zhang, Yushan and Cai, Yixi and Andersson, Olov and Jensfelt, Patric}, + year={2025}, + journal={arXiv preprint arXiv:2508.17054}, +} ``` And our excellent collaborators works contributed to this codebase also: diff --git a/assets/slurm/0_process.sh b/assets/slurm/0_process.sh deleted file mode 100644 index 44481f2..0000000 --- a/assets/slurm/0_process.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -#SBATCH -J pack_data -#SBATCH --gpus 0 -#SBATCH --cpus-per-task 64 -#SBATCH --mem 256G -#SBATCH --mincpus=64 -#SBATCH -t 1-00:00:00 -#SBATCH --mail-type=END,FAIL -#SBATCH --mail-user=qingwen@kth.se -#SBATCH --output /proj/berzelius-2023-154/users/x_qinzh/workspace/OpenSceneFlow/logs/slurm/%J_data.out -#SBATCH --error /proj/berzelius-2023-154/users/x_qinzh/workspace/OpenSceneFlow/logs/slurm/%J_data.err - -PYTHON=/proj/berzelius-2023-154/users/x_qinzh/mambaforge/envs/sftool/bin/python -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/proj/berzelius-2023-154/users/x_qinzh/mambaforge/lib -cd /proj/berzelius-2023-364/users/x_qinzh/workspace/OpenSceneFlow -# export HYDRA_FULL_ERROR=1 - - -$PYTHON dataprocess/extract_av2.py --nproc 64 \ - --av2_type sensor \ - --data_mode train \ - --argo_dir /proj/berzelius-2023-154/users/x_qinzh/av2 \ - --output_dir /proj/berzelius-2023-364/users/x_qinzh/data/av2/h5py - -$PYTHON dataprocess/extract_av2.py --nproc 64 \ - --av2_type sensor \ - --data_mode val \ - --argo_dir /proj/berzelius-2023-154/users/x_qinzh/av2 \ - --output_dir /proj/berzelius-2023-364/users/x_qinzh/data/av2/h5py \ - --mask_dir /proj/berzelius-2023-154/users/x_qinzh/av2/3d_scene_flow - -$PYTHON dataprocess/extract_av2.py --nproc 64 \ - --av2_type sensor \ - --data_mode test \ - --argo_dir /proj/berzelius-2023-154/users/x_qinzh/av2 \ - --output_dir /proj/berzelius-2023-364/users/x_qinzh/data/av2/h5py \ - --mask_dir /proj/berzelius-2023-154/users/x_qinzh/av2/3d_scene_flow \ No newline at end of file diff --git a/assets/slurm/data-process.sh b/assets/slurm/data-process.sh new file mode 100644 index 0000000..d132e91 --- /dev/null +++ b/assets/slurm/data-process.sh @@ -0,0 +1,44 @@ +#!/bin/bash +#SBATCH -J dataprocess +#SBATCH -p berzelius-cpu +#SBATCH --cpus-per-task 64 +#SBATCH --mem 128G +#SBATCH --mincpus=64 +#SBATCH -t 2-00:00:00 +#SBATCH --mail-type=END,FAIL +#SBATCH --mail-user=qingwen@kth.se +#SBATCH --output /proj/berzelius-2023-154/users/x_qinzh/OpenSceneFlow/logs/slurm/%J_data.out +#SBATCH --error /proj/berzelius-2023-154/users/x_qinzh/OpenSceneFlow/logs/slurm/%J_data.err + +PYTHON=/proj/berzelius-2023-154/users/x_qinzh/mambaforge/envs/sftool/bin/python +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/proj/berzelius-2023-154/users/x_qinzh/mambaforge/lib +cd /proj/berzelius-2023-154/users/x_qinzh/OpenSceneFlow/ +# export HYDRA_FULL_ERROR=1 + + +# =============== Argoverse2 +# $PYTHON dataprocess/extract_av2.py --nproc 64 \ +# --av2_type sensor \ +# --data_mode train \ +# --argo_dir /proj/berzelius-2023-154/users/x_qinzh/av2 \ +# --output_dir /proj/berzelius-2023-364/users/x_qinzh/data/av2/h5py + +# $PYTHON dataprocess/extract_av2.py --nproc 64 \ +# --av2_type sensor \ +# --data_mode val \ +# --argo_dir /proj/berzelius-2023-154/users/x_qinzh/av2 \ +# --output_dir /proj/berzelius-2023-364/users/x_qinzh/data/av2/h5py \ +# --mask_dir /proj/berzelius-2023-154/users/x_qinzh/av2/3d_scene_flow + +# $PYTHON dataprocess/extract_av2.py --nproc 64 \ +# --av2_type sensor \ +# --data_mode test \ +# --argo_dir /proj/berzelius-2023-154/users/x_qinzh/av2 \ +# --output_dir /proj/berzelius-2023-364/users/x_qinzh/data/av2/h5py \ +# --mask_dir /proj/berzelius-2023-154/users/x_qinzh/av2/3d_scene_flow + +# =============== MAN-Truckscenes +$PYTHON dataprocess/extract_truckscenes.py --nproc 12 \ + --data_dir /proj/berzelius-2023-364/users/x_qinzh/data/truckscenes/raw_data/trainval/man-truckscenes \ + --mode v1.0-trainval \ + --output_dir /proj/berzelius-2023-364/data/truckscenes/h5py \ No newline at end of file diff --git a/assets/tests/hdbscan_speed.py b/assets/tests/hdbscan_speed.py index f92580d..b839b2c 100644 --- a/assets/tests/hdbscan_speed.py +++ b/assets/tests/hdbscan_speed.py @@ -25,7 +25,7 @@ def vis(pc0, labels, title='HDBSCAN'): # visualize - vis = MyVisualizer(view_file=f'{BASEF_DIR}/assets/view/av2.json', window_title=title) + vis = MyVisualizer(view_file=f'{BASEF_DIR}/assets/view/demo.json', window_title=title) pcd = o3d.geometry.PointCloud() num_points = pc0.shape[0] pcd.points = o3d.utility.Vector3dVector(pc0) diff --git a/assets/view/av2.json b/assets/view/av2.json deleted file mode 100644 index b1bf1bb..0000000 --- a/assets/view/av2.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "class_name" : "ViewTrajectory", - "interval" : 29, - "is_loop" : false, - "trajectory" : - [ - { - "boundingbox_max" : [ 211.125, 117.1875, 20.53125 ], - "boundingbox_min" : [ -215.25, -166.625, -3.392578125 ], - "field_of_view" : 90.0, - "front" : [ -0.78264077936294429, -0.0063155949007044129, 0.62244158259166249 ], - "lookat" : [ 16.764474584958553, 0.042979235705968843, -6.3527807404873249 ], - "up" : [ 0.62244710369604039, 0.0012897206826678895, 0.78266080757948497 ], - "zoom" : 0.080000000000000002 - } - ], - "version_major" : 1, - "version_minor" : 0 -} \ No newline at end of file diff --git a/assets/view/demo.json b/assets/view/demo.json new file mode 100644 index 0000000..ddead1e --- /dev/null +++ b/assets/view/demo.json @@ -0,0 +1,19 @@ +{ + "class_name" : "ViewTrajectory", + "interval" : 29, + "is_loop" : false, + "trajectory" : + [ + { + "boundingbox_max" : [ 215.05035400390625, 80.301795959472656, 34.890830993652344 ], + "boundingbox_min" : [ -211.76950073242188, -125.19444274902344, -4.2829980850219727 ], + "field_of_view" : 90.0, + "front" : [ -0.58999680455306347, -0.15524870408634167, 0.79233932787454509 ], + "lookat" : [ 9.9369846496341676, 13.322769196068615, -16.164870152242376 ], + "up" : [ 0.79499677526385637, 0.059685607254847638, 0.60367023746967163 ], + "zoom" : 0.17999999999999999 + } + ], + "version_major" : 1, + "version_minor" : 0 +} \ No newline at end of file diff --git a/conf/others/truckscenes.toml b/conf/others/truckscenes.toml new file mode 100644 index 0000000..9ebb7e1 --- /dev/null +++ b/conf/others/truckscenes.toml @@ -0,0 +1,31 @@ + +[important] +height = 0 # ego height. since truckscene have multiple LiDARs, we set pc to ego-coord already. + +# 整个雷点点云以自己为中心 分为多少个segment,每个segment又分成多少个bin +[segments] +r_min = 0.1 # minimum point distance. 感兴趣的区域 +r_max = 100 # maximum point distance. +n_segments = 360 # number of radial segments. +n_bins = 160 # number of radial bins. + + +[ground] +# 直线的 slope 斜率,一个seg里拟合的线斜率 +min_slope = 0.0 # minimum slope of a ground line. [T_m_small] +max_slope = 0.1 # maximum slope of a ground line. [T_m] +# 如果拟合点超过这个 long threshold,判断其 height 变化是否超过 max_long_height +long_threshold = 2.0 # Distance at which points are considered far from each other. [T_d_prev] +max_long_height = 0.3 # maximum height change to previous point in long line. [T_b] +# 如果没有超过 long threshold,判断现在拟合的点与fitline高度变化是否超过 max_start_height +max_start_height = 0.2 # Maximum heigh of starting line to be labelled ground. +# 最大的拟合误差,超过这个误差的点不会被加入 line 拟合 +max_fit_error = 0.1 # maximum error of a point during line fit. [T_RMSE: will sqaure inside code.] + +# 判断是否是地面点的时候,需要满足的条件: +max_dist_to_line = 0.2 # maximum vertical distance of point to line to be considered ground. [T_d_ground] +line_search_angle = 2.0 # How far to search for a line in angular direction [rad]. + +[general] +n_threads = 8 # number of threads for parallel processing. +verbose = false # if you don't want to see every output, set this to false. \ No newline at end of file diff --git a/conf/ground/zod.toml b/conf/others/zod.toml similarity index 93% rename from conf/ground/zod.toml rename to conf/others/zod.toml index ab5e85b..3dfdcc4 100644 --- a/conf/ground/zod.toml +++ b/conf/others/zod.toml @@ -23,7 +23,7 @@ max_start_height = 0.2 # Maximum heigh of starting line to be labelled ground. max_fit_error = 0.1 # maximum error of a point during line fit. [T_RMSE: will sqaure inside code.] # 判断是否是地面点的时候,需要满足的条件: -max_dist_to_line = 0.2 # maximum vertical distance of point to line to be considered ground. [T_d_ground] +max_dist_to_line = 0.25 # maximum vertical distance of point to line to be considered ground. [T_d_ground] line_search_angle = 2.0 # How far to search for a line in angular direction [rad]. [general] diff --git a/dataprocess/README.md b/dataprocess/README.md index 2651afa..419098b 100644 --- a/dataprocess/README.md +++ b/dataprocess/README.md @@ -6,13 +6,13 @@ README for downloading and preprocessing the dataset. We includes waymo, argover - [Download](#download): includes how to download the dataset. - [Process](#process): run script to preprocess the dataset. -We've updated the process dataset for: +We've updated the process dataset for (Please cite the original dataset paper and involved work if you use them): - [x] Argoverse 2.0: check [here](#argoverse-20). The process script Involved from [DeFlow](https://github.com/KTH-RPL/DeFlow). - [x] Waymo: check [here](#waymo-dataset). The process script was involved from [SeFlow](https://github.com/KTH-RPL/SeFlow). - [x] nuScenes: check [here](#nuscenes), The process script was involved from [DeltaFlow](https://github.com/Kin-Zhang/DeltaFlow). - [x] ZOD (w/o gt): check [here](#zod-dataset). The process script was involved from [HiMo](https://kin-zhang.github.io/HiMo). (It could be a good first reference for users to extract other datasets in the future.) -- [ ] TruckScene: done coding, public after review. Will be involved later by another paper. +- [x] TruckScene: check [here](#truckscene). The process script was involved from [DoGFlow](https://github.com/ajinkyakhoche/DoGFlow). If you want to **use all datasets above**, there is a **specific environment** in [envsftool.yaml](../envsftool.yaml) to install all the necessary packages. As Waymo package have different configuration and conflict with the main environment. Setup through the following command: @@ -134,6 +134,46 @@ For HiMo, we only downloaded [drives-set](https://zod.zenseact.com/drives/) for Please check the scripts: [dataprocess/extract_zod.py](./extract_zod.py) in detail, current we only process one scene while feel free to comment out for all scene etc. +### TruckScene + +Please visit the [TruckScene dataset](https://brandportal.man/d/QSf8mPdU5Hgj/downloads#/-/dataset) page for privacy policy. You can download the dataset by following command: + +```bash +# mini set, ~11G recommended for debugging purpose +cd /home/kin/data/truckscene/mini +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/mini/man-truckscenes_metadata_v1.0-mini.zip +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/mini/man-truckscenes_sensordata_v1.0-mini.zip +unzip "man-truckscenes_*.zip" + +# full trainval set, ~630G +cd /home/kin/data/truckscene/trainval +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/trainval/man-truckscenes_metadata_v1.0-trainval.zip +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/trainval/man-truckscenes_sensordata01_v1.0-trainval.zip +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/trainval/man-truckscenes_sensordata02_v1.0-trainval.zip +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/trainval/man-truckscenes_sensordata03_v1.0-trainval.zip +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/trainval/man-truckscenes_sensordata04_v1.0-trainval.zip +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/trainval/man-truckscenes_sensordata05_v1.0-trainval.zip +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/trainval/man-truckscenes_sensordata06_v1.0-trainval.zip +wget https://man-truckscenes.s3.eu-central-1.amazonaws.com/release/trainval/man-truckscenes_sensordata07_v1.0-trainval.zip +unzip "man-truckscenes_*.zip" +``` + +Folder structure: +``` +truckscene/ + — samples/ + — sweeps/ + — v1.0-mini/ + — v1.0-trainval/ +``` + +#### Dataset frames + +| Dataset | # Total Scene | # Total Frames | +| ------- | ------------- | -------------- | +| train | 524 | 101902 / 20380 (w. gt) | +| val | 75 | 14625 / 2925 (w. gt) | + ## Process @@ -144,8 +184,9 @@ This directory contains the scripts to preprocess the datasets into `.h5` files. - `extract_nus.py`: Process the datasets in nuScenes. - `extract_waymo.py`: Process the datasets in Waymo. - `extract_zod.py`: Process the datasets in ZOD. +- `extract_truckscene.py`: Process the datasets in TruckScene. -Example Running command: +Example Running command, you can also check our [slurm data-process script](../assets/slurm/data-process.sh) for more details.: ```bash # av2: python dataprocess/extract_av2.py --av2_type sensor --data_mode train --argo_dir /home/kin/data/av2 --output_dir /home/kin/data/av2/h5py @@ -155,6 +196,9 @@ python dataprocess/extract_waymo.py --mode train --flow_data_dir /home/kin/data/ # nus: python dataprocess/extract_nus.py --mode v1.0-trainval --output_dir /home/kin/data/nus/h5py/full --nproc 24 + +# truckscene: +python dataprocess/extract_truckscene.py --data_dir /home/kin/data/man-truckscenes --mode v1.0-mini --output_dir /home/kin/data/nus/h5py ``` diff --git a/dataprocess/extract_nus.py b/dataprocess/extract_nus.py index 4fcd2ad..1718176 100644 --- a/dataprocess/extract_nus.py +++ b/dataprocess/extract_nus.py @@ -18,7 +18,7 @@ import multiprocessing from pathlib import Path from multiprocessing import Pool, current_process -from typing import Optional, Final +from typing import Optional from tqdm import tqdm import numpy as np import fire, time, h5py @@ -69,10 +69,13 @@ def _load_points_from_file(filename: str) -> np.ndarray: pc = pc.reshape((-1, 5))[:, :4] return pc -def get_pose(nusc, sweep_data): - # pose from lidar to world - ego2lidar = nusc.get('calibrated_sensor', sweep_data['calibrated_sensor_token']) - world2ego = nusc.get('ego_pose', sweep_data['ego_pose_token']) +def get_pose(data_fn, sweep_data, w2stf=True): + world2ego = data_fn.get('ego_pose', sweep_data['ego_pose_token']) + # without considering the sensor to ego transform, we will do it outside if multiple sensors are used. + if not w2stf: + return transform_matrix(world2ego['translation'], Quaternion(world2ego['rotation'])).astype(np.float64) + + ego2lidar = data_fn.get('calibrated_sensor', sweep_data['calibrated_sensor_token']) ego2lidar_np = transform_matrix(ego2lidar['translation'], Quaternion(ego2lidar['rotation'])) world2ego_np = transform_matrix(world2ego['translation'], Quaternion(world2ego['rotation'])) return np.dot(world2ego_np, ego2lidar_np) @@ -85,13 +88,13 @@ def if_annotated_frame(sample_ann_dict, ts0): break return gt_flow_flag -def _resample_data(nusc, sample_data, sample_ann_dict, resample2frequency=10): +def _resample_data(nusc, sample_data, sample_ann_dict, datafrequency=20, resample2frequency=10): """ NOTE(Qingwen) - 2025-05-18: We always want to start from the first GT frame, and then resample the data! So we have as many GT frames as possible... """ sweep_data_lst, timestamps_lst = [], [] - skipFrame = int(20 / resample2frequency) # since nuscenes sweep at 20Hz, we want to resample to 10Hz + skipFrame = int(datafrequency / resample2frequency) # since nuscenes sweep at 20Hz, we want to resample to 10Hz cnt = 0 # Find the first GT frame @@ -268,18 +271,17 @@ def proc(x, ignore_current_process=False): pos = 1 process_log(*x, n=pos) -def process_logs(nusc_mode, data_dir: Path, scene_list: list, output_dir: Path, nproc: int): - """Compute sceneflow for all logs in the dataset. Logs are processed in parallel. - Args: - data_dir: Argoverse 2.0 directory - output_dir: Output directory. +def process_logs(data_mode, data_dir: Path, scene_list: list, output_dir: Path, nproc: int): """ - + Compute sceneflow for all logs in the dataset. + Logs are processed in parallel. + """ + if not (data_dir).exists(): print(f'{data_dir} not found') return - args = sorted([(nusc_mode, data_dir, scene_num_id, output_dir) for scene_num_id in range(len(scene_list))]) + args = sorted([(data_mode, data_dir, scene_num_id, output_dir) for scene_num_id in range(len(scene_list))]) print(f'Using {nproc} processes') # # for debug @@ -295,7 +297,7 @@ def process_logs(nusc_mode, data_dir: Path, scene_list: list, output_dir: Path, res = list(tqdm(p.imap_unordered(proc, args), total=len(scene_list), ncols=100)) def main( - data_dir: str = "/home/kin/data/nus/v1.0", + data_dir: str = "/home/kin/data/nus/raw", mode: str = "v1.0-mini", output_dir: str ="/home/kin/data/nus/h5py/demo", nproc: int = (multiprocessing.cpu_count() - 1), diff --git a/dataprocess/extract_truckscenes.py b/dataprocess/extract_truckscenes.py new file mode 100644 index 0000000..dcc3008 --- /dev/null +++ b/dataprocess/extract_truckscenes.py @@ -0,0 +1,294 @@ +""" +# +# Created: 2025-09-06 09:16 +# Copyright (C) 2025-now, RPL, KTH Royal Institute of Technology +# Author: Qingwen Zhang (https://kin-zhang.github.io/), Ajinkya Khoche (https://ajinkyakhoche.github.io/) +# +# Description: Preprocess Data, save as h5df format for faster loading +# This one is for MAN TruckScenes dataset +# +# NOTE: truckscene follow really similar structure with TruckScenes format. That's why +# **NOT ALL** frames are annotated! So the SL training might be not that effective. +# Since truckscenes LiDAR is at 10Hz, we keep annotated flow to 10Hz also. +# +""" + +from collections import defaultdict +import os +# os.environ["OMP_NUM_THREADS"] = "1" + +from pathlib import Path +import multiprocessing +from multiprocessing import Pool, current_process +from typing import Optional +from tqdm import tqdm +import numpy as np +import fire, time, h5py + +from truckscenes import TruckScenes +from truckscenes.utils import splits +from truckscenes.utils.geometry_utils import transform_matrix +from truckscenes.utils.geometry_utils import points_in_box +from truckscenes.utils.data_classes import LidarPointCloud +from pyquaternion import Quaternion + +import os, sys +PARENT_DIR = os.path.abspath(os.path.join( os.path.dirname( __file__ ), '..')) +sys.path.append(PARENT_DIR) +from dataprocess.misc_data import create_reading_index, check_h5py_file_exists, ManNamMap, find_closest_integer_in_ref_arr +from dataprocess.extract_nus import remove_ego_points, get_pose +from src.utils import npcal_pose0to1 +from src.utils.av2_eval import CATEGORY_TO_INDEX +from linefit import ground_seg + + +GROUNDSEG_config = f"{PARENT_DIR}/conf/others/truckscenes.toml" +# NOTE(Qingwen): we only select 2x64 long-range LiDARs for processing, you can add more sensors if needed +# The timestamp reference will always be the last item in this list +SelectedSensor = ['LIDAR_RIGHT', 'LIDAR_LEFT'] # 2x64 +# SelectedSensor = ['LIDAR_TOP_FRONT', 'LIDAR_TOP_LEFT', 'LIDAR_TOP_RIGHT', 'LIDAR_REAR'] # 4x32 +# SelectedSensor = ['LIDAR_LEFT', 'LIDAR_RIGHT', 'LIDAR_TOP_FRONT', 'LIDAR_TOP_LEFT', 'LIDAR_TOP_RIGHT', 'LIDAR_REAR'] # all 6 LiDARs + +def process_log(data_mode, data_dir: Path, scene_num_id: int, output_dir: Path, n: Optional[int] = None) : + def create_group_data(group, pc, pose, lidar_id, lidar_center, gm = None, flow_0to1=None, flow_valid=None, flow_category=None, flow_instance=None, ego_motion=None): + group.create_dataset('lidar', data=pc.astype(np.float32)) + group.create_dataset('pose', data=pose.astype(np.float64)) + group.create_dataset('ground_mask', data=gm.astype(bool)) + group.create_dataset('lidar_id', data=lidar_id.astype(np.float32)) + group.create_dataset('lidar_center', data=lidar_center.astype(np.float32)) # shape: [LiDAR_num, 3] (x, y, z) + if ego_motion is not None: + group.create_dataset('ego_motion', data=ego_motion.astype(np.float32)) + if flow_0to1 is not None: + # ground truth flow information + group.create_dataset('flow', data=flow_0to1.astype(np.float32)) + group.create_dataset('flow_is_valid', data=flow_valid.astype(bool)) + group.create_dataset('flow_category_indices', data=flow_category.astype(np.uint8)) + group.create_dataset('flow_instance_id', data=flow_instance.astype(np.int16)) + + def compute_flow_simple(data_fn, pc0, pose0, pose1, ts0, ts1, sample_ann_list, dclass, DataNameMap=ManNamMap): + # compute delta transform between pose0 and pose1 + ego1_SE3_ego0 = npcal_pose0to1(pose0, pose1) + # flow due to ego motion + flow = np.zeros_like(pc0[:,:3]) + flow = pc0[:,:3] @ ego1_SE3_ego0[:3,:3].T + ego1_SE3_ego0[:3,3] - pc0[:,:3] # pose flow + + valid = np.ones(len(pc0), dtype=np.bool_) + classes = np.zeros(len(pc0), dtype=np.uint8) + instances = np.zeros(len(pc0), dtype=np.int16) + + delta_t = (ts1 - ts0) * 1e-6 + world_pc0 = pc0[:,:3] @ pose0[:3,:3].T + pose0[:3,3] + id_ = 0 + for ann in sample_ann_list: + # ann_vel = ann.velocity # it's nan in truckscenes, we need to compute... don't know why + ann_vel = data_fn.box_velocity(ann.token) # in world frame + if np.isnan(ann_vel).any(): + continue + + # previous x/y/z velocity is on world frame, need to transform to ego frame + ann_vel = ann_vel @ pose0[:3,:3] + cls = ann.name + + # extend box length according to velocity, ref HiMo. + ann.wlh[1] = ann.wlh[1] + (np.linalg.norm(ann_vel) * delta_t / 2) + ann.wlh[2] = ann.wlh[2] + 0.2 # some truck top missing points, extend a bit + points_in_box_mask = points_in_box(ann, world_pc0[:,:3].T, wlh_factor=1.1) + classes[points_in_box_mask] = CATEGORY_TO_INDEX[DataNameMap[cls]] + + if np.sum(points_in_box_mask) > 5: + obj_flow = np.ones_like(pc0[points_in_box_mask,:3]) * ann_vel * delta_t + flow[points_in_box_mask] += obj_flow + instances[points_in_box_mask] = (dclass[id_]+1) + id_ += 1 + else: + valid[points_in_box_mask] = False + + return {'flow_0_1': flow, 'valid_0': valid, 'classes_0': classes, + 'ego_motion': ego1_SE3_ego0, 'flow_instance_id': instances} + + mants = TruckScenes(dataroot=data_dir, version=data_mode, verbose=False) + scene = mants.scene[scene_num_id] + log_id = scene['name'] + + # In man-truckscenes, samples are annotated at 2 Hz and sweeps at 10 Hz. + sample_data_lst = [] + for sample in mants.sample: + if sample['scene_token'] != scene['token']: + continue + else: + sample_data_lst.append(sample) + + now_sample_token_str = scene['first_sample_token'] + sample = mants.get('sample', now_sample_token_str) + + # initialize full sweep data dict + full_sweep_data_dict, timestamps = {}, {} + for sensor_name in SelectedSensor: + full_sweep_data_dict[sensor_name] = [] + timestamps[sensor_name] = [] + + for channel, token in sample['data'].items(): + if channel in SelectedSensor: + sample_data = mants.get('sample_data', token) + while sample_data['next'] != '': + ts0 = sample_data['timestamp'] + full_sweep_data_dict[channel].append(sample_data) + timestamps[channel].append(ts0) + sample_data = mants.get('sample_data', sample_data['next']) + + if check_h5py_file_exists(output_dir/f'{log_id}.h5', timestamps[SelectedSensor[-1]]): + print(f'{log_id} already exists and all timestamps are , skip...') + return + + dclass = defaultdict(lambda: len(dclass)) + mygroundseg = ground_seg(GROUNDSEG_config) + + + with h5py.File(output_dir/f'{log_id}.h5', 'a') as f: + for cnt, sweep_data in enumerate(full_sweep_data_dict[SelectedSensor[-1]]): + ts0 = sweep_data['timestamp'] + pose0 = get_pose(mants, sweep_data, w2stf=False) + + lidar_list, lidar_center, lidar_dt, lidar_id = [], [], [], []; lidar_id_cnt = 0 + for single_sensor_name in full_sweep_data_dict.keys(): + # load closest sensor point cloud in reference ego frame + sensor_sweep_list = full_sweep_data_dict[single_sensor_name] + closest_ch_ind, closest_ch_timestamp, timestamp_diff = find_closest_integer_in_ref_arr( + ts0, np.array([t['timestamp'] for t in sensor_sweep_list]) + ) + sensor_sweep = sensor_sweep_list[closest_ch_ind] + + ego2lidar = mants.get('calibrated_sensor', sensor_sweep['calibrated_sensor_token']) + ego2lidar_np = transform_matrix(ego2lidar['translation'], Quaternion(ego2lidar['rotation'])) + + if 'LIDAR' in single_sensor_name: + pc = LidarPointCloud.from_file(os.path.join(str(data_dir), sensor_sweep['filename'])).points.T + # NOTE(Qingwen): we need all points in base_link (ego) coordinate, but we save sensor center also + pc[:,:3] = pc[:,:3] @ ego2lidar_np[:3,:3].T + ego2lidar_np[:3,3].T + lidar_list.append(pc) + + # lidar_dt.append(np.ones(pc.shape[0]) * timestamp_diff * 1e-6) # microsecond to s + lidar_id.append(np.ones(pc.shape[0]) * lidar_id_cnt) + # lidar_center.append(ego2lidar_np[:3,3].T) # x, y, z + lidar_center.append(ego2lidar_np) + lidar_id_cnt += 1 + + points = np.vstack(lidar_list) + # lidar_dt = np.hstack(lidar_dt) + lidar_id = np.hstack(lidar_id) + lidar_center = np.array(lidar_center) # shape Num_LiDAR x 4x4 + + points = remove_ego_points(points, length_threshold=2.0, width_threshold=7.0) + is_ground_0 = np.array(mygroundseg.run(points[:, :3])) + + if cnt == len(full_sweep_data_dict[SelectedSensor[-1]]) - 1: + group = f.create_group(str(ts0)) + create_group_data(group=group, pc=points, gm=is_ground_0.astype(np.bool_), pose=pose0, \ + lidar_id=lidar_id, lidar_center=lidar_center) + else: + sweep_data_next = full_sweep_data_dict[SelectedSensor[-1]][cnt+1] + ts1 = sweep_data_next['timestamp'] + pose1 = get_pose(mants, sweep_data_next, w2stf=False) + + group = f.create_group(str(ts0)) + # annotated frame, compute flow + if sweep_data['is_key_frame'] and sweep_data['prev'] != "": + curr_scene_ann = mants.get_boxes(sweep_data['token']) + scene_flow = compute_flow_simple(mants, points, pose0, pose1, ts0, ts1, curr_scene_ann, dclass, DataNameMap=ManNamMap) + + create_group_data(group=group, pc=points, gm=is_ground_0.astype(np.bool_), pose=pose0, \ + lidar_id=lidar_id, lidar_center=lidar_center, \ + flow_0to1=scene_flow['flow_0_1'], flow_valid=scene_flow['valid_0'], flow_category=scene_flow['classes_0'], \ + flow_instance=scene_flow['flow_instance_id'], + ego_motion=scene_flow['ego_motion']) + else: # no annotations, only save data + create_group_data(group=group, pc=points, gm=is_ground_0.astype(np.bool_), pose=pose0, \ + lidar_id=lidar_id, lidar_center=lidar_center, \ + ego_motion=npcal_pose0to1(pose0, pose1)) + +def proc(x, ignore_current_process=False): + if not ignore_current_process: + current=current_process() + pos = current._identity[0] + else: + pos = 1 + process_log(*x, n=pos) + +def process_logs(data_mode, data_dir: Path, scene_list: list, output_dir: Path, nproc: int): + """ + Compute sceneflow for all logs in the dataset. + Logs are processed in parallel. + """ + + if not (data_dir).exists(): + print(f'{data_dir} not found') + return + + args = sorted([(data_mode, data_dir, scene_num_id, output_dir) for scene_num_id in range(len(scene_list))]) + print(f'Using {nproc} processes') + + # # for debug + # for x in tqdm(args[:2]): + # proc(x, ignore_current_process=True) + # # break + + if nproc <= 1: + for x in tqdm(args): + proc(x, ignore_current_process=True) + else: + with Pool(processes=nproc) as p: + res = list(tqdm(p.imap_unordered(proc, args), total=len(scene_list), ncols=100)) + +def main( + data_dir: str = "/home/kin/data/truckscenes/man-truckscenes", + mode: str = "v1.0-mini", + output_dir: str ="/home/kin/data/truckscenes/h5py", + nproc: int = (multiprocessing.cpu_count() - 1), + only_index: bool = False, + split_name = None +): + available_vers = ['v1.0-trainval', 'v1.0-test', 'v1.0-mini'] # defined by nus. + assert mode in available_vers + # man = TruckScenes(dataroot=data_dir, version=mode, verbose=False) + # print(f"Processing {mode} dataset with {len(man.scene)} scenes...") + + if mode == 'v1.0-trainval': + train_scenes = splits.train + val_scenes = splits.val + if split_name is not None and split_name == 'train': + input_dict = {'train': train_scenes} + elif split_name is not None and split_name == 'val': + input_dict = {'val': val_scenes} + else: + input_dict = {'train': train_scenes, 'val': val_scenes} + elif mode == 'v1.0-test': + test_scenes = splits.test + input_dict = {'test': test_scenes} + elif mode == 'v1.0-mini': + train_scenes = splits.mini_train + val_scenes = splits.mini_val + input_dict = { + 'train': train_scenes, + 'val': val_scenes + } + # NOTE(Qingwen): or if you don't want to split mini, use below + # input_dict = {'mini': train_scenes + val_scenes} + else: + raise ValueError('unknown') + + for input_key, input_val in input_dict.items(): + output_dir_ = Path(output_dir) / input_key + print("[INFO] We are processing data to ", output_dir_) + if only_index: + create_reading_index(Path(output_dir_)) + create_reading_index(Path(output_dir_), flow_inside_check=True) + return + output_dir_.mkdir(exist_ok=True, parents=True) + process_logs(mode, Path(data_dir), input_val, output_dir_, nproc) + create_reading_index(output_dir_) + create_reading_index(Path(output_dir_), flow_inside_check=True) + +if __name__ == '__main__': + start_time = time.time() + fire.Fire(main) + print(f"\nTime used: {(time.time() - start_time)/60:.2f} mins") \ No newline at end of file diff --git a/dataprocess/extract_zod.py b/dataprocess/extract_zod.py index d60ecdd..295c0c7 100644 --- a/dataprocess/extract_zod.py +++ b/dataprocess/extract_zod.py @@ -25,7 +25,7 @@ from dataprocess.misc_data import create_reading_index from linefit import ground_seg -GROUNDSEG_config = f"{BASE_DIR}/conf/ground/zod.toml" +GROUNDSEG_config = f"{BASE_DIR}/conf/others/zod.toml" def process_log(data_dir: Path, log_id: str, output_dir: Path, n: Optional[int] = None) : diff --git a/dataprocess/misc_data.py b/dataprocess/misc_data.py index b749a3c..b292941 100644 --- a/dataprocess/misc_data.py +++ b/dataprocess/misc_data.py @@ -3,6 +3,10 @@ from pathlib import Path from tqdm import tqdm +from typing import Tuple, cast + +NDArrayInt = np.typing.NDArray[np.int64] + def check_h5py_file_exists(h5py_file: Path, timestamps: list, verbose: bool = False) -> bool: if not h5py_file.exists(): return False @@ -52,6 +56,32 @@ def create_reading_index(data_dir: Path, flow_inside_check=False): pickle.dump(data_index, f) print(f"Create {pkl_file_name} index Successfully, cost: {time.time() - start_time:.2f} s") +def find_closest_integer_in_ref_arr( + query_int: int, ref_arr: NDArrayInt +) -> Tuple[int, int, int]: + """Find the closest integer to any integer inside a reference array, and the corresponding difference. + + In our use case, the query integer represents a nanosecond-discretized timestamp, and the + reference array represents a numpy array of nanosecond-discretized timestamps. + + Instead of sorting the whole array of timestamp differences, we just + take the minimum value (to speed up this function). + + Args: + query_int: query integer, + ref_arr: Numpy array of integers + + Returns: + integer, representing the closest integer found in a reference array to a query + integer, representing the integer difference between the match and query integers + """ + closest_ind = np.argmin(np.absolute(ref_arr - query_int)) + closest_int = cast( + int, ref_arr[closest_ind] + ) # mypy does not understand numpy arrays + int_diff = np.absolute(query_int - closest_int) + return closest_ind, closest_int, int_diff + class SE2: def __init__(self, rotation: np.ndarray, translation: np.ndarray) -> None: @@ -155,4 +185,35 @@ def compose(self, right_se2: "SE2") -> "SE2": 'static.other': 'NONE', 'static.vegetation': 'NONE', 'vehicle.ego': 'NONE' +} + +## ====> MAN to Argoverse Mapping +ManNamMap = { + "animal": 'NONE', + "human.pedestrian.adult": 'PEDESTRIAN', + "human.pedestrian.child": 'PEDESTRIAN', + "human.pedestrian.construction_worker": 'PEDESTRIAN', + "human.pedestrian.personal_mobility": 'PEDESTRIAN', + "human.pedestrian.police_officer": 'PEDESTRIAN', + "human.pedestrian.stroller": 'STROLLER', + "human.pedestrian.wheelchair": 'WHEELCHAIR', + "movable_object.barrier": 'NONE', + "movable_object.debris": 'NONE', + "movable_object.pushable_pullable": 'NONE', + "movable_object.trafficcone": 'CONSTRUCTION_CONE', + "static_object.bicycle_rack": 'NONE', + "static_object.traffic_sign": 'SIGN', + "vehicle.bicycle": 'BICYCLE', + "vehicle.bus.bendy": 'ARTICULATED_BUS', + "vehicle.bus.rigid": 'BUS', + "vehicle.car": 'REGULAR_VEHICLE', + "vehicle.construction": 'LARGE_VEHICLE', + "vehicle.emergency.ambulance": 'LARGE_VEHICLE', + "vehicle.emergency.police": 'REGULAR_VEHICLE', + "vehicle.motorcycle": 'MOTORCYCLE', + "vehicle.trailer": 'VEHICULAR_TRAILER', + "vehicle.truck": 'TRUCK', + "vehicle.train": 'NONE', + "vehicle.other": 'NONE', + "vehicle.ego_trailer": 'NONE', } \ No newline at end of file diff --git a/envsftool.yaml b/envsftool.yaml index 887457a..4dc1f09 100644 --- a/envsftool.yaml +++ b/envsftool.yaml @@ -24,7 +24,7 @@ dependencies: - av2==0.2.1 - waymo-open-dataset-tf-2.11.0==1.5.0 - zod==0.5.0 - - truckscenes-devkit + - truckscenes-devkit==1.0.0 # Qingwen's public pkg: - dztimer - dufomap==1.1.0 diff --git a/tools/visualization.py b/tools/visualization.py index e1d5e41..67a3e43 100644 --- a/tools/visualization.py +++ b/tools/visualization.py @@ -30,7 +30,7 @@ from src.utils.o3d_view import MyVisualizer, MyMultiVisualizer, color_map, create_bev_square -VIEW_FILE = f"{BASE_DIR}/assets/view/av2.json" +VIEW_FILE = f"{BASE_DIR}/assets/view/demo.json" def check_flow( data_dir: str ="/home/kin/data/av2/preprocess/sensor/mini",