Files
wifi-densepose/docs/developer/architecture-overview.md
2025-06-07 11:44:19 +00:00

32 KiB

Architecture Overview

Overview

The WiFi-DensePose system is a distributed, microservices-based architecture that transforms WiFi Channel State Information (CSI) into real-time human pose estimation. This document provides a comprehensive overview of the system architecture, component interactions, and design principles.

Table of Contents

  1. System Architecture
  2. Core Components
  3. Data Flow
  4. Processing Pipeline
  5. API Architecture
  6. Storage Architecture
  7. Security Architecture
  8. Deployment Architecture
  9. Scalability and Performance
  10. Design Principles

System Architecture

High-Level Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        WiFi-DensePose System                    │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │   Client Apps   │  │   Web Dashboard │  │  Mobile Apps    │  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                        API Gateway                              │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │   REST API      │  │  WebSocket API  │  │   MQTT Broker   │  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                      Processing Layer                           │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │ Pose Estimation │  │    Tracking     │  │   Analytics     │  │
│  │    Service      │  │    Service      │  │    Service      │  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                       Data Layer                                │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │ CSI Processor   │  │  Data Pipeline  │  │  Model Manager  │  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                     Hardware Layer                              │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │  WiFi Routers   │  │ Processing Unit │  │   GPU Cluster   │  │
│  │   (CSI Data)    │  │   (CPU/Memory)  │  │  (Neural Net)   │  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Component Interaction Diagram

┌─────────────┐    CSI Data    ┌─────────────┐    Features    ┌─────────────┐
│   Router    │ ──────────────▶│ CSI         │ ──────────────▶│ Feature     │
│   Network   │                │ Processor   │                │ Extractor   │
└─────────────┘                └─────────────┘                └─────────────┘
                                       │                              │
                                       ▼                              ▼
┌─────────────┐    Poses       ┌─────────────┐    Inference   ┌─────────────┐
│   Client    │ ◀──────────────│ Pose        │ ◀──────────────│ Neural      │
│ Applications│                │ Tracker     │                │ Network     │
└─────────────┘                └─────────────┘                └─────────────┘
       │                               │                              │
       ▼                               ▼                              ▼
┌─────────────┐    Events      ┌─────────────┐    Models      ┌─────────────┐
│ Alert       │ ◀──────────────│ Analytics   │ ◀──────────────│ Model       │
│ System      │                │ Engine      │                │ Manager     │
└─────────────┘                └─────────────┘                └─────────────┘

Core Components

1. CSI Data Processor

Purpose: Receives and processes raw Channel State Information from WiFi routers.

Key Features:

  • Real-time CSI data ingestion from multiple routers
  • Signal preprocessing and noise reduction
  • Phase sanitization and amplitude normalization
  • Multi-antenna data fusion

Implementation: src/hardware/csi_processor.py

class CSIProcessor:
    """Processes raw CSI data from WiFi routers."""
    
    def __init__(self, config: CSIConfig):
        self.routers = self._initialize_routers(config.routers)
        self.buffer = CircularBuffer(config.buffer_size)
        self.preprocessor = CSIPreprocessor()
    
    async def process_stream(self) -> AsyncGenerator[CSIData, None]:
        """Process continuous CSI data stream."""
        async for raw_data in self._receive_csi_data():
            processed_data = self.preprocessor.process(raw_data)
            yield processed_data

2. Neural Network Service

Purpose: Performs pose estimation using deep learning models.

Key Features:

  • DensePose model inference
  • Batch processing optimization
  • GPU acceleration support
  • Model versioning and hot-swapping

Implementation: src/neural_network/inference.py

class PoseEstimationService:
    """Neural network service for pose estimation."""
    
    def __init__(self, model_config: ModelConfig):
        self.model = self._load_model(model_config.model_path)
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.batch_processor = BatchProcessor(model_config.batch_size)
    
    async def estimate_poses(self, csi_features: CSIFeatures) -> List[PoseEstimation]:
        """Estimate human poses from CSI features."""
        with torch.no_grad():
            predictions = self.model(csi_features.to(self.device))
            return self._postprocess_predictions(predictions)

