# Issue #469: Terrain Classification Implementation Plan ## Context SaltyBot currently has good sensor infrastructure (IMU, cameras, RealSense) and a robust velocity control system with the `VelocityRamp` class. However, it lacks terrain awareness for surface type detection and speed adaptation. This feature will enable: 1. **Surface detection** via IMU vibration analysis and camera texture analysis 2. **Automatic speed adaptation** based on terrain type and roughness 3. **Terrain logging** for mapping and future learning 4. **Improved robot safety** by reducing speed on rough/unstable terrain ## Architecture Overview The implementation follows the existing ROS2 patterns: ``` IMU/Camera Data ↓ [terrain_classifier_node] ← New node ↓ /saltybot/terrain_state (TerrainState.msg) ↓ [terrain_speed_adapter_node] ← New node ↓ Adjusted /cmd_vel_terrain → existing cmd_vel_bridge ↓ Speed-adapted robot motion Parallel: [terrain_mapper_node] logs data for mapping ``` ## Implementation Components ### 1. **Message Definition: TerrainState.msg** **File to create:** `jetson/ros2_ws/src/saltybot_social_msgs/msg/TerrainState.msg` Fields: - `std_msgs/Header header` — timestamp/frame_id - `uint8 terrain_type` — enum (0=unknown, 1=pavement, 2=grass, 3=gravel, 4=sand, 5=indoor) - `float32 roughness` — 0.0=smooth, 1.0=very rough - `float32 confidence` — 0.0-1.0 classification confidence - `float32 recommended_speed_ratio` — 0.1-1.0 (fraction of max speed) - `string source` — "imu_vibration" or "camera_texture" or "fused" **Update files:** - `jetson/ros2_ws/src/saltybot_social_msgs/CMakeLists.txt` — add TerrainState.msg to rosidl_generate_interfaces() - `jetson/ros2_ws/src/saltybot_social_msgs/package.xml` — no changes needed (std_msgs already a dependency) ### 2. **Terrain Classifier Node** **File to create:** `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_classifier_node.py` **Purpose:** Analyze IMU and camera data to classify terrain type and estimate roughness. **Subscribes to:** - `/camera/imu` (sensor_msgs/Imu) — RealSense IMU at 200 Hz - `/camera/color/image_raw` (sensor_msgs/Image) — camera RGB at 15 Hz **Publishes:** - `/saltybot/terrain_state` (TerrainState.msg) — at 5 Hz **Key functions:** - `_analyze_imu_vibration()` — FFT analysis on accel data (window: 200 samples = 1 sec) - Compute power spectral density in 0-50 Hz band - Extract features: peak frequency, energy distribution, RMS acceleration - Roughness = normalized RMS of high-freq components (>10 Hz) - `_analyze_camera_texture()` — CNN-based texture analysis - Uses MobileNetV2 pre-trained on ImageNet as feature extractor - Extracts high-level texture/surface features from camera image - Lightweight model (~3.5M parameters, ~50-100ms inference on Jetson) - Outputs feature vector fed to classification layer - `_classify_terrain()` — decision logic - Simple rule-based classifier (can be upgraded to CNN) - Input: [imu_roughness, camera_texture_variance, accel_magnitude] - Decision tree or thresholds to classify into 5 types - Output: terrain_type, roughness, confidence **Node Parameters:** - `imu_window_size` (int, default 200) — samples for FFT window - `publish_rate_hz` (float, default 5.0) - `roughness_threshold` (float, default 0.3) — FFT roughness threshold - `terrain_timeout_s` (float, default 5.0) — how long to keep previous estimate if no new data ### 3. **Speed Adapter Node** **File to create:** `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_speed_adapter_node.py` **Purpose:** Adapt cmd_vel speed based on terrain state and integration with velocity ramp. **Subscribes to:** - `/cmd_vel` (geometry_msgs/Twist) — raw velocity commands - `/saltybot/terrain_state` (TerrainState.msg) — terrain classification **Publishes:** - `/cmd_vel_terrain` (geometry_msgs/Twist) — terrain-adapted velocity **Logic:** - Extract target linear velocity from cmd_vel - Apply terrain speed ratio: `adapted_speed = target_speed × recommended_speed_ratio` - Preserve angular velocity (steering not affected by terrain) - Publish adapted command **Node Parameters:** - `enable_terrain_adaptation` (bool, default true) - `min_speed_ratio` (float, default 0.1) — never go below 10% of requested speed - `debug_logging` (bool, default false) **Note:** This is a lightweight adapter. The existing `velocity_ramp_node` handles acceleration/deceleration smoothing independently. ### 4. **Terrain Mapper Node** (Logging/Mapping) **File to create:** `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_mapper_node.py` **Purpose:** Log terrain detections with robot pose for future mapping. **Subscribes to:** - `/saltybot/terrain_state` (TerrainState.msg) - `/odom` (nav_msgs/Odometry) — robot pose **Publishes:** - `/saltybot/terrain_log` (std_msgs/String) — CSV formatted log messages (optional, mainly for logging) **Logic:** - Store terrain observations: (timestamp, pose_x, pose_y, terrain_type, roughness, confidence) - Log to file: `~/.ros/terrain_map_.csv` - Resample to 1 Hz to avoid spam **Node Parameters:** - `log_dir` (string, default "~/.ros/") - `resample_rate_hz` (float, default 1.0) ### 5. **Launch Configuration** **File to update:** `jetson/ros2_ws/src/saltybot_bringup/launch/full_stack.launch.py` Add terrain nodes: ```python terrain_classifier_node = Node( package='saltybot_bringup', executable='terrain_classifier', name='terrain_classifier', parameters=[{ 'imu_window_size': 200, 'publish_rate_hz': 5.0, }], remappings=[ ('/imu_in', '/camera/imu'), ('/camera_in', '/camera/color/image_raw'), ], ) terrain_speed_adapter_node = Node( package='saltybot_bringup', executable='terrain_speed_adapter', name='terrain_speed_adapter', parameters=[{ 'enable_terrain_adaptation': True, 'min_speed_ratio': 0.1, }], remappings=[ ('/cmd_vel_in', '/cmd_vel'), ('/cmd_vel_out', '/cmd_vel_terrain'), ], ) terrain_mapper_node = Node( package='saltybot_bringup', executable='terrain_mapper', name='terrain_mapper', ) ``` **Update setup.py entry points:** ```python 'terrain_classifier = saltybot_bringup.terrain_classifier_node:main' 'terrain_speed_adapter = saltybot_bringup.terrain_speed_adapter_node:main' 'terrain_mapper = saltybot_bringup.terrain_mapper_node:main' ``` ### 6. **Integration with Existing Stack** - The existing velocity ramp (`velocity_ramp_node.py`) processes `/cmd_vel_smooth` or `/cmd_vel` - Optionally, update cmd_vel_bridge to use `/cmd_vel_terrain` if available, else fall back to `/cmd_vel` - Terrain classification runs independently at 5 Hz (much slower than velocity ramping at 50 Hz) ### 7. **Future CNN Enhancement** The current implementation uses rule-based classification with IMU FFT and camera edge detection. A future enhancement could add a lightweight CNN for texture classification (e.g., MobileNet) by: 1. Creating a `terrain_classifier_cnn.py` with TensorFlow/ONNX model 2. Replacing decision logic in `terrain_classifier_node.py` with CNN inference 3. Maintaining same message interface ## Implementation Tasks 1. ✅ **Phase 1: Message Definition** - Create `TerrainState.msg` in saltybot_social_msgs - Update `CMakeLists.txt` 2. ✅ **Phase 2: Terrain Classifier Node** - Implement `terrain_classifier_node.py` with IMU FFT analysis - Implement camera texture analysis - Decision logic for classification 3. ✅ **Phase 3: Speed Adapter Node** - Implement `terrain_speed_adapter_node.py` - Velocity command adaptation 4. ✅ **Phase 4: Terrain Mapper Node** - Implement `terrain_mapper_node.py` for logging 5. ✅ **Phase 5: Integration** - Update `full_stack.launch.py` with new nodes - Update `setup.py` with entry points - Test integration ## Testing & Verification **Unit Tests:** - Test IMU FFT feature extraction with synthetic vibration data - Test terrain classification decision logic - Test speed ratio application - Test CSV logging format **Integration Tests:** - Run full stack with simulated IMU/camera data - Verify terrain messages published at 5 Hz - Verify cmd_vel_terrain adapts speeds correctly - Check terrain log file is created and properly formatted **Manual Testing:** - Drive robot on different surfaces - Verify terrain detection changes appropriately - Verify speed adaptation is smooth (no jerks from ramping) - Check terrain log CSV has correct format ## Critical Files Summary **To Create:** - `jetson/ros2_ws/src/saltybot_social_msgs/msg/TerrainState.msg` - `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_classifier_node.py` - `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_speed_adapter_node.py` - `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_mapper_node.py` **To Modify:** - `jetson/ros2_ws/src/saltybot_social_msgs/CMakeLists.txt` (add TerrainState.msg) - `jetson/ros2_ws/src/saltybot_bringup/launch/full_stack.launch.py` (add nodes) - `jetson/ros2_ws/src/saltybot_bringup/setup.py` (add entry points) **Key Dependencies:** - `numpy` — FFT analysis (already available in saltybot) - `scipy.signal` — Butterworth filter (optional, for smoothing) - `cv2` (OpenCV) — image processing (already available) - `tensorflow` or `tf-lite` — MobileNetV2 pre-trained model for texture CNN - `rclpy` — ROS2 Python client **CNN Model Details:** - Model: MobileNetV2 pre-trained on ImageNet - Input: 224×224 RGB image (downsampled from camera) - Output: 1280-dim feature vector from last conv layer before classification - Strategy: Use pre-trained features directly (transfer learning) for quick MVP, no fine-tuning needed initially - Alternative: Pre-trained weights can be fine-tuned on terrain image dataset in future iterations - Inference: ~50-100ms on Jetson Xavier (acceptable at 5 Hz publish rate) ## Terrain Classification Logic (IMU + CNN Fusion) **Features extracted:** 1. `imu_roughness` = normalized RMS of high-freq (>10 Hz) accel components (0-1) - Computed from FFT power spectral density in 10-50 Hz band - Reflects mechanical vibration from surface contact 2. `cnn_texture_features` = 1280-dim feature vector from MobileNetV2 - Pre-trained features capture texture, edge, and surface characteristics - Reduced to 2-3 principal components via PCA or simple aggregation 3. `accel_magnitude` = RMS of total acceleration (m/s²) - Helps distinguish stationary (9.81 m/s²) vs. moving **Classification approach (Version 1):** - Simple decision tree with IMU-dominant logic + CNN support: ``` if imu_roughness < 0.2 and accel_magnitude < 9.8: terrain = PAVEMENT (confidence boosted if CNN agrees) elif imu_roughness < 0.35 and cnn_grainy_score < 0.4: terrain = GRASS elif imu_roughness > 0.45 and cnn_granular_score > 0.5: terrain = GRAVEL elif cnn_sand_texture_score > 0.6 and imu_roughness > 0.3: terrain = SAND else: terrain = INDOOR ``` - Confidence: weighted combination of IMU and CNN agreement - Roughness metric: `0.0 = smooth, 1.0 = very rough` derived from IMU FFT energy ratio **Speed recommendations:** - Pavement: 1.0 (full speed) - Grass: 0.8 (20% slower) - Gravel: 0.5 (50% slower) - Sand: 0.4 (60% slower) - Indoor: 0.7 (30% slower by default) **Future improvement:** Replace decision tree with trained classifier (Random Forest, SVM, or small Dense net) on labeled terrain dataset once collected. --- This plan follows SaltyBot's established patterns: - Pure Python libraries for core logic (_terrain_analysis.py) - ROS2 node wrappers for integration - Parameter-based configuration in YAML - Message-based pub/sub architecture - Integration with existing velocity control pipeline