Skip to content

Add automatic saliency cropping - #46

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

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

Conversation

@TorstenDittmann

@TorstenDittmannTorstenDittmann 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 = newImage(\file_get_contents('image.jpg'));
$detectionJson = json_encode($image->detect(), JSON_THROW_ON_ERROR);
// Store $detectionJson in the database.// Image worker$image = newImage(\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-appsBot commented Jul 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a GRAVITY_AUTO crop mode to Image::crop() that uses a bundled U2NET ONNX saliency model to find the most visually salient region and position the crop window there, falling back to centered cropping when the saliency map is uniform. The integral-image sliding-window algorithm and center-proximity tiebreaking in findSalientCrop() are correct, and the PHPUnit tests (both mocked-saliency unit tests and a live-inference fixture test) cover the key edge cases well.

  • detectSaliency() preprocesses the image and runs U2NET inference via ankane/onnxruntime; the output is normalized to [0, 1] before the crop-window search.
  • findSalientCrop() builds a 2-D summed-area table and scores every candidate crop window in O(H×W) time, selecting the highest-scoring window nearest the image center on ties.
  • ankane/onnxruntime and ext-ffi are added to require (not suggest), making the ML inference stack a mandatory dependency for all consumers of the library.

Confidence Score: 3/5

The saliency crop feature works correctly for typical well-exposed images, but produces silently wrong model inputs for any image whose brightest pixel is below 255, and the hard ML runtime dependency in require is a breaking change for all library consumers.

The normalization in detectSaliency() divides by the image's own maximum pixel value instead of the fixed 255.0 that U2NET was trained with. For an underexposed image (max pixel ≈ 180), every channel value is inflated by 1.4× before mean subtraction, pushing the input distribution significantly off the model's training range — silently producing degraded or wrong saliency maps with no error or warning. This affects a meaningful class of real images and is a straightforward fix.

Files Needing Attention: src/Image/Image.php — the normalization divisor in detectSaliency(); composer.json — the hard ML runtime dependency

Important Files Changed

FilenameOverview
src/Image/Image.phpAdds GRAVITY_AUTO constant + detectSaliency() / findSalientCrop() methods. The integral-image algorithm and score-with-tiebreak logic are correct, but the pixel normalization in detectSaliency() uses the image's own max pixel value instead of the fixed 255.0 divisor that U2NET was trained with, causing silent preprocessing errors for non-full-range images.
composer.jsonAdds ankane/onnxruntime and ext-ffi to hard require, making a heavy ML inference stack mandatory for all consumers of this library even when focus/auto-cropping is never used.
tests/Image/ImageTest.phpAdds well-structured tests for GRAVITY_AUTO: unit tests mock detectSaliency to verify saliency-guided crop positioning, flat-saliency centering, and equal-score tie-breaking; one integration test exercises real U2NET inference against the kitten fixture.
Dockerfile-php-8.3Switches from appwrite/utopia-base Alpine image to php:8.3-cli-bullseye with manual ImageMagick copy and FFI extension install; changes composer update to reproducible composer install.
resources/models/u2net.onnx176 MB U2NET model added via Git LFS; provenance documented in NOTICE.md with MD5, SHA-256, upstream source, and Apache 2.0 license attribution.

Reviews (3): Last reviewed commit: "feat: add automatic saliency cropping" | Re-trigger Greptile

Comment threadsrc/Image/Image.php Outdated
Comment threadsrc/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.

P2tempnam 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 CodeFix in Codex

@TorstenDittmannTorstenDittmann changed the title Add semantic focus croppingAdd automatic saliency croppingJul 30, 2026
Sign up for freeto 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

@TorstenDittmann