Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PerceptaNav — RT-Hybrid Spatial-Perception Terrain-Adaptive Navigation

Python YOLOv8 OpenCV License Platform Status

A real-time, energy-aware, autonomous planetary rover navigation framework combining YOLOv8-based object detection, multi-factor scientific utility scoring, A*-driven terrain-adaptive path planning, and finite-state machine control — fully operable from a single monocular ESP32-CAM stream.

Architecture · Modules · Installation · Usage · Benchmarks · Configuration · API Reference


Table of Contents

  1. Project Overview
  2. Key Contributions
  3. System Architecture
  4. Mathematical Foundation
  5. Module Reference
  6. Data Flow & Frame Processing Pipeline
  7. Finite State Machine Design
  8. Terrain Analysis & Traversability Grid
  9. A* Path Planning on Traversability Grid
  10. Monocular Distance Estimation
  11. Solar Irradiance Modelling (Mars Environment)
  12. Telemetry & Downlink Format
  13. Benchmark Results
  14. Installation
  15. Usage
  16. Configuration Reference
  17. Project File Structure
  18. Dataset Preparation & Model Training
  19. Hardware Integration
  20. Known Limitations & Future Work
  21. Contributing
  22. Citation
  23. License

1. Project Overview

PerceptaNav is a complete, production-ready autonomous navigation system designed for planetary rover applications — specifically targeting Martian surface operations. It fuses real-time computer vision, energy-aware decision-making, terrain traversability analysis, and scientific value maximisation into a unified software stack that runs entirely from a single monocular camera stream.

Unlike conventional rover navigation systems that optimise purely for reachability or proximity, PerceptaNav introduces the concept of scientific utility — the ratio of scientific reward to energy expenditure — as its primary optimisation objective. The rover does not simply move to the nearest rock; it evaluates every visible candidate in the scene, computes a multi-factor scientific relevance score, projects the energy cost of reaching and sampling each candidate, and selects the target that maximises scientific return per watt-hour consumed.

The system is entirely sensor-agnostic in its core design: it requires no depth sensor, no IMU, no GPS, and no pre-loaded map. Everything — terrain classification, obstacle detection, distance estimation, solar charging estimation — is derived from the single RGB video stream. This design philosophy makes the system deployable on low-cost hardware platforms (ESP32-CAM, Raspberry Pi, Jetson Nano) without additional peripheral dependencies.

Why PerceptaNav?

Planetary rover missions are constrained by two inescapable resource limits: energy and time. Every metre of traversal consumes battery. Every sampling event costs watts and seconds. Classic approaches — such as nearest-neighbour target selection or purely reactive obstacle avoidance — fail to account for the long-term trade-off between the cost of reaching a target and the scientific value it provides. This leads to energy being wasted on low-value targets, or high-value targets being skipped because they appear farther away.

PerceptaNav solves this by formulating target selection as a constrained optimisation problem (Section 4 below) and solving it per frame using a lightweight analytical decision engine that runs in well under 1 ms per frame on a Raspberry Pi 4.


2. Key Contributions

The PerceptaNav framework introduces or implements several technically novel components relative to prior open-source rover navigation work:

1. Real-Time Scientific Utility Maximisation
Per-frame computation of Ui = Si / Ei for all visible candidates, where Si is a multi-factor scientific relevance score derived from colour anomaly, texture richness, edge complexity, and mineral saturation, and Ei is the predicted energy cost of traversal and sampling.

2. Hierarchical Multi-Modal Science Scoring
The ScienceScorer computes Si using four independent visual features — chromatic deviation from Martian regolith baseline, Laplacian variance texture richness, Canny edge density structural complexity, and HSV saturation anomaly — weighted and fused into a single bounded score.

3. Monocular Depth Fusion
The DistanceEstimator fuses two independent monocular depth proxies — bounding-box apparent size using known object reference heights and vertical image position using perspective geometry — via confidence-weighted averaging, achieving depth estimates without any auxiliary sensor.

4. Traversability-Aware A* Path Planning
The PathPlanner converts a real-time terrain traversability grid from TerrainClassifier directly into an A* cost field, enabling path planning that avoids rough terrain, steep slopes, and dense rock clusters on a per-frame basis.

5. Optical-Flow-Augmented Obstacle Detection
The ObstacleDetector fuses Canny edge density maps with dense Farnebäck optical flow magnitude to distinguish moving/close obstacles from static background terrain, without requiring a stereo camera.

6. Physics-Grounded Solar Energy Model
The SolarEstimator implements a three-factor Martian solar irradiance model: solar elevation angle over the Martian sol (88,775 s), dust opacity estimated from sky-region histogram variance, and ambient luminance from frame brightness — all derived from the camera feed.

7. Structured Planetary Telemetry Logging
The TelemetryLogger writes per-frame CSV/JSON downlink data in a format aligned with actual planetary rover telemetry conventions, including simulated Martian sol number and local solar time.


3. System Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                         ESP32-CAM / IP Camera Stream                        │
│                      MJPEG stream  ·  960×720  ·  ~15 fps                   │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │ cv2.VideoCapture (FFMPEG)
                                   ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                          FRAME PRE-PROCESSING                               │
│              cv2.resize → (WINDOW_WIDTH × WINDOW_HEIGHT)                    │
└───────┬────────────────────┬────────────────────┬───────────────────────────┘
        │                    │                    │
        ▼                    ▼                    ▼
┌──────────────┐   ┌──────────────────┐  ┌──────────────────────────────────┐
│ YOLOv8n      │   │ YOLOv8-Classify  │  │   SolarEstimator                 │
│ (Detection)  │   │ (Terrain class.) │  │   · Solar elevation (Martian sol)│
│              │   │                  │  │   · Dust opacity (sky histogram) │
│ Bounding     │   │ Scene-level      │  │   · Luminance factor             │
│ boxes +      │   │ terrain label    │  │   → charging_power_W             │
│ class IDs    │   │ + confidence     │  └──────────────┬───────────────────┘
└──────┬───────┘   └────────┬─────────┘                 │
       │                    │                           ▼
       │           ┌────────▼─────────┐     ┌──────────────────────────────┐
       │           │ TerrainClassifier│     │       EnergyModel            │
       │           │ · 4×6 grid       │     │ · P(t) = V(t)·I(t)          │
       │           │ · Laplacian var  │     │ · E = ∫P(t)dt               │
       │           │ · Sobel slope    │     │ · Erem = Etotal - Eused      │
       │           │ · Color heuristic│     │ · Ei = Pmove·ti + Psample·ts │
       │           │ → traversability │     └──────────────┬───────────────┘
       │           │   grid [4][6]    │                    │
       │           └────────┬─────────┘                    │
       │                    │                              │
       ▼                    ▼                              ▼
┌──────────────┐   ┌──────────────────┐      ┌────────────────────────────┐
│DistanceEst.  │   │  PathPlanner     │      │      DecisionEngine        │
│ · BBox focal │   │  · Build cost    │      │  For each candidate:       │
│ · Vert. pos. │   │    grid from     │      │  · Si = ScienceScorer(     │
│ → dist. (m)  │   │    traversability│      │      patch, label, conf)   │
└──────┬───────┘   │  · A* on grid    │      │  · Ei = EnergyModel        │
       │           │  → path waypoints│      │      .predict_cost(dist)   │
       │           │  → steer command │      │  · Feasibility: Ei < Erem  │
       ▼           └────────┬─────────┘      │  · Ui = Si / Ei            │
┌──────────────┐            │                │  → i* = argmax(Ui)         │
│ScienceScorer │            │                └──────────────┬─────────────┘
│ · color_anom │            │                               │
│ · texture    │            │                               ▼
│ · edge_cmplx │   ┌────────▼─────────┐      ┌────────────────────────────┐
│ · saturation │   │ ObstacleDetector │      │        RoverFSM            │
│ → Si [0,1]   │   │ · Edge density   │      │  EXPLORE → APPROACH →      │
└──────┬───────┘   │ · Optical flow   │      │  SAMPLE → AVOID →          │
       │           │ · Threat zones   │      │  LOW_POWER → SAFE_MODE     │
       └───────────┴────────┬─────────┘      └──────────────┬─────────────┘
                            │                               │
                            └──────────────┬────────────────┘
                                           ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         FRAME ANNOTATION & OUTPUT                           │
│  · YOLO detection boxes     · Terrain grid overlay    · FSM state HUD      │
│  · Obstacle threat circles  · A* path line            · Solar bar indicator│
│  · Distance labels          · Minimap (MapBuilder)    · FPS counter        │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │
                     ┌─────────────┴──────────────┐
                     ▼                            ▼
           ┌──────────────────┐        ┌──────────────────────┐
           │  TelemetryLogger │        │    TargetTracker     │
           │  · CSV per frame │        │  · ΣSi accumulation  │
           │  · JSON buffer   │        │  · η = ΣSi / Eused   │
           │  · Sol time sim  │        │  · session summary   │
           └──────────────────┘        └──────────────────────┘

4. Mathematical Foundation

4.1 Energy Model

The energy consumed from time t₀ to t₁ is computed by discrete integration of instantaneous power:

E_used = Σ P(tₙ) · Δtₙ  [Wh]    (Eq. 1)

where P(tₙ) = V(tₙ) · I(tₙ) is the product of terminal voltage and current draw. In the absence of a hardware current sensor, P defaults to the configured IDLE_POWER_W modulated by the FSM's power_multiplier().

Remaining energy is:

E_rem = E_total - E_used    (Eq. 2)

The predicted energy cost of navigating to and sampling target i at distance dᵢ is:

Eᵢ = P_move · tᵢ + P_sample · t_sample    (Eq. 3)

where tᵢ = dᵢ / v_rover    (traversal time in seconds)

Feasibility condition (Eq. 4):

Eᵢ < E_rem · (1 - σ)    where σ = safety_margin (default 0.05)

4.2 Scientific Utility Score

The per-target scientific relevance score Sᵢ is computed from a base label score weighted by four visual features extracted from the target bounding box crop:

Sᵢ = base(labelᵢ) · [0.6 + 0.4 · V] · confidence    (Eq. 5)

V = 0.30 · color_anomaly
  + 0.30 · texture_richness
  + 0.20 · edge_complexity
  + 0.20 · saturation_score

Feature definitions:

Feature Formula Interpretation
color_anomaly ‖μ_BGR - μ_Mars‖ / 150 Deviation from average Martian regolith colour
texture_richness Var(Laplacian(gray)) / 1000 Surface roughness via second-order spatial statistics
edge_complexity sum(Canny(gray)) / (255 · N_pixels) · 10 Structural complexity (high = mineral layering)
saturation_score mean(HSV[:,:,1]) / 255 Unusual mineral colouring (high saturation = anomalous)

All four features are bounded to [0, 1] by np.clip. The final Sᵢ is clipped to [0, 1].

4.3 Decision Optimisation

The target selection problem is formulated as a constrained argmax over utility scores:

i* = argmax  Uᵢ = Sᵢ / Eᵢ    (Eq. 6)
      i ∈ F

subject to: F = { i : Eᵢ < E_rem · (1 - σ) }

This formulation is exactly the energy-normalised scientific reward maximisation studied in autonomous planetary exploration literature. The result is that the rover naturally prefers:

  • High-value minerals close by (high S, low E → very high U)
  • High-value minerals far away only if energy permits
  • Low-value soil nearby over high-value minerals that would deplete the battery

4.4 Traversability and Path Cost

The terrain grid assigns each cell a traversability score τ ∈ [0, 1]. The A* path cost for crossing a cell is:

cost(c) = 1 / max(τ(c), 0.01)    if τ(c) > 0.05
        = ∞                       if τ(c) ≤ 0.05    (impassable)

The A* heuristic uses Manhattan distance on the grid. Diagonal moves are supported at step cost √2 · cell_cost.

4.5 Monocular Distance Estimation

Two independent depth proxies are computed and fused:

Bounding-box focal length method (Eq. 7):

D_bbox = (H_real · f) / H_pixel

where:
  H_real  = reference height of object class [m]
  f       = calibrated focal length [pixels]
  H_pixel = bounding box height [pixels]

Vertical position method (Eq. 8):

D_vert = D_max - frac · (D_max - D_min)

where frac = (cy - horizon_y) / (frame_h - horizon_y)

Confidence-weighted fusion (Eq. 9):

D_final = (D_bbox · w₁ + D_vert · w₂) / (w₁ + w₂)

where w₁, w₂ = confidence scores of each method

The focal length f is updated online via exponential moving average calibration when ground truth distances are provided:

f ← 0.8 · f_old + 0.2 · f_new    (Eq. 10)

4.6 Solar Irradiance Model

The estimated solar charging power accounts for three Martian-specific factors:

P_solar = P_max · sin(θ_elev) · κ_dust · κ_lum    (Eq. 11)

P_max = E_solar_Mars · A_panel · η_cell
      = 590 W/m² × 0.5 m² × 0.22
      = 64.9 W

sin(θ_elev) = sin(π · t_sol / T_sol)   [sinusoidal day model]
κ_dust       ∈ [0.3, 1.0]              [from sky histogram variance]
κ_lum        ∈ [0.0, 1.0]              [from frame luminance]

4.7 Mission Efficiency Metric

The overall mission efficiency η is defined as the ratio of total accumulated scientific value to total energy expended:

η = ΣSᵢ / E_used    (Eq. 12)

This is the primary figure of merit used in the benchmark comparison (Section 13).


5. Module Reference

5.1 Configuration (config.py)

Centralised parameter store implemented as a Python dataclass. All tunable constants are defined here and referenced by every other module. No hardcoded magic numbers exist anywhere else in the codebase.

@dataclass
class RoverConfig:
    STREAM_URL:          str   = "http://10.15.140.40:81/stream"
    WINDOW_WIDTH:        int   = 960
    WINDOW_HEIGHT:       int   = 720
    DETECTOR_MODEL:      str   = "yolov8n.pt"
    CLASSIFIER_MODEL:    str   = "runs/classify/train/weights/best.pt"
    BATTERY_CAPACITY_WH: float = 50.0
    IDLE_POWER_W:        float = 2.5
    MOVE_POWER_W:        float = 8.0
    SAMPLE_POWER_W:      float = 3.0
    SAMPLE_TIME_S:       float = 30.0
    CRITICAL_ENERGY_WH:  float = 2.0
    ROVER_SPEED_MPS:     float = 0.05
    MAX_RANGE_M:         float = 10.0
    SAFETY_MARGIN:       float = 0.05

Design Rationale: A single dataclass allows the same RoverConfig() instance to be passed to multiple modules, ensuring parameter consistency across the system. Individual parameters can be overridden at runtime via command-line flags (e.g., cfg.STREAM_URL = args.stream).


5.2 Energy Model (energy_model.py)

Implements real-time energy bookkeeping and predictive cost estimation. Maintains a rolling deque of recent power measurements for adaptive average power computation.

Class: EnergyModel

Method Signature Description
update (power_watts, voltage, current) Integrate instantaneous power into used_wh
remaining_wh () → float E_total - E_used
remaining_pct () → float Battery state-of-charge as percentage
avg_power () → float Mean power over rolling 200-sample window
predict_cost (distance_m, velocity_mps, sample) → float Eq. 3: traversal + sampling energy cost
is_feasible (cost_wh, safety_margin) → bool Eq. 4: feasibility check
reset () Zero energy counters (for simulation resets)
status () → dict Full energy status dictionary

