/** * POST /v1/segment — Segmentering (pixel-level) * Använder en enkel färg-baserad segmentering som proxy för riktig AI-segmentering * med fallback till AI-beskrivning av bildregioner. */ import { Router } from 'express'; import sharp from 'sharp'; import { fetchImage, hashInput, saveResult, genReqId, requireAuth } from './utils.mjs'; const router = Router(); // Simple color-based segmentation as proxy async function segmentImage(buffer) { const { data, info } = await sharp(buffer).resize(256, 256).raw().toBuffer({ resolveWithObject: true }); const w = info.width, h = info.height; const segments = []; const visited = new Uint8Array(w * h); const threshold = 30; function colorDist(i, j) { const r1 = data[i*3], g1 = data[i*3+1], b1 = data[i*3+2]; const r2 = data[j*3], g2 = data[j*3+1], b2 = data[j*3+2]; return Math.abs(r1-r2) + Math.abs(g1-g2) + Math.abs(b1-b2); } for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const idx = y * w + x; if (visited[idx]) continue; // Flood fill from this pixel const queue = [idx]; const region = []; visited[idx] = 1; const seedColor = { r: data[idx*3], g: data[idx*3+1], b: data[idx*3+2] }; while (queue.length) { const cur = queue.pop(); region.push(cur); const cx = cur % w, cy = Math.floor(cur / w); for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { const nx = cx + dx, ny = cy + dy; if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; const nidx = ny * w + nx; if (visited[nidx]) continue; if (colorDist(idx, nidx) < threshold) { visited[nidx] = 1; queue.push(nidx); } } } } if (region.length > 200) { const xs = region.map(i => i % w); const ys = region.map(i => Math.floor(i / w)); const avgR = region.reduce((s, i) => s + data[i*3], 0) / region.length; const avgG = region.reduce((s, i) => s + data[i*3+1], 0) / region.length; const avgB = region.reduce((s, i) => s + data[i*3+2], 0) / region.length; segments.push({ id: segments.length + 1, pixel_count: region.length, coverage_pct: parseFloat((region.length / (w * h) * 100).toFixed(2)), bbox: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) }, avg_color: { r: Math.round(avgR), g: Math.round(avgG), b: Math.round(avgB) }, }); } } } return segments.sort((a, b) => b.pixel_count - a.pixel_count).slice(0, 10); } router.post('/', requireAuth, async (req, res) => { const requestId = genReqId(); const start = Date.now(); try { const { image_url, image_base64, method = 'color' } = req.body || {}; const img = await fetchImage({ image_url, image_base64 }); const inputHash = hashInput(img.buffer); const segments = await segmentImage(img.buffer); const totalCoverage = segments.reduce((s, seg) => s + seg.coverage_pct, 0); const confidence = Math.min(0.95, parseFloat((0.5 + segments.length * 0.05).toFixed(4))); const result = { ok: true, endpoint: 'segment', request_id: requestId, method, segments_found: segments.length, total_coverage_pct: parseFloat(totalCoverage.toFixed(2)), segments, inference_time_ms: Date.now() - start, }; await saveResult('segment', requestId, inputHash, result, confidence, { method, source: img.source }); res.json(result); } catch (e) { console.error('[segment]', e); res.status(500).json({ ok: false, error: e.message, request_id: requestId }); } }); export default router;