3. Tracking Service

Purpose: Maintains temporal consistency and person identity across frames.

Key Features:

  • Multi-object tracking with Kalman filters
  • Person re-identification
  • Track lifecycle management
  • Trajectory smoothing

Implementation: src/tracking/tracker.py

class PersonTracker:
    """Tracks multiple persons across time."""
    
    def __init__(self, tracking_config: TrackingConfig):
        self.tracks = {}
        self.track_id_counter = 0
        self.kalman_filter = KalmanFilter()
        self.reid_model = ReIDModel()
    
    def update(self, detections: List[PoseDetection]) -> List[TrackedPose]:
        """Update tracks with new detections."""
        matched_tracks, unmatched_detections = self._associate_detections(detections)
        self._update_matched_tracks(matched_tracks)
        self._create_new_tracks(unmatched_detections)
        return self._get_active_tracks()

4. API Gateway

Purpose: Provides unified access to system functionality through REST and WebSocket APIs.

Key Features:

  • Authentication and authorization
  • Rate limiting and throttling
  • Request routing and load balancing
  • API versioning

Implementation: src/api/main.py

from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(
    title="WiFi-DensePose API",
    version="1.0.0",
    description="Privacy-preserving human pose estimation using WiFi signals"
)

# Middleware
app.add_middleware(CORSMiddleware, **get_cors_config())
app.add_middleware(RateLimitMiddleware)
app.add_middleware(AuthenticationMiddleware)

# Routers
app.include_router(pose_router, prefix="/api/v1/pose")
app.include_router(system_router, prefix="/api/v1/system")
app.include_router(analytics_router, prefix="/api/v1/analytics")

5. Analytics Engine

Purpose: Processes pose data to generate insights and trigger alerts.

Key Features:

  • Real-time event detection (falls, intrusions)
  • Statistical analysis and reporting
  • Domain-specific analytics (healthcare, retail, security)
  • Machine learning-based pattern recognition

Implementation: src/analytics/engine.py

class AnalyticsEngine:
    """Processes pose data for insights and alerts."""
    
    def __init__(self, domain_config: DomainConfig):
        self.domain = domain_config.domain
        self.event_detectors = self._load_event_detectors(domain_config)
        self.alert_manager = AlertManager(domain_config.alerts)
    
    async def process_poses(self, poses: List[TrackedPose]) -> AnalyticsResult:
        """Process poses and generate analytics."""
        events = []
        for detector in self.event_detectors:
            detected_events = await detector.detect(poses)
            events.extend(detected_events)
        
        await self.alert_manager.process_events(events)
        return AnalyticsResult(events=events, metrics=self._calculate_metrics(poses))

Data Flow

Real-Time Processing Pipeline

1. CSI Data Acquisition
   ┌─────────────┐
   │   Router 1  │ ──┐
   └─────────────┘   │
   ┌─────────────┐   │    ┌─────────────┐
   │   Router 2  │ ──┼───▶│ CSI Buffer  │
   └─────────────┘   │    └─────────────┘
   ┌─────────────┐   │           │
   │   Router N  │ ──┘           ▼
   └─────────────┘       ┌─────────────┐
                         │ Preprocessor│
                         └─────────────┘
                                │
2. Feature Extraction           ▼
   ┌─────────────┐       ┌─────────────┐
   │   Phase     │ ◀─────│ Feature     │
   │ Sanitizer   │       │ Extractor   │
   └─────────────┘       └─────────────┘
          │                     │
          ▼                     ▼
   ┌─────────────┐       ┌─────────────┐
   │ Amplitude   │       │ Frequency   │
   │ Processor   │       │ Analyzer    │
   └─────────────┘       └─────────────┘
          │                     │
          └──────┬──────────────┘
                 ▼
