Files
boc/quixzoom-capture-pipeline/ai-pipeline/cloud-vision-bootstrap.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

379 lines
11 KiB
JavaScript

/**
* QUIXZOOM Cloud Vision Bootstrap
*
* Fas 1: Använd Cloud Vision för att:
* - Automatiskt föreslå annotationer
* - Bygga första gold dataset
* - Validera UIOS-pipelinen
* - Testa Mission Planner och UKG med riktiga detektioner
*/
const vision = require('@google-cloud/vision');
class CloudVisionBootstrap {
constructor(apiKey) {
this.client = new vision.ImageAnnotatorClient({
keyFilename: apiKey || process.env.GOOGLE_APPLICATION_CREDENTIALS
});
// Infrastructure Vocabulary (300-500 objekt)
this.vocabulary = {
lighting: [
'street lamp', 'decorative lamp', 'flood light', 'bollard light',
'traffic light', 'pedestrian signal'
],
road: [
'asphalt', 'pothole', 'crack', 'speed bump', 'crosswalk',
'sidewalk', 'curb', 'manhole cover', 'drain', 'grate'
],
utility: [
'electrical cabinet', 'telecom cabinet', 'water valve',
'fire hydrant', 'utility pole', 'transformer box'
],
traffic: [
'stop sign', 'yield sign', 'parking sign', 'speed sign',
'direction sign', 'warning sign', 'traffic cone', 'barrier'
],
vegetation: [
'tree', 'bush', 'hedge', 'grass', 'flower bed', 'planter'
],
furniture: [
'bench', 'trash can', 'bike rack', 'bollard', 'fence',
'guard rail', 'handrail'
],
buildings: [
'building facade', 'window', 'door', 'roof', 'chimney',
'awning', 'shutter'
]
};
// Mappning från Cloud Vision etiketter till vårt vokabulär
this.labelMapping = {
'street light': 'street lamp',
'traffic signal': 'traffic light',
'manhole cover': 'manhole cover',
'fire hydrant': 'fire hydrant',
'bench': 'bench',
'tree': 'tree',
'sign': 'direction sign',
'stop sign': 'stop sign',
'speed limit sign': 'speed sign'
};
}
/**
* ============================================================
* HUVUDMETOD: Detektera infrastruktur i bild
* ============================================================
*/
async detectInfrastructure(imagePath) {
console.log(`[Cloud Vision] Analyserar: ${imagePath}`);
try {
// Kör flera detektioner parallellt
const [labelResult, objectResult, textResult] = await Promise.all([
this.client.labelDetection(imagePath),
this.client.objectLocalization(imagePath),
this.client.textDetection(imagePath)
]);
const labels = labelResult.labelAnnotations || [];
const objects = objectResult.localizedObjectAnnotations || [];
const texts = textResult.textAnnotations || [];
// Kombinera och filtrera
const detections = this.combineDetections(labels, objects, texts);
// Mappa till vårt vokabulär
const mapped = this.mapToVocabulary(detections);
return {
success: true,
image: imagePath,
detections: mapped,
raw: {
labels: labels.slice(0, 10),
objects: objects.slice(0, 10),
texts: texts.slice(0, 5)
},
stats: {
total: mapped.length,
byCategory: this.categorize(mapped)
}
};
} catch (error) {
console.error(`[Cloud Vision] Fel: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
combineDetections(labels, objects, texts) {
const detections = [];
// Från object localization (bounding boxes)
for (const obj of objects) {
detections.push({
type: 'object',
label: obj.name,
confidence: obj.score,
bbox: obj.boundingPoly ? this.normalizeBbox(obj.boundingPoly.normalizedVertices) : null
});
}
// Från labels (helbild)
for (const label of labels) {
if (label.score > 0.7) { // Bara höga confidence
detections.push({
type: 'label',
label: label.description,
confidence: label.score,
bbox: null // Labels har ingen bbox
});
}
}
// Från text (OCR)
for (const text of texts.slice(1)) { // Skippar första (hela texten)
detections.push({
type: 'text',
label: text.description,
confidence: 0.9, // OCR är vanligtvis säker
bbox: text.boundingPoly ? this.normalizeBbox(text.boundingPoly.vertices) : null
});
}
return detections;
}
normalizeBbox(vertices) {
if (!vertices || vertices.length < 4) return null;
const xs = vertices.map(v => v.x || 0);
const ys = vertices.map(v => v.y || 0);
return {
x: Math.min(...xs),
y: Math.min(...ys),
width: Math.max(...xs) - Math.min(...xs),
height: Math.max(...ys) - Math.min(...ys)
};
}
mapToVocabulary(detections) {
const mapped = [];
for (const det of detections) {
const normalizedLabel = det.label.toLowerCase().trim();
// Kolla direkt mappning
if (this.labelMapping[normalizedLabel]) {
mapped.push({
...det,
quixzoomLabel: this.labelMapping[normalizedLabel],
category: this.findCategory(this.labelMapping[normalizedLabel])
});
continue;
}
// Kolla om label innehåller något från vokabuläret
for (const [category, items] of Object.entries(this.vocabulary)) {
for (const item of items) {
if (normalizedLabel.includes(item.toLowerCase()) ||
item.toLowerCase().includes(normalizedLabel)) {
mapped.push({
...det,
quixzoomLabel: item,
category
});
break;
}
}
}
}
return mapped;
}
findCategory(label) {
for (const [category, items] of Object.entries(this.vocabulary)) {
if (items.includes(label)) return category;
}
return 'other';
}
categorize(detections) {
const counts = {};
for (const det of detections) {
const cat = det.category || 'other';
counts[cat] = (counts[cat] || 0) + 1;
}
return counts;
}
/**
* ============================================================
* GOLD DATASET BUILDER
* ============================================================
*/
async buildGoldDataset(imagePaths, options = {}) {
console.log(`[Gold Dataset] Bygger dataset från ${imagePaths.length} bilder`);
const dataset = {
images: [],
annotations: [],
categories: Object.keys(this.vocabulary),
stats: {
totalImages: 0,
totalAnnotations: 0,
byCategory: {}
}
};
for (let i = 0; i < imagePaths.length; i++) {
const path = imagePaths[i];
console.log(`[Gold Dataset] ${i + 1}/${imagePaths.length}: ${path}`);
const result = await this.detectInfrastructure(path);
if (result.success) {
dataset.images.push({
id: i,
file_name: path,
width: 1280, // Antaget
height: 720
});
for (const det of result.detections) {
if (det.bbox) { // Bara detektioner med bbox
dataset.annotations.push({
id: dataset.annotations.length,
image_id: i,
category: det.category,
label: det.quixzoomLabel,
bbox: [det.bbox.x, det.bbox.y, det.bbox.width, det.bbox.height],
confidence: det.confidence,
source: 'cloud_vision_bootstrap'
});
}
}
}
}
// Uppdatera stats
dataset.stats.totalImages = dataset.images.length;
dataset.stats.totalAnnotations = dataset.annotations.length;
dataset.stats.byCategory = this.categorize(dataset.annotations);
return dataset;
}
/**
* ============================================================
* EXPORT FÖR TRÄNING
* ============================================================
*/
exportForYOLO(dataset, outputDir) {
const fs = require('fs');
const path = require('path');
// Skapa katalogstruktur
const dirs = ['images/train', 'images/val', 'labels/train', 'labels/val'];
for (const dir of dirs) {
fs.mkdirSync(path.join(outputDir, dir), { recursive: true });
}
// Skapa data.yaml
const yaml = `
path: ${outputDir}
train: images/train
val: images/val
nc: ${Object.keys(this.vocabulary).length}
names: ${JSON.stringify(Object.keys(this.vocabulary))}
`;
fs.writeFileSync(path.join(outputDir, 'data.yaml'), yaml);
// Exportera annotationer
for (const ann of dataset.annotations) {
const labelFile = path.join(
outputDir,
'labels/train',
`${ann.image_id}.txt`
);
// YOLO format: class x_center y_center width height (normalized)
const line = `${Object.keys(this.vocabulary).indexOf(ann.category)} ${
ann.bbox[0] + ann.bbox[2] / 2} ${
ann.bbox[1] + ann.bbox[3] / 2} ${
ann.bbox[2]} ${
ann.bbox[3]}\n`;
fs.appendFileSync(labelFile, line);
}
console.log(`[Export] YOLO dataset sparat till: ${outputDir}`);
}
/**
* ============================================================
* STATS
* ============================================================
*/
getVocabularyStats() {
const stats = {};
let total = 0;
for (const [category, items] of Object.entries(this.vocabulary)) {
stats[category] = items.length;
total += items.length;
}
return {
total,
categories: Object.keys(this.vocabulary).length,
byCategory: stats
};
}
}
module.exports = CloudVisionBootstrap;
// Demo
if (require.main === module) {
const bootstrap = new CloudVisionBootstrap();
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ CLOUD VISION BOOTSTRAP ║');
console.log('╚════════════════════════════════════════════════════════════╝\n');
console.log('=== INFRASTRUCTURE VOCABULARY ===');
const vocabStats = bootstrap.getVocabularyStats();
console.log(`Totalt: ${vocabStats.total} objekt`);
console.log(`Kategorier: ${vocabStats.categories}`);
for (const [cat, count] of Object.entries(vocabStats.byCategory)) {
console.log(` ${cat}: ${count}`);
}
console.log('\n=== EXEMPEL PÅ MAPPNING ===');
console.log('Cloud Vision "street light" → QUIXZOOM "street lamp"');
console.log('Cloud Vision "traffic signal" → QUIXZOOM "traffic light"');
console.log('Cloud Vision "manhole cover" → QUIXZOOM "manhole cover"');
console.log('\n=== ANVÄNDNING ===');
console.log('1. Sätt GOOGLE_APPLICATION_CREDENTIALS');
console.log('2. Kör detectInfrastructure(imagePath)');
console.log('3. Granska och korrigera annotationer');
console.log('4. Bygg gold dataset');
console.log('5. Träna custom modell');
console.log('\n✅ Cloud Vision Bootstrap redo!');
}