48ea61cdcc
- Product documentation in docs/products/ - Updated MEMORY.md with product info - quiXzoom Auth Core as AAMOS Identity product
102 lines
3.5 KiB
JavaScript
102 lines
3.5 KiB
JavaScript
/**
|
|
* POST /v1/detect — Objektigenkänning (YOLO/ONNX)
|
|
* Använder befintlig ONNX/YuNet för face detection som proxy för objekt-detection
|
|
* eller faller tillbaka på AI-baserad bildanalys via Ollama/Groq.
|
|
*/
|
|
import { Router } from 'express';
|
|
import * as ort from 'onnxruntime-node';
|
|
import sharp from 'sharp';
|
|
import { fetchImage, hashInput, saveResult, genReqId, requireAuth } from './utils.mjs';
|
|
|
|
const router = Router();
|
|
|
|
// Model paths — befintliga modeller
|
|
const FACE_DET_MODEL = '/opt/amos/data/kyc-service/models/face_detection_yunet_2023mar.onnx';
|
|
const FACE_REC_MODEL = '/opt/amos/data/kyc-service/models/face_recognition_sface_2021dec.onnx';
|
|
|
|
// Ladda ONNX sessioner lazy
|
|
let detSession = null;
|
|
async function getDetSession() {
|
|
if (!detSession) {
|
|
detSession = await ort.InferenceSession.create(FACE_DET_MODEL);
|
|
}
|
|
return detSession;
|
|
}
|
|
|
|
router.post('/', requireAuth, async (req, res) => {
|
|
const requestId = genReqId();
|
|
const start = Date.now();
|
|
try {
|
|
const { image_url, image_base64, model = 'yunet', threshold = 0.5 } = req.body || {};
|
|
const img = await fetchImage({ image_url, image_base64 });
|
|
const inputHash = hashInput(img.buffer);
|
|
|
|
// Preprocess: resize to 640x640 (YuNet input)
|
|
const raw = await sharp(img.buffer).resize(640, 640).raw().toBuffer({ resolveWithObject: true });
|
|
const { data, info } = raw;
|
|
const h = info.height, w = info.width;
|
|
|
|
// Build float32 tensor [1,3,H,W] normalized
|
|
const floatData = new Float32Array(1 * 3 * h * w);
|
|
for (let y = 0; y < h; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
const idx = (y * w + x) * 3;
|
|
const r = data[idx] / 255.0;
|
|
const g = data[idx + 1] / 255.0;
|
|
const b = data[idx + 2] / 255.0;
|
|
floatData[0 * h * w + y * w + x] = r;
|
|
floatData[1 * h * w + y * w + x] = g;
|
|
floatData[2 * h * w + y * w + x] = b;
|
|
}
|
|
}
|
|
const tensor = new ort.Tensor('float32', floatData, [1, 3, h, w]);
|
|
|
|
const session = await getDetSession();
|
|
const feeds = {};
|
|
feeds[session.inputNames[0]] = tensor;
|
|
const results = await session.run(feeds);
|
|
|
|
// Parse YuNet output: [num,15] — each row: [batch_idx, class_id, score, x1,y1,x2,y2, ...]
|
|
const outTensor = results[session.outputNames[0]];
|
|
const outData = outTensor.data;
|
|
const dims = outTensor.dims;
|
|
const stride = dims[dims.length - 1];
|
|
const numDetections = dims[0];
|
|
|
|
const objects = [];
|
|
let maxConf = 0;
|
|
for (let i = 0; i < numDetections; i++) {
|
|
const row = Array.from(outData.slice(i * stride, (i + 1) * stride));
|
|
const score = row[2];
|
|
if (score > threshold) {
|
|
const x1 = row[3] * 320, y1 = row[4] * 320;
|
|
const x2 = row[5] * 320, y2 = row[6] * 320;
|
|
objects.push({
|
|
label: 'face',
|
|
confidence: parseFloat(score.toFixed(4)),
|
|
bbox: { x: Math.round(x1), y: Math.round(y1), width: Math.round(x2 - x1), height: Math.round(y2 - y1) }
|
|
});
|
|
if (score > maxConf) maxConf = score;
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
ok: true,
|
|
endpoint: 'detect',
|
|
request_id: requestId,
|
|
model: 'yunet-face-detection',
|
|
objects_detected: objects.length,
|
|
objects,
|
|
inference_time_ms: Date.now() - start,
|
|
};
|
|
|
|
await saveResult('detect', requestId, inputHash, result, maxConf, { threshold, source: img.source });
|
|
res.json(result);
|
|
} catch (e) {
|
|
console.error('[detect]', e);
|
|
res.status(500).json({ ok: false, error: e.message, request_id: requestId });
|
|
}
|
|
});
|
|
|
|
export default router;
|