145 lines
4.4 KiB
PHP
145 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Face\Services;
|
|
|
|
use CodeIgniter\HTTP\CURLRequest;
|
|
|
|
/**
|
|
* FaceService
|
|
*
|
|
* Bertugas berkomunikasi dengan engine embedding wajah eksternal (Python/ONNX),
|
|
* serta menyediakan helper cosine similarity.
|
|
*
|
|
* Konfigurasi ENV:
|
|
* - FACE_SERVICE_URL (contoh: http://localhost:5000)
|
|
* - FACE_EMBEDDING_DIM (default: 512)
|
|
* - FACE_SIM_THRESHOLD (default: 0.85)
|
|
*/
|
|
class FaceService
|
|
{
|
|
protected CURLRequest $http;
|
|
protected string $serviceUrl;
|
|
protected int $embeddingDim;
|
|
protected float $defaultThreshold;
|
|
|
|
public function __construct()
|
|
{
|
|
/** @var CURLRequest $http */
|
|
$http = service('curlrequest');
|
|
$this->http = $http;
|
|
$this->serviceUrl = rtrim((string) (env('FACE_SERVICE_URL') ?? 'http://localhost:5000'), '/');
|
|
$this->embeddingDim = (int) (env('FACE_EMBEDDING_DIM') ?? 512);
|
|
$this->defaultThreshold = (float) (env('FACE_SIM_THRESHOLD') ?? 0.85);
|
|
}
|
|
|
|
/**
|
|
* Kirim gambar ke service eksternal untuk mendapatkan embedding + metrik kualitas.
|
|
* Expected response JSON:
|
|
* {
|
|
* "embedding": [float...],
|
|
* "quality_score": float,
|
|
* "faces_count": int,
|
|
* "face_size": float,
|
|
* "blur": float,
|
|
* "brightness": float
|
|
* }
|
|
*
|
|
* @throws \RuntimeException jika gagal atau quality gate tidak lolos
|
|
*/
|
|
public function extractEmbeddingWithQuality(string $imagePath): array
|
|
{
|
|
if (! is_file($imagePath)) {
|
|
throw new \RuntimeException('File gambar tidak ditemukan: ' . $imagePath);
|
|
}
|
|
|
|
$url = $this->serviceUrl . '/embed';
|
|
|
|
$response = $this->http->post($url, [
|
|
'multipart' => [
|
|
[
|
|
'name' => 'image',
|
|
'contents' => fopen($imagePath, 'rb'),
|
|
'filename' => basename($imagePath),
|
|
],
|
|
],
|
|
'timeout' => 15,
|
|
]);
|
|
|
|
$status = $response->getStatusCode();
|
|
if ($status !== 200) {
|
|
throw new \RuntimeException('Face service HTTP error: ' . $status);
|
|
}
|
|
|
|
$data = json_decode($response->getBody(), true);
|
|
if (! is_array($data) || empty($data['embedding']) || ! is_array($data['embedding'])) {
|
|
throw new \RuntimeException('Face service response invalid');
|
|
}
|
|
|
|
$embedding = array_map('floatval', $data['embedding']);
|
|
if (count($embedding) !== $this->embeddingDim) {
|
|
throw new \RuntimeException('Embedding dimensi tidak sesuai: ' . count($embedding));
|
|
}
|
|
|
|
$facesCount = (int) ($data['faces_count'] ?? 0);
|
|
$faceSize = (float) ($data['face_size'] ?? 0);
|
|
$blur = (float) ($data['blur'] ?? 0);
|
|
$brightness = (float) ($data['brightness'] ?? 0);
|
|
|
|
// Quality gates (bisa di-tune lewat ENV nanti)
|
|
if ($facesCount !== 1) {
|
|
throw new \RuntimeException('Foto harus mengandung tepat 1 wajah (faces_count=' . $facesCount . ')');
|
|
}
|
|
if ($faceSize < (float) (env('FACE_MIN_SIZE', 80))) {
|
|
throw new \RuntimeException('Wajah terlalu kecil untuk verifikasi');
|
|
}
|
|
if ($blur < (float) (env('FACE_MIN_BLUR', 30))) {
|
|
throw new \RuntimeException('Foto terlalu blur, silakan ulangi');
|
|
}
|
|
if ($brightness < (float) (env('FACE_MIN_BRIGHTNESS', 0.2))) {
|
|
throw new \RuntimeException('Foto terlalu gelap, silakan cari cahaya lebih terang');
|
|
}
|
|
|
|
return [
|
|
'embedding' => $embedding,
|
|
'quality_score' => (float) ($data['quality_score'] ?? 1.0),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Cosine similarity antara dua embedding.
|
|
*
|
|
* @param float[] $a
|
|
* @param float[] $b
|
|
*/
|
|
public function cosineSimilarity(array $a, array $b): float
|
|
{
|
|
if (count($a) !== count($b) || count($a) === 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
$dot = 0.0;
|
|
$na = 0.0;
|
|
$nb = 0.0;
|
|
$n = count($a);
|
|
for ($i = 0; $i < $n; $i++) {
|
|
$va = (float) $a[$i];
|
|
$vb = (float) $b[$i];
|
|
$dot += $va * $vb;
|
|
$na += $va * $va;
|
|
$nb += $vb * $vb;
|
|
}
|
|
|
|
if ($na <= 0.0 || $nb <= 0.0) {
|
|
return 0.0;
|
|
}
|
|
|
|
return $dot / (sqrt($na) * sqrt($nb));
|
|
}
|
|
|
|
public function getDefaultThreshold(): float
|
|
{
|
|
return $this->defaultThreshold;
|
|
}
|
|
}
|
|
|