Skip to content

Commit a531803

Browse files
authored
fix(patchwork): close the 2.3 F1 gap vs the original Patchwork (+ USAGE.md) (#90)
* fix(patchwork): three deviations vs original Patchwork (gated by compile flags) cpp/patchwork/src/patchwork.cpp deviates from the original ~/git/patchwork in three places that together cost about 2.3 F1 on KITTI 00-10 under the Patchwork++ paper evaluation protocol with paper-matched parameters (uprightness_thr=0.707, using_global_thr=false). The fixes are gated behind PW_FIX_1, PW_FIX_2, PW_FIX_3 compile flags so an ablation can measure each independently: PW_FIX_1 — elevation_thr is GROUND-frame; subtract sensor_height to get the sensor-frame cutoff (the YAML in ~/git/patchwork documents this explicitly; the reimpl was using the raw value). PW_FIX_2 — plane-distance comparison: original uses uncentred `normal . p` against `th_dist - d_`. The reimpl uses the centred `normal . (p - mean)` against the same threshold, which shifts the cutoff by an extra `-2 * d_` (about 3.2 m on KITTI ground). PW_FIX_3 — tier index for elevation/flatness uses the GLOBAL ring index across all zones, not the local ring-or-zone-id collapse. Full sweep on KITTI 00-10 (23,201 frames), Patchwork++ paper protocol, paper params: current main : P=89.70 R=98.49 F1=93.73 fix/performance : P=94.64 R=97.58 F1=96.02 ~/git/patchwork : P=94.38 R=97.90 F1=96.05 (reference) paper Table I : P=94.23 R=97.62 F1=95.88 Build a variant with: pip install --no-build-isolation --force-reinstall \\ --config-settings=cmake.define.PW_FIX_1=ON \\ --config-settings=cmake.define.PW_FIX_2=ON \\ --config-settings=cmake.define.PW_FIX_3=ON \\ ./python/ See #89 for the full ablation report. Also adds: - python/examples/evaluate_semantickitti.py — KITTI eval driver with both Patchwork-paper and Patchwork++ paper protocols (#87, #88). - python/examples/aggregate_original_patchwork.py — aggregates the per-sequence txt files from ~/git/patchwork's eval mode. - scripts/ablation_sweep.sh — the 5-variant ablation runner. * fix(patchwork): apply the three Patchwork deviations unconditionally + USAGE.md The previous commit on this branch gated the three fixes behind compile flags (PW_FIX_1/2/3) for ablation. Measurement showed all three are needed to match the original Patchwork; this commit removes the toggles and applies the fixes unconditionally: 1. tier index = global ring index across all zones (not the previous `(zone==0) ? ring : zone` collapse). 2. elevation_thr is converted to the sensor frame by subtracting sensor_height (matches the comment in the original config/velodyne64.yaml). 3. plane-distance comparison uses uncentred `normal . p` so the cutoff of `th_dist - d_` actually represents "signed distance < th_dist" (the previous centred form shifted the cutoff by an extra -d_). The ablation runner (scripts/ablation_sweep.sh) is removed since it referenced the compile flags. Adds USAGE.md covering: - the two SemanticKITTI evaluation protocols (Patchwork vs Patchwork++ paper) and which to pick when reproducing each paper's Table I, - parameter tuning order (sensor_height -> uprightness_thr -> range bounds -> plane-fit thresholds -> elevation_thr / RNR / RVPF / TGR), - a copy-pasteable command to reproduce Patchwork++ Table I. See #87, #88, #89. * style: apply pre-commit auto-fixes (black, isort, mdformat) and drop unused import * chore(release): v1.3.0 with performance enhancement for pypatchworkpp.patchwork Bumps: - python/pyproject.toml 1.2.0 -> 1.3.0 - cpp/CMakeLists.txt 1.2.0 -> 1.3.0 Adds CHANGELOG.md with a v1.3.0 entry documenting the three performance fixes in cpp/patchwork/src/patchwork.cpp (full sweep: F1 93.73 -> 96.02 under the Patchwork++ paper evaluation protocol; matches the original Patchwork ROS 2 build and paper Table I within paper variance). pypatchworkpp.patchworkpp is unaffected. See #87, #88, #89, #90.
1 parent be1b5f4 commit a531803

9 files changed

Lines changed: 775 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Changelog
2+
3+
## v1.3.0
4+
5+
### Performance enhancement — `pypatchworkpp.patchwork` (classic Patchwork reimpl)
6+
7+
Three deviations between `cpp/patchwork/src/patchwork.cpp` and the original
8+
Patchwork (`url-kaist/patchwork`) were identified and fixed. With paper-matched
9+
parameters (`uprightness_thr=0.707`, `using_global_thr=false`) on SemanticKITTI
10+
sequences 00–10 (23,201 frames), under the Patchwork++ paper evaluation
11+
protocol (Sec. IV.A — VEGETATION excluded):
12+
13+
| Configuration | Precision | Recall | F1 |
14+
| --- | --- | --- | --- |
15+
| v1.2.0 (`pypatchworkpp.patchwork`) | 89.70 | 98.49 | 93.73 |
16+
| **v1.3.0 (`pypatchworkpp.patchwork`)** | **94.64** | **97.58** | **96.02** |
17+
| Original Patchwork ROS 2 (reference) | 94.38 | 97.90 | 96.05 |
18+
| Patchwork++ paper Table I, Patchwork \[1\] | 94.23 | 97.62 | 95.88 |
19+
20+
**+2.29 F1** vs v1.2.0; within ±0.14 F1 of the original Patchwork ROS 2 build
21+
and within paper run-to-run variance of Table I.
22+
23+
Fixes:
24+
25+
1. `elevation_thr` is now converted to the sensor frame by subtracting
26+
`sensor_height` (the YAML in the original repo documents these as
27+
ground-frame). Previously the raw value was used, so the elevation gate
28+
effectively never fired for normal ground.
29+
1. Plane-distance comparison now uses uncentred `normal · p` against
30+
`th_dist_d_ = th_dist − d_`, which is equivalent to "signed distance to
31+
plane \< th_dist". The previous centred form shifted the cutoff by an
32+
extra `−d_ ≈ |normal · mean| ≈ 1.6 m` on KITTI ground.
33+
1. The elevation/flatness tier index is now the GLOBAL ring index across all
34+
zones, so each of the first `elevation_thr.size()` rings gets its own
35+
threshold. The previous `(zone==0) ? ring : zone` collapse destroyed the
36+
per-ring tuning for zones 1+.
37+
38+
`pypatchworkpp.patchworkpp` (the actual Patchwork++) is **unaffected**
39+
all three deviations were in `cpp/patchwork/`, not `cpp/patchworkpp/`.
40+
41+
### Documentation
42+
43+
- Added `USAGE.md` covering (1) the two SemanticKITTI evaluation protocols
44+
and which to pick when reproducing each paper's Table I, (2) the parameter
45+
tuning order for a new sensor, (3) a copy-pasteable command to reproduce
46+
Patchwork++ Table I.
47+
- Added `python/examples/evaluate_semantickitti.py` (a paper-faithful
48+
evaluation driver with `--eval_protocol {patchwork, patchworkpp}`) and
49+
`python/examples/aggregate_original_patchwork.py`.
50+
51+
### References
52+
53+
- #87 — How to reproduce the performance on the paper?
54+
- #88 — Explanation about the evaluation protocol
55+
- #89 — Performance enhancement step-by-step ablation
56+
- #90 — PR landing the three fixes
57+
58+
## v1.2.0 and earlier
59+
60+
See the git log.

USAGE.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Patchwork++ — Usage Guide
2+
3+
This guide covers three things that are easy to get wrong on first contact:
4+
5+
1. [Choosing a SemanticKITTI evaluation protocol](#1-evaluation-protocols) — picks the right ground-truth definition so numbers match the paper.
6+
1. [Tuning the algorithm parameters for your sensor](#2-parameter-tuning) — what each knob does and which ones to touch first when results look bad.
7+
1. [Reproducing the paper's Table I](#3-reproducing-paper-table-i) — a one-command sweep.
8+
9+
For a quick start, jump to [§3](#3-reproducing-paper-table-i).
10+
11+
______________________________________________________________________
12+
13+
## 1. Evaluation protocols
14+
15+
The Patchwork and Patchwork++ papers use **different** ground-truth definitions on SemanticKITTI. The eval driver `python/examples/evaluate_semantickitti.py` supports both via `--eval_protocol {patchwork, patchworkpp}`.
16+
17+
### A. `--eval_protocol patchwork` (original Patchwork repo protocol)
18+
19+
- **Ground GT** = `{ROAD (40), PARKING (44), SIDEWALK (48), OTHER_GROUND (49), LANE_MARKING (60), VEGETATION (70, only if z < −1.30 m), TERRAIN (72)}`
20+
- VEGETATION above −1.30 m → counts as **non-ground**.
21+
- UNLABELED (0) and OUTLIER (1) can be excluded from the precision denominator (`--consider_outliers`, default on).
22+
- Source: Patchwork paper, *"the points annotated with selected classes, i.e. lane marking, road, parking, sidewalk, other ground, vegetation, and terrain, are considered to be ground-truth ground points... only points whose z values are below −1.3 m with respect to the sensor frame are considered as ground truths"*.
23+
24+
Use this when comparing against numbers from the **original Patchwork paper / `url-kaist/patchwork`**.
25+
26+
### B. `--eval_protocol patchworkpp` (Patchwork++ paper Table I protocol — DEFAULT for reproducing the Patchwork++ paper)
27+
28+
- **Ground GT** = `{ROAD, PARKING, SIDEWALK, OTHER_GROUND, LANE_MARKING, TERRAIN}`**no VEGETATION**.
29+
- VEGETATION, UNLABELED, OUTLIER are **fully excluded** from both numerator and denominator.
30+
- Source: Patchwork++ paper Sec. IV.A, *"unlike our previous work, the points labeled as vegetation are not evaluated as ground nor non-ground points exceptionally because it is impractical to regard the vegetation as a single ground or non-ground class. Note that this implies the points labeled as vegetation are only excluded in the evaluation step; the points are still included in the input point cloud"*.
31+
32+
Use this when comparing against numbers from the **Patchwork++ paper**.
33+
34+
### Why it matters
35+
36+
Same Patchwork++ inference, KITTI 00–10 macro average, two protocols:
37+
38+
| Protocol | Precision | Recall | F1 |
39+
|---|---|---|---|
40+
| `--eval_protocol patchwork` | 93.72 | 92.33 | 92.87 |
41+
| `--eval_protocol patchworkpp` | **95.55** | **97.16** | **96.29** |
42+
| Patchwork++ paper Table I | 94.92 | 98.18 | 96.51 |
43+
44+
3.4 F1 difference, entirely from the protocol switch. If your reproduction is 3 F1 low, this is almost certainly the cause.
45+
46+
______________________________________________________________________
47+
48+
## 2. Parameter tuning
49+
50+
If results look wrong on a new sensor (Velodyne 16/32, Ouster 64/128, Livox, etc.), tune in roughly this order. Defaults are in `cpp/patchworkpp/include/patchwork/patchworkpp.h` (Patchwork++) and `cpp/patchwork/include/patchwork/patchwork.h` (classic Patchwork).
51+
52+
### Step 1 — Get `sensor_height` right (the most important parameter)
53+
54+
`sensor_height` is the **height of the LiDAR origin above the ground** when the vehicle is stationary on flat pavement.
55+
56+
- KITTI / HDL-64E on a passenger car: `1.723` m (default).
57+
- Ouster OS0-128 on a UGV: typically 0.6–1.0 m.
58+
- Livox Mid-360 on a quadruped: typically 0.3–0.6 m.
59+
60+
**How to tell it is wrong**: precision is fine on far-range patches but ground points near the sensor are split between ground and non-ground in a striped pattern. The elevation threshold and adaptive seed selection both reference `sensor_height` directly.
61+
62+
If you cannot measure it, leave `ATAT_ON = true` and the All-Terrain Automatic heighT estimator will recover it from the first scan.
63+
64+
### Step 2 — Tune `uprightness_thr` for the surface roughness you expect
65+
66+
`uprightness_thr` is the cosine of the maximum tilt angle accepted for a patch's normal vs. world-up. Higher = stricter.
67+
68+
| Setting | Max tilt | When to use |
69+
|---|---|---|
70+
| 0.5 | ~60° | very rough terrain, off-road; library default for Patchwork++ |
71+
| **0.707** | **~45°** | **Patchwork paper / on-road / structured driving — recommended for KITTI** |
72+
| 0.866 | ~30° | flat indoor floors, parking lots |
73+
74+
If precision is low and you see ramps, low walls, or curbs being labelled as ground: increase to 0.707 or 0.866.
75+
If recall is low on hills, ramps, or rough pavement: lower to 0.5 or 0.4.
76+
77+
### Step 3 — Set range bounds `min_range` / `max_range`
78+
79+
- `min_range` (default 2.7 m): exclude the cone right under the sensor where points are noisy (vehicle body, multipath). Decrease only if your sensor mount is very low.
80+
- `max_range` (default 80.0 m): the cap of the concentric zone model. The CZM zone sizes scale with this; resetting it requires rebuilding `min_ranges`. For most rotating LiDARs leave at 80 m.
81+
82+
### Step 4 — Tune the plane-fit thresholds
83+
84+
- `th_seeds` (default 0.5 m): a point is an LPR seed if its `z` is within `th_seeds` of the lowest-z mean. Larger → more seeds → tolerates undulating ground but admits more outliers. Lower for very flat scenes.
85+
- `th_dist` (default 0.125 m): distance from the fitted plane below which a point counts as ground. **This is the single biggest precision/recall knob.** Increase (0.15–0.2 m) on rough/sloped ground if recall is low. Decrease (0.05–0.1 m) on parking lots if precision is low.
86+
- `num_iter` (default 3): plane-refit iterations per patch. 2–3 is enough; more is wasted CPU.
87+
88+
### Step 5 — `elevation_thr` and `flatness_thr` (only if you've changed the sensor mount or scene scale)
89+
90+
`elevation_thr = {0.523, 0.746, 0.879, 1.125}` are the **ground-frame** height cutoffs for the four closest CZM rings — patches whose mean is more than this above the ground are rejected unless their planarity (`flatness_thr`) saves them. The library converts these to sensor-frame internally by subtracting `sensor_height`.
91+
92+
Rule of thumb: scale them ∝ `expected_terrain_undulation / 1.723 m` if your sensor sits lower or higher than KITTI. Most users do **not** need to touch these.
93+
94+
### Step 6 — Patchwork++ extras (`pypatchworkpp.patchworkpp` only)
95+
96+
- `enable_RNR` (default true) — Reflected Noise Removal. Turn off only if your sensor has very clean returns near the bottom rings (most rotating LiDARs need it on).
97+
- `enable_RVPF` (default true) — Region-wise Vertical Plane Fitting. Helps on retaining walls / curbs. Keep on.
98+
- `enable_TGR` (default true) — Temporal Ground Revert. Reverts FN under-segmentation. Keep on.
99+
- `RNR_intensity_thr` (default 0.2) — RNR's intensity gate. Calibrate to your sensor's intensity scale: if intensities are 0–255, set to ~50.
100+
101+
______________________________________________________________________
102+
103+
## 3. Reproducing paper Table I
104+
105+
```bash
106+
# 1. Install once
107+
pip install -v ./python/
108+
109+
# 2. Reproduce Patchwork++ Table I row on KITTI 00–10
110+
python python/examples/evaluate_semantickitti.py \
111+
--method patchworkpp \
112+
--eval_protocol patchworkpp \
113+
--dataset_path /path/to/SemanticKITTI/sequences \
114+
--output_csv summary_patchworkpp.csv
115+
```
116+
117+
Expected output (full sweep, 23,201 frames):
118+
119+
| seq | frames | P | R | F1 |
120+
|---|---|---|---|---|
121+
| Avg | 23201 | **95.55** | **97.16** | **96.29** |
122+
123+
Paper Table I: P=94.92, R=98.18, F1=96.51 — match within ±0.22 F1.
124+
125+
### Quick smoke test (3 frames per seq, ~5 s total)
126+
127+
```bash
128+
python python/examples/evaluate_semantickitti.py \
129+
--method patchworkpp \
130+
--eval_protocol patchworkpp \
131+
--dataset_path /path/to/SemanticKITTI/sequences \
132+
--max_frames 3 --verbose
133+
```
134+
135+
### Apples-to-apples vs. the original Patchwork repo
136+
137+
```bash
138+
# Compare the in-repo classic Patchwork against the original ROS 2 patchwork
139+
python python/examples/evaluate_semantickitti.py \
140+
--method patchwork \
141+
--eval_protocol patchworkpp \
142+
--dataset_path /path/to/SemanticKITTI/sequences \
143+
--output_csv summary_patchwork.csv
144+
```
145+
146+
`--method patchwork` will be paper-faithful after the fixes on this branch (#89) land — until then it is ~2.3 F1 below the original Patchwork on the same protocol.
147+
148+
______________________________________________________________________
149+
150+
## See also
151+
152+
- [`python/examples/demo_visualize.py`](python/examples/demo_visualize.py) — single-frame visualisation.
153+
- [`python/examples/demo_sequential.py`](python/examples/demo_sequential.py) — iterate over a folder of `.bin` files.
154+
- Issues: [#87](https://github.com/url-kaist/patchwork-plusplus/issues/87) (reproduce paper), [#88](https://github.com/url-kaist/patchwork-plusplus/issues/88) (evaluation protocol), [#89](https://github.com/url-kaist/patchwork-plusplus/issues/89) (performance enhancement).

cpp/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
cmake_minimum_required(VERSION 3.11)
2-
project(patchworkpp VERSION 1.2.0)
2+
project(patchworkpp VERSION 1.3.0)
33

44
option(USE_SYSTEM_EIGEN3 "Use system pre-installed Eigen" OFF)
55
option(INCLUDE_CPP_EXAMPLES "Include C++ example codes, which require Open3D for visualization" OFF)

cpp/patchwork/src/patchwork.cpp

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,19 @@ PatchStatus PatchWork::determine_gle_status(int zone_idx,
150150
return PatchStatus::TooTilted;
151151
}
152152

153-
// The first elevation_thr.size() tiers get tier-specific elevation/flatness thresholds.
154-
const int tier = (zone_idx == 0) ? ring_idx : zone_idx;
153+
// Use the GLOBAL ring index across all zones for tier lookup so that
154+
// each of the first elevation_thr.size() rings gets its own threshold,
155+
// matching the original Patchwork.
156+
int tier = ring_idx;
157+
for (int z = 0; z < zone_idx; ++z) tier += params_.num_rings_each_zone[z];
158+
155159
if (tier < static_cast<int>(params_.elevation_thr.size())) {
156160
const double mean_z = feature.mean_(2);
157-
if (mean_z > params_.elevation_thr[tier]) {
161+
// elevation_thr is GROUND-frame (see config/velodyne64.yaml in the
162+
// original Patchwork repo); convert to the sensor frame by
163+
// subtracting sensor_height.
164+
const double elev_cut = -params_.sensor_height + params_.elevation_thr[tier];
165+
if (mean_z > elev_cut) {
158166
// Recoverable if the patch is very flat
159167
if (feature.singular_values_(2) < params_.flatness_thr[tier]) {
160168
return PatchStatus::FlatEnough;
@@ -204,8 +212,13 @@ void PatchWork::perform_regionwise_segmentation(int zone_idx,
204212
std::vector<PointXYZ> nonground;
205213
for (const auto& p : sorted) {
206214
Eigen::Vector3f v(p.x, p.y, p.z);
207-
const float distance = feature.normal_.dot(v - feature.mean_);
208-
if (distance < feature.th_dist_d_) {
215+
// Original Patchwork compares the uncentred normal . p to
216+
// th_dist_d_ = th_dist - d_, which is equivalent to "signed
217+
// distance to plane < th_dist". The previous centred form here
218+
// shifted the cutoff by an extra -d_ ~ |normal . mean|, which on
219+
// KITTI ground is ~1.6 m and effectively disabled the cutoff.
220+
const float signed_dist = feature.normal_.dot(v);
221+
if (signed_dist < feature.th_dist_d_) {
209222
ground.push_back(p);
210223
} else {
211224
nonground.push_back(p);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""Aggregate per-sequence txt outputs produced by the original Patchwork
2+
(`~/git/patchwork`) eval mode into a summary CSV matching the format produced
3+
by `evaluate_semantickitti.py`.
4+
5+
Per-frame row format in ~/patchwork/<seq>.txt:
6+
Legacy (6 cols): frame_idx, time, precision, recall, precision_naive, recall_naive
7+
New (8 cols): + precision_pp, recall_pp (Patchwork++ paper protocol, Sec IV.A)
8+
"""
9+
10+
import argparse
11+
import csv
12+
import os
13+
14+
import numpy as np
15+
16+
17+
def f1(p: float, r: float) -> float:
18+
return 2.0 * p * r / (p + r) if (p + r) > 0 else 0.0
19+
20+
21+
def aggregate_seq(path: str, protocol: str = "patchwork") -> dict:
22+
rows = np.loadtxt(path, delimiter=",")
23+
if rows.ndim == 1:
24+
rows = rows.reshape(1, -1)
25+
if protocol == "patchworkpp":
26+
if rows.shape[1] < 8:
27+
raise ValueError(
28+
f"{path} only has {rows.shape[1]} columns; rerun patchwork "
29+
"with the patched main.cpp/utils.hpp to produce the 8-col format."
30+
)
31+
p = rows[:, 6]
32+
r = rows[:, 7]
33+
p_n = rows[:, 6]
34+
r_n = rows[:, 7]
35+
else:
36+
p = rows[:, 2]
37+
r = rows[:, 3]
38+
p_n = rows[:, 4]
39+
r_n = rows[:, 5]
40+
return {
41+
"num_frames": int(rows.shape[0]),
42+
"precision": float(p.mean()),
43+
"recall": float(r.mean()),
44+
"f1": float(np.mean([f1(pi, ri) for pi, ri in zip(p, r)])),
45+
"precision_naive": float(p_n.mean()),
46+
"recall_naive": float(r_n.mean()),
47+
"f1_naive": float(np.mean([f1(pi, ri) for pi, ri in zip(p_n, r_n)])),
48+
}
49+
50+
51+
def main():
52+
ap = argparse.ArgumentParser(description=__doc__)
53+
ap.add_argument("--txt_dir", default=os.path.expanduser("~/patchwork"))
54+
ap.add_argument(
55+
"--output_csv",
56+
default="/home/url/git/patchwork-plusplus/summary_patchwork_original.csv",
57+
)
58+
ap.add_argument("--seqs", nargs="+", default=[f"{i:02d}" for i in range(11)])
59+
ap.add_argument(
60+
"--protocol", choices=["patchwork", "patchworkpp"], default="patchwork"
61+
)
62+
args = ap.parse_args()
63+
64+
rows: list[tuple[str, dict]] = []
65+
for seq in args.seqs:
66+
path = os.path.join(args.txt_dir, f"{seq}.txt")
67+
if not os.path.isfile(path):
68+
print(f"[WARN] missing {path}")
69+
continue
70+
rows.append((seq, aggregate_seq(path, args.protocol)))
71+
72+
avg = {
73+
k: float(np.mean([m[k] for _, m in rows]))
74+
for k in (
75+
"precision",
76+
"recall",
77+
"f1",
78+
"precision_naive",
79+
"recall_naive",
80+
"f1_naive",
81+
)
82+
}
83+
avg["num_frames"] = int(sum(m["num_frames"] for _, m in rows))
84+
rows.append(("Avg", avg))
85+
86+
with open(args.output_csv, "w", newline="") as fp:
87+
w = csv.writer(fp)
88+
w.writerow(
89+
[
90+
"seq",
91+
"num_frames",
92+
"precision",
93+
"recall",
94+
"f1",
95+
"precision_naive",
96+
"recall_naive",
97+
"f1_naive",
98+
]
99+
)
100+
for name, m in rows:
101+
w.writerow(
102+
[
103+
name,
104+
m["num_frames"],
105+
f"{m['precision']:.4f}",
106+
f"{m['recall']:.4f}",
107+
f"{m['f1']:.4f}",
108+
f"{m['precision_naive']:.4f}",
109+
f"{m['recall_naive']:.4f}",
110+
f"{m['f1_naive']:.4f}",
111+
]
112+
)
113+
114+
header = f"{'seq':>5} | {'frames':>6} | {'P':>6} {'R':>6} {'F1':>6}"
115+
print(header)
116+
print("-" * len(header))
117+
for name, m in rows:
118+
print(
119+
f"{name:>5} | {m['num_frames']:>6d} | "
120+
f"{m['precision']:6.2f} {m['recall']:6.2f} {m['f1']:6.2f}"
121+
)
122+
print(f"\nWritten {args.output_csv}")
123+
124+
125+
if __name__ == "__main__":
126+
main()

0 commit comments

Comments
 (0)