Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Facedetection Laravel Package

mauricesvay/php-facedetection

Pure-PHP face detection library for images. Scans photos for frontal faces and returns bounding-box coordinates, with no OpenCV or external binaries required. Useful for cropping avatars, tagging photos, or adding basic image analysis to Laravel/PHP apps.

View on GitHub
Deep Wiki
Context7

Getting Started

mauricesvay/php-facedetection is a single-class, pure-PHP Haar-cascade face detector that depends only on GD. It returns axis-aligned boxes for detected faces, which you then crop, blur, or tag yourself. The newly published 0.1.0 adds a cropFaceToJpeg() convenience method, which removes the most common boilerplate — but does not change anything about the detector's speed, accuracy, or licensing. Before writing a line of code, know two things: the license is GPL-2.0 (see Gotchas) and the upstream code is archived since 2016, so treat 0.1.0 as a pinned snapshot you own rather than a maintained dependency that will keep receiving fixes.

1. Verify GD is available

php -m | grep -i gd
php -r "var_dump(function_exists('imagecreatefromjpeg'), function_exists('imagecrop'));"

GD is usually compiled in, but if imagecreatefromjpeg is missing you have a stripped build and the package is a non-starter. imagecrop matters too now: cropFaceToJpeg() almost certainly delegates to it.

2. Pin the 0.1.0 tag

If the re-published tag ships a composer.json, take it as a VCS repository and pin the exact tag — do not float a constraint on a pre-1.0 package with a single maintainer:

"repositories": [
  { "type": "vcs", "url": "[email protected]:your-org/php-facedetection.git" }
],
"require": { "mauricesvay/php-facedetection": "0.1.0" }

If it still has no Composer metadata (upstream never did), keep the classmap route:

app/Support/FaceDetection/
├── FaceDetector.php
└── detection_cascade.xml   # the cascade that ships next to the class
// composer.json
"autoload": { "classmap": ["app/Support/FaceDetection/"] }
composer dump-autoload

The class lives in the global namespace, so reference it as \FaceDetector or alias it: use \FaceDetector as PhpFaceDetector;.

3. Where to look first

  • FaceDetector.php — still short. Read faceDetect() for the return shape (['x' => int, 'y' => int, 'w' => int]) and cropFaceToJpeg() for its exact signature. The release note names the method but not its arguments, return value, JPEG quality, or whether it writes to disk or returns a binary string — confirm from source (or ReflectionMethod::getParameters()) before wiring it in.
  • The cascade XML — this is the model. If detection quality is poor, the cascade and the working image size matter more than any code you write.
  • The README for the constructor signature (new FaceDetector($imagePath, $cascadeFile = null)).

4. First use case: avatar cropping on profile-photo upload

use \FaceDetector;

$path = Storage::path($media->path);        // absolute path, not a URL
$detector = new FaceDetector($path);
$faces = $detector->faceDetect();           // [] when nothing is found

if ($faces) {
    $face = $faces[0];                      // already sorted by size in most versions
    // 0.1.0: one call instead of open/crop/encode.
    // Verify argument order + whether it writes a file or returns a JPEG string.
    $detector->cropFaceToJpeg($face, Storage::path("avatars/{$media->id}.jpg"));
}

Run this once, manually, on a single uploaded photo before building anything else. You will immediately see the three constraints that shape the whole integration: it is slow on full-size camera images, it returns a box with no height, and cropFaceToJpeg() only fixes the cropping, not the detection cost — it still detects on whatever image you handed the constructor.

Implementation Patterns

Wrap it behind a contract immediately. The package's age and license mean you may need to swap it out; do not let \FaceDetector leak into controllers. Keep the contract box-shaped (portable) and treat cropFaceToJpeg() as an adapter-level optimisation, not part of the interface:

interface FaceDetectorContract
{
    /** @return array<int, array{x:int,y:int,w:int}> */
    public function detect(string $absolutePath): array;
}
class PhpFaceDetector implements FaceDetectorContract
{
    public function detect(string $absolutePath): array
    {
        return (new \FaceDetector($absolutePath))->faceDetect();
    }

    /** Convenience for callers that only want the avatar. */
    public function cropToJpeg(string $absolutePath, array $face, string $out): void
    {
        (new \FaceDetector($absolutePath))->cropFaceToJpeg($face, $out);
    }
}

