Skip to content

Commit e410197

Browse files
committed
Add dor_numpy: numpy-only dynamic-object-removal adapter.
Detector-free range, scan-ratio, and temporal map cleaning via pip install dynamic-object-removal; includes Semantic-KITTI seq 00/05 SA/DA/AA results.
1 parent 8b60f36 commit e410197

4 files changed

Lines changed: 193 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Offline (need prior map).
6969
- [x] BeautyMap (Ours 🚀): [RAL'24](https://arxiv.org/abs/2405.07283), [**Official Code**](https://github.com/MKJia/BeautyMap)
7070
- [x] ERASOR: [RAL'21 official link](https://github.com/LimHyungTae/ERASOR), [**benchmark implementation**](https://github.com/Kin-Zhang/ERASOR/tree/feat/no_ros)
7171
- [x] Removert: [IROS 2020 official link](https://github.com/irapkaist/removert), [**benchmark implementation**](https://github.com/Kin-Zhang/removert)
72+
- [x] dynamic-object-removal (numpy-only): [GitHub](https://github.com/rsasaki0109/dynamic-3d-object-removal), [**Benchmark adapter**](methods/dor_numpy) — detector-free fusion / range / scan-ratio / temporal, `pip install git+https://github.com/rsasaki0109/dynamic-3d-object-removal.git`
7273

7374
Please note that we provided the comparison methods also but modified a little bit for us to run the experiments quickly, but no modified on their methods' core. Please check the LICENSE of each method in their official link before using it.
7475

methods/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ All Running commands, `-1` means all pcd files in the `pcd` folder, otherwise th
8282
./build/octomap_run ${data_path} ${config.toml} -1
8383
python BeautyMap/main.py --data_dir ${data_path} --run_file_num -1
8484
./build/dufomap_run ${data_path} ${config.toml}
85+
pip install "dynamic-object-removal>=0.3" && python dor_numpy/main.py --data_dir ${data_path} --algorithm range
8586
```
8687

8788
<!-- ./build/removert_run ${data_path} ${config.yaml} -1

methods/dor_numpy/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# dynamic-object-removal (numpy-only)
2+
3+
Detector-free dynamic map cleaning via [dynamic-object-removal](https://github.com/rsasaki0109/dynamic-3d-object-removal) — installable straight from GitHub, numpy only.
4+
5+
## Install
6+
7+
```bash
8+
pip install git+https://github.com/rsasaki0109/dynamic-3d-object-removal.git
9+
```
10+
11+
## Run
12+
13+
From this folder (`methods/dor_numpy/`):
14+
15+
```bash
16+
python main.py --data_dir /path/to/00 --algorithm fusion # highest accuracy
17+
python main.py --data_dir /path/to/00 --algorithm range
18+
python main.py --data_dir /path/to/00 --algorithm scan_ratio
19+
python main.py --data_dir /path/to/00 --algorithm temporal
20+
```
21+
22+
Each command writes `dor_<algorithm>_output.pcd` into `data_dir`. Use the benchmark's
23+
`export_eval_pcd` + `scripts/py/eval/evaluate_all.py` for SA/DA/AA/HA.
24+
25+
## Semantic-KITTI teaser results (seq 00 / 05)
26+
27+
| algorithm | seq 00 SA | seq 00 DA | seq 00 AA | seq 05 SA | seq 05 DA | seq 05 AA |
28+
|---|---|---|---|---|---|---|
29+
| fusion | 98.9 | 98.3 | **98.6** | 98.0 | 98.1 | **98.0** |
30+
| range | 99.6 | 34.5 | 58.6 | 99.8 | 25.9 | 50.9 |
31+
| scan_ratio | 98.0 | 92.8 | 95.4 | 96.0 | 97.9 | 96.9 |
32+
| temporal | 97.0 | 46.6 | 67.2 | 97.3 | 25.9 | 50.2 |
33+
34+
`fusion` (library v0.5.0) OR-combines three evidence channels computed per scan
35+
against the accumulated map: ray-sampled free-space carving with per-scan hit
36+
precedence, DUFOMap-style eroded void confirmation (hit inflation + full
37+
26-neighborhood erosion), and the `scan_ratio` votes at a stricter fraction. The
38+
channels fail in complementary regimes — fractional free-space voting nails
39+
transient traffic (seq 00), absolute void counts catch slow movers and late
40+
leavers (seq 05) — so the union scores high on both.
41+
42+
`scan_ratio` normalizes votes per point: a map point is removed only when a majority
43+
of the scans that actually revisit its polar column flag it as vacated (library default
44+
since v0.4.0). Rarely-observed static points no longer accumulate spurious votes over
45+
the sequence, which lifts SA to ~96-98% at near-unchanged DA.
46+
47+
Reproduce end-to-end (download + eval) from the upstream library repo:
48+
49+
```bash
50+
git clone https://github.com/rsasaki0109/dynamic-3d-object-removal.git
51+
python3 dynamic-3d-object-removal/scripts/run_dynamicmap_benchmark.py --sequences 00 05
52+
```

methods/dor_numpy/main.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
"""DynamicMap_Benchmark adapter for dynamic-object-removal (numpy-only).
3+
4+
Usage (from a cloned DynamicMap_Benchmark repo, after
5+
``pip install git+https://github.com/rsasaki0109/dynamic-3d-object-removal.git``):
6+
python main.py --data_dir /path/to/00 --algorithm fusion
7+
python main.py --data_dir /path/to/00 --algorithm range
8+
python main.py --data_dir /path/to/00 --algorithm scan_ratio
9+
python main.py --data_dir /path/to/00 --algorithm temporal
10+
11+
Writes ``dor_<algorithm>_output.pcd`` into ``data_dir`` (cleaned accumulated map).
12+
Run ``export_eval_pcd`` + ``evaluate_all.py`` in the benchmark repo for SA/DA/AA/HA.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import argparse
18+
from pathlib import Path
19+
20+
import numpy as np
21+
22+
try:
23+
import dynamic_object_removal as core
24+
except ImportError as exc:
25+
raise SystemExit(
26+
"Install first: pip install git+https://github.com/rsasaki0109/dynamic-3d-object-removal.git"
27+
) from exc
28+
29+
30+
def _load_sequence(pcd_dir: Path) -> tuple[np.ndarray, list[tuple[np.ndarray, np.ndarray]], list[tuple[int, int]]]:
31+
scan_files = sorted(pcd_dir.glob("*.pcd"))
32+
if not scan_files:
33+
raise SystemExit(f"No scans in {pcd_dir}")
34+
35+
chunks: list[np.ndarray] = []
36+
scans: list[tuple[np.ndarray, np.ndarray]] = []
37+
slices: list[tuple[int, int]] = []
38+
cursor = 0
39+
for path in scan_files:
40+
scan = core.load_pcd_scan(path)
41+
if scan.viewpoint is None:
42+
raise SystemExit(f"Missing VIEWPOINT in {path}")
43+
origin = scan.viewpoint[:3]
44+
n = len(scan.points)
45+
chunks.append(scan.points)
46+
scans.append((scan.points, origin))
47+
slices.append((cursor, cursor + n))
48+
cursor += n
49+
return np.concatenate(chunks, axis=0), scans, slices
50+
51+
52+
def _clean(
53+
algorithm: str,
54+
acc_map: np.ndarray,
55+
scans: list[tuple[np.ndarray, np.ndarray]],
56+
slices: list[tuple[int, int]],
57+
*,
58+
h_res: float,
59+
v_res: float,
60+
voxel_size: float,
61+
temporal_min_hits: int,
62+
sr_min_votes: int | None,
63+
fusion_workers: int,
64+
) -> np.ndarray:
65+
if algorithm == "fusion":
66+
# OR-fusion of free-space carving, DUFOMap-style eroded voids, and
67+
# scan-ratio votes — the library's highest-accuracy map cleaner.
68+
_, keep = core.clean_map_by_fusion(acc_map, scans, workers=fusion_workers)
69+
elif algorithm == "range":
70+
ground_z = float(np.percentile(acc_map[:, 2], 2))
71+
_, keep = core.clean_map_by_visibility(
72+
acc_map,
73+
scans,
74+
h_res_deg=h_res,
75+
v_res_deg=v_res,
76+
ground_z=ground_z,
77+
)
78+
elif algorithm == "scan_ratio":
79+
# min_votes=None: the library normalizes votes per point by the number of
80+
# scans that revisit its polar column (majority rule).
81+
_, keep = core.clean_map_by_scan_ratio(acc_map, scans, min_votes=sr_min_votes)
82+
elif algorithm == "temporal":
83+
keep = np.ones(len(acc_map), dtype=bool)
84+
tfilter = core.TemporalConsistencyFilter(
85+
voxel_size=voxel_size,
86+
window_size=len(scans),
87+
min_hits=temporal_min_hits,
88+
)
89+
# Two passes over the same scans: the first fills the hit counter
90+
# (window_size == number of scans, so nothing is evicted), the second
91+
# judges each scan against the full-sequence counts — re-inserting a
92+
# scan evicts its own first-pass copy, leaving the counter unchanged.
93+
for s, e in slices:
94+
tfilter.filter(acc_map[s:e])
95+
for s, e in slices:
96+
_, keep_f = tfilter.filter(acc_map[s:e])
97+
keep[s:e] = keep_f
98+
else:
99+
raise SystemExit(f"Unknown algorithm: {algorithm}")
100+
return acc_map[keep]
101+
102+
103+
def main() -> int:
104+
parser = argparse.ArgumentParser(description="DynamicMap_Benchmark adapter (numpy-only).")
105+
parser.add_argument("--data_dir", required=True, help="Sequence folder with pcd/ and gt_cloud.pcd.")
106+
parser.add_argument("--algorithm", choices=["fusion", "range", "scan_ratio", "temporal"], default="fusion")
107+
parser.add_argument("--h-res", type=float, default=1.0)
108+
parser.add_argument("--v-res", type=float, default=1.0)
109+
parser.add_argument("--voxel-size", type=float, default=core.DEFAULT_TEMPORAL_VOXEL_SIZE)
110+
parser.add_argument("--temporal-min-hits", type=int, default=2)
111+
parser.add_argument("--sr-min-votes", type=int, default=None,
112+
help="Fixed absolute vote threshold (default: majority of each point's column revisits).")
113+
parser.add_argument("--fusion-workers", type=int, default=6,
114+
help="Process pool size for the fusion carving channels.")
115+
args = parser.parse_args()
116+
117+
data_dir = Path(args.data_dir)
118+
pcd_dir = data_dir / "pcd"
119+
acc_map, scans, slices = _load_sequence(pcd_dir)
120+
cleaned = _clean(
121+
args.algorithm,
122+
acc_map,
123+
scans,
124+
slices,
125+
h_res=args.h_res,
126+
v_res=args.v_res,
127+
voxel_size=args.voxel_size,
128+
temporal_min_hits=args.temporal_min_hits,
129+
sr_min_votes=args.sr_min_votes,
130+
fusion_workers=args.fusion_workers,
131+
)
132+
out = data_dir / f"dor_{args.algorithm}_output.pcd"
133+
core.save_points(out, cleaned, fmt="pcd")
134+
print(f"Wrote {len(cleaned):,} points -> {out}")
135+
return 0
136+
137+
138+
if __name__ == "__main__":
139+
raise SystemExit(main())

0 commit comments

Comments
 (0)