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
379 lines
11 KiB
JavaScript
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!');
|
|
}
|