/** * QUIXZOOM Cloud Vision Integration * * Använder Google Cloud Vision API för objektdetektering. * Fallback till mock om API-nyckel saknas. */ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); class CloudVisionDetector { constructor(config = {}) { this.config = { apiKey: config.apiKey || process.env.GOOGLE_VISION_API_KEY, maxResults: config.maxResults || 50, ...config, }; this.available = !!this.config.apiKey; this.classMap = this.buildClassMap(); } buildClassMap() { return { 'Street light': 'street_lamp', 'Traffic sign': 'traffic_sign', 'Tree': 'tree', 'Manhole cover': 'manhole', 'Utility pole': 'utility_box', 'Bench': 'bench', 'Trash can': 'trash_can', 'Bicycle rack': 'bicycle_rack', 'Parking meter': 'utility_box', 'Fire hydrant': 'utility_box', 'Light': 'street_lamp', 'Pole': 'street_lamp', 'Sign': 'traffic_sign', 'Plant': 'tree', }; } async detect(imagePath) { if (!this.available) { console.log('[Cloud Vision] No API key, using mock'); return this.mockDetect(imagePath); } try { const imageData = fs.readFileSync(imagePath); const base64Image = imageData.toString('base64'); const requestBody = { requests: [{ image: { content: base64Image }, features: [ { type: 'OBJECT_LOCALIZATION', maxResults: this.config.maxResults }, { type: 'LABEL_DETECTION', maxResults: 20 }, ], }], }; const response = execSync('curl -s -X POST "https://vision.googleapis.com/v1/images:annotate?key=' + this.config.apiKey + '" -H "Content-Type: application/json" -d \'' + JSON.stringify(requestBody) + '\'', { encoding: 'utf-8', timeout: 30000, }); const result = JSON.parse(response); return this.parseResult(result, imagePath); } catch (error) { console.warn(`[Cloud Vision] Error: ${error.message}`); return this.mockDetect(imagePath); } } parseResult(result, imagePath) { const objects = []; if (!result.responses || !result.responses[0]) { return objects; } const response = result.responses[0]; // Object localization if (response.localizedObjectAnnotations) { for (const obj of response.localizedObjectAnnotations) { const quixType = this.classMap[obj.name]; if (!quixType) continue; objects.push({ type: quixType, confidence: obj.score, bbox: this.normalizeBoundingBox(obj.boundingPoly), name: obj.name, }); } } // Label detection (fallback) if (objects.length === 0 && response.labelAnnotations) { for (const label of response.labelAnnotations) { const quixType = this.classMap[label.description]; if (!quixType) continue; objects.push({ type: quixType, confidence: label.score, name: label.description, }); } } return objects; } normalizeBoundingBox(poly) { if (!poly || !poly.normalizedVertices) return null; const vertices = poly.normalizedVertices; 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), }; } mockDetect(imagePath) { // Samma mock som YOLO men med mer varierande confidence const frameNum = parseInt(path.basename(imagePath).match(/\d+/)?.[0] || '0'); const objects = []; const templates = [ { type: 'street_lamp', prob: 0.6, confRange: [0.65, 0.95] }, { type: 'traffic_sign', prob: 0.3, confRange: [0.55, 0.85] }, { type: 'tree', prob: 0.5, confRange: [0.60, 0.90] }, { type: 'utility_box', prob: 0.2, confRange: [0.50, 0.80] }, { type: 'manhole', prob: 0.25, confRange: [0.45, 0.75] }, ]; const numObjects = 1 + Math.floor(Math.random() * 3); for (let i = 0; i < numObjects; i++) { const template = templates[Math.floor(Math.random() * templates.length)]; if (Math.random() < template.prob) { const confidence = template.confRange[0] + Math.random() * (template.confRange[1] - template.confRange[0]); objects.push({ type: template.type, confidence: confidence, source: 'mock', }); } } return objects; } async detectBatch(imagePaths) { const results = []; for (const path of imagePaths) { const detections = await this.detect(path); results.push({ image: path, objects: detections, }); } return results; } } module.exports = CloudVisionDetector;