freearhey/laravel-face-detection
Install and publish
composer require freearhey/laravel-face-detection
php artisan vendor:publish --tag=face-detection-config # verify tag in the README
The service provider is auto-discovered on Laravel 8+. There is essentially one config file and one facade, so the entire integration surface is small — read config/face-detection.php before writing any code, because it controls where the model/asset/binary path lives and that is the #1 source of "it works locally, not in Docker."
Where to look first
config/face-detection.php — runtime paths, thresholds, temp directory.src/ — find the class the facade resolves to; that is your real API, not the README.tests/ — the package's own fixtures are the fastest way to confirm input format (absolute path vs. UploadedFile vs. binary string).First use case
Profile photos. Enforce "exactly one detectable face" on avatar upload. It is a one-screen change, it has an obvious user-facing payoff, and it forces you to learn the two things that matter: the accepted input shape and how slow the call actually is.
// app/Services/FaceDetector.php
namespace App\Services;
use Freearhey\FaceDetection\Facades\FaceDetection; // confirm namespace from src/
class FaceDetector
{
public function faces(string $absolutePath): array
{
return FaceDetection::detect($absolutePath);
}
}
Resolve it from the container rather than calling the facade inline — you will want to swap it in tests (see Gotchas).
Wrap the facade, never call it from controllers. The package gives you a static facade; your application should depend on an interface. Face detection is slow, environment-sensitive, and non-deterministic, which is exactly the set of properties you want sealed behind a boundary you control.
interface FaceDetectorInterface {
public function detect(string $absolutePath): array;
}
Bind the real implementation in AppServiceProvider, and a fixture-returning fake in tests/TestCase.
Normalise the output immediately. Whatever the package hands back (bounding boxes, confidence scores, descriptors), map it into your own DTO and persist as JSON. Do not let package-specific array keys leak into your models or API responses — you will want to swap the driver later, and this package's last release was December 2023.
$faces = $detector->detect($path);
$normalized = collect($faces)->map(fn ($f) => [
'x' => $f->x / $width, // store normalised 0..1
'y' => $f->y / $height,
'w' => $f->width / $width,
'h' => $f->height / $height,
'score' => $f->score,
])->all();
Normalised coordinates survive thumbnail regeneration and CDN resizing; pixel boxes do not.
Always detect against a local path. Store the upload, then resolve the real filesystem path:
$path = $uploadedFile->store('avatars', 'local'); // local disk, not S3
$absolute = Storage::disk('local')->path($path);
If originals live on S3, copy to a temp file first, detect, then delete. Streaming a remote URL into the detector is not worth the failure modes.
Queue every detection. Budget a few hundred milliseconds to a couple of seconds per image depending on the runtime underneath. Put it in a job with $timeout = 120 and a tries = 2, store results in a face_data JSON column, and let the UI show a pending state. Synchronous detection inside a request will eventually collide with your PHP max_execution_time on a large upload.
Two-tier thresholds. Use a permissive threshold to find candidate faces and a stricter one to accept them. For avatar validation, "zero faces above 0.7 confidence" and "two or more faces above 0.4" are different rejections and deserve different user messages ("we couldn't see a face" vs. "please use a photo of just yourself").
Cropping and thumbnails. The bounding box is the useful part. Feed it to Intervention Image or Glide to generate a face-centred avatar instead of a centre-cropped one. This is usually the highest-perceived-value use of the package and it does not require trusting the confidence score at all.
Descriptors for re-identification. If the package exposes face descriptors (embeddings), enrol once at signup, store the vector as JSON (or a pgvector column), and compare later with Euclidean distance. Keep the threshold in config, not in code — you will tune it against real users.
Backfill command. Existing media will not be analysed. Write php artisan faces:backfill --chunk=100 early; it doubles as your load test and tells you the real per-image timing on your hardware.
The runtime dependency is the whole story. This package is a PHP wrapper around a detection engine that is not PHP. Whatever that engine is — a JS model set driven through headless Chrome, a Python/OpenCV binary, a native extension — must exist in every environment including CI and the production image. Pin the Dockerfile. A composer require that works on your laptop and dies in ECS with "binary not found" is the standard failure. Test it in a container from day one.
Model/asset files are large and must not be committed. Download or mount them during the image build, and make sure the configured cache/model directory is writable by www-data. Most "first run" bugs are a permissions error on that path.
EXIF orientation will silently break portrait photos. Phone uploads carry an orientation flag; the detector reads raw pixels. Auto-orient before detection or you will get "no face found" on perfectly good selfies and a bug report you cannot reproduce with your own test images.
Downscale before detecting. Resizing the longest edge to ~640px before running detection typically gives a large speedup with negligible accuracy loss. It also caps your memory usage, which matters if the engine underneath spawns a browser process per call.
Kill the browser/process leaks in long-running workers. If the underlying driver spawns a subprocess, queue workers accumulate them. Set --max-jobs on the worker, or restart after N detections. Watch RSS during the backfill.
Ordering of returned faces is not guaranteed and detection is not perfectly deterministic across engine versions. Do not write logic like "the first face is the user."
Never run real detection in the test suite. It is slow, flaky, and environment-dependent. Fake the interface, ship two or three known-good fixtures for a single integration test that is skippable via @group slow.
Do not treat this as a security control. There is no liveness detection here. It is fine for "is there a face in this avatar" and "is this the same person as last time, probably." It is not identity verification, and it is not a moderation filter — a face detector will happily miss a small background face in a group photo.
Version and compatibility drift. Last tagged release December 2023 means Laravel 11/12 support is unpinned territory. Check the composer.json constraints against your framework version before committing; if they conflict, constrain Laravel or fork the package. Watch for abandoned transitive dependencies in the lock file.
Extension points worth knowing. The config file is the intended customisation surface — driver selection, threshold, model path. If you need behaviour beyond that, the cleanest move is a decorator around your FaceDetectorInterface (caching, retry, downscaling) rather than editing the package. Results for a given image and model are stable, so cache by file hash and you will never pay the detection cost twice for the same photo.
How can I help you explore Laravel packages today?