# Train Measurement System - Developer Guide ## Architecture Overview This is a **configuration-driven, plugin-based point cloud measurement system** for train inspection. The core design principle: measurements are defined in YAML, not hardcoded in Python. ### Key Design Decisions **Why configuration-driven?** - Users need to measure 20+ different features per train scan - Measurement locations are consistent across scans but vary by train model - Non-programmers should be able to add new measurement tasks by editing YAML **Why plugin-based measurement types?** - Three fundamentally different measurement algorithms: plane-to-plane, point-to-plane, point-to-point - Each type has different input requirements (ROI bboxes vs exact coordinates) - Easy to add new types (e.g., circle-to-circle) without modifying core logic **Why single large point cloud input?** - Train scans are 100M points covering the entire vehicle - ROI extraction happens at runtime based on bbox parameters - Avoids pre-segmentation and maintains spatial context ## Common Development Tasks ### Running measurements ```bash conda activate pcc-experiment cd train_measurement_system python main.py ``` Results: `results/measurement_results_YYYYMMDD_HHMMSS.xlsx` Visualizations: `visualizations/*.ply` ### Adding a new measurement task 1. **Edit `config/measurements.yaml`** - add task definition with appropriate type 2. **Set `enabled: true`** and provide required parameters for that type 3. **Run `python main.py`** - the task will execute automatically 4. **Check Excel results** - verify mean, std dev, inlier ratio 5. **Inspect PLY files** - confirm ROI extraction is correct ### Adding a new measurement type 1. **Create `src/measurements/your_type.py`** inheriting from `BaseMeasurement` 2. **Implement `execute() -> MeasurementResult`** with your algorithm 3. **Register in `src/measurements/__init__.py`** - add to `__all__` 4. **Update `src/task_manager.py`** - add branch in `_create_measurement_task()` 5. **Document in README.md** - add config example and parameter descriptions ## Architecture Flow ``` YAML Config → TaskManager → [Measurement Plugin] → Excel + PLY ↓ PointCloudLoader (cached) ↓ ROIExtractor / PlaneFitter (shared utilities) ``` **TaskManager** orchestrates everything: - Loads point cloud once, caches in memory - Iterates through enabled tasks - Instantiates appropriate measurement class based on `type` field - Collects results and delegates to ExcelExporter **Measurement plugins** are stateless: - Receive full point cloud + task config in constructor - Extract ROI, run algorithm, return `MeasurementResult` - No shared state between tasks ## Critical Implementation Details ### Coordinate System - Point cloud uses **global coordinates** (mm units) - Z-axis is vertical (up positive) - ROI bboxes are in global coordinates - No coordinate transformations applied during extraction ### RANSAC Plane Fitting - `inlier_ratio` in results indicates ROI quality, not measurement accuracy - 100% inlier ratio = ROI perfectly contains only the target plane - Low inlier ratio (<70%) = ROI includes multiple surfaces or noise - **Automatic downsampling**: ROIs >10k points are sampled to 10k before SVD fitting (prevents memory explosion) ### Point-to-Plane Distance - Two modes: `exact_points` (KDTree nearest neighbor) vs `bbox_filtered` (extract then filter) - `exact_points` is faster but requires knowing precise coordinates - `bbox_filtered` supports filters like `max_abs_coordinate` for finding extreme points ### Plane-to-Plane Distance - **Unidirectional sampling**: samples points from source plane, measures to target plane - Not symmetric: swapping source/target may give different results if planes aren't parallel - Standard deviation indicates parallelism quality ## Testing Strategy **When modifying measurement algorithms:** 1. Disable all tasks except one test case (`enabled: false`) 2. Run and verify Excel output matches expected values 3. Open PLY files in CloudCompare/MeshLab to visually verify ROI extraction 4. Check console output for warnings (low inlier ratio, insufficient points) **When adding new measurement types:** 1. Start with a simple test case with known ground truth 2. Verify the `MeasurementResult.data` dictionary contains all required fields 3. Ensure visualization saves the relevant points (not the entire ROI) 4. Test with edge cases: empty ROI, single point, coplanar points ## Common Pitfalls - **Forgetting to set `enabled: true`** - task silently skipped - **Wrong coordinate order** - YAML uses `[x, y, z]`, some tools use `[z, y, x]` - **Bbox too large** - includes multiple surfaces, low inlier ratio - **Bbox too small** - insufficient points for robust fitting - **Missing `visualization_dir`** - no PLY files generated (optional but recommended) - **Modifying cached point cloud** - reload by restarting Python process ## Performance Notes - Point cloud loading: ~10-30 seconds for 100M points (cached after first load) - Single plane-to-plane task: ~5-10 seconds (ROI extraction + RANSAC + sampling) - Bottleneck is usually RANSAC iterations, not point cloud I/O - Visualization (PLY save) adds ~1-2 seconds per task ## Extension Points **Want to add CAD-based ROI extraction?** - Modify `roi_extractor.py` to accept plane parameters from CAD - Keep bbox-based extraction as fallback - Add `roi_source: "cad"` vs `"bbox"` in config **Want to add statistical outlier removal?** - Add preprocessing step in measurement plugins before RANSAC - Make it optional via `params.remove_outliers: true` **Want to support multiple point cloud files?** - Modify `task_manager.py` to accept per-task `pointcloud_file` override - Update `pointcloud_loader.py` to cache multiple files by path **Want to add 2D projections or cross-sections?** - Create new measurement type `cross_section_profile` - Use `roi_extractor` to get 3D points, then project to 2D plane - Output profile data as additional sheet in Excel