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
595 lines
15 KiB
JavaScript
595 lines
15 KiB
JavaScript
/**
|
|
* QUIXZOOM Video Pipeline — Dataset Builder
|
|
*
|
|
* Bygger träningsdataset från AI-annoterade frames:
|
|
* - Roads
|
|
* - Sidewalks
|
|
* - Crosswalks
|
|
* - Buildings
|
|
* - Trees
|
|
* - Utility Infrastructure
|
|
* - Traffic Infrastructure
|
|
* - Signage
|
|
* - Lighting
|
|
* - Accessibility
|
|
* - Commercial Areas
|
|
* - Urban Density
|
|
*
|
|
* Output: JSONL-format för ML-träning
|
|
*/
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Dataset-konfiguration
|
|
*/
|
|
const DATASET_CONFIG = {
|
|
version: '1.0.0',
|
|
format: 'jsonl',
|
|
splits: {
|
|
train: 0.7,
|
|
validation: 0.15,
|
|
test: 0.15,
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Bygg dataset från annoterade frames
|
|
*/
|
|
async function buildDataset(inputDir, outputDir, options = {}) {
|
|
console.log(`[DATASET] Building dataset from: ${inputDir}`);
|
|
|
|
// Läs alla annotations
|
|
const annotations = await loadAnnotations(inputDir);
|
|
console.log(`[DATASET] Loaded ${annotations.length} annotations`);
|
|
|
|
// Filtrera efter kvalitet
|
|
const qualityFilter = options.minQuality || 60;
|
|
const filtered = annotations.filter(a =>
|
|
(a.aiAnalysis?.qualityScore?.overall || 0) >= qualityFilter
|
|
);
|
|
console.log(`[DATASET] ${filtered.length} annotations passed quality filter (${qualityFilter}+)`);
|
|
|
|
// Bygg dataset för varje kategori
|
|
const datasets = {
|
|
roads: buildRoadDataset(filtered),
|
|
sidewalks: buildSidewalkDataset(filtered),
|
|
crosswalks: buildCrosswalkDataset(filtered),
|
|
buildings: buildBuildingDataset(filtered),
|
|
trees: buildTreeDataset(filtered),
|
|
utilityInfrastructure: buildUtilityDataset(filtered),
|
|
trafficInfrastructure: buildTrafficDataset(filtered),
|
|
signage: buildSignageDataset(filtered),
|
|
lighting: buildLightingDataset(filtered),
|
|
accessibility: buildAccessibilityDataset(filtered),
|
|
commercialAreas: buildCommercialDataset(filtered),
|
|
urbanDensity: buildDensityDataset(filtered),
|
|
};
|
|
|
|
// Spara dataset
|
|
await saveDatasets(datasets, outputDir);
|
|
|
|
// Generera statistik
|
|
const stats = generateStats(datasets);
|
|
await fs.writeFile(
|
|
path.join(outputDir, 'stats.json'),
|
|
JSON.stringify(stats, null, 2)
|
|
);
|
|
|
|
console.log(`[DATASET] Complete. Saved to: ${outputDir}`);
|
|
|
|
return { datasets, stats };
|
|
}
|
|
|
|
/**
|
|
* Läs annotations från katalog
|
|
*/
|
|
async function loadAnnotations(inputDir) {
|
|
const files = await fs.readdir(inputDir);
|
|
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
|
|
const annotations = [];
|
|
for (const file of jsonFiles) {
|
|
const content = await fs.readFile(path.join(inputDir, file), 'utf8');
|
|
annotations.push(JSON.parse(content));
|
|
}
|
|
|
|
return annotations;
|
|
}
|
|
|
|
/**
|
|
* Bygg väg-dataset
|
|
*/
|
|
function buildRoadDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const roadSurface = annotation.roadSurface;
|
|
const segmentation = annotation.segmentation;
|
|
|
|
if (!roadSurface && !segmentation) continue;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
surface_type: roadSurface?.topSurface?.type || 'unknown',
|
|
surface_confidence: roadSurface?.topSurface?.confidence || 0,
|
|
has_cracks: roadSurface?.hasCracks || false,
|
|
crack_severity: annotation.cracks?.severity || 'none',
|
|
road_pixels: segmentation?.distribution?.road || 0,
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg trottoar-dataset
|
|
*/
|
|
function buildSidewalkDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const sidewalk = annotation.sidewalk;
|
|
const segmentation = annotation.segmentation;
|
|
|
|
if (!sidewalk && !segmentation) continue;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
present: sidewalk?.present || false,
|
|
width: sidewalk?.width || 0,
|
|
condition: sidewalk?.condition || 'unknown',
|
|
wheelchair_accessible: sidewalk?.accessibility?.wheelchair || false,
|
|
tactile_paving: sidewalk?.accessibility?.tactile_paving || false,
|
|
sidewalk_pixels: segmentation?.distribution?.sidewalk || 0,
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg övergångsställe-dataset
|
|
*/
|
|
function buildCrosswalkDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const objects = annotation.objects?.objects || [];
|
|
const trafficSigns = annotation.trafficSigns || [];
|
|
|
|
const hasCrosswalk = objects.some(o => o.class === 'pedestrian crossing') ||
|
|
trafficSigns.some(s => s.type === 'pedestrian_crossing');
|
|
|
|
if (!hasCrosswalk) continue;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
present: true,
|
|
type: 'zebra', // Kan detekteras mer specifikt
|
|
traffic_light_nearby: objects.some(o => o.class === 'traffic light'),
|
|
sign_present: trafficSigns.some(s => s.type === 'pedestrian_crossing'),
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg byggnads-dataset
|
|
*/
|
|
function buildBuildingDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const buildings = annotation.buildings;
|
|
const segmentation = annotation.segmentation;
|
|
const ocr = annotation.ocr;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
count: buildings?.count || 0,
|
|
types: buildings?.buildings?.map(b => b.type) || [],
|
|
heights: buildings?.buildings?.map(b => b.height) || [],
|
|
building_pixels: segmentation?.distribution?.building || 0,
|
|
has_hotel: ocr?.storefronts?.some(s => /HOTEL/i.test(s)) || false,
|
|
has_restaurant: ocr?.storefronts?.some(s => /RESTAURANT/i.test(s)) || false,
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg träd-dataset
|
|
*/
|
|
function buildTreeDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const vegetation = annotation.vegetation;
|
|
const segmentation = annotation.segmentation;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
tree_count: vegetation?.treeCount || 0,
|
|
coverage: vegetation?.coverage || 0,
|
|
health: vegetation?.health || 'unknown',
|
|
vegetation_pixels: segmentation?.distribution?.vegetation || 0,
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg utility-infrastruktur-dataset
|
|
*/
|
|
function buildUtilityDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const poles = annotation.poles || [];
|
|
const utilityBoxes = annotation.utilityBoxes || [];
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
pole_count: poles.length,
|
|
pole_types: poles.map(p => p.type),
|
|
utility_box_count: utilityBoxes.length,
|
|
utility_box_types: utilityBoxes.map(b => b.type),
|
|
has_overhead_wires: poles.some(p => p.type === 'utility_pole'),
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg trafik-infrastruktur-dataset
|
|
*/
|
|
function buildTrafficDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const objects = annotation.objects?.objects || [];
|
|
const trafficSigns = annotation.trafficSigns || [];
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
traffic_light_count: objects.filter(o => o.class === 'traffic light').length,
|
|
traffic_sign_count: trafficSigns.length,
|
|
sign_types: trafficSigns.map(s => s.type),
|
|
has_stop_sign: trafficSigns.some(s => s.type === 'stop'),
|
|
has_speed_limit: trafficSigns.some(s => s.type === 'speed_limit'),
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg skylt-dataset
|
|
*/
|
|
function buildSignageDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const ocr = annotation.ocr;
|
|
const trafficSigns = annotation.trafficSigns;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
text_detected: ocr?.text || '',
|
|
text_confidence: ocr?.confidence || 0,
|
|
signs: ocr?.signs || [],
|
|
storefronts: ocr?.storefronts || [],
|
|
traffic_signs: trafficSigns?.map(s => s.type) || [],
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg belysnings-dataset
|
|
*/
|
|
function buildLightingDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const lighting = annotation.lighting;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
street_light_count: lighting?.streetLights?.length || 0,
|
|
building_light_count: lighting?.buildingLights?.length || 0,
|
|
time_of_day: lighting?.timeOfDay || 'unknown',
|
|
natural_light_level: lighting?.naturalLight || 0,
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg tillgänglighets-dataset
|
|
*/
|
|
function buildAccessibilityDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const accessibility = annotation.accessibility;
|
|
const sidewalk = annotation.sidewalk;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
wheelchair_ramp: accessibility?.wheelchairRamp || false,
|
|
tactile_paving: accessibility?.tactilePaving || false,
|
|
audible_signals: accessibility?.audibleSignals || false,
|
|
braille_signage: accessibility?.brailleSignage || false,
|
|
accessible_parking: accessibility?.accessibleParking || false,
|
|
overall_score: accessibility?.overallScore || 0,
|
|
sidewalk_width: sidewalk?.width || 0,
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg kommersiellt område-dataset
|
|
*/
|
|
function buildCommercialDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const storefronts = annotation.storefronts || [];
|
|
const ocr = annotation.ocr;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
business_count: storefronts.length,
|
|
business_types: storefronts.map(s => s.type),
|
|
business_names: storefronts.map(s => s.name),
|
|
open_businesses: storefronts.filter(s => s.open).length,
|
|
has_hotel: ocr?.storefronts?.some(s => /HOTEL/i.test(s)) || false,
|
|
has_restaurant: ocr?.storefronts?.some(s => /RESTAURANT/i.test(s)) || false,
|
|
has_cafe: ocr?.storefronts?.some(s => /CAFE/i.test(s)) || false,
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Bygg urban densitet-dataset
|
|
*/
|
|
function buildDensityDataset(annotations) {
|
|
const samples = [];
|
|
|
|
for (const annotation of annotations) {
|
|
const objects = annotation.objects;
|
|
const segmentation = annotation.segmentation;
|
|
|
|
const sample = {
|
|
image: annotation.framePath,
|
|
labels: {
|
|
person_count: objects?.grouped?.person?.length || 0,
|
|
vehicle_count: (
|
|
(objects?.grouped?.car?.length || 0) +
|
|
(objects?.grouped?.motorcycle?.length || 0) +
|
|
(objects?.grouped?.bus?.length || 0) +
|
|
(objects?.grouped?.truck?.length || 0)
|
|
),
|
|
building_density: segmentation?.distribution?.building || 0,
|
|
vegetation_density: segmentation?.distribution?.vegetation || 0,
|
|
road_density: segmentation?.distribution?.road || 0,
|
|
total_objects: objects?.totalCount || 0,
|
|
},
|
|
metadata: {
|
|
city: annotation.city,
|
|
country: annotation.country,
|
|
timestamp: annotation.timestamp,
|
|
},
|
|
};
|
|
|
|
samples.push(sample);
|
|
}
|
|
|
|
return { samples, count: samples.length };
|
|
}
|
|
|
|
/**
|
|
* Spara dataset i JSONL-format
|
|
*/
|
|
async function saveDatasets(datasets, outputDir) {
|
|
await fs.mkdir(outputDir, { recursive: true });
|
|
|
|
for (const [name, dataset] of Object.entries(datasets)) {
|
|
if (dataset.count === 0) continue;
|
|
|
|
const filePath = path.join(outputDir, `${name}.jsonl`);
|
|
const lines = dataset.samples.map(s => JSON.stringify(s)).join('\n');
|
|
|
|
await fs.writeFile(filePath, lines + '\n');
|
|
console.log(`[DATASET] Saved ${name}: ${dataset.count} samples`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generera statistik
|
|
*/
|
|
function generateStats(datasets) {
|
|
const stats = {
|
|
version: DATASET_CONFIG.version,
|
|
createdAt: new Date().toISOString(),
|
|
totalSamples: 0,
|
|
categories: {},
|
|
cities: new Set(),
|
|
countries: new Set(),
|
|
};
|
|
|
|
for (const [name, dataset] of Object.entries(datasets)) {
|
|
stats.categories[name] = {
|
|
count: dataset.count,
|
|
};
|
|
stats.totalSamples += dataset.count;
|
|
|
|
// Samla unika städer och länder
|
|
for (const sample of dataset.samples) {
|
|
if (sample.metadata?.city) stats.cities.add(sample.metadata.city);
|
|
if (sample.metadata?.country) stats.countries.add(sample.metadata.country);
|
|
}
|
|
}
|
|
|
|
stats.cities = Array.from(stats.cities);
|
|
stats.countries = Array.from(stats.countries);
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Splitsa dataset i train/validation/test
|
|
*/
|
|
async function splitDataset(dataset, splits = DATASET_CONFIG.splits) {
|
|
const shuffled = shuffleArray([...dataset.samples]);
|
|
|
|
const trainEnd = Math.floor(shuffled.length * splits.train);
|
|
const valEnd = trainEnd + Math.floor(shuffled.length * splits.validation);
|
|
|
|
return {
|
|
train: shuffled.slice(0, trainEnd),
|
|
validation: shuffled.slice(trainEnd, valEnd),
|
|
test: shuffled.slice(valEnd),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Blanda array
|
|
*/
|
|
function shuffleArray(array) {
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
}
|
|
return array;
|
|
}
|
|
|
|
module.exports = {
|
|
buildDataset,
|
|
buildRoadDataset,
|
|
buildSidewalkDataset,
|
|
buildCrosswalkDataset,
|
|
buildBuildingDataset,
|
|
buildTreeDataset,
|
|
buildUtilityDataset,
|
|
buildTrafficDataset,
|
|
buildSignageDataset,
|
|
buildLightingDataset,
|
|
buildAccessibilityDataset,
|
|
buildCommercialDataset,
|
|
buildDensityDataset,
|
|
splitDataset,
|
|
};
|