Skip to content

Add automatic saliency cropping - #46

Open
TorstenDittmann wants to merge 7 commits into
mainfrom
feat/semantic-focus-crop
Open

Add automatic saliency cropping#46
TorstenDittmann wants to merge 7 commits into
mainfrom
feat/semantic-focus-crop

Conversation

@TorstenDittmann

@TorstenDittmann TorstenDittmann commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add Image::GRAVITY_AUTO for saliency-aware cropping
  • use the bundled full U2NET model through ankane/onnxruntime
  • add Image::detect() so model inference can run in a dedicated worker
  • allow crop() to consume a persisted detection result without loading ONNX
  • validate persisted saliency masks before using them
  • fall back to centered cropping for empty or uniform saliency maps
  • cache the ONNX model once per PHP worker
  • use Debian Bullseye/glibc test images for the supported ONNX Runtime binaries

Usage

// Detection worker
$image = new Image(\file_get_contents('image.jpg'));
$detectionJson = json_encode($image->detect(), JSON_THROW_ON_ERROR);
// Store $detectionJson in the database.

// Image worker
$image = new Image(\file_get_contents('image.jpg'));
$detection = json_decode($detectionJson, true, flags: JSON_THROW_ON_ERROR);
$image->crop(400, 300, Image::GRAVITY_AUTO, $detection);

The detection result contains width, height, and a normalized two-dimensional mask.

Model and runtime

  • full U2NET ONNX model stored with Git LFS
  • SHA-256: 8d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491
  • model source and Apache-2.0 attribution are in resources/models/NOTICE.md
  • applications must add OnnxRuntime\Vendor::check to root Composer post-install and post-update scripts
  • prebuilt Linux ONNX Runtime artifacts require glibc; Alpine/musl needs a compatible custom runtime

Performance

Measured on an Apple M3 Pro with a 1280x837 JPEG cropped to 180x320:

  • first automatic crop: about 491 ms
  • warm automatic crop: about 405-438 ms
  • regular centered crop: about 14 ms
  • first-crop process RSS: about 550 MiB
  • long-running process RSS: about 766-768 MiB

Native ONNX Runtime and Imagick allocations are not fully represented by PHP's memory counter. Automatic detection is best suited to an asynchronous, controlled-concurrency worker.

Testing

  • vendor/bin/pint --test
  • PHPStan level max
  • Docker PHP 8.3: 60 tests, 328 assertions reached; the two new detection-worker tests pass
  • the end-to-end U2NET test cannot load the model in this checkout because only its Git LFS pointer is present

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds saliency-aware automatic cropping (GRAVITY_AUTO) to the image library using a bundled U2NET ONNX model via ankane/onnxruntime, and introduces a detect() method so inference can be pre-computed in a dedicated worker and the result passed to crop() later.

  • detectSaliency() resizes the image to 320×320, exports pixels via PIXEL_CHAR, applies ImageNet normalization, runs U2NET, and returns a normalized float mask. findSalientCrop() uses a correct summed-area table to locate the highest-saliency window, falling back to center when the mask is flat.
  • normalizeDetection() validates and clamps persisted masks before use; the static $saliencyModel caches the loaded ONNX model once per PHP worker process.
  • All three Dockerfiles are migrated from Alpine to Debian Bullseye to satisfy glibc requirements of the prebuilt ONNX Runtime binaries; ankane/onnxruntime and ext-ffi are added as hard require dependencies (see outstanding comment about moving the ML runtime to suggest).

Confidence Score: 3/5

  • The new saliency logic is internally correct, but ankane/onnxruntime and ext-ffi in hard require will force a multi-hundred-MB platform binary download and block installation on FFI-disabled environments for every consumer of this library, even those that never use GRAVITY_AUTO.
  • The hard dependency on the ONNX runtime in require (not suggest) is the dominant concern: it unconditionally changes the installation experience for all existing and future consumers of utopia-php/image, including those that never use automatic cropping. This is an API/packaging decision with broad impact that was flagged in a prior review and remains unresolved in the current diff. The image processing logic itself is well-structured and the test coverage for the new code paths is solid.
  • composer.json — the placement of ankane/onnxruntime and ext-ffi in require vs suggest determines whether the ML stack is opt-in or mandatory for all downstream users.

Important Files Changed

Filename Overview
src/Image/Image.php Adds GRAVITY_AUTO, detect(), detectSaliency(), normalizeDetection(), and findSalientCrop(). Core logic is sound (correct integral-image SAT, validated mask input, first-frame clone for animated images, graceful flat-mask fallback). Minor: pixel normalization uses image-specific max instead of fixed 255, deviating from U2NET's training preprocessing.
composer.json Adds ankane/onnxruntime and ext-ffi to hard require, triggering platform-binary downloads and FFI availability checks for all consumers of the library, even those that never use GRAVITY_AUTO. Also adds post-install/post-update scripts for OnnxRuntime\Vendor::check.
tests/Image/ImageTest.php Good coverage of the new saliency path using anonymous subclasses that override detectSaliency(). The test_crop_auto_with_u2net end-to-end test has no skip guard for when the LFS binary is absent (acknowledged by the author). All other new tests are self-contained and deterministic.
Dockerfile-php-8.3 Migrated from Alpine-based utopia-base image to debian:bullseye with imagemagick and php:8.3-cli-bullseye, adding FFI, GD, and imagick extensions. Required for ONNX Runtime glibc binaries.

Reviews (5): Last reviewed commit: "Merge main into feat/semantic-focus-crop" | Re-trigger Greptile

Comment thread src/Image/Image.php Outdated
Comment thread src/Image/Image.php Outdated
{
self::$focusDetector ??= pipeline('zero-shot-object-detection');

$path = tempnam(sys_get_temp_dir(), 'utopia-image-focus-');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 tempnam prefix truncated on Windows

The prefix 'utopia-image-focus-' is 19 characters. PHP's tempnam() documentation states that on Windows only the first 3 characters of the prefix are used, so the created file gets the prefix uto instead. Keeping the prefix at 5 characters or fewer would work reliably on all platforms.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Image/Image.php
Line: 273

Comment:
**`tempnam` prefix truncated on Windows**

The prefix `'utopia-image-focus-'` is 19 characters. PHP's `tempnam()` documentation states that on Windows only the first 3 characters of the prefix are used, so the created file gets the prefix `uto` instead. Keeping the prefix at 5 characters or fewer would work reliably on all platforms.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

@TorstenDittmann TorstenDittmann changed the title Add semantic focus cropping Add automatic saliency cropping Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant