107 lines
3.8 KiB
JavaScript
107 lines
3.8 KiB
JavaScript
|
|
/**
|
||
|
|
* 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;
|