Compare commits

...

8 Commits

Author SHA1 Message Date
ruv 51140f599f fix: update image references and remove obsolete screenshots 2026-03-02 11:11:58 -05:00
ruv 47d0640c49 fix: dark mode for pose canvas controls, single-row layout with icons
- All buttons converted to dark translucent style with colored accents:
  Start (green), Stop (red), Reconnect (blue), Demo (purple)
- Header, wrapper, status badge all use dark backgrounds
- Controls in single flat row (no wrapping)
- Mode select dropdown styled for dark theme
- HTML entity icons on all buttons

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-02 11:10:31 -05:00
ruv 6959668e21 docs: update ADR-035 with dark mode, render modes, pose_source fix
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-02 11:08:13 -05:00
ruv 6a408b30e8 Refactor code structure for improved readability and maintainability 2026-03-02 11:07:41 -05:00
ruv 64dae5b1c1 feat: implement heatmap and dense pose render modes
- Heatmap: Gaussian radial blobs per keypoint with per-person hue,
  faint skeleton overlay at 25% opacity
- Dense: body region segmentation with colored filled polygons for
  head, torso, arms, legs — thick strokes + joint circles
- Keypoints: now also renders bounding box and confidence
- Previously both heatmap and dense were stubs falling back to skeleton

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-02 11:05:25 -05:00
ruv 8e487c54ea fix: dark mode for Live Demo tab + pose_source passthrough
- Convert all Live Demo sidebar panels to dark theme matching rest of UI
- Fix pose_source not reaching LiveDemoTab (was lost in
  convertZoneDataToRestFormat — now passes through from WS message)
- Dark backgrounds, muted text, consistent border opacity throughout
- Estimation Mode badge colors adjusted for dark background contrast

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-02 11:03:09 -05:00
ruv 135d7d3d8c docs: add live pose detection screenshot to README
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-02 11:00:02 -05:00
ruv 9dd61bdbfa docs: update UI README with sensing tab, Rust backend, data sources
Reflects current state: Rust sensing server as primary backend,
sensing tab with 3D signal field, data source indicators, estimation
mode badge, setup guide, Docker deployment with CSI_SOURCE, and
updated file tree with all components/services.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-02 10:57:56 -05:00
8 changed files with 446 additions and 444 deletions

View File

@ -12,6 +12,7 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
[![ESP32 Ready](https://img.shields.io/badge/ESP32--S3-CSI%20streaming-purple.svg)](#esp32-s3-hardware-pipeline)
[![crates.io](https://img.shields.io/crates/v/wifi-densepose-ruvector.svg)](https://crates.io/crates/wifi-densepose-ruvector)
> | What | How | Speed |
> |------|-----|-------|
> | **Pose estimation** | CSI subcarrier amplitude/phase → DensePose UV maps | 54K fps (Rust) |
@ -54,6 +55,12 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
---
<img src="assets/screen.png" alt="WiFi DensePose — Live pose detection with setup guide" width="800">
<br>
<em>Real-time pose skeleton from WiFi CSI signals — no cameras, no wearables</em>
## 🚀 Key Features
### Sensing

BIN
assets/screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

View File

@ -60,14 +60,37 @@ Issue #86 reported that the live demo shows a static/barely-animated stick figur
- Goertzel filter bank adds ~O(9×N) computation per frame (negligible at 100 frames).
- Users with only 1 ESP32 may still be disappointed that arm tracking doesn't work — but the UI now explains why.
### 5. Dark mode consistency
- Live Demo tab converted from light theme to dark mode matching the rest of the UI.
- All sidebar panels, badges, buttons, dropdowns use dark backgrounds with muted text.
### 6. Render mode implementations
All four render modes in the pose visualization dropdown now produce distinct visual output:
| Mode | Rendering |
|------|-----------|
| **Skeleton** | Green lines connecting joints + red keypoint dots |
| **Keypoints** | Large colored dots with glow and labels, no connecting lines |
| **Heatmap** | Gaussian radial blobs per keypoint (hue per person), faint skeleton overlay at 25% opacity |
| **Dense** | Body region segmentation with colored filled polygons — head (red), torso (blue), left arm (green), right arm (orange), left leg (purple), right leg (yellow) |
Previously heatmap and dense were stubs that fell back to skeleton mode.
### 7. pose_source passthrough fix
The `pose_source` field from the WebSocket message was being dropped in `convertZoneDataToRestFormat()` in `pose.service.js`. Now passed through so the Estimation Mode badge displays correctly.
## Files Changed
- `docker/Dockerfile.rust``CSI_SOURCE=auto` env, shell entrypoint for variable expansion
- `docker/docker-compose.yml``CSI_SOURCE=${CSI_SOURCE:-auto}`, shell command string
- `wifi-densepose-sensing-server/src/main.rs` — frame history buffer, Goertzel breathing estimation, temporal motion score, signal-driven pose derivation, pose_source field, 100ms tick default
- `ui/services/sensing.service.js``dataSource` state, delayed simulation fallback, `_simulated` marker
- `ui/services/pose.service.js``pose_source` passthrough in data conversion
- `ui/components/SensingTab.js` — data source banner, "About This Data" card
- `ui/components/LiveDemoTab.js` — estimation mode badge, setup guide panel
- `ui/style.css` — banner, badge, and guide panel styles
- `ui/components/LiveDemoTab.js` — estimation mode badge, setup guide panel, dark mode theme
- `ui/utils/pose-renderer.js` — heatmap (Gaussian blobs) and dense (body region segmentation) render modes
- `ui/style.css` — banner, badge, guide panel, and about-text styles
- `README.md` — live pose detection screenshot
- `assets/screen.png` — screenshot asset
## References
- Issue: https://github.com/ruvnet/wifi-densepose/issues/86

View File

@ -1,46 +1,73 @@
# WiFi DensePose UI
A modular, modern web interface for the WiFi DensePose human tracking system. This UI provides real-time monitoring, configuration, and visualization of WiFi-based pose estimation.
A modular, modern web interface for the WiFi DensePose human tracking system. Provides real-time monitoring, WiFi sensing visualization, and pose estimation from CSI (Channel State Information).
## 🏗️ Architecture
## Architecture
The UI follows a modular architecture with clear separation of concerns:
```
ui/
├── app.js # Main application entry point
├── index.html # Updated HTML with modular structure
├── style.css # Complete CSS with additional styles
├── config/ # Configuration modules
│ └── api.config.js # API endpoints and configuration
├── services/ # Service layer for API communication
│ ├── api.service.js # HTTP API client
│ ├── websocket.service.js # WebSocket client
│ ├── pose.service.js # Pose estimation API wrapper
│ ├── health.service.js # Health monitoring API wrapper
│ └── stream.service.js # Streaming API wrapper
├── components/ # UI components
│ ├── TabManager.js # Tab navigation component
│ ├── DashboardTab.js # Dashboard component with live data
│ ├── HardwareTab.js # Hardware configuration component
│ └── LiveDemoTab.js # Live demo with streaming
├── utils/ # Utility functions and helpers
│ └── mock-server.js # Mock server for testing
└── tests/ # Comprehensive test suite
├── test-runner.html # Test runner UI
├── test-runner.js # Test framework and cases
└── integration-test.html # Integration testing page
├── app.js # Main application entry point
├── index.html # HTML shell with tab structure
├── style.css # Complete CSS design system
├── config/
│ └── api.config.js # API endpoints and configuration
├── services/
│ ├── api.service.js # HTTP API client
│ ├── websocket.service.js # WebSocket connection manager
│ ├── websocket-client.js # Low-level WebSocket client
│ ├── pose.service.js # Pose estimation API wrapper
│ ├── sensing.service.js # WiFi sensing data service (live + simulation fallback)
│ ├── health.service.js # Health monitoring API wrapper
│ ├── stream.service.js # Streaming API wrapper
│ └── data-processor.js # Signal data processing utilities
├── components/
│ ├── TabManager.js # Tab navigation component
│ ├── DashboardTab.js # Dashboard with live system metrics
│ ├── SensingTab.js # WiFi sensing visualization (3D signal field, metrics)
│ ├── LiveDemoTab.js # Live pose detection with setup guide
│ ├── HardwareTab.js # Hardware configuration
│ ├── SettingsPanel.js # Settings panel
│ ├── PoseDetectionCanvas.js # Canvas-based pose skeleton renderer
│ ├── gaussian-splats.js # 3D Gaussian splat signal field renderer (Three.js)
│ ├── body-model.js # 3D body model
│ ├── scene.js # Three.js scene management
│ ├── signal-viz.js # Signal visualization utilities
│ ├── environment.js # Environment/room visualization
│ └── dashboard-hud.js # Dashboard heads-up display
├── utils/
│ ├── backend-detector.js # Auto-detect backend availability
│ ├── mock-server.js # Mock server for testing
│ └── pose-renderer.js # Pose rendering utilities
└── tests/
├── test-runner.html # Test runner UI
├── test-runner.js # Test framework and cases
└── integration-test.html # Integration testing page
```
## 🚀 Features
## Features
### Smart Backend Detection
- **Automatic Detection**: Automatically detects if your FastAPI backend is running
- **Real Backend Priority**: Always uses the real backend when available
- **Mock Fallback**: Falls back to mock server only when backend is unavailable
- **Testing Mode**: Can force mock mode for testing and development
### WiFi Sensing Tab
- 3D Gaussian-splat signal field visualization (Three.js)
- Real-time RSSI, variance, motion band, breathing band metrics
- Presence/motion classification with confidence scores
- **Data source banner**: green "LIVE - ESP32", yellow "RECONNECTING...", or red "SIMULATED DATA"
- Sparkline RSSI history graph
- "About This Data" card explaining CSI capabilities per sensor count
### Real-time Dashboard
### Live Demo Tab
- WebSocket-based real-time pose skeleton rendering
- **Estimation Mode badge**: green "Signal-Derived" or blue "Model Inference"
- **Setup Guide panel** showing what each ESP32 count provides:
- 1 ESP32: presence, breathing, gross motion
- 2-3 ESP32s: body localization, motion direction
- 4+ ESP32s + trained model: individual limb tracking, full pose
- Debug mode with log export
- Zone selection and force-reconnect controls
- Performance metrics sidebar (frames, uptime, errors)
### Dashboard
- Live system health monitoring
- Real-time pose detection statistics
- Zone occupancy tracking
@ -53,284 +80,118 @@ ui/
- Configuration panels
- Hardware status monitoring
### Live Demo
- WebSocket-based real-time streaming
- Signal visualization
- Pose detection visualization
- Interactive controls
## Data Sources
### API Integration
- Complete REST API coverage
- WebSocket streaming support
- Authentication handling
- Error management
- Request/response interceptors
The sensing service (`sensing.service.js`) supports three connection states:
## 📋 API Coverage
| State | Banner Color | Description |
|-------|-------------|-------------|
| **LIVE - ESP32** | Green | Connected to the Rust sensing server receiving real CSI data |
| **RECONNECTING** | Yellow (pulsing) | WebSocket disconnected, retrying (up to 20 attempts) |
| **SIMULATED DATA** | Red | Fallback to client-side simulation after 5+ failed reconnects |
The UI integrates with all WiFi DensePose API endpoints:
Simulated frames include a `_simulated: true` marker so code can detect synthetic data.
### Health Endpoints
- `GET /health/health` - System health check
- `GET /health/ready` - Readiness check
- `GET /health/live` - Liveness check
- `GET /health/metrics` - System metrics
- `GET /health/version` - Version information
## Backends
### Pose Estimation
- `GET /api/v1/pose/current` - Current pose data
- `POST /api/v1/pose/analyze` - Trigger analysis
- `GET /api/v1/pose/zones/{zone_id}/occupancy` - Zone occupancy
- `GET /api/v1/pose/zones/summary` - All zones summary
- `POST /api/v1/pose/historical` - Historical data
- `GET /api/v1/pose/activities` - Recent activities
- `POST /api/v1/pose/calibrate` - System calibration
- `GET /api/v1/pose/stats` - Statistics
### Rust Sensing Server (primary)
The Rust-based `wifi-densepose-sensing-server` serves the UI and provides:
- `GET /health` — server health
- `GET /api/v1/sensing/latest` — latest sensing features
- `GET /api/v1/vital-signs` — vital sign estimates (HR/RR)
- `GET /api/v1/model/info` — RVF model container info
- `WS /ws/sensing` — real-time sensing data stream
- `WS /api/v1/stream/pose` — real-time pose keypoint stream
### Streaming
- `WS /api/v1/stream/pose` - Real-time pose stream
- `WS /api/v1/stream/events` - Event stream
- `GET /api/v1/stream/status` - Stream status
- `POST /api/v1/stream/start` - Start streaming
- `POST /api/v1/stream/stop` - Stop streaming
- `GET /api/v1/stream/clients` - Connected clients
- `DELETE /api/v1/stream/clients/{client_id}` - Disconnect client
### Python FastAPI (legacy)
The original Python backend on port 8000 is still supported. The UI auto-detects which backend is available via `backend-detector.js`.
## 🧪 Testing
### Test Runner
Open `tests/test-runner.html` to run the complete test suite:
## Quick Start
### With Docker (recommended)
```bash
# Serve the UI directory on port 3000 (to avoid conflicts with FastAPI on 8000)
cd /workspaces/wifi-densepose/ui
cd docker/
# Default: auto-detects ESP32 on UDP 5005, falls back to simulation
docker-compose up
# Force real ESP32 data
CSI_SOURCE=esp32 docker-compose up
# Force simulation (no hardware needed)
CSI_SOURCE=simulated docker-compose up
```
Open http://localhost:3000/ui/index.html
### With local Rust binary
```bash
cd rust-port/wifi-densepose-rs
cargo build -p wifi-densepose-sensing-server --no-default-features
# Run with simulated data
../../target/debug/sensing-server --source simulated --tick-ms 100 --ui-path ../../ui --http-port 3000
# Run with real ESP32
../../target/debug/sensing-server --source esp32 --tick-ms 100 --ui-path ../../ui --http-port 3000
```
Open http://localhost:3000/ui/index.html
### With Python HTTP server (legacy)
```bash
# Start FastAPI backend on port 8000
wifi-densepose start
# Serve the UI on port 3000
cd ui/
python -m http.server 3000
# Open http://localhost:3000/tests/test-runner.html
```
Open http://localhost:3000
### Test Categories
- **API Configuration Tests** - Configuration and URL building
- **API Service Tests** - HTTP client functionality
- **WebSocket Service Tests** - WebSocket connection management
- **Pose Service Tests** - Pose estimation API wrapper
- **Health Service Tests** - Health monitoring functionality
- **UI Component Tests** - Component behavior and interaction
- **Integration Tests** - End-to-end functionality
## Pose Estimation Modes
### Integration Testing
Use `tests/integration-test.html` for visual integration testing:
| Mode | Badge | Requirements | Accuracy |
|------|-------|-------------|----------|
| **Signal-Derived** | Green | 1+ ESP32, no model needed | Presence, breathing, gross motion |
| **Model Inference** | Blue | 4+ ESP32s + trained `.rvf` model | Full 17-keypoint COCO pose |
To use model inference, start the server with a trained model:
```bash
# Open http://localhost:3000/tests/integration-test.html
sensing-server --source esp32 --model path/to/model.rvf --ui-path ./ui
```
Features:
- Mock server with realistic API responses
- Visual testing of all components
- Real-time data simulation
- Error scenario testing
- WebSocket stream testing
## 🛠️ Usage
### Basic Setup
```html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<!-- Your content -->
</div>
<script type="module" src="app.js"></script>
</body>
</html>
```
### Using Services
```javascript
import { poseService } from './services/pose.service.js';
import { healthService } from './services/health.service.js';
// Get current pose data
const poseData = await poseService.getCurrentPose();
// Subscribe to health updates
healthService.subscribeToHealth(health => {
console.log('Health status:', health.status);
});
// Start pose streaming
poseService.startPoseStream({
minConfidence: 0.7,
maxFps: 30
});
poseService.subscribeToPoseUpdates(update => {
if (update.type === 'pose_update') {
console.log('New pose data:', update.data);
}
});
```
### Using Components
```javascript
import { TabManager } from './components/TabManager.js';
import { DashboardTab } from './components/DashboardTab.js';
// Initialize tab manager
const container = document.querySelector('.container');
const tabManager = new TabManager(container);
tabManager.init();
// Initialize dashboard
const dashboardContainer = document.getElementById('dashboard');
const dashboard = new DashboardTab(dashboardContainer);
await dashboard.init();
```
## 🔧 Configuration
## Configuration
### API Configuration
Edit `config/api.config.js` to modify API settings:
Edit `config/api.config.js`:
```javascript
export const API_CONFIG = {
BASE_URL: window.location.origin,
API_VERSION: '/api/v1',
// Rate limiting
RATE_LIMITS: {
REQUESTS_PER_MINUTE: 60,
BURST_LIMIT: 10
},
// WebSocket configuration
WS_CONFIG: {
RECONNECT_DELAY: 5000,
MAX_RECONNECT_ATTEMPTS: 5,
MAX_RECONNECT_ATTEMPTS: 20,
PING_INTERVAL: 30000
}
};
```
### Authentication
```javascript
import { apiService } from './services/api.service.js';
## Testing
// Set authentication token
apiService.setAuthToken('your-jwt-token');
Open `tests/test-runner.html` to run the test suite:
// Add request interceptor for auth
apiService.addRequestInterceptor((url, options) => {
// Modify request before sending
return { url, options };
});
```
## 🎨 Styling
The UI uses a comprehensive CSS design system with:
- CSS Custom Properties for theming
- Dark/light mode support
- Responsive design
- Component-based styling
- Smooth animations and transitions
### Key CSS Variables
```css
:root {
--color-primary: rgba(33, 128, 141, 1);
--color-background: rgba(252, 252, 249, 1);
--color-surface: rgba(255, 255, 253, 1);
--color-text: rgba(19, 52, 59, 1);
--space-16: 16px;
--radius-lg: 12px;
}
```
## 🔍 Monitoring & Debugging
### Health Monitoring
```javascript
import { healthService } from './services/health.service.js';
// Start automatic health checks
healthService.startHealthMonitoring(30000); // Every 30 seconds
// Check if system is healthy
const isHealthy = healthService.isSystemHealthy();
// Get specific component status
const apiStatus = healthService.getComponentStatus('api');
```
### Error Handling
```javascript
// Global error handling
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
});
// API error handling
apiService.addResponseInterceptor(async (response, url) => {
if (!response.ok) {
console.error(`API error: ${response.status} for ${url}`);
}
return response;
});
```
## 🚀 Deployment
### Development
**Option 1: Use the startup script**
```bash
cd /workspaces/wifi-densepose/ui
./start-ui.sh
```
**Option 2: Manual setup**
```bash
# First, start your FastAPI backend (runs on port 8000)
wifi-densepose start
# or from the main project directory:
python -m wifi_densepose.main
# Then, start the UI server on a different port to avoid conflicts
cd /workspaces/wifi-densepose/ui
cd ui/
python -m http.server 3000
# or
npx http-server . -p 3000
# Open the UI at http://localhost:3000
# The UI will automatically detect and connect to your backend
# Open http://localhost:3000/tests/test-runner.html
```
### Backend Detection Behavior
- **Real Backend Available**: UI connects to `http://localhost:8000` and shows ✅ "Connected to real backend"
- **Backend Unavailable**: UI automatically uses mock server and shows ⚠️ "Mock server active - testing mode"
- **Force Mock Mode**: Set `API_CONFIG.MOCK_SERVER.ENABLED = true` for testing
Test categories: API configuration, API service, WebSocket, pose service, health service, UI components, integration.
### Production
1. Configure `API_CONFIG.BASE_URL` for your backend
2. Set up HTTPS for WebSocket connections
3. Configure authentication if required
4. Optimize assets (minify CSS/JS)
5. Set up monitoring and logging
## Styling
## 🤝 Contributing
Uses a CSS design system with custom properties, dark/light mode, responsive layout, and component-based styling. Key variables in `:root` of `style.css`.
1. Follow the modular architecture
2. Add tests for new functionality
3. Update documentation
4. Ensure TypeScript compatibility
5. Test with mock server
## License
## 📄 License
This project is part of the WiFi-DensePose system. See the main project LICENSE file for details.
Part of the WiFi-DensePose system. See the main project LICENSE file.

View File

@ -229,8 +229,8 @@ export class LiveDemoTab {
flex-direction: column;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
background: #0a0f1a;
color: #e0e0e0;
}
.demo-header {
@ -238,10 +238,10 @@ export class LiveDemoTab {
justify-content: space-between;
align-items: center;
padding: 20px 24px;
background: rgba(255, 255, 255, 0.95);
background: rgba(15, 20, 35, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 10;
}
@ -254,10 +254,10 @@ export class LiveDemoTab {
.demo-title h2 {
margin: 0;
color: #333;
color: #e0e0e0;
font-size: 22px;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #667eea 0%, #a78bfa 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@ -268,9 +268,9 @@ export class LiveDemoTab {
align-items: center;
gap: 10px;
padding: 8px 16px;
background: rgba(248, 249, 250, 0.8);
background: rgba(30, 40, 60, 0.8);
border-radius: 20px;
border: 1px solid rgba(222, 226, 230, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.status-indicator {
@ -304,7 +304,7 @@ export class LiveDemoTab {
.status-text {
font-size: 13px;
font-weight: 500;
color: #495057;
color: #b0b8c8;
}
.demo-controls {
@ -338,19 +338,19 @@ export class LiveDemoTab {
.btn--primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
}
.btn--secondary {
background: #f8f9fa;
color: #495057;
border-color: #dee2e6;
background: rgba(30, 40, 60, 0.8);
color: #b0b8c8;
border-color: rgba(255, 255, 255, 0.1);
}
.btn--secondary:hover:not(:disabled) {
background: #e9ecef;
background: rgba(40, 50, 75, 0.9);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.btn:disabled {
@ -368,19 +368,20 @@ export class LiveDemoTab {
.zone-select {
padding: 10px 14px;
border: 1px solid #dee2e6;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: white;
background: rgba(30, 40, 60, 0.8);
color: #b0b8c8;
font-size: 14px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
}
.zone-select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.demo-content {
@ -388,18 +389,17 @@ export class LiveDemoTab {
flex: 1;
gap: 24px;
padding: 24px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
background: #0a0f1a;
}
.demo-main {
flex: 2;
min-height: 500px;
background: white;
background: #111827;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.pose-detection-container {
@ -416,15 +416,15 @@ export class LiveDemoTab {
}
.metrics-panel, .health-panel, .debug-panel {
background: #fff;
border: 1px solid #ddd;
background: rgba(17, 24, 39, 0.9);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 15px;
}
.metrics-panel h4, .health-panel h4, .debug-panel h4 {
margin: 0 0 15px 0;
color: #333;
color: #e0e0e0;
font-size: 14px;
font-weight: 600;
}
@ -438,12 +438,12 @@ export class LiveDemoTab {
}
.metric label, .health-check label {
color: #666;
color: #8899aa;
}
.metric span, .health-check span {
font-weight: 500;
color: #333;
color: #c8d0dc;
}
.debug-actions {
@ -455,18 +455,20 @@ export class LiveDemoTab {
.debug-info textarea {
width: 100%;
border: 1px solid #ddd;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
padding: 8px;
font-family: monospace;
font-size: 11px;
resize: vertical;
background: #0a0f1a;
color: #c8d0dc;
}
.error-display {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
background: rgba(220, 53, 69, 0.15);
color: #f5a0a8;
border: 1px solid rgba(220, 53, 69, 0.3);
border-radius: 4px;
padding: 12px;
margin: 10px 20px;
@ -479,15 +481,15 @@ export class LiveDemoTab {
/* Pose estimation mode indicator */
.pose-source-panel {
background: #fff;
border: 1px solid #ddd;
background: rgba(17, 24, 39, 0.9);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 15px;
}
.pose-source-panel h4 {
margin: 0 0 12px 0;
color: #333;
color: #e0e0e0;
font-size: 14px;
font-weight: 600;
}
@ -510,40 +512,40 @@ export class LiveDemoTab {
}
.pose-source-unknown {
background: #f0f0f0;
color: #6c757d;
border: 1px solid #dee2e6;
background: rgba(108, 117, 125, 0.15);
color: #8899aa;
border: 1px solid rgba(108, 117, 125, 0.3);
}
.pose-source-signal {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #a5d6a7;
background: rgba(0, 204, 136, 0.12);
color: #00cc88;
border: 1px solid rgba(0, 204, 136, 0.3);
}
.pose-source-model {
background: #e3f2fd;
color: #1565c0;
border: 1px solid #90caf9;
background: rgba(102, 126, 234, 0.12);
color: #8ea4f0;
border: 1px solid rgba(102, 126, 234, 0.3);
}
.pose-source-description {
margin: 0;
font-size: 11px;
color: #666;
color: #8899aa;
line-height: 1.4;
}
.setup-guide-panel {
background: #fff;
border: 1px solid #ddd;
background: rgba(17, 24, 39, 0.9);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 15px;
}
.setup-guide-panel h4 {
margin: 0 0 12px 0;
color: #333;
color: #e0e0e0;
font-size: 14px;
font-weight: 600;
}
@ -560,8 +562,8 @@ export class LiveDemoTab {
gap: 10px;
padding: 8px;
border-radius: 6px;
background: #f8f9fa;
border: 1px solid #e9ecef;
background: rgba(30, 40, 60, 0.6);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.setup-level-icon {
@ -580,25 +582,26 @@ export class LiveDemoTab {
.setup-level-info strong {
font-size: 12px;
color: #333;
color: #c8d0dc;
display: block;
}
.setup-level-info p {
margin: 2px 0 0;
font-size: 11px;
color: #666;
color: #8899aa;
}
.setup-note {
margin: 10px 0 0;
font-size: 11px;
color: #888;
color: #6b7a8d;
line-height: 1.5;
}
.setup-note code {
background: #f0f0f0;
background: rgba(102, 126, 234, 0.12);
color: #8ea4f0;
padding: 1px 4px;
border-radius: 3px;
font-size: 10px;

View File

@ -89,21 +89,17 @@ export class PoseDetectionCanvas {
</div>
</div>
<div class="pose-canvas-controls" id="controls-${this.containerId}">
<div class="control-group primary-controls">
<button class="btn btn-start" id="start-btn-${this.containerId}">Start</button>
<button class="btn btn-stop" id="stop-btn-${this.containerId}" disabled>Stop</button>
<button class="btn btn-reconnect" id="reconnect-btn-${this.containerId}" disabled>Reconnect</button>
<button class="btn btn-demo" id="demo-btn-${this.containerId}">Demo</button>
</div>
<div class="control-group secondary-controls">
<select class="mode-select" id="mode-select-${this.containerId}">
<option value="skeleton">Skeleton</option>
<option value="keypoints">Keypoints</option>
<option value="heatmap">Heatmap</option>
<option value="dense">Dense</option>
</select>
<button class="btn btn-settings" id="settings-btn-${this.containerId}"> Settings</button>
</div>
<button class="btn btn-start" id="start-btn-${this.containerId}">&#9654; Start</button>
<button class="btn btn-stop" id="stop-btn-${this.containerId}" disabled>&#9632; Stop</button>
<button class="btn btn-reconnect" id="reconnect-btn-${this.containerId}" disabled>&#8635; Reconnect</button>
<button class="btn btn-demo" id="demo-btn-${this.containerId}">&#9881; Demo</button>
<select class="mode-select" id="mode-select-${this.containerId}">
<option value="skeleton">Skeleton</option>
<option value="keypoints">Keypoints</option>
<option value="heatmap">Heatmap</option>
<option value="dense">Dense</option>
</select>
<button class="btn btn-settings" id="settings-btn-${this.containerId}">&#9881; Settings</button>
</div>
</div>
<div class="pose-canvas-container">
@ -124,20 +120,20 @@ export class PoseDetectionCanvas {
const style = document.createElement('style');
style.textContent = `
.pose-detection-canvas-wrapper {
border: 1px solid #ddd;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
overflow: hidden;
background: #f9f9f9;
font-family: Arial, sans-serif;
background: #0d1117;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.pose-canvas-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: #f0f0f0;
border-bottom: 1px solid #ddd;
padding: 12px 16px;
background: rgba(15, 20, 35, 0.95);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.pose-canvas-title {
@ -148,156 +144,166 @@ export class PoseDetectionCanvas {
.pose-canvas-title h3 {
margin: 0;
color: #333;
color: #e0e0e0;
font-size: 16px;
font-weight: 600;
}
.connection-status {
display: flex;
align-items: center;
gap: 5px;
gap: 6px;
padding: 4px 10px;
background: rgba(30, 40, 60, 0.6);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.status-indicator {
width: 10px;
height: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #ccc;
background: #4a5568;
transition: background-color 0.3s;
}
.status-indicator.connected { background: #28a745; }
.status-indicator.connecting { background: #ffc107; }
.status-indicator.error { background: #dc3545; }
.status-indicator.disconnected { background: #6c757d; }
.status-indicator.connected { background: #00cc88; box-shadow: 0 0 6px rgba(0, 204, 136, 0.5); }
.status-indicator.connecting { background: #fbbf24; box-shadow: 0 0 6px rgba(251, 191, 36, 0.5); animation: pulse 1.5s ease-in-out infinite; }
.status-indicator.error { background: #ef4444; box-shadow: 0 0 6px rgba(239, 68, 68, 0.5); }
.status-indicator.disconnected { background: #4a5568; }
.status-text {
font-size: 12px;
color: #666;
min-width: 80px;
font-size: 11px;
color: #8899aa;
min-width: 70px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
.pose-canvas-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.primary-controls {
flex: 1;
}
.secondary-controls {
flex-shrink: 0;
flex-wrap: nowrap;
}
.btn {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 6px;
background: #ffffff;
color: #333333;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(30, 40, 60, 0.8);
color: #c8d0dc;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
text-decoration: none;
display: inline-block;
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 80px;
text-align: center;
justify-content: center;
}
.btn:hover:not(:disabled) {
background: #f8f9fa;
border-color: #adb5bd;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.btn:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.btn:disabled {
opacity: 0.6;
opacity: 0.35;
cursor: not-allowed;
background: #e9ecef;
color: #6c757d;
background: rgba(20, 30, 50, 0.6);
color: #4a5568;
transform: none !important;
box-shadow: none !important;
}
.btn-start {
background: #28a745;
color: white;
border-color: #28a745;
.btn-start {
background: rgba(0, 204, 136, 0.15);
color: #00cc88;
border-color: rgba(0, 204, 136, 0.3);
}
.btn-start:hover:not(:disabled) {
background: #218838;
border-color: #1e7e34;
.btn-start:hover:not(:disabled) {
background: rgba(0, 204, 136, 0.25);
border-color: rgba(0, 204, 136, 0.5);
box-shadow: 0 4px 12px rgba(0, 204, 136, 0.2);
}
.btn-stop {
background: #dc3545;
color: white;
border-color: #dc3545;
.btn-stop {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.btn-stop:hover:not(:disabled) {
background: #c82333;
border-color: #bd2130;
.btn-stop:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.25);
border-color: rgba(239, 68, 68, 0.5);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
}
.btn-reconnect {
background: #17a2b8;
color: white;
border-color: #17a2b8;
.btn-reconnect {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
border-color: rgba(59, 130, 246, 0.3);
}
.btn-reconnect:hover:not(:disabled) {
background: #138496;
border-color: #117a8b;
.btn-reconnect:hover:not(:disabled) {
background: rgba(59, 130, 246, 0.25);
border-color: rgba(59, 130, 246, 0.5);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
.btn-demo {
background: #6f42c1;
color: white;
border-color: #6f42c1;
.btn-demo {
background: rgba(139, 92, 246, 0.15);
color: #a78bfa;
border-color: rgba(139, 92, 246, 0.3);
}
.btn-demo:hover:not(:disabled) {
background: #5a32a3;
border-color: #512a97;
.btn-demo:hover:not(:disabled) {
background: rgba(139, 92, 246, 0.25);
border-color: rgba(139, 92, 246, 0.5);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2);
}
.btn-settings {
background: #6c757d;
color: white;
border-color: #6c757d;
.btn-settings {
background: rgba(100, 116, 139, 0.15);
color: #94a3b8;
border-color: rgba(100, 116, 139, 0.3);
}
.btn-settings:hover:not(:disabled) {
background: #5a6268;
border-color: #545b62;
.btn-settings:hover:not(:disabled) {
background: rgba(100, 116, 139, 0.25);
border-color: rgba(100, 116, 139, 0.5);
}
.mode-select {
padding: 5px 8px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
font-size: 12px;
padding: 8px 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(30, 40, 60, 0.8);
color: #b0b8c8;
font-size: 13px;
cursor: pointer;
}
.mode-select:focus {
outline: none;
border-color: rgba(139, 92, 246, 0.5);
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.15);
}
.mode-select option {
background: #1a2234;
color: #c8d0dc;
}
.pose-canvas-container {

View File

@ -511,6 +511,7 @@ export class PoseService {
persons: persons,
zone_summary: zoneSummary,
processing_time_ms: zoneData.metadata?.processing_time_ms || 0,
pose_source: originalMessage.pose_source || zoneData.pose_source || null,
metadata: {
mock_data: false,
source: 'websocket',

View File

@ -183,31 +183,132 @@ export class PoseRenderer {
}
}
// Keypoints only mode
// Keypoints only mode — large colored dots with labels, no skeleton lines
renderKeypointsMode(poseData, metadata) {
const persons = poseData.persons || [];
persons.forEach((person, index) => {
if (person.confidence >= this.config.confidenceThreshold && person.keypoints) {
this.renderKeypoints(person.keypoints, person.confidence, true);
// Render bounding box
if (this.config.showBoundingBox && person.bbox) {
this.renderBoundingBox(person.bbox, person.confidence, index);
}
if (this.config.showConfidence) {
this.renderConfidenceScore(person, index);
}
}
});
if (this.config.showZones && poseData.zone_summary) {
this.renderZones(poseData.zone_summary);
}
}
// Heatmap rendering mode
// Heatmap rendering mode — Gaussian blobs around each keypoint
renderHeatmapMode(poseData, metadata) {
// This would render a heatmap visualization
// For now, fall back to skeleton mode
this.logger.debug('Heatmap mode not fully implemented, using skeleton mode');
this.renderSkeletonMode(poseData, metadata);
const persons = poseData.persons || [];
persons.forEach((person, personIdx) => {
if (person.confidence < this.config.confidenceThreshold || !person.keypoints) return;
const hue = (personIdx * 60) % 360; // different hue per person
person.keypoints.forEach((kp) => {
if (kp.confidence <= this.config.keypointConfidenceThreshold) return;
const cx = this.scaleX(kp.x);
const cy = this.scaleY(kp.y);
const radius = 30 + kp.confidence * 20;
const grad = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
grad.addColorStop(0, `hsla(${hue}, 100%, 55%, ${kp.confidence * 0.7})`);
grad.addColorStop(0.5, `hsla(${hue}, 100%, 45%, ${kp.confidence * 0.3})`);
grad.addColorStop(1, `hsla(${hue}, 100%, 40%, 0)`);
this.ctx.fillStyle = grad;
this.ctx.fillRect(cx - radius, cy - radius, radius * 2, radius * 2);
});
// Light skeleton overlay so joints are connected
if (person.keypoints) {
this.ctx.globalAlpha = 0.25;
this.renderSkeleton(person.keypoints, person.confidence);
this.ctx.globalAlpha = 1.0;
}
if (this.config.showConfidence) {
this.renderConfidenceScore(person, personIdx);
}
});
if (this.config.showZones && poseData.zone_summary) {
this.renderZones(poseData.zone_summary);
}
}
// Dense pose rendering mode
// Dense pose rendering mode — body region segmentation with filled polygons
renderDenseMode(poseData, metadata) {
// This would render dense pose segmentation
// For now, fall back to skeleton mode
this.logger.debug('Dense mode not fully implemented, using skeleton mode');
this.renderSkeletonMode(poseData, metadata);
const persons = poseData.persons || [];
// Body part groups: [start_kp, end_kp, color]
const bodyParts = [
{ name: 'head', kps: [0, 1, 2, 3, 4], color: 'rgba(255, 100, 100, 0.4)' },
{ name: 'torso', kps: [5, 6, 12, 11], color: 'rgba(100, 200, 255, 0.4)' },
{ name: 'left_arm', kps: [5, 7, 9], color: 'rgba(100, 255, 150, 0.4)' },
{ name: 'right_arm', kps: [6, 8, 10], color: 'rgba(255, 200, 100, 0.4)' },
{ name: 'left_leg', kps: [11, 13, 15], color: 'rgba(200, 100, 255, 0.4)' },
{ name: 'right_leg', kps: [12, 14, 16], color: 'rgba(255, 255, 100, 0.4)' },
];
persons.forEach((person, personIdx) => {
if (person.confidence < this.config.confidenceThreshold || !person.keypoints) return;
const kps = person.keypoints;
bodyParts.forEach((part) => {
// Collect valid keypoints for this body part
const points = part.kps
.filter(i => kps[i] && kps[i].confidence > this.config.keypointConfidenceThreshold)
.map(i => ({ x: this.scaleX(kps[i].x), y: this.scaleY(kps[i].y) }));
if (points.length < 2) return;
// Draw filled region with padding around joints
this.ctx.fillStyle = part.color;
this.ctx.strokeStyle = part.color.replace('0.4', '0.7');
this.ctx.lineWidth = 8;
this.ctx.lineJoin = 'round';
this.ctx.lineCap = 'round';
// Draw thick path as a "region"
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
this.ctx.lineTo(points[i].x, points[i].y);
}
this.ctx.stroke();
// Draw circles at each joint to widen the region
points.forEach(p => {
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, 10, 0, Math.PI * 2);
this.ctx.fill();
});
});
// Subtle keypoint dots on top
this.renderKeypoints(kps, person.confidence, false);
if (this.config.showConfidence) {
this.renderConfidenceScore(person, personIdx);
}
});
if (this.config.showZones && poseData.zone_summary) {
this.renderZones(poseData.zone_summary);
}
}
// Render skeleton connections