/** * QUIXZOOM Object Identity Engine v2 * * Justerad för riktig data: * - Högre spatial vikt (objekt nära varandra i verkligheten) * - Lägre visual vikt (AI-attribut är osäkra) * - Kontext-vikt för att skilja liknande objekt * - Temporal vikt för förändring över tid */ const crypto = require('crypto'); class ObjectIdentityPipelineV2 { constructor(config = {}) { this.config = { // Justerade vikter baserat på Bangkok-test spatialWeight: config.spatialWeight || 0.50, // Högre — GPS är pålitlig visualWeight: config.visualWeight || 0.20, // Lägre — AI-attribut är osäkra contextWeight: config.contextWeight || 0.20, // Ny — vägtyp, närliggande objekt temporalWeight: config.temporalWeight || 0.10, // Samma — tid är sekundär // Justerade trösklar autoMergeThreshold: config.autoMergeThreshold || 0.75, // Högre — färre felaktiga merges waitThreshold: config.waitThreshold || 0.50, // Lägre — mer konservativ // Spatiala parametrar gridSize: config.gridSize || 50, // meter maxSearchRadius: config.maxSearchRadius || 100, // meter ...config, }; this.objects = new Map(); this.spatialIndex = new Map(); this.stats = { processed: 0, merged: 0, created: 0, uncertain: 0, }; } /** * Processa observation */ async process(observation) { this.stats.processed++; // 1. Hitta kandidater const candidates = this.findCandidates(observation); if (candidates.length === 0) { // Ingen kandidat — skapa nytt objekt return this.createObject(observation); } // 2. Beräkna matchnings-confidence för varje kandidat const matches = candidates.map(candidate => ({ object: candidate, confidence: this.calculateConfidence(observation, candidate), })); // 3. Välj bästa match const bestMatch = matches.reduce((best, current) => current.confidence > best.confidence ? current : best ); // 4. Bestäm åtgärd baserat på confidence if (bestMatch.confidence >= this.config.autoMergeThreshold) { // Auto-merge return this.mergeObservation(bestMatch.object, observation, bestMatch.confidence); } else if (bestMatch.confidence >= this.config.waitThreshold) { // Osäker — vänta på mer data this.stats.uncertain++; return { action: 'wait', objectId: bestMatch.object.id, confidence: bestMatch.confidence, reason: 'confidence_below_auto_merge', }; } else { // För låg confidence — nytt objekt return this.createObject(observation); } } /** * Hitta kandidater i spatialt index */ findCandidates(observation) { const radius = this.calculateSearchRadius(observation); const gridKeys = this.getGridKeysInRadius(observation.location, radius); const candidates = []; const seen = new Set(); for (const key of gridKeys) { const objectsInGrid = this.spatialIndex.get(key); if (!objectsInGrid) continue; for (const objectId of objectsInGrid) { if (seen.has(objectId)) continue; seen.add(objectId); const object = this.objects.get(objectId); if (!object) continue; // Snabb spatial check const distance = this.calculateDistance(observation.location, object.location); if (distance <= radius) { candidates.push(object); } } } return candidates; } /** * Beräkna matchnings-confidence */ calculateConfidence(observation, object) { const spatial = this.calculateSpatialConfidence(observation, object); const visual = this.calculateVisualConfidence(observation, object); const context = this.calculateContextConfidence(observation, object); const temporal = this.calculateTemporalConfidence(observation, object); // Viktat medelvärde const confidence = spatial * this.config.spatialWeight + visual * this.config.visualWeight + context * this.config.contextWeight + temporal * this.config.temporalWeight; return confidence; } /** * Spatial confidence — avstånd mellan observationer */ calculateSpatialConfidence(observation, object) { const distance = this.calculateDistance(observation.location, object.location); const accuracy = Math.max(observation.gpsAccuracy || 3, object.gpsAccuracy || 3); // Confidence minskar med avståndet // Max confidence vid 0m, 0 vid 3*accuracy const maxDistance = accuracy * 3; const confidence = Math.max(0, 1 - (distance / maxDistance)); return confidence; } /** * Visual confidence — attribut-matchning */ calculateVisualConfidence(observation, object) { const obsAttrs = observation.attributes || {}; const objAttrs = object.attributes || {}; const keys = Object.keys(obsAttrs); if (keys.length === 0) return 0.5; let matches = 0; let totalWeight = 0; for (const key of keys) { const weight = this.getAttributeWeight(key); totalWeight += weight; if (obsAttrs[key] === objAttrs[key]) { matches += weight; } else if (this.areAttributesSimilar(key, obsAttrs[key], objAttrs[key])) { matches += weight * 0.5; } } return totalWeight > 0 ? matches / totalWeight : 0.5; } /** * Context confidence — vägtyp, närliggande objekt */ calculateContextConfidence(observation, object) { let confidence = 0.5; // Samma objekttyp if (observation.objectType === object.type) { confidence += 0.3; } // Närliggande objekt (om tillgängligt) if (observation.nearbyObjects && object.nearbyObjects) { const common = observation.nearbyObjects.filter(o => object.nearbyObjects.includes(o) ); confidence += Math.min(0.2, common.length * 0.05); } return Math.min(1, confidence); } /** * Temporal confidence — rimlighet över tid */ calculateTemporalConfidence(observation, object) { if (!observation.timestamp || !object.lastSeen) return 0.5; const timeDiff = Math.abs(new Date(observation.timestamp) - new Date(object.lastSeen)); const daysDiff = timeDiff / (1000 * 60 * 60 * 24); // Mindre confidence ju längre tid som gått if (daysDiff < 1) return 1.0; if (daysDiff < 7) return 0.9; if (daysDiff < 30) return 0.8; if (daysDiff < 90) return 0.7; return 0.6; } /** * Hjälpmetoder */ calculateDistance(loc1, loc2) { const R = 6371000; // Jordens radie i meter const dLat = (loc2.lat - loc1.lat) * Math.PI / 180; const dLon = (loc2.lng - loc1.lng) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(loc1.lat * Math.PI / 180) * Math.cos(loc2.lat * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } calculateSearchRadius(observation) { return Math.min( this.config.maxSearchRadius, (observation.gpsAccuracy || 3) * 5 ); } getGridKey(location) { const lat = Math.floor(location.lat * 1000 / this.config.gridSize); const lng = Math.floor(location.lng * 1000 / this.config.gridSize); return `${lat},${lng}`; } getGridKeysInRadius(location, radius) { const keys = []; const latDelta = radius / 111000; // grader const lngDelta = radius / (111000 * Math.cos(location.lat * Math.PI / 180)); const latSteps = Math.ceil(latDelta * 1000 / this.config.gridSize); const lngSteps = Math.ceil(lngDelta * 1000 / this.config.gridSize); const centerLat = Math.floor(location.lat * 1000 / this.config.gridSize); const centerLng = Math.floor(location.lng * 1000 / this.config.gridSize); for (let lat = centerLat - latSteps; lat <= centerLat + latSteps; lat++) { for (let lng = centerLng - lngSteps; lng <= centerLng + lngSteps; lng++) { keys.push(`${lat},${lng}`); } } return keys; } getAttributeWeight(key) { const weights = { height: 0.8, material: 0.7, paint: 0.5, light: 0.9, rust: 0.6, lean: 0.7, signType: 0.9, reflective: 0.6, damaged: 0.8, species: 0.7, health: 0.6, diameter: 0.5, type: 0.8, condition: 0.7, }; return weights[key] || 0.5; } areAttributesSimilar(key, val1, val2) { if (typeof val1 === 'number' && typeof val2 === 'number') { return Math.abs(val1 - val2) / Math.max(val1, val2) < 0.2; } return false; } createObject(observation) { const id = `obj_${crypto.randomBytes(6).toString('hex')}`; const object = { id, type: observation.objectType, location: { ...observation.location }, gpsAccuracy: observation.gpsAccuracy || 3, attributes: { ...observation.attributes }, firstSeen: observation.timestamp || new Date(), lastSeen: observation.timestamp || new Date(), evidence: [observation.id], confidence: 0.5, }; this.objects.set(id, object); this.addToSpatialIndex(object); this.stats.created++; return { action: 'new_object', objectId: id, confidence: 0.5, }; } mergeObservation(object, observation, confidence) { object.evidence.push(observation.id); object.lastSeen = observation.timestamp || new Date(); object.confidence = Math.max(object.confidence, confidence); // Uppdatera attribut med viktat medelvärde if (observation.attributes) { for (const [key, value] of Object.entries(observation.attributes)) { if (typeof value === 'number' && typeof object.attributes[key] === 'number') { object.attributes[key] = (object.attributes[key] + value) / 2; } else if (!object.attributes[key]) { object.attributes[key] = value; } } } this.stats.merged++; return { action: 'merge', objectId: object.id, confidence, }; } addToSpatialIndex(object) { const key = this.getGridKey(object.location); if (!this.spatialIndex.has(key)) { this.spatialIndex.set(key, new Set()); } this.spatialIndex.get(key).add(object.id); } getStats() { return { ...this.stats, objectCount: this.objects.size, }; } } module.exports = ObjectIdentityPipelineV2;