3. Neural Network Inference
   ┌─────────────┐
   │ DensePose   │
   │   Model     │
   └─────────────┘
          │
          ▼
   ┌─────────────┐
   │ Pose        │
   │ Decoder     │
   └─────────────┘
          │
4. Tracking and Analytics      ▼
   ┌─────────────┐       ┌─────────────┐
   │ Person      │ ◀─────│ Raw Pose    │
   │ Tracker     │       │ Detections  │
   └─────────────┘       └─────────────┘
          │
          ▼
   ┌─────────────┐
   │ Analytics   │
   │ Engine      │
   └─────────────┘
          │
5. Output and Storage          ▼
   ┌─────────────┐       ┌─────────────┐
   │ WebSocket   │ ◀─────│ Tracked     │
   │ Streams     │       │ Poses       │
   └─────────────┘       └─────────────┘
          │                     │
          ▼                     ▼
   ┌─────────────┐       ┌─────────────┐
   │ Client      │       │ Database    │
   │ Applications│       │ Storage     │
   └─────────────┘       └─────────────┘

Data Models

CSI Data Structure

@dataclass
class CSIData:
    """Channel State Information data structure."""
    timestamp: datetime
    router_id: str
    antenna_pairs: List[AntennaPair]
    subcarriers: List[SubcarrierData]
    metadata: CSIMetadata

@dataclass
class SubcarrierData:
    """Individual subcarrier information."""
    frequency: float
    amplitude: complex
    phase: float
    snr: float

Pose Data Structure

@dataclass
class PoseEstimation:
    """Human pose estimation result."""
    person_id: Optional[int]
    confidence: float
    bounding_box: BoundingBox
    keypoints: List[Keypoint]
    dense_pose: Optional[DensePoseResult]
    timestamp: datetime

@dataclass
class TrackedPose:
    """Tracked pose with temporal information."""
    track_id: int
    pose: PoseEstimation
    velocity: Vector2D
    track_age: int
    track_confidence: float

Processing Pipeline

1. CSI Preprocessing

class CSIPreprocessor:
    """Preprocesses raw CSI data for neural network input."""
    
    def __init__(self, config: PreprocessingConfig):
        self.phase_sanitizer = PhaseSanitizer()
        self.amplitude_normalizer = AmplitudeNormalizer()
        self.noise_filter = NoiseFilter(config.filter_params)
    
    def process(self, raw_csi: RawCSIData) -> ProcessedCSIData:
        """Process raw CSI data."""
        # Phase unwrapping and sanitization
        sanitized_phase = self.phase_sanitizer.sanitize(raw_csi.phase)
        
        # Amplitude normalization
        normalized_amplitude = self.amplitude_normalizer.normalize(raw_csi.amplitude)
        
        # Noise filtering
        filtered_data = self.noise_filter.filter(sanitized_phase, normalized_amplitude)
        
        return ProcessedCSIData(
            phase=filtered_data.phase,
            amplitude=filtered_data.amplitude,
            timestamp=raw_csi.timestamp,
            metadata=raw_csi.metadata
        )

2. Feature Extraction

class FeatureExtractor:
    """Extracts features from processed CSI data."""
    
    def __init__(self, config: FeatureConfig):
        self.window_size = config.window_size
        self.feature_types = config.feature_types
        self.pca_reducer = PCAReducer(config.pca_components)
    
    def extract_features(self, csi_data: ProcessedCSIData) -> CSIFeatures:
        """Extract features for neural network input."""
        features = {}
        
        if 'amplitude' in self.feature_types:
            features['amplitude'] = self._extract_amplitude_features(csi_data)
        
        if 'phase' in self.feature_types:
            features['phase'] = self._extract_phase_features(csi_data)
        
        if 'doppler' in self.feature_types:
            features['doppler'] = self._extract_doppler_features(csi_data)
        
        # Dimensionality reduction
        reduced_features = self.pca_reducer.transform(features)
        
        return CSIFeatures(
            features=reduced_features,
            timestamp=csi_data.timestamp,
            feature_types=self.feature_types
        )

