/** * 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!'); }