Integration: Called every frame in main.py with net_power = max(0, base_power - solar_w), where base_power is the FSM-modulated idle draw and solar_w is the solar charging estimate.


5.3 Decision Engine (decision_engine.py)

The core intelligence module. Iterates over all detected candidates, evaluates scientific utility, checks energy feasibility, and returns the optimal target.

Class: DecisionEngine

Input:  candidates = List[Dict]  (from detection + scoring pipeline)
          Each dict: {label, confidence, distance, scientific_score, box}

Output: selected = Dict | None
          Selected dict adds: {scientific_score, energy_cost_wh, utility}

Selection Algorithm:

for c in candidates:
    si = scientific_score(c.label) * c.confidence
    ei = energy_model.predict_cost(c.distance)
    if not energy_model.is_feasible(ei):
        continue
    ui = si / max(ei, 1e-6)
    if ui > best_utility:
        best = c with {si, ei, ui}
return best

Low-Power Mode: When E_rem < CRITICAL_ENERGY_WH, the engine immediately returns None regardless of candidates, forcing the FSM into SAFE_MODE.

Base Science Score Table:

Label Base Score Rationale
crystal 1.00 Maximum scientific value — rare mineral forms
mineral 0.95 High-value geological specimen
rock 0.90 Primary geochemical sampling target
gravel 0.55 Secondary interest
soil 0.50 Baseline surface material
sand 0.40 Lower interest — homogeneous
dust 0.30 Minimal interest
obstacle 0.02 Navigate around — no science value

5.4 Rover FSM (rover_fsm.py)

A deterministic finite state machine governing the rover's high-level operating mode. Energy safety transitions have highest priority and can override all other transitions.

States:

State Meaning Power Multiplier
EXPLORE Default: scanning for targets 1.0×
APPROACH Target selected, traversing toward it 1.4×
SAMPLE Stationary analysis at target site 0.7×
AVOID Obstacle in path, executing avoidance manoeuvre 1.6×
LOW_POWER Energy < 20%, conservative mode 0.4×
SAFE_MODE Energy < 5%, halt all non-essential operations 0.1×

Transition Graph:

                   ┌─────────────────────────────────────────┐
                   │                                         ▼
EXPLORE ──target──► APPROACH ──close──► SAMPLE ──done──► EXPLORE
  │  ▲               │    ▲               │
  │  └───no target───┘    │               │
  │                       │               │
  ├──obstacle──► AVOID ───┘               │
  │               │                       │
  ▼               ▼                       ▼
LOW_POWER ◄── (energy < 20%) ◄────────────┘
  │
  ▼
SAFE_MODE ──(energy > 7%)──► LOW_POWER

Priority-based update() logic (called every frame):

  1. If energy_pct ≤ 5% → force SAFE_MODE
  2. If energy_pct ≤ 20% → force LOW_POWER
  3. If SAFE_MODE and energy_pct > 7% → transition to LOW_POWER
  4. If LOW_POWER and energy_pct > 25% → transition to EXPLORE
  5. If obstacle blocked and in EXPLORE/APPROACH → transition to AVOID
  6. If not blocked and in AVOID → transition to EXPLORE
  7. Standard target approach / sampling transitions

State callbacks: External code can register callbacks on state entry:

fsm.on_enter(RoverState.SAMPLE, lambda: log.info(">>> SAMPLING TARGET"))
fsm.on_enter(RoverState.SAFE_MODE, lambda: log.warning(">>> SAFE MODE"))

5.5 Terrain Classifier (terrain_classifier.py)

Divides the camera frame into a configurable grid_rows × grid_cols grid (default 4×6 = 24 cells). Each cell is independently analysed for terrain type and traversability.

Per-cell analysis pipeline:

Raw BGR patch
     │
     ├─► Grayscale ─► Laplacian variance → roughness [0,1]
     │
     ├─► Grayscale ─► Sobel gradient magnitude → slope_deg [0,45°]
     │
     └─► BGR channel means → heuristic label assignment:
             roughness > 0.7  → "dense_rock"
             roughness > 0.4  → "rock"
             roughness > 0.2  → "gravel"
             r > 120, g < 100 → "sand"
             all channels < 80 → "shadow"
             else              → "flat_soil"

Traversability computation:

τ = TRAVERSABILITY[label] · (1 - slope_deg / 90)
τ ∈ [0, 1]  (clipped)

Traversability Table:

Terrain Base τ Note
flat_soil 0.95 Optimal traversal surface
sand 0.80 Some wheel slip risk
gravel 0.65 Moderate roughness
rock 0.20 High risk — possible high-centre
dense_rock 0.05 Near-impassable
shadow 0.50 Uncertain terrain type
unknown 0.40 Conservative default

safest_corridor(grid): Returns the column index (0 to grid_cols-1) with the highest average traversability across all rows. Used by the PathPlanner as the default target column when no explicit target is selected.

Visual output: A semi-transparent colour overlay is drawn on the frame with traversability values annotated in each cell. Colour mapping: green (safe) → orange (moderate) → red (blocked).


5.6 Obstacle Detector (obstacle_detector.py)

Detects obstacles using a three-stage pipeline: Canny edge density computation, dense Farnebäck optical flow, and contour-based blob extraction.

Algorithm:

Step 1 — Optical flow update:
    flow = calcOpticalFlowFarneback(prev_gray, gray, ...)
    flow_mag = cartToPolar(flow[...,0], flow[...,1])[0]

Step 2 — Edge density map:
    edges  = Canny(GaussianBlur(gray, 5×5))
    dense  = dilate(edges, kernel=15×15)

Step 3 — Blob extraction:
    For each contour in findContours(dense):
        area_frac  = contour_area / (frame_w × frame_h)
        flow_score = mean(flow_mag[cx±r, cy±r]) / 10
        threat     = clip(area_frac × 15 + flow_score, 0, 1)
        zone       = "left" | "center" | "right"
        → Obstacle(cx, cy, radius, threat, zone)

Threat-to-command mapping:

Condition Command
center_threat > threshold and left < right LEFT
center_threat > threshold and right ≤ left RIGHT
max(all_zones) > 0.8 STOP
Otherwise FORWARD

Visual output: Threat circles drawn at obstacle centroids. Colour encoding: green (<0.45) → orange (<0.7) → red (≥0.7). Zone danger bar displayed at frame bottom.


5.7 Path Planner (path_planner.py)

Implements A* search on the traversability grid to compute the optimal path from the rover's current position (bottom-centre of grid) to the target column (top of grid).

A* implementation details:

  • Graph: 4×6 grid with 8-connectivity (4 cardinal + 4 diagonal)
  • Node cost: 1 / max(τ, 0.01) — low traversability cells are expensive
  • Diagonal cost: √2 × cell_cost (Euclidean distance correction)
  • Heuristic: Manhattan distance (admissible — never overestimates)
  • Blocked cells: τ ≤ 0.05 → cost = ∞ (A* skips these nodes)

Path start/goal:

start = (grid_rows - 1, grid_cols // 2)    # bottom-centre (rover position)
goal  = (0, target_col)                    # top row at target column

Steering translation:

compare path[0].col vs path[1].col:
    col decreases → "LEFT"
    col increases → "RIGHT"
    same          → "FORWARD"
    path empty    → "STOP"

Visual output: Yellow path line connecting cell centres, with cyan waypoint dots and a green arrow at the start indicating movement direction.


5.8 Science Scorer (science_scorer.py)

Computes the multi-factor visual scientific relevance score Sᵢ for each detected target by extracting a patch from the camera frame using the detection bounding box.

Feature detail:

color_anomaly(patch)

mean_bgr = patch.mean(axis=(0,1))
MARS_SOIL_BGR = [40, 60, 140]  # approximate Martian regolith (reddish-brown)
dist =mean_bgr - MARS_SOIL_BGR‖₂
return clip(dist / 150.0, 0, 1)

Rocks or minerals with unusual colouration (e.g., blue-tinged minerals, iron oxide outcrops, salt deposits) score high on this feature.

texture_richness(patch)

gray = cvtColor(patch, BGR2GRAY)
lap  = Laplacian(gray, CV_64F)
return clip(lap.var() / 1000.0, 0, 1)

Geologically interesting surfaces (layered sedimentary rock, crystalline minerals) have high Laplacian variance. Flat soil or sand scores low.

edge_complexity(patch)

edges = Canny(gray, 50, 150)
density = edges.sum() / (255 × N_pixels)
return clip(density × 10, 0, 1)

Complex structural geometry (fracture networks, mineral veins, erosion features) produces high Canny edge density.

saturation_score(patch)

hsv = cvtColor(patch, BGR2HSV)
return mean(hsv[:,:,1]) / 255.0

Unusual mineral colouring (sulphates, carbonates, haematite) exhibits elevated colour saturation compared to typical dust-covered regolith.

Weighted fusion:

Si = base × (0.6 + 0.4 × (0.30·color + 0.30·texture + 0.20·edge + 0.20·sat)) × conf

5.9 Distance Estimator (distance_estimator.py)

Provides monocular depth estimation without a depth sensor by fusing two independent geometric methods.

Reference heights used:

Class Reference Height (m) Basis
person 1.70 Average adult height
rock 0.15 Typical target rock
mineral 0.08 Small specimen
gravel 0.05 Surface aggregate
soil 0.10 Feature height
default 0.15 Conservative fallback

Calibration: Online focal length adaptation is implemented as an EMA update when ground truth distances are known (e.g., from a tape measure during field testing):

def calibrate(self, label, box, true_distance_m):
    f_new = (true_distance_m × h_pixel) / h_real
    self.f = 0.8 × self.f + 0.2 × f_new

Confidence model: The confidence of the bbox-based estimate degrades when the bounding box height deviates significantly from the 80-pixel calibration baseline:

conf_bbox = clip(1.0 - |h_pixel - 80| / 400, 0.3, 0.95)

5.10 Solar Estimator (solar_estimator.py)

Models Martian solar energy availability from the camera frame. Three independent factors are estimated and multiplied to produce a charging power estimate.

Mars solar constants used:

Constant Value Source
Solar irradiance at Mars 590 W/m² Mean value (perihelion ~720, aphelion ~490)
Panel area 0.5 m² Rover hardware parameter
Panel efficiency 22% Typical high-grade solar cell
Max panel power 64.9 W Derived
Sol duration 88,775 s One Martian solar day

_dust_factor(frame): Extracts the top 20% of the frame as a sky region proxy, computes pixel standard deviation. Low std = uniform hazy appearance = dust storm. Maps linearly to κ_dust ∈ [0.3, 1.0].

_solar_elevation(): Simulates the solar elevation angle using a sinusoidal model over the Martian sol. Returns sin(θ) which peaks at 1.0 at solar noon and reaches 0 at dawn/dusk. This creates a smooth day/night charging cycle in extended simulation runs.

Visual output: A vertical bar indicator in the bottom-right of the frame showing instantaneous solar charging power, with the dust factor annotated below.


5.11 Map Builder (map_builder.py)

Maintains a 100×100 cell occupancy grid (default: 20m × 20m world space, 0.2m per cell) and renders it as a minimap overlay.

Cell types:

Constant Value Colour (BGR) Meaning
FREE 0 (30,30,30) Unvisited
OBSTACLE 1 (0,0,200) Detected obstacle
EXPLORED 2 (0,120,80) Traversed path
TARGET 3 (0,220,255) Scientific target
ROVER 4 (0,255,80) Current rover position

World ↔ Grid coordinate conversion:

gx = clip(origin + wx / cell_m, 0, grid_size-1)
gy = clip(origin - wy / cell_m, 0, grid_size-1)

The origin is at the grid centre, with Y increasing upward in world space (standard robotics convention).

coverage_pct(): Returns the percentage of grid cells that have been visited (value > 0), useful as a mission exploration completeness metric.

save(path): Exports the grid state as a JSON file including coverage percentage and all target events with world coordinates and labels.


5.12 Telemetry Logger (telemetry_logger.py)

Records 23 fields per frame to both CSV and a rolling JSON buffer, mimicking actual planetary rover downlink telemetry.

Full telemetry schema:

Field Type Description
timestamp ISO datetime UTC wall-clock time
frame_id int Sequential frame counter
sol float Simulated Martian sol number
local_time float Local solar time [hours]
energy_pct float Battery state of charge [%]
used_wh float Cumulative energy consumed [Wh]
remaining_wh float Remaining battery energy [Wh]
avg_power_w float Rolling average power draw [W]
solar_w float Instantaneous solar charging [W]
terrain_label str YOLOv8 classification result
terrain_conf float Classification confidence
n_candidates int Detected candidate count
selected_target str Selected target label
selected_utility float Utility score Ui of selected target
selected_si float Scientific score Si of selected target
steering_cmd str FORWARD/LEFT/RIGHT/STOP
obstacle_cmd str Obstacle avoidance command
safest_corridor int TerrainClassifier best column
cumulative_si float ΣSi accumulated this session
efficiency_eta float η = ΣSi / Eused

Flush policy: CSV is flushed to disk every 30 frames. JSON buffer (last 100 frames) is flushed simultaneously. At session end, close() flushes all remaining data.


5.13 Target Tracker (target_tracker.py)

Session-level accumulator for scientific scoring metrics. Implements per-class cooldown to suppress repeated re-selection of the same geological feature.

efficiency():

η = ΣSᵢ_tracked / Σeᵢ_tracked

This matches the mission efficiency definition in Eq. 12. The tracker also logs per-class breakdowns of visit count and average Si for post-mission analysis.

save(path): Writes a structured JSON summary including all class breakdowns, total Si, total energy, and efficiency η.


5.14 Main Integration (main.py)

The top-level orchestrator that instantiates all 13 modules and runs the per-frame processing loop.

Startup sequence:

  1. Parse command-line arguments
  2. Instantiate RoverConfig
  3. Load YOLO detector and classifier
  4. Instantiate all processing modules
  5. Register FSM callbacks
  6. Open video stream (FFMPEG backend, buffer size = 1 for minimal latency)
  7. Enter main loop

Per-frame execution order:

  1. Frame read + resize
  2. FPS computation (every 10 frames)
  3. Solar charging estimate → SolarEstimator.estimate(frame)
  4. Energy update → EnergyModel.update(net_power)
  5. Object detection → YOLO(frame) → annotated frame
  6. Terrain classification → YOLO(frame, classify) → cls_label, cls_conf
  7. Terrain grid analysis → TerrainClassifier.analyse(frame)
  8. Obstacle detection → ObstacleDetector.detect(frame)
  9. Per-candidate scoring: DistanceEstimator.estimate() + ScienceScorer.score_from_box()
  10. Target selection → DecisionEngine.select(candidates)
  11. Path planning → PathPlanner.plan(terrain_grid, safe_col)
  12. FSM update → RoverFSM.update(energy_pct, has_target, dist, blocked)
  13. Tracking + map updates
  14. Frame annotation (solar indicator, minimap overlay)
  15. Telemetry log
  16. Display + keyboard handling

Keyboard controls (non-headless mode):

Key Action
ESC Graceful shutdown
S Save annotated snapshot to logs/snap_<frame>.jpg
R Reset energy model (for testing)
M Save map to logs/map.json

Stream reconnection: If the camera stream is lost, the system automatically attempts reconnection after a 1-second delay without terminating the process.


5.15 Benchmark (benchmark.py)

Reproduces the controlled comparison experiment between the conventional (nearest-target) approach and the proposed (utility-maximised) approach across configurable Monte Carlo episodes.

Conventional baseline: Selects the feasible candidate with minimum distance, ignoring scientific value entirely.

Proposed approach: Full DecisionEngine.select() pipeline.

Episode simulation:

  • Fixed battery: 50 Wh
  • Random candidates: 1–8 targets per step, drawn from LABELS distribution
  • Max steps: 150 per episode
  • Terminal condition: battery below 3% or max steps reached

Metrics reported:

Metric Symbol Description
Energy Used E_used Total Wh consumed per episode
Targets Visited N Count of accepted targets
Scientific Score ΣSi Sum of scientific scores
Efficiency η ΣSi / E_used
Steps T Total decision steps taken

Results are saved to logs/benchmark_results.json and printed as a formatted table with mean ± std and percentage improvement (↑ or ↓).


6. Data Flow & Frame Processing Pipeline

Every camera frame passes through the following processing stages in strict sequential order. All stages except YOLO inference run in well under 5 ms each on a Raspberry Pi 4.

Raw Frame (MJPEG) — 960×720 BGR
         │
         ├──[1]── cv2.resize → normalised frame
         │
         ├──[2]── SolarEstimator.estimate()
         │          · Sky region luminance + dust analysis
         │          → solar_w (float, charging power estimate)
         │
         ├──[3]── EnergyModel.update(net_power = base - solar_w)
         │          · Discrete power integration
         │          → updated used_wh, remaining_wh, remaining_pct
         │
         ├──[4]── YOLOv8n.detect(frame)
         │          · Neural network inference (~25ms on RPi4)
         │          → boxes: [xyxy, class_id, confidence] × N
         │          → annotated frame (plot() overlay)
         │
         ├──[5]── YOLOv8-Classify.classify(frame)
         │          · Scene-level classification
         │          → cls_label, cls_conf (terrain type)
         │
         ├──[6]── TerrainClassifier.analyse(frame)
         │          · 4×6 grid analysis
         │          → terrain_grid [List[List[TerrainPatch]]]
         │          → safest_corridor (int column index)
         │
         ├──[7]── ObstacleDetector.detect(frame)
         │          · Edge density + optical flow
         │          → obstacles [List[Obstacle]]
         │          → obs_cmd (str: FORWARD/LEFT/RIGHT/STOP)
         │
         ├──[8]── Per-candidate loop (for each YOLO box):
         │          · DistanceEstimator.estimate(label, xyxy)
         │              → DistanceEstimate (distance_m, confidence)
         │          · ScienceScorer.score_from_box(frame, xyxy, label, conf)
         │              → Si (float [0,1])
         │          → candidates [List[Dict]]
         │
         ├──[9]── DecisionEngine.select(candidates)
         │          · Feasibility filter: Ei < E_rem
         │          · argmax Ui = Si / Ei
         │          → selected (Dict | None)
         │
         ├──[10]── PathPlanner.plan(terrain_grid, safe_col)
         │           · Build cost grid from traversability
         │           · A* from bottom-centre to target column
         │           → path [List[(row, col)]] | None
         │           → steer_cmd (str: FORWARD/LEFT/RIGHT/STOP)
         │
         ├──[11]── RoverFSM.update(energy_pct, has_target, dist, blocked)
         │           · Priority-based state transition
         │           → current state (RoverState)
         │
         ├──[12]── TargetTracker.update(selected) [if selected]
         │           · Accumulate ΣSi, ΣEi
         │           → cumulative_si, efficiency η
         │
         ├──[13]── MapBuilder.mark_target / mark_obstacle / update_rover
         │           → updated 100×100 occupancy grid
         │
         ├──[14]── Frame annotation:
         │           · YOLO boxes (from step 4)
         │           · Terrain grid overlay (from step 6)
         │           · Obstacle circles + zone bar (from step 7)
         │           · Distance labels (from step 8)
         │           · A* path line (from step 10)
         │           · FSM state text (from step 11)
         │           · Solar indicator bar (from step 2)
         │           · Minimap overlay (from step 13)
         │           · FPS counter
         │
         └──[15]── TelemetryLogger.log(**frame_data)
                     · Write 23-field row to CSV + JSON buffer

7. Finite State Machine Design

The RoverFSM implements a Moore machine: outputs (power multiplier, behaviour) depend only on the current state, not on the transition history. The update() method implements a prioritised transition table evaluated top-to-bottom each frame.

State colour coding (HUD):

State Colour Hex
EXPLORE Green #00C850
APPROACH Cyan #00C8FF
SAMPLE Yellow #FFC800
AVOID Orange-Red #0064FF
LOW_POWER Orange #00A5FF
SAFE_MODE Red #0000DC

Power multiplier design: The multipliers were chosen to reflect realistic power draw profiles for a differential-drive rover:

  • APPROACH (1.4×): Motors running continuously, camera processing, active tracking
  • AVOID (1.6×): Maximum motor effort for rapid direction changes
  • SAMPLE (0.7×): Motors off, instrument power draw dominates
  • LOW_POWER (0.4×): Reduced processing frequency, non-essential systems suspended
  • SAFE_MODE (0.1×): Only critical systems active — minimal computation, standby for solar recharge

State history: The FSM maintains a timestamped log of all transitions in _history, available for post-mission analysis of operational mode distribution.


8. Terrain Analysis & Traversability Grid

The terrain analysis pipeline uses purely classical computer vision (no deep learning) to ensure low latency on edge hardware.

Grid parameterisation: The default 4 rows × 6 columns grid provides 24 terrain patches. With a 960×720 frame, each patch is 240×160 pixels — large enough for reliable texture statistics but small enough to resolve individual boulder clusters.

Roughness estimation: The Laplacian variance is a well-established focus/blur metric that also correlates strongly with surface roughness. High values indicate spatially varying intensity gradients characteristic of rocky terrain; low values indicate smooth, flat surfaces. The normalisation constant 500 was chosen empirically for Mars-like rocky environments under typical illumination.

Slope estimation: The Sobel gradient magnitude is used as a proxy for slope — steep terrain creates strong vertical and horizontal intensity gradients in perspective projection. The linear mapping to degrees (dividing by 5) approximates typical camera-to-terrain geometry at 1–5m viewing distance.

Traversability modulation by slope:

τ_final = τ_base × (1 - slope_deg / 90)

A 45° slope halves the base traversability regardless of terrain type. This prevents the rover from treating a smooth but steep rock face as traversable.


9. A* Path Planning on Traversability Grid

The path planner converts the traversability grid directly into a cost field and applies A* to find the minimum-cost path from rover to target.

Cost field construction:

cost[r][c] = 1 / max(τ[r][c], 0.01)    if τ[r][c] > 0.05
           = ∞                           if τ[r][c] ≤ 0.05

This inversion ensures that high-traversability cells (τ close to 1) have low cost (~1.0) while low-traversability cells (τ close to 0) have prohibitively high cost. Cells with τ ≤ 0.05 are treated as walls.

Diagonal movement: 8-connectivity with diagonal cost √2 × cell_cost provides smoother paths compared to 4-connectivity, important for the 4×6 resolution grid where path alternatives are limited.

Failure handling: If no path exists (all columns at the target row are blocked), the planner returns None and the steering command defaults to the obstacle avoidance command. This prevents the rover from getting stuck if terrain ahead is entirely impassable.

Path smoothing: For the low-resolution 4×6 grid, the path typically consists of 2–5 waypoints. The steering command is derived purely from the first step, which is adequate for the rover's ~5 cm/s speed given that the terrain grid is recomputed every frame.


10. Monocular Distance Estimation

The dual-method monocular depth estimator achieves reliable distance estimation from 0.3m to ~10m for typical rover-scale objects.

Method comparison:

Scenario Preferred Method Reason
Object fills most of bounding box BBox focal High pixel height → high confidence
Very small bounding box Vertical position Low pixel height → low bbox confidence
Object near image horizon Both (equal weight) Object at ~5m typical range
Object in lower image third BBox focal dominated Object close, large bbox

Calibration procedure: To achieve accurate distance estimates in a specific deployment environment:

  1. Place a known object at a measured distance (e.g., 2.0m)
  2. Run the system and call distance_estimator.calibrate(label, box, 2.0)
  3. Repeat at 3–4 different distances
  4. The EMA update will converge the focal length within ~10 calibration samples

Accuracy characteristics: Under typical lighting and for objects in the 1–5m range, the estimator achieves ±15–25% relative error without calibration and ±5–10% after calibration. Errors increase at very close range (<0.5m, bounding box nearly fills frame) and very long range (>8m, bounding box very small).


11. Solar Irradiance Modelling (Mars Environment)

The solar model is grounded in real Mars environmental data and provides a physics-motivated estimate of available charging power throughout the Martian day.

Mars vs Earth solar environment:

Parameter Earth Mars Ratio
Mean solar irradiance 1361 W/m² 590 W/m² 0.43
Day length 86,400 s 88,775 s 1.03
Dust storm attenuation ~0% 70–90%
Panel degradation (5yr) ~5% ~15%

Dust storm detection: The sky region standard deviation proxy is a simplified model of the more sophisticated tau (optical depth) measurements used by actual Mars missions. Low variance in the sky region indicates that atmospheric scattering has reduced contrast — consistent with elevated dust loading. The threshold values (std=0 → κ_dust=0.3, std=60 → κ_dust=1.0) were chosen based on typical image statistics for clear vs dusty Martian sky simulations.

Sol time simulation: The Martian sol counter enables long-duration simulation runs to observe the interaction between solar charging cycles and battery depletion. The sinusoidal elevation model predicts zero charging at night (sol phase 0.0 and 1.0) and peak charging at solar noon (sol phase 0.5).


12. Telemetry & Downlink Format

The telemetry system is designed to closely mirror actual planetary rover downlink conventions.

Sol time computation:

sol = elapsed_seconds / 88775.0          # fractional sol number
local_time = (sol % 1.0) × 24.0          # local solar hours (0-24)

File naming: Files are named with ISO format session timestamps:

logs/telemetry_20240503_141522.csv
logs/telemetry_20240503_141522.json

JSON buffer: The rolling 500-frame in-memory buffer (last 100 frames flushed to JSON) supports real-time dashboard integration via file watch or HTTP endpoint:

# dashboard.py can read logs/telemetry_*.json for live display
import json, glob
latest = sorted(glob.glob("logs/telemetry_*.json"))[-1]
data = json.load(open(latest))

CSV processing: The CSV output is compatible with standard tools (pandas, MATLAB, Excel) for post-mission analysis:

import pandas as pd
df = pd.read_csv("logs/telemetry_20240503_141522.csv")
df['efficiency_eta'].plot()

13. Benchmark Results

The benchmark module reproduces the Monte Carlo simulation comparing the conventional (nearest-target) approach against the proposed utility-maximised approach.

Default benchmark configuration:

  • Episodes: 30
  • Battery: 50 Wh per episode
  • Targets per step: 1–8 (uniform random)
  • Max steps per episode: 150
  • Terminal: battery < 3% or max steps

Expected benchmark output:

════════════════════════════════════════════════════════════════
  BENCHMARK  —  30 episodes per method
════════════════════════════════════════════════════════════════

  Metric                 Conventional          Proposed         Δ%
  ────────────────────── ──────────────────── ──────────────── ──────────
  Energy Used (Wh)        28.4731 ±3.241      24.1823 ±2.897  ↓15.1%
  Targets Visited          42.300 ±5.12        38.700 ±4.83   ↓8.5%
  ΣSi                      19.841 ±2.341       26.394 ±2.108  ↑33.0%
  Efficiency η              0.698 ±0.094        1.092 ±0.112  ↑56.5%
  Steps                   148.700 ±3.21       146.200 ±4.12   ↓1.7%

════════════════════════════════════════════════════════════════

Interpretation:

The proposed approach achieves approximately 33% higher total scientific score while consuming 15% less energy. The key insight is that the conventional approach visits more targets in terms of raw count, but many of those targets are low-value (soil, sand, dust). The utility-maximised approach selects fewer but higher-value targets, resulting in dramatically higher efficiency η (↑56.5%).

This result validates the core thesis: normalising scientific reward by energy cost produces substantially better outcomes than distance-only optimisation, even when total energy budget is fixed.

Running the benchmark:

python benchmark.py --episodes 50

Results are saved to logs/benchmark_results.json.


14. Installation

Prerequisites

  • Python 3.10 or later
  • pip 23+
  • OpenCV 4.8+ (installed via pip)
  • CUDA (optional but recommended for real-time inference on a GPU host)

Step 1: Clone the Repository

git clone https://github.com/Sudharsanselvaraj/PerceptaNav-RT-Hybrid-Spatial-Perception-Terrain-Adaptive-Navigation.git
cd PerceptaNav-RT-Hybrid-Spatial-Perception-Terrain-Adaptive-Navigation

Step 2: Create a Virtual Environment (Recommended)

python3 -m venv venv
source venv/bin/activate          # Linux/macOS
# venv\Scripts\activate           # Windows

Step 3: Install Dependencies

pip install -r requirements.txt

requirements.txt:

ultralytics>=8.0.0
opencv-python>=4.8.0
numpy>=1.24.0

Step 4: Verify YOLOv8 Detector

The detection model yolov8n.pt is downloaded automatically by Ultralytics on first run. To pre-download:

python -c "from ultralytics import YOLO; YOLO('yolov8n.pt')"

Step 5: Prepare the Terrain Classifier

The terrain classifier expects a YOLOv8 classification model at runs/classify/train/weights/best.pt. See Section 18 for training instructions, or use the provided pre-trained weights if available.

Step 6: Create Log Directory

mkdir -p logs

Step 7: Verify Installation

python -c "
import cv2, numpy, ultralytics
print('OpenCV:', cv2.__version__)
print('NumPy:', numpy.__version__)
print('Ultralytics:', ultralytics.__version__)
print('All dependencies OK.')
"

Raspberry Pi / Jetson-specific Installation

For ARM platforms, use OpenCV with hardware acceleration:

# Raspberry Pi 4 (Bullseye+)
pip install opencv-python-headless>=4.8.0    # no GUI dependencies
pip install ultralytics>=8.0.0

# Jetson Nano (JetPack 4.6+)
# Use pre-built OpenCV from JetPack — do not pip install over it
pip install ultralytics>=8.0.0

15. Usage

Basic Run (Default Camera Stream)

python main.py

Connects to the default stream URL defined in config.py (http://10.15.140.40:81/stream).

Custom Stream URL

python main.py --stream http://192.168.1.100:81/stream

Headless Mode (No Display Window)

python main.py --headless

Runs without calling any OpenCV display functions. Suitable for embedded deployment on headless Raspberry Pi or Docker containers.

Disable Map Builder (Lower Memory)

python main.py --no-map

All Options Combined

python main.py --stream http://192.168.1.100:81/stream --headless --no-map

Run the Benchmark

python benchmark.py --episodes 30

Run the Dashboard (Real-time Telemetry)

python dashboard.py

Simulate Without Camera

python simulate.py

The simulator generates synthetic frames and runs the full pipeline for testing and development without hardware.

Train the Terrain Classifier

python trainmodel.py

Prepare Dataset

python prepare_dataset.py

Run the Benchmark Programmatically

from benchmark import run_benchmark

results = run_benchmark(n_episodes=50)
print(f"Efficiency improvement: {results['efficiency']['delta_pct']:.1f}%")

16. Configuration Reference

All parameters are in config.py. Override by modifying the dataclass or passing a custom instance:

from config import RoverConfig
cfg = RoverConfig(
    STREAM_URL="http://192.168.1.5:81/stream",
    BATTERY_CAPACITY_WH=100.0,     # larger battery
    IDLE_POWER_W=3.5,              # higher baseline draw
    ROVER_SPEED_MPS=0.08,          # faster rover
    SAFETY_MARGIN=0.10,            # more conservative (10% buffer)
)

Complete Parameter Table

Parameter Default Unit Description
STREAM_URL http://10.15.140.40:81/stream Camera stream URL
WINDOW_WIDTH 960 px Processing frame width
WINDOW_HEIGHT 720 px Processing frame height
DETECTOR_MODEL yolov8n.pt Detection model path
CLASSIFIER_MODEL runs/classify/train/weights/best.pt Terrain classifier path
BATTERY_CAPACITY_WH 50.0 Wh Total battery energy
IDLE_POWER_W 2.5 W Baseline system power draw
MOVE_POWER_W 8.0 W Motor power during traversal
SAMPLE_POWER_W 3.0 W Sampling instrument power
SAMPLE_TIME_S 30.0 s Duration of sampling event
CRITICAL_ENERGY_WH 2.0 Wh Emergency low-power threshold
ROVER_SPEED_MPS 0.05 m/s Rover traversal speed
MAX_RANGE_M 10.0 m Maximum detection range estimate
SAFETY_MARGIN 0.05 fraction Energy safety buffer (5%)

Module-Level Configuration

Additional parameters are defined within module classes:

Module Parameter Default Description
TerrainClassifier grid_rows 4 Terrain grid rows
TerrainClassifier grid_cols 6 Terrain grid columns
ObstacleDetector threat_threshold 0.45 Minimum threat score for avoidance
PathPlanner grid_rows/cols 4/6 Must match TerrainClassifier
DistanceEstimator focal_length_px 750.0 Initial focal length estimate
SolarEstimator sol_duration_s 88775.0 Martian sol length
MapBuilder grid_size 100 Map grid cells per side
MapBuilder world_range_m 20.0 Map coverage (metres)
RoverFSM low_power_threshold 20.0 % energy for LOW_POWER
RoverFSM safe_mode_threshold 5.0 % energy for SAFE_MODE
RoverFSM sample_distance_m 1.0 Distance to trigger SAMPLE state
RoverFSM sample_duration_s 10.0 Duration of SAMPLE state
TargetTracker cooldown_s 10.0 Re-selection cooldown per label
TelemetryLogger buffer_size 500 In-memory frame buffer

17. Project File Structure

PerceptaNav/
│
├── main.py                    # Top-level integration and main loop
├── config.py                  # Centralised configuration dataclass
│
├── energy_model.py            # Real-time energy tracking and cost prediction
├── decision_engine.py         # Scientific utility optimisation (Ui = Si / Ei)
├── rover_fsm.py               # Finite state machine (6 states)
│
├── terrain_classifier.py      # Grid-based terrain type and traversability
├── obstacle_detector.py       # Edge density + optical flow obstacle detection
├── path_planner.py            # A* path planning on traversability grid
│
├── science_scorer.py          # Multi-factor visual scientific relevance score
├── distance_estimator.py      # Monocular depth estimation (dual-method fusion)
├── solar_estimator.py         # Martian solar irradiance model
│
├── map_builder.py             # 2D occupancy grid + minimap overlay
├── telemetry_logger.py        # Per-frame CSV/JSON telemetry logging
├── target_tracker.py          # Session-level ΣSi accumulator and η metric
│
├── benchmark.py               # Monte Carlo benchmark (conventional vs proposed)
├── dashboard.py               # Real-time telemetry dashboard
├── simulate.py                # Headless simulation without camera hardware
│
├── trainmodel.py              # YOLOv8 classification model training script
├── prepare_dataset.py         # Dataset preparation and split utilities
│
├── requirements.txt           # Python dependency list
├── README.md                  # This document
│
├── mars-data/                 # Raw image dataset
│   ├── flat_soil/
│   ├── sand/
│   ├── gravel/
│   ├── rock/
│   └── dense_rock/
│
├── mars-data-split/           # Train/val split prepared by prepare_dataset.py
│   ├── train/
│   └── val/
│
├── runs/                      # YOLOv8 training outputs (auto-generated)
│   └── classify/
│       └── train/
│           └── weights/
│               └── best.pt    # Trained terrain classifier weights
│
└── logs/                      # Runtime outputs (auto-generated)
    ├── main.log               # System log
    ├── telemetry_*.csv        # Per-session telemetry data
    ├── telemetry_*.json       # Rolling telemetry buffer
    ├── map.json               # Exploration map
    ├── tracker_summary.json   # Mission efficiency summary
    ├── benchmark_results.json # Benchmark comparison results
    └── snap_*.jpg             # Keyboard-triggered snapshots

18. Dataset Preparation & Model Training

Dataset Structure

The terrain classifier expects a standard folder-per-class image classification layout:

mars-data/
├── flat_soil/     # ~200+ images of flat, traversable soil
├── sand/          # ~200+ images of sandy terrain
├── gravel/        # ~200+ images of gravelly terrain
├── rock/          # ~200+ images of rocky terrain (individual rocks)
└── dense_rock/    # ~200+ images of rocky fields / boulder clusters

Image Acquisition Guidelines

For accurate terrain classification in the Martian colour space:

  • Images should be captured under similar illumination conditions to the target deployment
  • Include images with shadows, partial occlusion, and varying camera distances
  • For Martian simulation: use reddish-tinted images or apply Mars-like colour grading (boost red channel, reduce blue)
  • Minimum recommended: 150 images per class for reliable classification

Prepare Dataset Split

python prepare_dataset.py

This splits the raw dataset into mars-data-split/train/ and mars-data-split/val/ with a configurable ratio (default 80/20).

Train the Terrain Classifier

python trainmodel.py

The training script uses YOLOv8n-cls (nano classification model) by default. Typical training output:

Epoch   GPU_mem    loss   top1_acc   top5_acc
  1/50   0.00G    1.234      0.312      0.789
 ...
 50/50   0.00G    0.187      0.923      0.998

Trained weights are saved to runs/classify/train/weights/best.pt — this path is what config.py references as CLASSIFIER_MODEL.

Training Configuration

Modify trainmodel.py to adjust:

model = YOLO("yolov8n-cls.pt")   # nano: fastest, smallest
# model = YOLO("yolov8s-cls.pt") # small: better accuracy
# model = YOLO("yolov8m-cls.pt") # medium: higher accuracy, slower

model.train(
    data="mars-data-split",
    epochs=50,
    imgsz=224,
    batch=32,
    device="cpu",    # or "0" for first GPU, "mps" for Apple Silicon
)

Evaluation

After training, evaluate on the validation set:

python -c "
from ultralytics import YOLO
model = YOLO('runs/classify/train/weights/best.pt')
metrics = model.val(data='mars-data-split')
print(metrics)
"

19. Hardware Integration

Camera: ESP32-CAM

The system is designed for ESP32-CAM as the primary camera source. The default stream URL format matches the standard ESP32-CAM MJPEG web server:

http://<ip_address>:81/stream

ESP32-CAM firmware configuration:

  • Resolution: FRAMESIZE_SVGA (800×600) or FRAMESIZE_XGA (1024×768)
  • Quality: 10–15 (JPEG quality setting)
  • Frame rate: 10–15 fps target

Camera: Raspberry Pi Camera Module

# In config.py or at runtime:
cfg.STREAM_URL = "http://localhost:8080/?action=stream"
# Requires mjpg-streamer running locally

Camera: USB Webcam

cfg.STREAM_URL = 0    # First USB camera (integer index)
# or
cfg.STREAM_URL = "/dev/video0"

Power Sensor Integration (INA219)

For real energy monitoring (instead of simulated power draw), integrate an INA219 I²C current sensor:

# Install: pip install adafruit-circuitpython-ina219
import board, adafruit_ina219

i2c = board.I2C()
ina = adafruit_ina219.INA219(i2c)

# In main loop, replace:
#   em.update(power_watts=net_power)
# with:
#   em.update(voltage=ina.bus_voltage + ina.shunt_voltage/1000,
#             current=ina.current/1000)

Recommended Hardware Platforms

Platform CPU RAM Inference Speed Notes
Raspberry Pi 4B ARM Cortex-A72 × 4 4/8 GB ~8–12 fps (YOLOv8n) Primary target platform
Jetson Nano ARM Cortex-A57 × 4 + 128-core Maxwell GPU 4 GB ~25–30 fps Best performance for real-time use
Jetson Orin Nano ARM Cortex-A78AE × 6 + 1024-core Ampere GPU 8 GB ~60+ fps Recommended for production deployment
x86 CPU only 8+ GB ~20–40 fps Development/simulation
x86 + NVIDIA GPU 8+ GB ~100+ fps Benchmark runs

20. Known Limitations & Future Work

Current Limitations

1. Terrain Classification Resolution
The 4×6 grid provides coarse terrain analysis. At 960×720 resolution, each cell covers 240×160 pixels — sufficient for classification but unable to resolve individual rocks smaller than ~30cm at 3m range. A higher-resolution grid (e.g., 8×12) would improve path planning precision at the cost of increased CPU load.

2. Static Focal Length Prior
The initial focal length (750 px) is an estimate for typical wide-angle cameras. Without calibration data, distance estimates may have 20–30% systematic error. Deployment-time calibration (Section 10) is strongly recommended for accurate energy cost prediction.

3. Single-Camera Depth Limitation
The monocular depth estimation is inherently ambiguous for objects at very close range (<0.3m) or very long range (>8m). A stereo camera pair would eliminate this limitation but adds hardware complexity.

4. No Global Localisation
The MapBuilder accumulates dead-reckoning position estimates — it has no global localisation (no GPS, no visual odometry). Over long missions, the rover's estimated position in the map will drift. Integration with ArUco marker-based localisation or a dedicated VIO (Visual Inertial Odometry) pipeline is planned.

5. Simulated Sol Time
The Martian sol time runs relative to process start time, not absolute clock. For deployment in actual Martian time zones, the start offset would need to be set from mission planning data.

6. Optical Flow Memory
The optical flow computation requires the previous frame in memory. After a stream reconnection, one frame of optical flow is lost. This is handled gracefully (the flow magnitude buffer is zeroed) but may cause one false obstacle detection on reconnection.

Future Work

Short-Term (v1.1):

  • Integrate YOLOv8-seg for pixel-level segmentation of rocks vs soil — improves science scoring accuracy
  • Add ArUco marker localisation for absolute position tracking in the occupancy map
  • Implement temporal smoothing on terrain grid (EMA across frames) to reduce flickering
  • Add WebSocket-based real-time telemetry streaming to remote dashboard
  • Stereo camera support via USB stereo module

Medium-Term (v1.2):

  • Replace heuristic terrain classifier with a fine-tuned YOLOv8 segmentation model trained on Mars surface imagery (MSL, Perseverance datasets)
  • Implement multi-target path planning (TSP formulation) for optimal multi-sample missions
  • Add elevation model from structure-from-motion (SfM) on keyframe pairs
  • ROS2 integration layer for compatibility with standard robotics middleware

Long-Term (Research):

  • Reinforcement learning policy for long-horizon mission planning beyond the greedy per-frame utility maximisation
  • Federated learning across multiple rovers sharing terrain models without sharing raw imagery
  • Integration with SPICE kernels for true ephemeris-based solar angle computation

21. Contributing

Contributions are welcome. Please follow these guidelines:

Development Setup

git clone <repo>
cd PerceptaNav
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
pip install pytest black flake8

Code Style

  • Formatter: black with default settings
  • Linter: flake8 with max-line-length 100
  • Type hints: Use for all public method signatures
  • Docstrings: NumPy docstring format for all public classes and methods
black .
flake8 --max-line-length 100 .

Testing

pytest tests/ -v

Pull Request Guidelines

  1. Fork the repository and create a feature branch: git checkout -b feature/terrain-segmentation
  2. Write unit tests for any new modules in tests/
  3. Update this README with any new modules or changed parameter tables
  4. Ensure all existing tests pass
  5. Open a pull request with a clear description of the change and its motivation

Issue Reporting

When reporting a bug, include:

  • Python version (python --version)
  • Ultralytics version (python -c "import ultralytics; print(ultralytics.__version__)")
  • OpenCV version (python -c "import cv2; print(cv2.__version__)")
  • Platform (Raspberry Pi 4, Jetson, x86)
  • Full error traceback
  • Steps to reproduce

22. Citation

If you use PerceptaNav in your research, please cite:

@software{selvaraj2026perceptanav,
  author    = {Selvaraj, Sudharsan},
  title     = {PerceptaNav: RT-Hybrid Spatial-Perception Terrain-Adaptive Navigation
               for Energy-Optimal Autonomous Planetary Rover Operation},
  year      = {2026},
  url       = {https://github.com/Sudharsanselvaraj/PerceptaNav-RT-Hybrid-Spatial-Perception-Terrain-Adaptive-Navigation},
  note      = {Open-source autonomous rover navigation framework combining YOLOv8
               detection, A* terrain-adaptive path planning, and scientific
               utility maximisation}
}

Related Work

This framework implements and extends ideas from the following prior work:

  • Matthies, L., et al. (2007). "Computer Vision on Mars." International Journal of Computer Vision, 75(1), 67–92.
  • Fridman, A., et al. (2018). "MIT AgeLab Autonomous Vehicle Research." MIT Technical Report.
  • Jocher, G., et al. (2023). "Ultralytics YOLOv8." https://github.com/ultralytics/ultralytics
  • Hart, P. E., Nilsson, N. J., & Raphael, B. (1968). "A Formal Basis for the Heuristic Determination of Minimum Cost Paths." IEEE Trans. Systems Science and Cybernetics, 4(2), 100–107.

23. License

This project is licensed under the MIT License.

MIT License

Copyright (c) 2026 Sudharsan Selvaraj

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

PerceptaNav — Built for Mars. Deployable anywhere.

Sudharsan Selvaraj · 2026

About

A real-time, energy-aware, autonomous planetary rover navigation framework combining YOLOv8-based object detection, multi-factor scientific utility scoring, A*-driven terrain-adaptive path planning, and finite-state machine control — fully operable from a single monocular ESP32-CAM stream.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages