Compare commits
8 Commits
8166d8d822
...
51140f599f
| Author | SHA1 | Date |
|---|---|---|
|
|
51140f599f | |
|
|
47d0640c49 | |
|
|
6959668e21 | |
|
|
6a408b30e8 | |
|
|
64dae5b1c1 | |
|
|
8e487c54ea | |
|
|
135d7d3d8c | |
|
|
9dd61bdbfa |
|
|
@ -12,6 +12,7 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
|
|||
[](#esp32-s3-hardware-pipeline)
|
||||
[](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
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
|
|
@ -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
|
||||
|
|
|
|||
403
ui/README.md
403
ui/README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}">▶ 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>
|
||||
<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>
|
||||
</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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue