/** * POST /v1/compare — Jämförelse av två bilder (face similarity) * Använder befintlig SFace ONNX-modell för face recognition/embedding */ 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(); 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'; let detSession = null, recSession = null; async function getDetSession() { if (!detSession) detSession = await ort.InferenceSession.create(FACE_DET_MODEL); return detSession; } async function getRecSession() { if (!recSession) recSession = await ort.InferenceSession.create(FACE_REC_MODEL); return recSession; } async function detectFace(buffer) { const raw = await sharp(buffer).resize(640, 640).raw().toBuffer({ resolveWithObject: true }); const { data, info } = raw; const h = info.height, w = info.width; 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; floatData[0 * h * w + y * w + x] = data[idx] / 255.0; floatData[1 * h * w + y * w + x] = data[idx + 1] / 255.0; floatData[2 * h * w + y * w + x] = data[idx + 2] / 255.0; } } const tensor = new ort.Tensor('float32', floatData, [1, 3, h, w]); const sess = await getDetSession(); const feeds = {}; feeds[sess.inputNames[0]] = tensor; const out = await sess.run(feeds); const outTensor = out[sess.outputNames[0]]; const outData = outTensor.data; const dims = outTensor.dims; const stride = dims[dims.length - 1]; let bestScore = 0, bestBox = null; for (let i = 0; i < dims[0]; i++) { const row = Array.from(outData.slice(i * stride, (i + 1) * stride)); if (row[2] > bestScore) { bestScore = row[2]; bestBox = row; } } if (!bestBox || bestScore < 0.5) return null; return { box: bestBox, score: bestScore }; } async function getEmbedding(buffer, box) { const orig = await sharp(buffer).raw().toBuffer({ resolveWithObject: true }); const ow = orig.info.width, oh = orig.info.height; const x1 = Math.max(0, Math.round(box[3] * ow)); const y1 = Math.max(0, Math.round(box[4] * oh)); const x2 = Math.min(ow, Math.round(box[5] * ow)); const y2 = Math.min(oh, Math.round(box[6] * oh)); const faceBuf = await sharp(buffer) .extract({ left: x1, top: y1, width: x2 - x1, height: y2 - y1 }) .resize(112, 112) .raw() .toBuffer(); const floatData = new Float32Array(1 * 3 * 112 * 112); for (let i = 0; i < 112 * 112; i++) { floatData[0 * 12544 + i] = faceBuf[i * 3] / 255.0; floatData[1 * 12544 + i] = faceBuf[i * 3 + 1] / 255.0; floatData[2 * 12544 + i] = faceBuf[i * 3 + 2] / 255.0; } const tensor = new ort.Tensor('float32', floatData, [1, 3, 112, 112]); const sess = await getRecSession(); const feeds = {}; feeds[sess.inputNames[0]] = tensor; const out = await sess.run(feeds); return out[sess.outputNames[0]].data; } function cosineSimilarity(a, b) { let dot = 0, na = 0, nb = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-6); } router.post('/', requireAuth, async (req, res) => { const requestId = genReqId(); const start = Date.now(); try { const { image_url_1, image_base64_1, image_url_2, image_base64_2 } = req.body || {}; if ((!image_url_1 && !image_base64_1) || (!image_url_2 && !image_base64_2)) { return res.status(400).json({ ok: false, error: 'Two images required (image_url_1/image_base64_1 and image_url_2/image_base64_2)' }); } const img1 = await fetchImage({ image_url: image_url_1, image_base64: image_base64_1 }); const img2 = await fetchImage({ image_url: image_url_2, image_base64: image_base64_2 }); const inputHash = hashInput(Buffer.concat([img1.buffer, img2.buffer])); const face1 = await detectFace(img1.buffer); const face2 = await detectFace(img2.buffer); if (!face1 || !face2) { return res.status(400).json({ ok: false, error: 'Could not detect face in one or both images', face1_found: !!face1, face2_found: !!face2 }); } const emb1 = await getEmbedding(img1.buffer, face1.box); const emb2 = await getEmbedding(img2.buffer, face2.box); const similarity = parseFloat(cosineSimilarity(emb1, emb2).toFixed(4)); const match = similarity > 0.6; const confidence = similarity; const result = { ok: true, endpoint: 'compare', request_id: requestId, similarity, match, threshold: 0.6, face1: { detected: true, confidence: parseFloat(face1.score.toFixed(4)) }, face2: { detected: true, confidence: parseFloat(face2.score.toFixed(4)) }, inference_time_ms: Date.now() - start, }; await saveResult('compare', requestId, inputHash, result, confidence, { source1: img1.source, source2: img2.source }); res.json(result); } catch (e) { console.error('[compare]', e); res.status(500).json({ ok: false, error: e.message, request_id: requestId }); } }); export default router;