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

290 lines
9.0 KiB
JavaScript

/**
* QUIXZOOM Object Identity Engine v3
*
* Anpassad för video-data:
* - Video-ID som kontext (observationer från samma video är nära)
* - Spatial search med större radie för video-walks
* - Tidsbaserad gruppering för samma session
*/
const crypto = require('crypto');
class ObjectIdentityPipelineV3 {
constructor(config = {}) {
this.config = {
spatialWeight: config.spatialWeight || 0.45,
visualWeight: config.visualWeight || 0.20,
contextWeight: config.contextWeight || 0.25, // Högre för video-kontext
temporalWeight: config.temporalWeight || 0.10,
autoMergeThreshold: config.autoMergeThreshold || 0.60,
waitThreshold: config.waitThreshold || 0.40,
gridSize: config.gridSize || 100, // Större för video-walks
maxSearchRadius: config.maxSearchRadius || 200, // Större radie
...config,
};
this.objects = new Map();
this.spatialIndex = new Map();
this.stats = { processed: 0, merged: 0, created: 0, uncertain: 0 };
}
async process(observation) {
this.stats.processed++;
const candidates = this.findCandidates(observation);
if (candidates.length === 0) {
return this.createObject(observation);
}
const matches = candidates.map(candidate => ({
object: candidate,
confidence: this.calculateConfidence(observation, candidate),
}));
const bestMatch = matches.reduce((best, current) =>
current.confidence > best.confidence ? current : best
);
if (bestMatch.confidence >= this.config.autoMergeThreshold) {
return this.mergeObservation(bestMatch.object, observation, bestMatch.confidence);
} else if (bestMatch.confidence >= this.config.waitThreshold) {
this.stats.uncertain++;
return {
action: 'wait',
objectId: bestMatch.object.id,
confidence: bestMatch.confidence,
};
} else {
return this.createObject(observation);
}
}
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;
const distance = this.calculateDistance(observation.location, object.location);
if (distance <= radius) {
candidates.push(object);
}
}
}
return candidates;
}
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);
return spatial * this.config.spatialWeight +
visual * this.config.visualWeight +
context * this.config.contextWeight +
temporal * this.config.temporalWeight;
}
calculateSpatialConfidence(observation, object) {
const distance = this.calculateDistance(observation.location, object.location);
const accuracy = Math.max(observation.gpsAccuracy || 5, object.gpsAccuracy || 5);
// Större max-avstånd för video-walks
const maxDistance = Math.max(accuracy * 5, 50); // Minst 50m
const confidence = Math.max(0, 1 - (distance / maxDistance));
return confidence;
}
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;
}
calculateContextConfidence(observation, object) {
let confidence = 0.5;
// Samma objekttyp
if (observation.objectType === object.type) {
confidence += 0.3;
}
// Samma video-session (närliggande i tid)
if (observation.source && object.lastSource) {
const sameVideo = observation.source.videoId === object.lastSource.videoId;
if (sameVideo) {
confidence += 0.2; // Samma video = sannolikt samma område
}
}
return Math.min(1, confidence);
}
calculateTemporalConfidence(observation, object) {
if (!observation.timestamp || !object.lastSeen) return 0.5;
const timeDiff = Math.abs(new Date(observation.timestamp) - new Date(object.lastSeen));
const minutesDiff = timeDiff / (1000 * 60);
// Inom samma video-session (< 30 min)
if (minutesDiff < 30) return 1.0;
if (minutesDiff < 60) return 0.9;
if (minutesDiff < 120) return 0.8;
return 0.7;
}
calculateDistance(loc1, loc2) {
const R = 6371000;
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);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
calculateSearchRadius(observation) {
return Math.min(
this.config.maxSearchRadius,
Math.max((observation.gpsAccuracy || 5) * 10, 100) // Minst 100m för video
);
}
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;
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 || 5,
attributes: { ...observation.attributes },
firstSeen: observation.timestamp || new Date(),
lastSeen: observation.timestamp || new Date(),
lastSource: observation.source,
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.lastSource = observation.source;
object.confidence = Math.max(object.confidence, confidence);
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 = ObjectIdentityPipelineV3;