Files
boc/aamos-api-v1/api/v1/compare.mjs
T

137 lines
5.2 KiB
JavaScript
Raw Normal View History

/**
* 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;