Files
boc/quixzoom-capture-pipeline/app/guided-capture.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

842 lines
24 KiB
JavaScript

/**
* 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);
}