Skip to content

Commit 2cfe21e

Browse files
committed
docs: update data link, and data-demo img.
* link author-response file also. * test(backup): upload test script (mainly for gt zip file for online evaluaiton. 终于快整理完了!差visualization part
1 parent dabd800 commit 2cfe21e

5 files changed

Lines changed: 1100 additions & 7 deletions

File tree

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ HiMo: High-Speed Objects Motion Compensation in Point Clouds
55
[![page](https://img.shields.io/badge/Project-Page-green)](https://kin-zhang.github.io/HiMo)
66
[![video](https://img.shields.io/badge/video-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/rofaKfezIx0?si=59mMPLYUMgvrkRGj)
77

8-
Note: I knew sometime we might want to see codes asap, so I upload all my experiment codes without cleaning up (some lib might missing etc).
8+
Note 2025-08-28: I knew sometime we might want to see codes asap, so I upload all my experiment codes without cleaning up (some lib might missing etc).
99
I will try my best to cleanup TBD list here:
1010

11-
Update 2025-12-30 I'm back and updating the script now.... Hope I can finish all it before 2026-01-13.
11+
Update 2025-12-30: I'm back and updating the script now.... Hope I can finish all it before 2026-01-13.
1212

1313
- [x] Update the repo README.
1414
- [x] Update OpenSceneFlow repo for dataprocess and SeFlow++.
1515
- [x] Test successfully evaluation codes on Scania and Argoverse2.
1616
- [ ] Test successfully visualization codes.
17-
- [ ] Upload Scania validation set (w/o gt).
17+
- [x] Upload Scania validation set (w/o gt).
1818
- [x] Setup leaderboard for users get their Scania val score.
1919
- [x] Downstream task two repos README update.
2020
- [x] Public the [author-response file](https://github.com/KTH-RPL/HiMo/discussions/1) for readers to check some discussion and future directions etc.
@@ -68,7 +68,14 @@ For further method, you can refer this script for the same format saving.
6868

6969
### Scania
7070

71-
You need upload your result files to the public leaderboard page, we present the best model we have in the paper:
71+
First download the Scania validation set from [huggingface](https://huggingface.co/datasets/KTH/HiMo) (~ 2GB) with 10 scenes.
72+
```bash
73+
# setup hf cli if you don't have it
74+
# curl -LsSf https://hf.co/cli/install.sh | bash
75+
hf download KTH/HiMo --repo-type dataset
76+
```
77+
78+
Get the result files with HiMo by following the best model we have in the paper, and save the .zip files for afterward online evaluation:
7279
```bash
7380
cd OpenSceneFlow
7481
# (feed-forward): load ckpt
@@ -139,9 +146,7 @@ For Video animation example, we use [manim](https://www.manim.community/). I may
139146
}
140147
```
141148

142-
💞 Thanks to Bogdan Timus and Magnus Granström from Scania and Ci Li from KTH RPL, who helped with this work.
143-
We also thank Yixi Cai, Yuxuan Liu, Peizheng Li and Shenghai Yuan for helpful discussions during revision.
144-
We also thank the anonymous reviewers for their useful comments.
149+
💞 We sincerely thank Bogdan Timus and Magnus Granström (Scania) and Ci Li (KTH RPL) for their contributions to this work. We also appreciate Yixi Cai, Yuxuan Liu, Peizheng Li, and Shenghai Yuan for their insightful discussions, as well as the anonymous reviewers for their valuable feedback.
145150

146151
This work was partially supported by the Wallenberg AI, Autonomous Systems and Software Program (WASP) funded by the Knut and Alice Wallenberg Foundation and Prosense (2020-02963) funded by Vinnova.
147152
The computations were enabled by the supercomputing resource Berzelius provided by National Supercomputer Centre at Linköping University and the Knut and Alice Wallenberg Foundation, Sweden.

assets/docs/scania-val.png

387 KB
Loading

tools/test/repack_h5_scania.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
Repack Scania h5 files:
3+
1. Rename 'SensorsCenter' to 'lidar_center'
4+
2. Change lidar_center to (N, 4, 4) with identity rotation
5+
3. Delete 'himu_seflowpp' key
6+
4. Fix data types for PyTorch compatibility (uint32 -> int64, etc.)
7+
5. Repack the h5 file to reclaim space
8+
9+
Usage:
10+
python tools/repack_h5_scania.py --data_dir /home/kin/data/scania/val_v1repacked
11+
"""
12+
13+
import os
14+
import h5py
15+
import numpy as np
16+
from pathlib import Path
17+
from tqdm import tqdm
18+
import argparse
19+
import shutil
20+
21+
# Define expected dtypes for each key (PyTorch compatible)
22+
# Supported types: float64, float32, float16, complex64, complex128, int64, int32, int16, int8, uint8, bool
23+
DTYPE_MAP = {
24+
# 'timestamp': np.int64,
25+
# 'flow_instance_id': np.int32, # uint32 -> int32
26+
'lidar_id': np.uint8,
27+
# 'flow_category_indices': np.uint8,
28+
# 'flow_is_valid': np.bool_,
29+
'ground_mask': np.bool_,
30+
'lidar': np.float32,
31+
'lidar_dt': np.float32,
32+
# 'flow': np.float32,
33+
'pose': np.float64,
34+
'ego_motion': np.float32,
35+
'lidar_center': np.float32,
36+
}
37+
38+
39+
def process_single_h5(h5_path: Path, output_path: Path):
40+
"""Process a single h5 file and save to output path."""
41+
42+
with h5py.File(h5_path, 'r') as f_in:
43+
with h5py.File(output_path, 'w') as f_out:
44+
for timestamp_key in f_in.keys():
45+
group_in = f_in[timestamp_key]
46+
group_out = f_out.create_group(timestamp_key)
47+
48+
for sub_key in group_in.keys():
49+
# Skip himu_seflowpp
50+
if sub_key in ['seflowpp_best']:
51+
continue
52+
53+
# Rename SensorsCenter to lidar_center and convert to (N, 4, 4)
54+
# Also handle existing lidar_center that needs to be converted
55+
if sub_key == 'SensorsCenter' or sub_key == 'lidar_center':
56+
data = group_in[sub_key][:]
57+
# Convert to (N, 4, 4) with identity rotation and translation from original data
58+
if len(data.shape) == 3 and data.shape[1] == 4 and data.shape[2] == 4:
59+
# Already (N, 4, 4), keep as is
60+
new_data = data.astype(np.float32)
61+
elif len(data.shape) == 2 and data.shape[1] == 3:
62+
# (N, 3) -> (N, 4, 4) with identity rotation
63+
N = data.shape[0]
64+
new_data = np.zeros((N, 4, 4), dtype=np.float32)
65+
new_data[:, :3, :3] = np.eye(3) # identity rotation
66+
new_data[:, :3, 3] = data # translation
67+
new_data[:, 3, 3] = 1.0
68+
else:
69+
print(f"Warning: Unexpected shape {data.shape} for {sub_key} in {h5_path}/{timestamp_key}")
70+
new_data = data.astype(np.float32)
71+
72+
group_out.create_dataset('lidar_center', data=new_data)
73+
else:
74+
if sub_key not in DTYPE_MAP:
75+
print(f"Warning: {sub_key} not in DTYPE_MAP, skip this key.")
76+
continue
77+
# Copy other datasets with proper dtype conversion
78+
# Handle scalar datasets (e.g., timestamp)
79+
if group_in[sub_key].shape == ():
80+
data = group_in[sub_key][()]
81+
else:
82+
data = group_in[sub_key][:]
83+
84+
# Convert dtype if needed for PyTorch compatibility
85+
if sub_key in DTYPE_MAP:
86+
data = np.array(data, dtype=DTYPE_MAP[sub_key])
87+
elif data.dtype == np.uint32:
88+
# uint32 not supported by PyTorch, convert to int64
89+
data = data.astype(np.int64)
90+
elif data.dtype == np.uint64:
91+
# uint64 not supported by PyTorch, convert to int64
92+
data = data.astype(np.int64)
93+
94+
group_out.create_dataset(sub_key, data=data)
95+
96+
97+
def main(data_dir: str, in_place: bool = True):
98+
data_path = Path(data_dir)
99+
100+
if not data_path.exists():
101+
print(f"Error: {data_path} does not exist")
102+
return
103+
104+
h5_files = sorted(list(data_path.glob("*.h5")))
105+
print(f"Found {len(h5_files)} h5 files in {data_path}")
106+
107+
if len(h5_files) == 0:
108+
print("No h5 files found!")
109+
return
110+
111+
# Create temp directory for repacked files
112+
temp_dir = data_path / "_temp_repack"
113+
temp_dir.mkdir(exist_ok=True)
114+
115+
for h5_file in tqdm(h5_files, desc="Processing h5 files"):
116+
temp_output = temp_dir / h5_file.name
117+
118+
try:
119+
process_single_h5(h5_file, temp_output)
120+
121+
if in_place:
122+
# Replace original file with repacked one
123+
shutil.move(str(temp_output), str(h5_file))
124+
except Exception as e:
125+
print(f"Error processing {h5_file}: {e}")
126+
if temp_output.exists():
127+
temp_output.unlink()
128+
continue
129+
130+
# Clean up temp directory
131+
if temp_dir.exists():
132+
shutil.rmtree(temp_dir)
133+
134+
print("Done!")
135+
136+
137+
if __name__ == "__main__":
138+
parser = argparse.ArgumentParser(description="Repack Scania h5 files")
139+
parser.add_argument("--data_dir", type=str, required=True,
140+
help="Path to directory containing h5 files")
141+
parser.add_argument("--no_in_place", action="store_true",
142+
help="If set, don't replace original files (for testing)")
143+
144+
args = parser.parse_args()
145+
main(args.data_dir, in_place=not args.no_in_place)

tools/test/save_zip_gt.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""
2+
# Created: 2024-04-15 17:32
3+
# Copyright (C) 2024-now, RPL, KTH Royal Institute of Technology
4+
# Author: Qingwen Zhang (https://kin-zhang.github.io/)
5+
6+
# Description:
7+
# Save the ground truth compensation distance into feather files inside a zip file for
8+
# afterward easy evaluation on the public benchmark.
9+
10+
11+
PUT THIS FILE INTO HIMO FOLDER AND RUN.
12+
"""
13+
14+
import fire, time, json
15+
from tqdm import tqdm
16+
import numpy as np
17+
import numpy.typing as npt
18+
NDArrayFloat = npt.NDArray[np.float64]
19+
20+
from typing import Tuple
21+
from pathlib import Path
22+
import pandas as pd
23+
from zipfile import ZipFile
24+
from io import BytesIO
25+
import time, os, sys
26+
27+
BASE_DIR = os.path.abspath(os.path.join( os.path.dirname( __file__ ), 'OpenSceneFlow' ))
28+
sys.path.append(BASE_DIR)
29+
from src.dataset import HDF5Dataset
30+
from src.utils.av2_eval import CLOSE_DISTANCE_THRESHOLD
31+
from utils import check_valid, ego_pts_mask, flow2compDis, refine_pts
32+
33+
34+
def read_output_zip(
35+
zip_path: str,
36+
sweep_uuid: Tuple[str, int],
37+
) -> np.ndarray:
38+
"""Read compensation distance predictions from a zip file.
39+
40+
Args:
41+
zip_path: Path to the zip file containing predictions.
42+
sweep_uuid: Identifier of the sweep being predicted (log_id, timestamp_ns).
43+
44+
Returns:
45+
compensation_dis: (N, 3) compensation distance predictions.
46+
"""
47+
with ZipFile(zip_path, 'r') as myzip:
48+
feather_path = f"{sweep_uuid[0]}/{sweep_uuid[1]}.feather"
49+
with myzip.open(feather_path) as f:
50+
df = pd.read_feather(BytesIO(f.read()))
51+
52+
compensation_dis = np.stack([
53+
df['comp_dis_x_m'].values.astype(np.float32),
54+
df['comp_dis_y_m'].values.astype(np.float32),
55+
df['comp_dis_z_m'].values.astype(np.float32),
56+
], axis=1)
57+
58+
# If category/instance columns are present, they will be available in the DataFrame
59+
eval_mask = df['eval_mask'].values.astype(bool)
60+
flow_category = df['flow_category_indices'].values.astype(np.uint8) if 'flow_category_indices' in df.columns else None
61+
flow_instance = df['flow_instance_id'].values.astype(np.uint32) if 'flow_instance_id' in df.columns else None
62+
return compensation_dis, eval_mask, flow_category, flow_instance
63+
64+
def write_output_file(
65+
compensation_dis: NDArrayFloat,
66+
sweep_uuid: Tuple[str, int],
67+
output_dir: Path,
68+
eval_mask: NDArrayFloat,
69+
flow_category_indices: np.ndarray = None,
70+
flow_instance_id: np.ndarray = None,
71+
gt_flow_norm: np.ndarray = None,
72+
pc0: np.ndarray = None,
73+
) -> None:
74+
"""Write an output predictions file in the correct format for submission.
75+
76+
Args:
77+
compensation_dis: (N,3) compensation_dis predictions.
78+
sweep_uuid: Identifier of the sweep being predicted (log_id, timestamp_ns).
79+
output_dir: Top level directory containing all predictions.
80+
pc0: (N,3) original point cloud coordinates (needed for Chamfer calculation).
81+
"""
82+
output_log_dir = output_dir / sweep_uuid[0]
83+
output_log_dir.mkdir(exist_ok=True, parents=True)
84+
compensation_dis_x_m = compensation_dis[:, 0].astype(np.float32)
85+
compensation_dis_y_m = compensation_dis[:, 1].astype(np.float32)
86+
compensation_dis_z_m = compensation_dis[:, 2].astype(np.float32)
87+
88+
# Build the output DataFrame; include category/instance if provided
89+
data_dict = {
90+
"comp_dis_x_m": compensation_dis_x_m,
91+
"comp_dis_y_m": compensation_dis_y_m,
92+
"comp_dis_z_m": compensation_dis_z_m,
93+
"eval_mask": eval_mask.astype(np.uint8),
94+
}
95+
if flow_category_indices is not None:
96+
data_dict['flow_category_indices'] = flow_category_indices.astype(np.uint8)
97+
if flow_instance_id is not None:
98+
data_dict['flow_instance_id'] = flow_instance_id.astype(np.uint32)
99+
if gt_flow_norm is not None:
100+
data_dict['gt_flow_norm'] = gt_flow_norm.astype(np.float32)
101+
# Save pc0 for Chamfer calculation (GT only)
102+
if pc0 is not None:
103+
data_dict['pc0_x'] = pc0[:, 0].astype(np.float32)
104+
data_dict['pc0_y'] = pc0[:, 1].astype(np.float32)
105+
data_dict['pc0_z'] = pc0[:, 2].astype(np.float32)
106+
107+
output = pd.DataFrame(data_dict)
108+
output.to_feather(output_log_dir / f"{sweep_uuid[1]}.feather")
109+
110+
111+
def zip_res(res_folder, output_file="submit.zip"):
112+
all_scenes = [f for f in os.listdir(res_folder) if os.path.isdir(os.path.join(res_folder, f))]
113+
with ZipFile(output_file, "w") as myzip:
114+
for scene in all_scenes:
115+
scene_folder = os.path.join(res_folder, scene)
116+
# only directory
117+
all_logs = [f for f in os.listdir(scene_folder) if os.path.isfile(os.path.join(scene_folder, f)) and f.endswith('.feather')]
118+
for log in all_logs:
119+
file_path = os.path.join(scene, log)
120+
myzip.write(os.path.join(res_folder, file_path), arcname=file_path)
121+
# remove the folder after zipping
122+
for scene in all_scenes:
123+
scene_folder = os.path.join(res_folder, scene)
124+
os.system(f"rm -rf {scene_folder}")
125+
print(f"Zipped results to {res_folder} into {output_file}. Submit your result by uploading this zip file.")
126+
# print()
127+
return output_file
128+
129+
def main(
130+
# data_dir: str ="/home/kin/data/Scania/preprocess/val",
131+
data_dir: str ="/home/kin/data/av2/h5py/sensor/himo/demo",
132+
output_dir: str = "/home/kin/data/av2/h5py/sensor/himo/results",
133+
res_name: str = "flow"
134+
):
135+
data_dir = Path(data_dir)
136+
output_dir = Path(output_dir)
137+
output_dir.mkdir(exist_ok=True, parents=True)
138+
data_name, _ = check_valid(str(data_dir), res_name, None)
139+
140+
dataset = HDF5Dataset(data_dir, vis_name=res_name, eval=True)
141+
for data_id in tqdm(range(0, len(dataset)), ncols=120, desc=f"Extracting {res_name} from {data_dir}"):
142+
data = dataset[data_id]
143+
pc0, pose0, pose1 = data['pc0'], data['pose0'], data['pose1']
144+
ego_pose = np.linalg.inv(pose1) @ pose0
145+
pose_flow = pc0[:, :3] @ ego_pose[:3, :3].T + ego_pose[:3, 3] - pc0[:, :3]
146+
pc_dis = np.linalg.norm(pc0[:, :2], axis=1)
147+
dis_mask = pc_dis <= CLOSE_DISTANCE_THRESHOLD
148+
notgm_mask = ~data['gm0']
149+
150+
# scania
151+
if data_name == "scania":
152+
mask_eval = dis_mask & data['flow_is_valid'] & notgm_mask & ego_pts_mask(pc0)
153+
else:
154+
mask_eval = dis_mask & notgm_mask & ego_pts_mask(pc0, min_bound=[-1.5, -1.5, -2.0], max_bound=[1.5, 1.5, 2.0])
155+
156+
try:
157+
est_flow = np.zeros_like(pose_flow) if res_name == "raw" else (data[res_name] - pose_flow)
158+
except:
159+
print(f"Warning: {data['scene_id']} {data['timestamp']} has no result for {res_name}, set zero flow.")
160+
est_flow = np.zeros_like(pose_flow)
161+
162+
# NOTE: we compensated to the latest observation. dts: (N,1) Nanosecond offsets _from_ the start of the sweep.
163+
# NOTE: we compensated to the latest observation. dts: (N,1) Nanosecond offsets _from_ the start of the sweep.
164+
dt0 = max(data['lidar_dt']) - data['lidar_dt']
165+
166+
# GT flow and GT compensation distance
167+
gt_flow = data['flow'] - pose_flow # GT flow in ego-motion compensated frame
168+
gt_comp_dis = flow2compDis(gt_flow, dt0, sensor_dt=0.1) # GT compensation distance
169+
gt_flow_norm = np.linalg.norm(gt_flow, axis=1).astype(np.float32)
170+
171+
# Attach category/instance arrays so the saved feather contains labels for GT
172+
flow_cat = data['flow_category_indices'] if 'flow_category_indices' in data else None
173+
flow_inst = data['flow_instance_id'] if 'flow_instance_id' in data else None
174+
175+
# Save GT compensation distance (for GT zip file)
176+
write_output_file(gt_comp_dis, (data['scene_id'], str(data['timestamp'])), output_dir, mask_eval,
177+
flow_category_indices=flow_cat, flow_instance_id=flow_inst,
178+
gt_flow_norm=gt_flow_norm, pc0=pc0[:, :3])
179+
180+
zip_res(output_dir, output_file=f"{output_dir}/{res_name}-submit.zip")
181+
182+
if __name__ == '__main__':
183+
start_time = time.time()
184+
fire.Fire(main)
185+
print(f"Time used: {time.time() - start_time:.2f} s")

0 commit comments

Comments
 (0)