bae705aa97
- 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
361 lines
10 KiB
JavaScript
361 lines
10 KiB
JavaScript
/**
|
|
* 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;
|