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
181 lines
4.9 KiB
JavaScript
181 lines
4.9 KiB
JavaScript
/**
|
|
* 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;
|