Files
boc/aamos-api-v1/api/v1/segment.mjs
T
Bernt 48ea61cdcc docs: add quixzoom-auth-core product to AAMOS
- Product documentation in docs/products/
- Updated MEMORY.md with product info
- quiXzoom Auth Core as AAMOS Identity product
2026-07-14 09:58:53 +00:00

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;