3. Neural Network Architecture

class DensePoseNet(nn.Module):
    """DensePose neural network for WiFi-based pose estimation."""
    
    def __init__(self, config: ModelConfig):
        super().__init__()
        self.backbone = self._build_backbone(config.backbone)
        self.feature_pyramid = FeaturePyramidNetwork(config.fpn)
        self.pose_head = PoseEstimationHead(config.pose_head)
        self.dense_pose_head = DensePoseHead(config.dense_pose_head)
    
    def forward(self, csi_features: torch.Tensor) -> Dict[str, torch.Tensor]:
        """Forward pass through the network."""
        # Feature extraction
        backbone_features = self.backbone(csi_features)
        pyramid_features = self.feature_pyramid(backbone_features)
        
        # Pose estimation
        pose_predictions = self.pose_head(pyramid_features)
        dense_pose_predictions = self.dense_pose_head(pyramid_features)
        
        return {
            'poses': pose_predictions,
            'dense_poses': dense_pose_predictions
        }

API Architecture

REST API Design

The REST API follows RESTful principles with clear resource hierarchies:

/api/v1/
├── auth/
│   ├── token          # POST: Get authentication token
│   └── verify         # POST: Verify token validity
├── system/
│   ├── status         # GET: System health status
│   ├── start          # POST: Start pose estimation
│   ├── stop           # POST: Stop pose estimation
│   └── diagnostics    # GET: System diagnostics
├── pose/
│   ├── latest         # GET: Latest pose data
│   ├── history        # GET: Historical pose data
│   └── query          # POST: Complex pose queries
├── config/
│   └── [resource]     # GET/PUT: Configuration management
└── analytics/
    ├── healthcare     # GET: Healthcare analytics
    ├── retail         # GET: Retail analytics
    └── security       # GET: Security analytics

WebSocket API Design

class WebSocketManager:
    """Manages WebSocket connections and subscriptions."""
    
    def __init__(self):
        self.connections: Dict[str, WebSocket] = {}
        self.subscriptions: Dict[str, Set[str]] = {}
    
    async def handle_connection(self, websocket: WebSocket, client_id: str):
        """Handle new WebSocket connection."""
        await websocket.accept()
        self.connections[client_id] = websocket
        
        try:
            async for message in websocket.iter_text():
                await self._handle_message(client_id, json.loads(message))
        except WebSocketDisconnect:
            self._cleanup_connection(client_id)
    
    async def broadcast_pose_update(self, pose_data: TrackedPose):
        """Broadcast pose updates to subscribed clients."""
        message = {
            'type': 'pose_update',
            'data': pose_data.to_dict(),
            'timestamp': datetime.utcnow().isoformat()
        }
        
        for client_id in self.subscriptions.get('pose_updates', set()):
            if client_id in self.connections:
                await self.connections[client_id].send_text(json.dumps(message))

Storage Architecture

Database Design

Time-Series Data (PostgreSQL + TimescaleDB)

-- Pose data table with time-series optimization
CREATE TABLE pose_data (
    id BIGSERIAL PRIMARY KEY,
    timestamp TIMESTAMPTZ NOT NULL,
    frame_id BIGINT NOT NULL,
    person_id INTEGER,
    track_id INTEGER,
    confidence REAL NOT NULL,
    bounding_box JSONB NOT NULL,
    keypoints JSONB NOT NULL,
    dense_pose JSONB,
    metadata JSONB,
    environment_id VARCHAR(50) NOT NULL
);

-- Convert to hypertable for time-series optimization
SELECT create_hypertable('pose_data', 'timestamp');

-- Create indexes for common queries
CREATE INDEX idx_pose_data_timestamp ON pose_data (timestamp DESC);
CREATE INDEX idx_pose_data_person_id ON pose_data (person_id, timestamp DESC);
CREATE INDEX idx_pose_data_environment ON pose_data (environment_id, timestamp DESC);

Configuration Storage (PostgreSQL)

-- System configuration
CREATE TABLE system_config (
    id SERIAL PRIMARY KEY,
    domain VARCHAR(50) NOT NULL,
    environment_id VARCHAR(50) NOT NULL,
    config_data JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(domain, environment_id)
);

-- Model metadata
CREATE TABLE model_metadata (
    id SERIAL PRIMARY KEY,
    model_name VARCHAR(100) NOT NULL,
    model_version VARCHAR(20) NOT NULL,
    model_path TEXT NOT NULL,
    config JSONB NOT NULL,
    performance_metrics JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(model_name, model_version)
);

Caching Strategy (Redis)

class CacheManager:
    """Manages Redis caching for frequently accessed data."""
    
    def __init__(self, redis_client: Redis):
        self.redis = redis_client
        self.default_ttl = 300  # 5 minutes
    
    async def cache_pose_data(self, pose_data: TrackedPose, ttl: int = None):
        """Cache pose data with automatic expiration."""
        key = f"pose:latest:{pose_data.track_id}"
        value = json.dumps(pose_data.to_dict(), default=str)
        await self.redis.setex(key, ttl or self.default_ttl, value)
    
    async def get_cached_poses(self, track_ids: List[int]) -> List[TrackedPose]:
        """Retrieve cached pose data for multiple tracks."""
        keys = [f"pose:latest:{track_id}" for track_id in track_ids]
        cached_data = await self.redis.mget(keys)
        
        poses = []
        for data in cached_data:
            if data:
                pose_dict = json.loads(data)
                poses.append(TrackedPose.from_dict(pose_dict))
        
        return poses

Security Architecture

Authentication and Authorization

class SecurityManager:
    """Handles authentication and authorization."""
    
    def __init__(self, config: SecurityConfig):
        self.jwt_secret = config.jwt_secret
        self.jwt_algorithm = config.jwt_algorithm
        self.token_expiry = config.token_expiry
    
    def create_access_token(self, user_data: dict) -> str:
        """Create JWT access token."""
        payload = {
            'sub': user_data['username'],
            'exp': datetime.utcnow() + timedelta(hours=self.token_expiry),
            'iat': datetime.utcnow(),
            'permissions': user_data.get('permissions', [])
        }
        return jwt.encode(payload, self.jwt_secret, algorithm=self.jwt_algorithm)
    
    def verify_token(self, token: str) -> dict:
        """Verify and decode JWT token."""
        try:
            payload = jwt.decode(token, self.jwt_secret, algorithms=[self.jwt_algorithm])
            return payload
        except jwt.ExpiredSignatureError:
            raise HTTPException(status_code=401, detail="Token expired")
        except jwt.InvalidTokenError:
            raise HTTPException(status_code=401, detail="Invalid token")

Data Privacy

class PrivacyManager:
    """Manages data privacy and anonymization."""
    
    def __init__(self, config: PrivacyConfig):
        self.anonymization_enabled = config.anonymization_enabled
        self.data_retention_days = config.data_retention_days
        self.encryption_key = config.encryption_key
    
    def anonymize_pose_data(self, pose_data: TrackedPose) -> TrackedPose:
        """Anonymize pose data for privacy protection."""
        if not self.anonymization_enabled:
            return pose_data
        
        # Remove or hash identifying information
        anonymized_data = pose_data.copy()
        anonymized_data.track_id = self._hash_track_id(pose_data.track_id)
        
        # Apply differential privacy to keypoints
        anonymized_data.pose.keypoints = self._add_noise_to_keypoints(
            pose_data.pose.keypoints
        )
        
        return anonymized_data