Bind the interface in a service provider and inject it everywhere. Your tests then bind a fake that returns canned boxes, so the suite never touches GD. Anything calling cropToJpeg() should be covered by one integration test against a fixture image, not the unit suite.

Detect in a queued job, never in the request cycle. Chain detection into your existing image pipeline:

class DetectFaces implements ShouldQueue
{
    public int $timeout = 120;
    public int $tries = 1;

    public function __construct(public Media $media) {}

    public function handle(FaceDetectorContract $detector): void
    {
        $faces = $detector->detect(Storage::path($this->media->path));

        $this->media->update(['faces' => $faces]);   // JSON column
        FaceDetected::dispatch($this->media, $faces);
    }
}

Dispatch from an observer on created, or from your upload action via DetectFaces::dispatch($media)->onQueue('images'). Keeping detection on a dedicated queue protects your web requests from a CPU-bound pure-PHP loop.

Downscale before detection, then map the coordinates back. Still the single highest-leverage optimisation, and cropFaceToJpeg() does nothing to change it. Detection cost scales with pixel count; a 12 MP upload is roughly 20× slower than an 800 px working copy and adds nothing in accuracy.

$original = imagecreatefromjpeg($path);
[$ow, $oh] = [imagesx($original), imagesy($original)];

$target = 800;                                  // longest edge
$scale  = min(1, $target / max($ow, $oh));
$work   = imagescale($original, (int) ($ow * $scale), (int) ($oh * $scale), IMG_BICUBIC);

$tmp = tempnam(sys_get_temp_dir(), 'fd') . '.jpg';
imagejpeg($work, $tmp, 90);

try {
    $faces = (new \FaceDetector($tmp))->faceDetect();
} finally {
    @unlink($tmp);
}

$faces = array_map(fn ($f) => [
    'x' => (int) round($f['x'] / $scale),
    'y' => (int) round($f['y'] / $scale),
    'w' => (int) round($f['w'] / $scale),
], $faces);

Note the split: detect on the downscaled file, then crop from the original using mapped coordinates. cropFaceToJpeg() on a FaceDetector built from the downscaled path will give you the downscaled crop — acceptable for a 200 px avatar, wrong for a hero image.

Let cropFaceToJpeg() handle the boring case, not the interesting one. It is right for: avatar thumbnails, notification icons, a single face, JPEG output, default quality. It is wrong for: format control (PNG/WebP), quality tuning, padding or aspect-ratio changes, composite output, multiple faces per image, or privacy redaction work. Keep one clampBox() helper and your own imagecrop() path for those.

Normalise the box before you use it. Boxes can start at negative coordinates or extend past the edge; height must be derived from width. Verify whether the new method clamps internally — if it doesn't, clamp first.

function clampBox(array $face, int $imgW, int $imgH): array
{
    $size = (int) min($face['w'], $imgW, $imgH);
    return [
        'x' => max(0, min((int) $face['x'], $imgW - $size)),
        'y' => max(0, min((int) $face['y'], $imgH - $size)),
        'w' => $size,
        'h' => $size,
    ];
}

Cache by content hash. Detection is deterministic for a given file and cascade, so sha1_file($path) makes a good cache key. Store the boxes in a JSON column or cache entry and skip the work on re-uploads, retries, and edits that don't touch pixels. Cache the boxes, not the cropped file, so you can re-render at new sizes without re-detecting.

Compose with Intervention Image for the downstream work. Detection gives you coordinates; cropping, blurring for privacy redaction, and format conversion are better handled by intervention/image. Use cropFaceToJpeg() only where its defaults match your needs; draw overlays during debugging with imagerectangle() on a copy to confirm boxes line up.

Backfill with a chunked artisan command. Any table that gains a faces column after launch needs images:detect-faces --chunk=50 --queue. Because the library is CPU-bound, one worker per core and no more; oversubscribing just thrashes.

Gotchas and Tips

  • The new method does not make detection faster. cropFaceToJpeg() is a convenience wrapper around the crop/encode step. The expensive part — the cascade scan — still runs on the image you passed to the constructor. Downscale, cap the working image, keep it on a queue.
  • Verify the cropFaceToJpeg() contract before trusting it. The release note is one sentence. Check: argument order (box first or output path first?), does it write a file or return a JPEG string, what quality does it default to, does it accept a quality parameter, does it throw or silently return false on a bad box. Write one test with a fixture image and assert getimagesize() on the output.
  • It re-reads and re-decodes the source. If the method opens the original path again, you pay the GD decode cost twice (and peak memory for both). If you already have a GD resource open for cropping, do it yourself.
  • JPEG-only output. Fine for avatars, a blocker if your pipeline is WebP or if you need transparency handling. The method gives you no hook for that; fall back to imagecrop().
  • Performance is the defining property. Pure-PHP cascade scanning is on the order of seconds to tens of seconds per megapixel. A full-resolution photo will happily exceed max_execution_time. If you need real throughput, that is the point to look at OpenCV bindings or a cloud vision API instead.
  • Memory. GD decodes the entire image into a resource: a 4000×3000 JPEG is roughly 48 MB before the detector allocates anything. Set --memory on the queue worker, and avoid loading the original twice — crop from the already-open resource.
  • EXIF orientation bites silently. GD ignores EXIF orientation, so a portrait phone photo arrives rotated, and the cascade — trained on upright frontal faces — will miss or misfire. Orient the image before detection (exif_read_data() or Intervention's orientate()), and make sure cropFaceToJpeg() is fed the same oriented file, not the raw upload.
  • No h in the results. Boxes are square by convention. Deriving height from width is right for faces, but if you add a padding margin, pad symmetrically so the aspect ratio stays square.
  • Empty array is a normal outcome, not an error. Low light, profile views, sunglasses, small faces, and busy backgrounds all produce []. Design the UX around "no face found" (fallback to a centre crop) rather than surfacing a failure.
  • Validate the file before constructing the detector. The class picks a GD loader and a file with a misleading extension will throw or emit warnings. Use exif_imagetype() or getimagesize() and switch on the constant; wrap the constructor in a try/catch and convert warnings with a scoped error handler.
  • Alpha and palette images. PNGs with transparency and GIFs with palettes can break pixel sampling — and will produce odd JPEG crops if cropFaceToJpeg() composites onto black. Flatten to a truecolour image on a white background before detection.
  • Cascade path resolution. The bundled XML is located relative to the class or the current working directory depending on the version. In Laravel, always pass an absolute path via the constructor's second argument or setCascadeFile(), built with base_path(). If the cascade is missing you generally get zero faces rather than an exception, which is a painful bug to chase.
  • PHP 8 compatibility. This code predates PHP 8. Audit for removed functions (each(), create_function()), dynamic-property deprecations, and implicit float-to-int conversions. The new method is new code inside an old codebase — it may use imagecrop() (PHP 5.5+) and nullable types or typed properties that narrow your supported PHP range. Read the diff, don't assume. Run the detector against a fixture image in CI so a PHP upgrade cannot silently break it.
  • Global namespace. \FaceDetector is easy to collide with. Alias on import and keep it inside a single adapter class.
  • GPL-2.0 is the big one. This is a copyleft licence, and it applies to cropFaceToJpeg() too — the new method does not relicense anything and does not move the package out of copyleft. Serving a SaaS internally is usually fine, but shipping the application to customers, embedding it in an appliance, or distributing a binary that bundles it can oblige you to release source under GPL-2.0. Get a legal sign-off before this ships in anything distributed. For internal-only use, wrapping it behind your own interface keeps the option to swap in a permissively licensed alternative.
  • Archived upstream, fresh tag. A 0.1.0 means someone is packaging it again — confirm who, and whether the tag is a straight republish or a fork with fixes. No fixes, no guaranteed PHP 8+ support, no Composer metadata historically. Fork it into your org, pin your copy, and note in the fork README exactly what you changed and which tag you based on. Do not expect the cascade to be updated for modern camera hardware.
  • Extension points. The useful ones are subclassing FaceDetector to override the scale-step properties, and setCascadeFile() to point at a different cascade (only the frontal-face cascade ships — no profile detection). If you need more than that, you are better served by a wrapper contract than by patching the class.
  • Debugging technique. When boxes look wrong, save the working (downscaled) image alongside the original and log both coordinate sets. Nine times out of ten a mismatch is a half-applied scale factor between the working image and the stored coordinates — and if you are now calling cropFaceToJpeg() directly, that mismatch shows up as a crop of empty wall instead of a face.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo