# Tier2SpatialAnalyzer

## 1. High-Level Overview

**Purpose**: Analyzes spatial patterns from Tier 1 detections — both continuous segmentation masks (footpaths) and discrete point clusters (defense systems, vehicle groups). Produces an ordered list of waypoints for the gimbal to follow, with a classification at each waypoint.

**Architectural Pattern**: Stateless processing pipeline with two strategies (mask tracing, cluster tracing) producing a unified output.

**Upstream dependencies**: Config helper, Types helper

**Downstream consumers**: ScanController

## 2. Internal Interfaces

### Interface: Tier2SpatialAnalyzer

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `trace_mask(mask, gsd)` | numpy (H,W) binary mask, float gsd | SpatialAnalysisResult | No | TracingError |
| `trace_cluster(detections, frame, scenario)` | list[Detection], numpy (H,W,3), SearchScenario | SpatialAnalysisResult | No | ClusterError |
| `analyze_roi(frame, bbox)` | numpy (H,W,3), bbox tuple | WaypointClassification | No | ClassificationError |

### Strategy: Mask Tracing (footpaths, roads, linear features)

Input: binary segmentation mask from Tier 1
Algorithm: skeletonize → prune → extract endpoints → classify each endpoint ROI
Output: waypoints at skeleton endpoints, trajectory along skeleton centerline

### Strategy: Cluster Tracing (AA systems, radar networks, vehicle groups)

Input: list of point detections from Tier 1
Algorithm: spatial clustering → visit order → per-point ROI classify
Output: waypoints at each cluster member, trajectory as point-to-point path

### Unified Output: SpatialAnalysisResult

```
pattern_type: str — "mask_trace" or "cluster_trace"
waypoints: list[Waypoint] — ordered visit sequence
trajectory: list[tuple(x, y)] — full gimbal trajectory
overall_direction: (dx, dy) — for gimbal PID
skeleton: numpy array (H,W) or None — only for mask_trace
cluster_bbox: tuple(cx, cy, w, h) or None — bounding box of cluster, only for cluster_trace
```

### Waypoint

```
x: int
y: int
dx: float — direction vector x component
dy: float — direction vector y component
label: str — "concealed_position", "branch_pile", "radar_dish", "unknown", etc.
confidence: float (0-1)
freshness_tag: str or None — "high_contrast" / "low_contrast" (mask_trace only)
roi_thumbnail: numpy array — cropped ROI for logging
```

## 5. Implementation Details

### Mask Tracing Algorithm (footpaths)

1. Morphological closing to connect nearby mask fragments
2. Skeletonize mask using Zhang-Suen (scikit-image `skeletonize`)
3. Prune short branches (< config `min_branch_length` pixels)
4. Select longest connected skeleton component
5. Find endpoints via hit-miss morphological operation
6. For each endpoint: extract ROI (size = config `base_roi_px` * gsd_factor)
7. Classify each endpoint via `analyze_roi` heuristic
8. Build trajectory from skeleton pixel coordinates
9. Compute overall_direction from skeleton start→end vector

### Cluster Tracing Algorithm (discrete objects)

1. Filter detections to scenario's `target_classes`
2. Compute pairwise distances between detection centers (in pixels)
3. Group detections within `cluster_radius_px` of each other (union-find on distance graph)
4. Discard clusters smaller than `min_cluster_size`
5. For the largest valid cluster: plan visit order via nearest-neighbor greedy traversal from current gimbal position
6. For each waypoint: extract ROI around detection bbox, run `analyze_roi`
7. Build trajectory as ordered point-to-point path
8. Compute overall_direction from first waypoint to last
9. Compute cluster_bbox as bounding box enclosing all cluster members

### ROI Classification Heuristic (`analyze_roi`)

Shared by both strategies:
1. Extract ROI from frame at given bbox
2. Compute: mean_darkness = mean intensity in ROI center 50%
3. Compute: contrast = (surrounding_mean - center_mean) / surrounding_mean
4. Compute: freshness_tag based on path-vs-terrain contrast ratio (mask_trace only, None for clusters)
5. Classify: if darkness < threshold AND contrast > threshold → label from scenario target_classes; else "unknown"

### Key Dependencies

| Library | Version | Purpose |
|---------|---------|---------|
| scikit-image | — | Skeletonization (Zhang-Suen), morphology |
| OpenCV | 4.x | ROI cropping, intensity calculations, morphological closing |
| numpy | — | Mask and image operations, pairwise distance computation |
| scipy.spatial | — | Distance matrix for cluster grouping (cdist) |

### Error Handling Strategy

- Empty mask → return empty SpatialAnalysisResult (no waypoints)
- Skeleton has no endpoints (circular path) → fallback to mask centroid as single waypoint
- ROI extends beyond frame → clip to frame boundaries
- No detections match target_classes → return empty SpatialAnalysisResult
- All clusters smaller than min_cluster_size → return empty SpatialAnalysisResult
- Single detection (cluster_size=1, below min) → return empty result; single points handled by zoom_classify investigation type instead

## 7. Caveats & Edge Cases

**Known limitations**:
- Mask heuristic will have high false positive rate (dark shadows, water puddles)
- Skeletonization on noisy/fragmented masks produces spurious branches (hence pruning + closing)
- Freshness assessment is contrast metadata, not a reliable classifier
- Cluster tracing depends on Tier 1 detecting enough cluster members in a single frame — wide-area L1 frames at medium zoom may not resolve small objects
- Nearest-neighbor visit order is not globally optimal (but adequate for <10 waypoints)

**Performance bottlenecks**:
- Skeletonization: ~10-20ms for a 1080p mask
- Pruning + endpoint detection: ~5ms
- Pairwise distance + clustering: ~1ms for <50 detections
- Total mask_trace: ~30ms per mask
- Total cluster_trace: ~5ms per cluster (no skeletonization)

## 8. Dependency Graph

**Must be implemented after**: Config helper, Types helper
**Can be implemented in parallel with**: Tier1Detector, VLMClient, GimbalDriver, OutputManager
**Blocks**: ScanController (needs Tier2 for L2 investigation)

## 9. Logging Strategy

| Log Level | When | Example |
|-----------|------|---------|
| ERROR | Skeletonization crash, distance computation failure | `Skeleton extraction failed on frame 1234` |
| WARN | No endpoints found, cluster too small, fallback | `Cluster has 1 member (min=2), skipping` |
| INFO | Waypoint classified, cluster formed | `mask_trace: 3 waypoints, direction=(0.7, 0.3)` / `cluster_trace: 4 waypoints (aa_launcher×2, radar_dish×2)` |