Deployment Architecture

Container Architecture

# docker-compose.yml
version: '3.8'
services:
  wifi-densepose-api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/wifi_densepose
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - postgres
      - redis
      - neural-network
    volumes:
      - ./data:/app/data
      - ./models:/app/models
  
  neural-network:
    build: ./neural_network
    runtime: nvidia
    environment:
      - CUDA_VISIBLE_DEVICES=0
    volumes:
      - ./models:/app/models
  
  postgres:
    image: timescale/timescaledb:latest-pg14
    environment:
      - POSTGRES_DB=wifi_densepose
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
  
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

Kubernetes Deployment

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wifi-densepose-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: wifi-densepose-api
  template:
    metadata:
      labels:
        app: wifi-densepose-api
    spec:
      containers:
      - name: api
        image: wifi-densepose:latest
        ports:
        - containerPort: 8000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: database-secret
              key: url
        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "4Gi"
            cpu: "2000m"

Scalability and Performance

Horizontal Scaling

class LoadBalancer:
    """Distributes processing load across multiple instances."""
    
    def __init__(self, config: LoadBalancerConfig):
        self.processing_nodes = config.processing_nodes
        self.load_balancing_strategy = config.strategy
        self.health_checker = HealthChecker()
    
    async def distribute_csi_data(self, csi_data: CSIData) -> str:
        """Distribute CSI data to available processing nodes."""
        available_nodes = await self.health_checker.get_healthy_nodes()
        
        if self.load_balancing_strategy == 'round_robin':
            node = self._round_robin_selection(available_nodes)
        elif self.load_balancing_strategy == 'least_loaded':
            node = await self._least_loaded_selection(available_nodes)
        else:
            node = random.choice(available_nodes)
        
        await self._send_to_node(node, csi_data)
        return node.id

Performance Optimization

class PerformanceOptimizer:
    """Optimizes system performance based on runtime metrics."""
    
    def __init__(self, config: OptimizationConfig):
        self.metrics_collector = MetricsCollector()
        self.auto_scaling_enabled = config.auto_scaling_enabled
        self.optimization_interval = config.optimization_interval
    
    async def optimize_processing_pipeline(self):
        """Optimize processing pipeline based on current metrics."""
        metrics = await self.metrics_collector.get_current_metrics()
        
        # Adjust batch size based on GPU utilization
        if metrics.gpu_utilization < 0.7:
            await self._increase_batch_size()
        elif metrics.gpu_utilization > 0.9:
            await self._decrease_batch_size()
        
        # Scale processing nodes based on queue length
        if metrics.processing_queue_length > 100:
            await self._scale_up_processing_nodes()
        elif metrics.processing_queue_length < 10:
            await self._scale_down_processing_nodes()

Design Principles

1. Modularity and Separation of Concerns

  • Each component has a single, well-defined responsibility
  • Clear interfaces between components
  • Pluggable architecture for easy component replacement

2. Scalability

  • Horizontal scaling support through microservices
  • Stateless service design where possible
  • Efficient resource utilization and load balancing

3. Reliability and Fault Tolerance

  • Graceful degradation under failure conditions
  • Circuit breaker patterns for external dependencies
  • Comprehensive error handling and recovery mechanisms

4. Performance

  • Optimized data structures and algorithms
  • Efficient memory management and garbage collection
  • GPU acceleration for compute-intensive operations

5. Security and Privacy

  • Defense in depth security model
  • Data encryption at rest and in transit
  • Privacy-preserving data processing techniques

6. Observability

  • Comprehensive logging and monitoring
  • Distributed tracing for request flow analysis
  • Performance metrics and alerting

7. Maintainability

  • Clean code principles and consistent coding standards
  • Comprehensive documentation and API specifications
  • Automated testing and continuous integration

This architecture overview provides the foundation for understanding the WiFi-DensePose system. For implementation details, see: