MorphGuard / static /js /3d_mapper.js
juanquy's picture
Initial clean commit of modular MorphGuard
2978bba
Raw
History Blame Contribute Delete
32.9 kB
/**
* 3D Face Mapper Module v1.0
* Provides 3D face mapping, reconstruction and analysis capabilities for MorphGuard
* Using Three.js, Tensorflow.js and WebAssembly for optimal performance
*/
class FaceMapper3D {
constructor(options = {}) {
// Configuration
this.config = Object.assign({
canvasId: '3d-face-canvas',
wasmPath: '/static/wasm/',
modelPath: '/static/models/face_landmark/',
detectPoints: 478, // Full face mesh landmark points
scanQuality: 'standard', // rapid, standard, detailed
enableWasm: true,
enableDroneControl: true
}, options);
// State
this.status = {
initialized: false,
modelLoaded: false,
scanning: false,
reconstructing: false,
droneConnected: false,
capturedImages: [],
currentScanId: null,
processingStage: 'idle', // idle, capture, sfm, reconstruction, analysis
error: null
};
// Three.js objects
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.model = null;
// Face mesh detection
this.faceMeshModel = null;
// WebAssembly module
this.wasmModule = null;
// Initialize
this.init();
}
/**
* Initialize the 3D face mapper
*/
async init() {
try {
// Initialize Three.js scene
this.initScene();
// Load TensorFlow.js and face mesh model
await this.loadFaceMeshModel();
// Initialize WebAssembly module if enabled
if (this.config.enableWasm) {
await this.initWasmModule();
}
// Set up event listeners
this.setupEventListeners();
// Mark as initialized
this.status.initialized = true;
// Trigger initialized event
this.triggerEvent('initialized');
} catch (error) {
console.error('Error initializing 3D Face Mapper:', error);
this.status.error = error.message;
this.triggerEvent('error', { error });
}
}
/**
* Initialize Three.js scene
*/
initScene() {
// Get canvas
const canvas = document.getElementById(this.config.canvasId);
if (!canvas) {
throw new Error(`Canvas with ID ${this.config.canvasId} not found`);
}
// Create scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x111827);
// Create camera
this.camera = new THREE.PerspectiveCamera(
75,
canvas.clientWidth / canvas.clientHeight,
0.1,
1000
);
this.camera.position.z = 2;
// Create renderer
this.renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true
});
this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
// Create orbit controls
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
// Add lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(0, 1, 1);
this.scene.add(directionalLight);
// Add grid helper for reference
const gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x222222);
gridHelper.rotation.x = Math.PI / 2;
this.scene.add(gridHelper);
// Handle window resize
window.addEventListener('resize', () => this.handleResize());
// Start animation loop
this.animate();
}
/**
* Handle window resize
*/
handleResize() {
const canvas = this.renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (this.camera && this.renderer) {
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height, false);
}
}
/**
* Animation loop
*/
animate() {
requestAnimationFrame(() => this.animate());
// Update controls
if (this.controls) {
this.controls.update();
}
// Render scene
if (this.scene && this.camera) {
this.renderer.render(this.scene, this.camera);
}
}
/**
* Load TensorFlow.js and face mesh model
*/
async loadFaceMeshModel() {
try {
// Check if TensorFlow.js is loaded
if (typeof tf === 'undefined') {
await this.loadScript('https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.19.0/dist/tf.min.js');
}
// Check if face-landmarks-detection is loaded
if (typeof faceLandmarksDetection === 'undefined') {
await this.loadScript('https://cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@1.0.2/dist/face-landmarks-detection.min.js');
}
// Load face mesh model
this.faceMeshModel = await faceLandmarksDetection.load(
faceLandmarksDetection.SupportedPackages.mediapipeFacemesh,
{ maxFaces: 1 }
);
this.status.modelLoaded = true;
this.triggerEvent('modelLoaded');
return this.faceMeshModel;
} catch (error) {
console.error('Error loading face mesh model:', error);
this.status.error = 'Failed to load face mesh model';
this.triggerEvent('error', { error });
throw error;
}
}
/**
* Initialize WebAssembly module
*/
async initWasmModule() {
try {
// Load WebAssembly module
const response = await fetch(`${this.config.wasmPath}/face_reconstruction.wasm`);
const wasmBinary = await response.arrayBuffer();
// Initialize module
this.wasmModule = await WebAssembly.instantiate(wasmBinary, {
env: {
memory: new WebAssembly.Memory({ initial: 256, maximum: 512 }),
abort: (_, __, ___, ____) => console.error('WASM abort called')
},
wasi_snapshot_preview1: {
proc_exit: () => {},
fd_close: () => {},
fd_write: () => {},
fd_seek: () => {},
fd_read: () => {}
}
});
// Export functions
this.wasmFunctions = {
reconstructFace: this.wasmModule.instance.exports.reconstructFace,
analyzeMorphing: this.wasmModule.instance.exports.analyzeMorphing,
getMemoryBuffer: () => new Uint8Array(this.wasmModule.instance.exports.memory.buffer)
};
this.triggerEvent('wasmLoaded');
return this.wasmModule;
} catch (error) {
console.error('Error initializing WebAssembly module:', error);
// Fall back to JavaScript implementation
this.config.enableWasm = false;
this.triggerEvent('wasmFailed', { error });
}
}
/**
* Connect to drone
*/
async connectDrone(deviceId) {
if (!this.config.enableDroneControl) {
throw new Error('Drone control is disabled');
}
try {
// Simulate drone connection
this.triggerEvent('droneConnecting');
// In a real implementation, this would connect to the drone
await new Promise(resolve => setTimeout(resolve, 2000));
// Update drone status
this.status.droneConnected = true;
this.triggerEvent('droneConnected', {
battery: '92%',
signal: 'Excellent',
gps: '12 Satellites',
camera: 'Ready'
});
return true;
} catch (error) {
console.error('Error connecting to drone:', error);
this.status.error = 'Failed to connect to drone';
this.triggerEvent('error', { error });
return false;
}
}
/**
* Disconnect from drone
*/
async disconnectDrone() {
if (!this.status.droneConnected) {
return false;
}
try {
// Simulate drone disconnection
this.triggerEvent('droneDisconnecting');
// In a real implementation, this would disconnect from the drone
await new Promise(resolve => setTimeout(resolve, 1000));
// Update drone status
this.status.droneConnected = false;
this.triggerEvent('droneDisconnected');
return true;
} catch (error) {
console.error('Error disconnecting from drone:', error);
this.status.error = 'Failed to disconnect from drone';
this.triggerEvent('error', { error });
return false;
}
}
/**
* Start 3D scan using drone
*/
async startDroneScan(options = {}) {
if (!this.status.droneConnected) {
throw new Error('Drone is not connected');
}
if (this.status.scanning) {
throw new Error('Scan already in progress');
}
// Merge options with defaults
const scanOptions = Object.assign({
pattern: 'orbital', // orbital, hemispheric, spiral
quality: this.config.scanQuality,
subjectId: 'unknown'
}, options);
try {
// Generate scan ID
this.status.currentScanId = 'SCAN_' + Date.now().toString();
// Update status
this.status.scanning = true;
this.status.capturedImages = [];
this.status.processingStage = 'capture';
// Clear previous data
this.clearModel();
// Trigger scan started event
this.triggerEvent('scanStarted', {
scanId: this.status.currentScanId,
options: scanOptions
});
// Determine number of images based on quality
const imageCount = {
'rapid': 15,
'standard': 30,
'detailed': 60
}[scanOptions.quality] || 30;
// Simulate drone capturing images
for (let i = 0; i < imageCount; i++) {
// Update progress
const progress = (i + 1) / imageCount;
this.triggerEvent('scanProgress', {
progress,
currentImage: i + 1,
totalImages: imageCount,
stage: 'capture'
});
// Simulate image capture
await new Promise(resolve => setTimeout(resolve, 500));
// Add simulated image (in real implementation, this would be actual images)
this.status.capturedImages.push({
id: `img_${i}`,
position: this.simulateCameraPosition(i, imageCount, scanOptions.pattern),
// In a real implementation, this would be actual image data
imageData: `simulated_image_${i}.jpg`
});
}
// Start reconstruction process
await this.reconstructFaceModel();
// Mark scan as completed
this.status.scanning = false;
this.status.processingStage = 'completed';
// Trigger scan completed event
this.triggerEvent('scanCompleted', {
scanId: this.status.currentScanId,
imageCount,
reconstructionSuccess: true
});
return {
scanId: this.status.currentScanId,
imageCount,
subjectId: scanOptions.subjectId,
timestamp: new Date().toISOString(),
success: true
};
} catch (error) {
console.error('Error scanning with drone:', error);
// Update status
this.status.scanning = false;
this.status.error = error.message;
this.status.processingStage = 'error';
// Trigger error event
this.triggerEvent('error', { error });
return {
success: false,
error: error.message
};
}
}
/**
* Cancel current scan
*/
cancelScan() {
if (!this.status.scanning) {
return false;
}
// Update status
this.status.scanning = false;
this.status.processingStage = 'cancelled';
// Trigger cancel event
this.triggerEvent('scanCancelled', {
scanId: this.status.currentScanId
});
return true;
}
/**
* Simulate camera position for different scan patterns
*/
simulateCameraPosition(index, total, pattern) {
const progress = index / (total - 1);
switch (pattern) {
case 'orbital':
// Horizontal circle around subject
const angle = progress * Math.PI * 2;
return {
x: Math.sin(angle) * 2,
y: 0.2, // Slightly above eye level
z: Math.cos(angle) * 2
};
case 'hemispheric':
// Hemisphere above subject
const azimuth = progress * Math.PI * 2;
const elevation = (0.2 + progress * 0.6) * Math.PI / 2;
return {
x: 2 * Math.sin(azimuth) * Math.cos(elevation),
y: 2 * Math.sin(elevation),
z: 2 * Math.cos(azimuth) * Math.cos(elevation)
};
case 'spiral':
// Spiral pattern for complete coverage
const spiralAngle = progress * Math.PI * 6;
const spiralHeight = Math.sin(progress * Math.PI) * 1.5;
const radius = 1.5 + Math.sin(progress * Math.PI * 2) * 0.5;
return {
x: Math.sin(spiralAngle) * radius,
y: spiralHeight,
z: Math.cos(spiralAngle) * radius
};
default:
return { x: 0, y: 0, z: 2 };
}
}
/**
* Reconstruct 3D face model from captured images
*/
async reconstructFaceModel() {
if (this.status.capturedImages.length === 0) {
throw new Error('No images captured');
}
try {
// Update status
this.status.reconstructing = true;
this.status.processingStage = 'sfm';
// Trigger reconstruction started event
this.triggerEvent('reconstructionStarted', {
scanId: this.status.currentScanId,
imageCount: this.status.capturedImages.length
});
// Structure from Motion stage
this.triggerEvent('reconstructionProgress', {
stage: 'sfm',
progress: 0.0,
message: 'Extracting features...'
});
// Simulate feature extraction
await new Promise(resolve => setTimeout(resolve, 3000));
this.triggerEvent('reconstructionProgress', {
stage: 'sfm',
progress: 0.5,
message: 'Matching features...'
});
// Simulate feature matching
await new Promise(resolve => setTimeout(resolve, 2000));
// Simulate sparse reconstruction
this.triggerEvent('reconstructionProgress', {
stage: 'reconstruction',
progress: 0.0,
message: 'Building sparse point cloud...'
});
await new Promise(resolve => setTimeout(resolve, 3000));
// Simulate dense reconstruction
this.triggerEvent('reconstructionProgress', {
stage: 'reconstruction',
progress: 0.5,
message: 'Building dense model...'
});
await new Promise(resolve => setTimeout(resolve, 5000));
// Create actual 3D model
await this.createFaceModel();
// Analyze the reconstructed model
this.triggerEvent('reconstructionProgress', {
stage: 'analysis',
progress: 0.0,
message: 'Analyzing model for morphing...'
});
// Perform morphing analysis
const analysisResult = await this.analyzeMorphing();
// Update status
this.status.reconstructing = false;
this.status.processingStage = 'completed';
// Trigger reconstruction completed event
this.triggerEvent('reconstructionCompleted', {
scanId: this.status.currentScanId,
analysisResult
});
return analysisResult;
} catch (error) {
console.error('Error reconstructing face model:', error);
// Update status
this.status.reconstructing = false;
this.status.error = error.message;
this.status.processingStage = 'error';
// Trigger error event
this.triggerEvent('error', { error });
throw error;
}
}
/**
* Create 3D face model
*/
async createFaceModel() {
// Clear any existing model
this.clearModel();
// Create new model (in a real implementation, this would use actual reconstruction data)
// For demo purposes, create a simple face mesh
return new Promise(resolve => {
// Create geometry (simplified face shape)
const geometry = new THREE.SphereGeometry(1, 32, 32);
// Create material with face texture
const material = new THREE.MeshStandardMaterial({
color: 0xf5f5f5,
roughness: 0.7,
metalness: 0.1,
wireframe: false
});
// Create mesh
this.model = new THREE.Mesh(geometry, material);
this.scene.add(this.model);
// Move camera to front view
this.camera.position.set(0, 0, 3);
this.controls.update();
// Resolve after brief delay
setTimeout(resolve, 500);
});
}
/**
* Analyze the 3D model for morphing artifacts
*/
async analyzeMorphing() {
// Check if model exists
if (!this.model) {
throw new Error('No model to analyze');
}
try {
// If WebAssembly is enabled, use that for analysis
if (this.config.enableWasm && this.wasmModule) {
return this.analyzeWithWasm();
} else {
return this.analyzeWithJS();
}
} catch (error) {
console.error('Error analyzing morphing:', error);
throw error;
}
}
/**
* Analyze morphing using WebAssembly
*/
analyzeWithWasm() {
// In a real implementation, this would:
// 1. Extract model data
// 2. Pass to WebAssembly module
// 3. Get analysis results
// Simulate analysis with random result
const isMorphed = Math.random() > 0.5;
const confidence = isMorphed ?
0.7 + (Math.random() * 0.3) : // Higher confidence for morphed (70-100%)
0.8 + (Math.random() * 0.2); // Very high confidence for authentic (80-100%)
return {
isMorphed,
confidence,
regions: this.generateRegions(isMorphed),
metrics: {
asymmetry: isMorphed ? Math.random() * 0.5 + 0.5 : Math.random() * 0.3,
textureConsistency: isMorphed ? Math.random() * 0.5 : Math.random() * 0.5 + 0.5,
depthCoherence: isMorphed ? Math.random() * 0.4 : Math.random() * 0.3 + 0.7,
featureAlignment: isMorphed ? Math.random() * 0.6 : Math.random() * 0.2 + 0.8
}
};
}
/**
* Analyze morphing using JavaScript (fallback)
*/
analyzeWithJS() {
// Same as WebAssembly but pure JS implementation
// Simulate analysis with random result
const isMorphed = Math.random() > 0.5;
const confidence = isMorphed ?
0.7 + (Math.random() * 0.25) : // Slightly lower confidence (70-95%)
0.8 + (Math.random() * 0.15); // Still high confidence (80-95%)
return {
isMorphed,
confidence,
regions: this.generateRegions(isMorphed),
metrics: {
asymmetry: isMorphed ? Math.random() * 0.5 + 0.5 : Math.random() * 0.3,
textureConsistency: isMorphed ? Math.random() * 0.5 : Math.random() * 0.5 + 0.5,
depthCoherence: isMorphed ? Math.random() * 0.4 : Math.random() * 0.3 + 0.7,
featureAlignment: isMorphed ? Math.random() * 0.6 : Math.random() * 0.2 + 0.8
}
};
}
/**
* Generate regions data
*/
generateRegions(isMorphed) {
if (!isMorphed) {
return [{
name: 'Overall Assessment',
confidence: Math.random() * 0.1,
description: 'No suspicious regions detected. All facial features show normal 3D structure and texture consistency.'
}];
}
// Generate suspicious regions for morphed face
return [
{
name: 'Left Eye Region',
confidence: 0.8 + Math.random() * 0.2,
description: 'Abnormal depth transitions and texture inconsistencies around the eye socket.'
},
{
name: 'Mouth Area',
confidence: 0.7 + Math.random() * 0.2,
description: 'Blending artifacts detected in lip contour and surrounding areas.'
},
{
name: 'Nose Bridge',
confidence: 0.6 + Math.random() * 0.3,
description: 'Structural anomalies in the bridge and sides of the nose.'
}
];
}
/**
* Clear the current 3D model
*/
clearModel() {
if (this.model) {
this.scene.remove(this.model);
this.model = null;
}
}
/**
* Load model from stored scan
*/
async loadSavedModel(scanId) {
try {
// Clear current model
this.clearModel();
// Update status
this.status.processingStage = 'loading';
// Trigger loading event
this.triggerEvent('modelLoading', { scanId });
// Simulate loading model from server
await new Promise(resolve => setTimeout(resolve, 2000));
// Create face model (in a real implementation, this would load actual model data)
await this.createFaceModel();
// Set current scan ID
this.status.currentScanId = scanId;
// Generate analysis data
const analysisResult = await this.analyzeMorphing();
// Update status
this.status.processingStage = 'loaded';
// Trigger loaded event
this.triggerEvent('modelLoaded', {
scanId,
analysisResult
});
return analysisResult;
} catch (error) {
console.error('Error loading saved model:', error);
// Update status
this.status.error = error.message;
this.status.processingStage = 'error';
// Trigger error event
this.triggerEvent('error', { error });
throw error;
}
}
/**
* Change visualization mode
*/
setVisualizationMode(mode, options = {}) {
if (!this.model) {
throw new Error('No model loaded');
}
switch (mode) {
case 'standard':
this.model.material.wireframe = false;
this.model.material.color.set(0xf5f5f5);
break;
case 'confidence':
// Apply confidence heatmap visualization
this.createHeatmapVisualization(options.threshold || 0.5);
break;
case 'wireframe':
this.model.material.wireframe = true;
this.model.material.color.set(0x3b82f6);
break;
case 'points':
// Convert mesh to point cloud
this.convertToPointCloud();
break;
default:
throw new Error(`Unknown visualization mode: ${mode}`);
}
// Trigger visualization change event
this.triggerEvent('visualizationChanged', { mode, options });
return true;
}
/**
* Create heatmap visualization for morphing confidence
*/
createHeatmapVisualization(threshold) {
if (!this.model) return;
// Create gradient material
const material = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.7,
metalness: 0.2
});
// Get geometry
const geometry = this.model.geometry.clone();
// Create colors array
const colors = [];
const positions = geometry.attributes.position.array;
// For each vertex, assign color based on position
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const y = positions[i + 1];
const z = positions[i + 2];
// Generate a confidence value based on position
// In a real implementation, this would use actual confidence values
const distance = Math.sqrt(x * x + y * y + z * z);
const normalizedDistance = Math.min(1, distance / 1.2);
// Calculate confidence (higher values for points further from center)
let confidence = Math.abs(normalizedDistance - 0.5) * 2;
// Apply threshold
confidence = confidence < threshold ? 0 : confidence;
// Create color (green to yellow to red)
if (confidence < 0.3) {
// Green (low confidence)
colors.push(0.0, 1.0, 0.0);
} else if (confidence < 0.7) {
// Yellow (medium confidence)
colors.push(1.0, 1.0, 0.0);
} else {
// Red (high confidence)
colors.push(1.0, 0.0, 0.0);
}
}
// Add colors to geometry
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
// Create new mesh with colored material
this.scene.remove(this.model);
this.model = new THREE.Mesh(geometry, material);
this.scene.add(this.model);
}
/**
* Convert mesh to point cloud
*/
convertToPointCloud() {
if (!this.model) return;
// Get geometry
const geometry = this.model.geometry.clone();
// Create point cloud material
const material = new THREE.PointsMaterial({
color: 0x3b82f6,
size: 0.01,
sizeAttenuation: true
});
// Create point cloud
this.scene.remove(this.model);
this.model = new THREE.Points(geometry, material);
this.scene.add(this.model);
}
/**
* Reset camera view
*/
resetView() {
this.camera.position.set(0, 0, 3);
this.camera.lookAt(0, 0, 0);
this.controls.reset();
}
/**
* Rotate view to preset position
*/
rotateView(position) {
switch (position) {
case 'front':
this.camera.position.set(0, 0, 3);
break;
case 'side':
this.camera.position.set(3, 0, 0);
break;
case 'top':
this.camera.position.set(0, 3, 0);
break;
default:
return;
}
this.camera.lookAt(0, 0, 0);
this.controls.update();
}
/**
* Export 3D model as OBJ
*/
exportModel(format = 'obj') {
if (!this.model) {
throw new Error('No model to export');
}
try {
let result;
switch (format.toLowerCase()) {
case 'obj':
// In real implementation, this would use OBJExporter
result = 'Simulated OBJ export data';
break;
case 'stl':
// In real implementation, this would use STLExporter
result = 'Simulated STL export data';
break;
case 'ply':
// In real implementation, this would use PLYExporter
result = 'Simulated PLY export data';
break;
default:
throw new Error(`Unsupported export format: ${format}`);
}
// In real implementation, this would return a Blob
return new Blob([result], { type: 'application/octet-stream' });
} catch (error) {
console.error('Error exporting model:', error);
throw error;
}
}
/**
* Load external script
*/
loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
/**
* Set up event listeners
*/
setupEventListeners() {
// In a real implementation, this would set up various event listeners
}
/**
* Trigger custom event
*/
triggerEvent(eventName, data = {}) {
// Create and dispatch custom event
const event = new CustomEvent(`facemapper:${eventName}`, {
detail: { ...data, source: this }
});
document.dispatchEvent(event);
// Call callback if defined
const callbackName = `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`;
if (typeof this.config[callbackName] === 'function') {
this.config[callbackName](data);
}
}
}
// Export for use in main script
window.FaceMapper3D = FaceMapper3D;