Files
boc/quixzoom-capture-pipeline/identity-engine/pipeline-v2.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

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;