/** * QUIXZOOM Guided Capture — AI-driven data collection * * Fyra lägen: * 1. Recording Mode — Filma gata från början till slut * 2. Guided Mode — AI ger realtidsinstruktioner * 3. Mission Mode — Specifika uppgifter (övergångsställen, lyktstolpar) * 4. Adaptive Intelligence Mode — AI planerar nästa bästa observation * * Teknik: TensorFlow.js, WebRTC, WebSocket */ const tf = require('@tensorflow/tfjs-node'); const EventEmitter = require('events'); class GuidedCapture extends EventEmitter { constructor(options = {}) { super(); this.mode = options.mode || 'recording'; // recording, guided, mission, adaptive this.confidenceThreshold = options.confidenceThreshold || 0.7; this.uncertaintyThreshold = options.uncertaintyThreshold || 0.3; this.knowledgeGapThreshold = options.knowledgeGapThreshold || 0.5; // Kunskapsbas — vad vi redan vet om området this.knowledgeBase = new Map(); // Pågående uppdrag this.currentMission = null; // Active learning state this.uncertaintyMap = new Map(); this.informationGain = new Map(); // Real-time analysis this.frameBuffer = []; this.analysisInterval = null; } /** * ============================================================ * NIVÅ 1 — RECORDING MODE * ============================================================ * Basinsamling: Zoomern filmar hela gatan. * Systemet loggar kontinuerligt GPS, IMU, kompass, höjd. */ async startRecording(sessionConfig) { this.mode = 'recording'; console.log('[GUIDED] Starting Recording Mode'); console.log('[GUIDED] Config:', sessionConfig); const session = { id: this.generateSessionId(), startTime: Date.now(), mode: 'recording', config: sessionConfig, frames: [], telemetry: [], path: [], // GPS-spår }; // Starta kontinuerlig insamling this.analysisInterval = setInterval(() => { this.processRecordingFrame(session); }, 100); // 10 Hz this.emit('sessionStarted', session); return session; } async processRecordingFrame(session) { // Samla in telemetri const telemetry = await this.captureTelemetry(); session.telemetry.push(telemetry); session.path.push({ lat: telemetry.gps.latitude, lng: telemetry.gps.longitude, timestamp: telemetry.timestamp, }); // Grundläggande analys — spara men analysera inte djupt const frame = await this.captureFrame(); // Snabb kvalitetskontroll const quality = await this.quickQualityCheck(frame); if (quality.isUsable) { session.frames.push({ timestamp: telemetry.timestamp, frameId: this.generateFrameId(), quality: quality.score, gps: telemetry.gps, }); this.emit('frameCaptured', { sessionId: session.id, frameId: frame.frameId, quality: quality.score, }); } // Uppdatera kunskapsbas med grov information this.updateKnowledgeBase(session.path[session.path.length - 1], { type: 'recording', coverage: this.calculateCoverage(session.path), }); } /** * ============================================================ * NIVÅ 2 — GUIDED MODE * ============================================================ * AI analyserar videoströmmen i realtid och ger instruktioner: * "Stanna två sekunder", "Vrid kameran åt vänster", * "Gå närmare belysningsstolpen", "Filma undersidan av skylten" */ async startGuided(sessionConfig) { this.mode = 'guided'; console.log('[GUIDED] Starting Guided Mode'); const session = { id: this.generateSessionId(), startTime: Date.now(), mode: 'guided', config: sessionConfig, instructions: [], completedInstructions: [], }; this.analysisInterval = setInterval(() => { this.processGuidedFrame(session); }, 500); // 2 Hz — mer tid för analys this.emit('sessionStarted', session); return session; } async processGuidedFrame(session) { const frame = await this.captureFrame(); const telemetry = await this.captureTelemetry(); // Djup analys av nuvarande vy const analysis = await this.analyzeFrame(frame); // Identifiera vad som saknas eller är osäkert const gaps = this.identifyKnowledgeGaps(analysis); const uncertainties = this.identifyUncertainties(analysis); // Generera instruktion const instruction = this.generateInstruction(gaps, uncertainties, telemetry); if (instruction) { session.instructions.push({ timestamp: Date.now(), ...instruction, }); this.emit('instruction', { sessionId: session.id, instruction: instruction.text, priority: instruction.priority, reason: instruction.reason, }); } // Kontrollera om tidigare instruktion är utförd this.checkCompletedInstructions(session, analysis); } generateInstruction(gaps, uncertainties, telemetry) { // Prioritera: osäkerhet > kunskapsgap > generell förbättring if (uncertainties.length > 0) { const topUncertainty = uncertainties[0]; return { text: this.instructionForUncertainty(topUncertainty), priority: 'high', reason: `Low confidence (${(topUncertainty.confidence * 100).toFixed(0)}%) on: ${topUncertainty.object}`, target: topUncertainty, }; } if (gaps.length > 0) { const topGap = gaps[0]; return { text: this.instructionForGap(topGap), priority: 'medium', reason: `Missing view: ${topGap.description}`, target: topGap, }; } // Generell förbättring if (Math.random() < 0.1) { // 10% chans för generell instruktion return { text: this.randomImprovementInstruction(), priority: 'low', reason: 'General quality improvement', }; } return null; } instructionForUncertainty(uncertainty) { const instructions = { 'blurry': 'Stanna två sekunder och håll kameran stadigare', 'occluded': 'Flytta dig åt vänster eller höger för bättre sikt', 'too_far': 'Gå närmare objektet', 'too_close': 'Backa två meter', 'poor_lighting': 'Håll kameran högre eller vänta på bättre ljus', 'reflection': 'Vinkla kameran nedåt för att undvika reflexer', }; return instructions[uncertainty.type] || 'Försök få en tydligare vy'; } instructionForGap(gap) { const instructions = { 'missing_back_view': 'Gå runt objektet och filma baksidan', 'missing_top_view': 'Håll kameran högre och filma uppifrån', 'missing_detail': 'Zooma in eller gå närmare för detalj', 'missing_context': 'Backa och filma med mer omgivning', 'occluded_text': 'Vrid kameran för att undvika skugga på texten', 'partial_sign': 'Flytta dig för att se hela skylten', }; return instructions[gap.type] || `Komplettera med: ${gap.description}`; } randomImprovementInstruction() { const instructions = [ 'Vrid kameran långsamt åt vänster', 'Filma i tre sekunder till utan att röra dig', 'Gå närmare trottoarkanten', 'Håll kameran i brösthöjd', 'Dokumentera gatans andra sida', ]; return instructions[Math.floor(Math.random() * instructions.length)]; } /** * ============================================================ * NIVÅ 3 — MISSION MODE * ============================================================ * Specifika uppgifter: * "Dokumentera samtliga övergångsställen på denna sträcka" * "Fotografera alla gatlyktor mellan punkt A och B" * "Kartlägg alla sprickor i vägen" */ async startMission(missionConfig) { this.mode = 'mission'; this.currentMission = missionConfig; console.log('[GUIDED] Starting Mission Mode'); console.log('[GUIDED] Mission:', missionConfig.type); const session = { id: this.generateSessionId(), startTime: Date.now(), mode: 'mission', mission: missionConfig, targets: [], // Objekt att dokumentera completed: [], remaining: [], }; // Generera mål baserat på uppdragstyp session.targets = await this.generateMissionTargets(missionConfig); session.remaining = [...session.targets]; this.analysisInterval = setInterval(() => { this.processMissionFrame(session); }, 500); this.emit('missionStarted', { sessionId: session.id, missionType: missionConfig.type, targetCount: session.targets.length, }); return session; } async generateMissionTargets(config) { const targetGenerators = { 'crosswalks': () => this.findCrosswalks(config.area), 'streetlights': () => this.findStreetlights(config.area), 'potholes': () => this.findPotholes(config.area), 'signs': () => this.findSigns(config.area), 'utility_poles': () => this.findUtilityPoles(config.area), 'buildings': () => this.findBuildings(config.area), 'sidewalks': () => this.findSidewalks(config.area), 'cracks': () => this.findCracks(config.area), 'custom': () => config.targets || [], }; const generator = targetGenerators[config.type]; if (!generator) { throw new Error(`Unknown mission type: ${config.type}`); } return generator(); } async processMissionFrame(session) { const frame = await this.captureFrame(); const telemetry = await this.captureTelemetry(); // Detektera objekt i nuvarande vy const detections = await this.detectObjects(frame); // Matcha mot återstående mål for (const detection of detections) { const match = this.findMatchingTarget(detection, session.remaining); if (match) { // Kontrollera kvalitet const quality = await this.assessCaptureQuality(frame, detection, match); if (quality.isAdequate) { // Markera som komplett session.completed.push({ target: match, capture: { frameId: this.generateFrameId(), timestamp: Date.now(), gps: telemetry.gps, quality: quality.score, }, }); session.remaining = session.remaining.filter(t => t.id !== match.id); this.emit('targetCompleted', { sessionId: session.id, targetId: match.id, remaining: session.remaining.length, completed: session.completed.length, }); } else { // Be om bättre bild this.emit('instruction', { sessionId: session.id, instruction: `Förbättra bild av ${match.type}: ${quality.issues.join(', ')}`, priority: 'high', target: match, }); } } } // Uppdatera progress const progress = session.completed.length / session.targets.length; if (session.remaining.length === 0) { this.emit('missionCompleted', { sessionId: session.id, completed: session.completed.length, duration: Date.now() - session.startTime, }); this.stop(); } else { this.emit('progress', { sessionId: session.id, progress, remaining: session.remaining.length, nextTarget: this.suggestNextTarget(session, telemetry), }); } } suggestNextTarget(session, currentPosition) { // Hitta närmaste återstående mål let closest = null; let closestDistance = Infinity; for (const target of session.remaining) { const distance = this.calculateDistance(currentPosition, target.location); if (distance < closestDistance) { closestDistance = distance; closest = target; } } return closest; } /** * ============================================================ * NIVÅ 4 — ADAPTIVE INTELLIGENCE MODE * ============================================================ * AI planerar själv nästa bästa observation baserat på: * - Vad den redan vet om området * - Vad som saknas * - Vilken information som ger störst nytta */ async startAdaptive(areaConfig) { this.mode = 'adaptive'; console.log('[GUIDED] Starting Adaptive Intelligence Mode'); console.log('[GUIDED] Area:', areaConfig); // Ladda existerande kunskap om området await this.loadAreaKnowledge(areaConfig); const session = { id: this.generateSessionId(), startTime: Date.now(), mode: 'adaptive', area: areaConfig, observations: [], plannedObservations: [], informationGain: new Map(), }; // Planera initiala observationer session.plannedObservations = await this.planObservations(areaConfig); this.analysisInterval = setInterval(() => { this.processAdaptiveFrame(session); }, 1000); // 1 Hz — mer tid för planering this.emit('adaptiveStarted', { sessionId: session.id, plannedObservations: session.plannedObservations.length, knowledgeCoverage: this.calculateKnowledgeCoverage(), }); return session; } async planObservations(areaConfig) { const plans = []; // 1. Identifiera kunskapsgap const gaps = this.identifyAreaKnowledgeGaps(areaConfig); // 2. Beräkna informationsvärde för varje gap for (const gap of gaps) { const informationValue = this.calculateInformationValue(gap); plans.push({ type: 'close_gap', target: gap, priority: informationValue, estimatedTime: this.estimateObservationTime(gap), expectedInformationGain: informationValue, }); } // 3. Identifiera osäkerheter som behöver reduceras const uncertainties = this.identifyModelUncertainties(areaConfig); for (const uncertainty of uncertainties) { const informationValue = this.calculateUncertaintyReductionValue(uncertainty); plans.push({ type: 'reduce_uncertainty', target: uncertainty, priority: informationValue, estimatedTime: this.estimateObservationTime(uncertainty), expectedInformationGain: informationValue, }); } // 4. Identifiera nya områden att utforska const unexplored = this.findUnexploredRegions(areaConfig); for (const region of unexplored) { plans.push({ type: 'explore', target: region, priority: region.potentialValue, estimatedTime: region.estimatedTime, expectedInformationGain: region.potentialValue, }); } // Sortera efter informationsvärde / tid (effektivitet) plans.sort((a, b) => { const efficiencyA = a.expectedInformationGain / a.estimatedTime; const efficiencyB = b.expectedInformationGain / b.estimatedTime; return efficiencyB - efficiencyA; }); return plans; } async processAdaptiveFrame(session) { const frame = await this.captureFrame(); const telemetry = await this.captureTelemetry(); // Analysera nuvarande observation const analysis = await this.analyzeFrame(frame); // Uppdatera kunskapsbas this.updateKnowledgeBase(telemetry.gps, { timestamp: Date.now(), analysis, quality: await this.assessCaptureQuality(frame, null, null), }); session.observations.push({ timestamp: Date.now(), gps: telemetry.gps, analysis: analysis.summary, }); // Omplanera baserat på ny information if (session.observations.length % 10 === 0) { // Omplanera var 10:e observation const newPlans = await this.planObservations(session.area); // Kombinera med befintliga planer session.plannedObservations = this.mergePlans( session.plannedObservations, newPlans ); this.emit('planUpdated', { sessionId: session.id, newPlans: newPlans.length, topPriority: session.plannedObservations[0], }); } // Ge nästa instruktion const nextPlan = session.plannedObservations[0]; if (nextPlan) { this.emit('adaptiveInstruction', { sessionId: session.id, instruction: this.adaptiveInstructionText(nextPlan), priority: nextPlan.priority, reason: `Information gain: ${(nextPlan.expectedInformationGain * 100).toFixed(0)}%`, estimatedTime: nextPlan.estimatedTime, }); } // Uppdatera coverage const coverage = this.calculateKnowledgeCoverage(); this.emit('adaptiveProgress', { sessionId: session.id, observations: session.observations.length, coverage, remainingPlans: session.plannedObservations.length, }); } adaptiveInstructionText(plan) { const templates = { 'close_gap': `Dokumentera ${plan.target.description} — kunskap saknas`, 'reduce_uncertainty': `Förbättra bild av ${plan.target.object} — osäkerhet: ${(plan.target.uncertainty * 100).toFixed(0)}%`, 'explore': `Utforska ${plan.target.description} — potentiellt högt värde`, }; return templates[plan.type] || `Prioriterad observation: ${plan.target.description}`; } calculateInformationValue(gap) { // Faktorer som påverkar informationsvärde: // 1. Hur kritisk är informationen? // 2. Hur sällsynt är denna typ av data? // 3. Hur mycket skulle modellen förbättras? const criticality = gap.criticality || 0.5; const rarity = gap.rarity || 0.5; const modelImpact = gap.modelImpact || 0.5; return (criticality + rarity + modelImpact) / 3; } calculateUncertaintyReductionValue(uncertainty) { // Värdet av att reducera osäkerhet beror på: // 1. Hur osäker är modellen? // 2. Hur viktigt är objektet? // 3. Hur mycket data finns redan? const uncertaintyLevel = uncertainty.confidence < 0.3 ? 1.0 : uncertainty.confidence < 0.5 ? 0.7 : 0.4; const importance = uncertainty.importance || 0.5; const dataScarcity = uncertainty.existingSamples < 10 ? 1.0 : uncertainty.existingSamples < 100 ? 0.7 : 0.4; return (uncertaintyLevel + importance + dataScarcity) / 3; } /** * ============================================================ * HJÄLPMETODER * ============================================================ */ async captureTelemetry() { // Simulerad telemetri — i praktiken från iPhone sensors return { timestamp: Date.now(), gps: { latitude: 13.7563 + (Math.random() - 0.5) * 0.01, longitude: 100.5018 + (Math.random() - 0.5) * 0.01, accuracy: 5 + Math.random() * 10, altitude: 10 + Math.random() * 5, }, imu: { accelerometer: { x: Math.random(), y: Math.random(), z: Math.random() }, gyroscope: { x: Math.random(), y: Math.random(), z: Math.random() }, }, compass: Math.random() * 360, device: { model: 'iPhone14,2', os: 'iOS 17.1', }, }; } async captureFrame() { // Simulerad frame — i praktiken från kamera return { frameId: this.generateFrameId(), timestamp: Date.now(), data: null, // Buffer med bilddata }; } async analyzeFrame(frame) { // Simulerad analys — i praktiken AI-modeller return { objects: [], scene: 'urban_street', confidence: 0.8, summary: 'Street scene with buildings and vehicles', }; } async quickQualityCheck(frame) { // Snabb kvalitetskontroll const score = 0.7 + Math.random() * 0.3; return { score, isUsable: score > 0.5, }; } async assessCaptureQuality(frame, detection, target) { // Djupare kvalitetsbedömning const issues = []; if (detection) { if (detection.confidence < 0.7) issues.push('low_confidence'); if (detection.occlusion > 0.3) issues.push('occluded'); if (detection.blur > 0.2) issues.push('blurry'); } return { score: issues.length === 0 ? 0.9 : 0.9 - (issues.length * 0.2), isAdequate: issues.length < 2, issues, }; } identifyKnowledgeGaps(analysis) { // Identifiera vad som saknas i nuvarande vy const gaps = []; // Exempel: saknas baksida? if (!analysis.backView) { gaps.push({ type: 'missing_back_view', description: 'Baksida av objektet inte synlig', criticality: 0.6, }); } // Exempel: saknas detalj? if (analysis.objects?.some(o => o.confidence < 0.5)) { gaps.push({ type: 'missing_detail', description: 'Detaljer osynliga på grund av avstånd', criticality: 0.7, }); } return gaps; } identifyUncertainties(analysis) { // Identifiera osäkra detektioner const uncertainties = []; for (const obj of analysis.objects || []) { if (obj.confidence < this.confidenceThreshold) { uncertainties.push({ type: 'low_confidence', object: obj.class, confidence: obj.confidence, }); } } return uncertainties; } updateKnowledgeBase(location, data) { const key = `${location.lat.toFixed(4)},${location.lng.toFixed(4)}`; if (!this.knowledgeBase.has(key)) { this.knowledgeBase.set(key, []); } this.knowledgeBase.get(key).push(data); } calculateKnowledgeCoverage() { // Beräkna hur mycket av området som är täckt const totalCells = 100; // Förenklad grid const coveredCells = this.knowledgeBase.size; return coveredCells / totalCells; } calculateCoverage(path) { // Beräkna spatial täckning av en väg if (path.length < 2) return 0; let totalDistance = 0; for (let i = 1; i < path.length; i++) { totalDistance += this.calculateDistance(path[i-1], path[i]); } return totalDistance; } calculateDistance(a, b) { // Haversine-formel const R = 6371e3; // Jordens radie i meter const φ1 = a.lat * Math.PI / 180; const φ2 = b.lat * Math.PI / 180; const Δφ = (b.lat - a.lat) * Math.PI / 180; const Δλ = (b.lng - a.lng) * Math.PI / 180; const x = Math.sin(Δφ/2) * Math.sin(Δφ/2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2); const c = 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1-x)); return R * c; } generateSessionId() { return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } generateFrameId() { return `frame_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } stop() { if (this.analysisInterval) { clearInterval(this.analysisInterval); this.analysisInterval = null; } this.emit('sessionEnded', { mode: this.mode, duration: Date.now() - this.startTime, }); } } // Exportera module.exports = GuidedCapture; // Demo if (require.main === module) { const capture = new GuidedCapture(); capture.on('instruction', (data) => { console.log(`[INSTRUCTION] ${data.instruction}`); }); capture.on('missionStarted', (data) => { console.log(`[MISSION] Started: ${data.missionType}, ${data.targetCount} targets`); }); capture.on('adaptiveStarted', (data) => { console.log(`[ADAPTIVE] Started: ${data.plannedObservations} planned observations`); }); // Testa olika lägen console.log('=== TESTING GUIDED CAPTURE ===\n'); // Test Recording Mode console.log('1. Recording Mode'); capture.startRecording({ area: 'sukhumvit_22' }); setTimeout(() => { capture.stop(); // Test Guided Mode console.log('\n2. Guided Mode'); capture.startGuided({ area: 'sukhumvit_22' }); setTimeout(() => { capture.stop(); // Test Mission Mode console.log('\n3. Mission Mode'); capture.startMission({ type: 'crosswalks', area: 'sukhumvit_22', }); setTimeout(() => { capture.stop(); // Test Adaptive Mode console.log('\n4. Adaptive Intelligence Mode'); capture.startAdaptive({ bounds: { lat: 13.75, lng: 100.50, radius: 500 }, }); setTimeout(() => { capture.stop(); console.log('\n=== TEST COMPLETE ==='); }, 3000); }, 3000); }, 3000); }, 3000); }