Skip to content

fix(patchwork): close the 2.3 F1 gap vs the original Patchwork (+ USAGE.md)#90

Merged
LimHyungTae merged 4 commits into
masterfrom
fix/performance
May 21, 2026
Merged

fix(patchwork): close the 2.3 F1 gap vs the original Patchwork (+ USAGE.md)#90
LimHyungTae merged 4 commits into
masterfrom
fix/performance

Conversation

@LimHyungTae

Copy link
Copy Markdown
Contributor

What

Three localised fixes to cpp/patchwork/src/patchwork.cpp (the Patchwork reimplementation exposed as pypatchworkpp.patchwork) so it matches the original Patchwork on KITTI 00-10:

  1. elevation_thr is converted to the sensor frame by subtracting sensor_height. The YAML thresholds in the original repo are documented as ground-frame; the reimpl was using the raw value, so the elevation gate effectively never fired for normal ground.
  2. Plane-distance comparison uses uncentred normal . p against th_dist_d_ = th_dist - d_. The previous centred form shifted the cutoff by an extra -d_ (~ 1.6 m on KITTI), which absorbed far-from-plane points into ground.
  3. Tier index is the GLOBAL ring index across all zones, so each of the first elevation_thr.size() rings gets its own threshold. The previous (zone==0) ? ring : zone collapse destroyed the per-ring tuning.

Plus 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 for a new sensor (sensor_height → uprightness_thr → range → plane-fit → elevation/RNR/RVPF/TGR),
  • a copy-pasteable command to reproduce Patchwork++ Table I.

Results (KITTI 00-10, all 23,201 frames, paper-matched params: uprightness_thr=0.707, using_global_thr=false)

Configuration Eval protocol P R F1
pypatchworkpp.patchwork, current main Patchwork-paper 86.94 96.75 91.37
pypatchworkpp.patchwork, current main Patchwork++ paper 89.70 98.49 93.73
pypatchworkpp.patchwork, this PR Patchwork-paper 92.77 93.66 93.08
pypatchworkpp.patchwork, this PR Patchwork++ paper 94.64 97.58 96.02
~/git/patchwork original ROS 2 (reference) Patchwork++ paper 94.38 97.90 96.05
Patchwork++ paper Table I, Patchwork [1] Patchwork++ paper 94.23 97.62 95.88

+2.29 F1 vs main under the Patchwork++ paper protocol; within ±0.14 F1 of the original Patchwork ROS 2 build; within paper run-to-run variance of Table I.

Ablation

500 frames/seq × 11 seqs = 5,500 frames per sweep on a per-fix branch state (compile-flagged before this PR squashed the toggles out). Patchwork++ paper protocol:

Variant P R F1 ΔF1 vs baseline
baseline 91.44 98.06 94.45
+ fix 1 only (elevation_thr offset) 92.34 96.98 94.41 ~0
+ fix 2 only (plane distance) 94.81 97.75 96.17 +1.72
+ fix 3 only (tier mapping) 91.42 98.07 94.44 ~0
+ all three 95.53 97.07 96.20 +1.75

Fix 2 is the dominant win on the Patchwork++ paper protocol; Fix 1 contributes more on the Patchwork-paper protocol (+0.80 F1 alone). Fix 3 is small in isolation but logically required for elevation_thr[2..3] to apply where the paper intended.

Test plan

  • Build with pip install -v ./python/ from a clean conda env.
  • Smoke test: 50 frames of seq 00, Patchwork++ paper protocol → P=94.93, R=99.52, F1=97.17.
  • Full sweep KITTI 00-10, both protocols, paper-matched params (table above).
  • pypatchworkpp.patchworkpp (the actual Patchwork++) unaffected — the three fixes are in cpp/patchwork/, not cpp/patchworkpp/. Numbers identical before/after.
  • Suggest squash-merge so the ablation commit + cleanup commit collapse to a single PR commit on master.

References

…ile 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.
…+ 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.
….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.
@LimHyungTae LimHyungTae merged commit a531803 into master May 21, 2026
18 checks passed
@LimHyungTae LimHyungTae deleted the fix/performance branch May 21, 2026 03:18
LimHyungTae added a commit that referenced this pull request May 21, 2026
…th (#94)

cpp/patchwork/ and cpp/patchworkpp/ each carried their own copies of
PointXYZ, PCAFeature, and the small geometry helpers (xy2theta,
xy2radius, point_z_cmp). The plane-fit SVD itself lived in three
slightly-different forms across cpp/patchwork/src/patchwork.cpp,
cpp/patchworkpp/src/patchworkpp.cpp, and the upstream
~/git/patchwork. PR #90 (Fix 2) was a concrete bug caused by that
drift.

This commit factors them into a new tiny static library:

  cpp/common/
    include/patchwork/types.h      — PointXYZ, PCAFeature, PatchStatus
    include/patchwork/plane_fit.h  — xy2theta, xy2radius, point_z_cmp,
                                     estimate_plane(seeds, out, th_dist)
    src/plane_fit.cpp              — SVD-based plane fit (single
                                     canonical implementation)

PCAFeature now carries the `principal_` (largest singular vector)
field that exists in the original Patchwork repo; the classic
Patchwork's PCAFeature was missing it. The current pipeline does not
read `principal_` yet, but adding it costs ~12 bytes per patch and
keeps the type signature aligned with the original.

Dependents updated:
- cpp/patchwork/include/patchwork/patchwork.h:  delete duplicated
  PCAFeature/PatchStatus, include patchwork/types.h.
- cpp/patchwork/src/patchwork.cpp:  delete in-class xy2theta /
  xy2radius / estimate_plane, route call sites to the namespace-level
  helpers in patchwork/plane_fit.h.
- cpp/patchworkpp/include/patchwork/patchworkpp.h:  delete duplicated
  PointXYZ + xy2theta / xy2radius member decls, include
  patchwork/types.h.
- cpp/patchworkpp/src/patchworkpp.cpp:  delete its file-scope
  point_z_cmp and the PatchWorkpp::xy2theta / xy2radius definitions,
  include patchwork/plane_fit.h.

`estimate_plane` is left untouched in patchworkpp.cpp (it still
writes to instance-level pc_mean_/normal_/singular_values_) because
refactoring it requires the thread-safety changes that will arrive
with the TBB work in the next PR.

CMake: new cpp/common/CMakeLists.txt builds ground_seg_common (static,
PIC) linked against Eigen3::Eigen. Both classic and patchworkpp link
to it. The patchworkpp build-tree export() set now also lists
ground_seg_common to keep find_package(patchworkpp) usable from a
build directory.

Numerical equivalence verified on KITTI 00-10 full sweep (23,201
frames) for all four method × protocol combinations against the
v1.3.1 baseline: every macro-average P / R / F1 cell within 0.0013 of
the v1.3.1 number (two combinations byte-identical), well under the
±0.05 budget set in the refactor plan.

Sets up the TBB parallelisation that will land in the follow-up PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance enhancement for pypatchworkpp.patchwork (the original-Patchwork reimpl) — step-by-step ablation

1 participant