Skip to main content

photogrep

Search your Apple Photos library from the terminal by what is in the picture, and find which photo an image was cut from.

Photos already knows a lot about every picture: scene labels, places, the people you have named, faces (adults and children), words that appear in the image, and an aesthetic score. Its search bar only uses part of that. photogrep reads the same data and lets you combine it.

$ photogrep search --label Beach --label Sunset --place Maui --max-faces 0 --landscape --top 3
  1  2024-07-12  aes 0.941  faces  0 (kids 0)  4032x3024  IMG_4512.HEIC  1A2B3C4D-...  Kaanapali, Maui  [Beach, Ocean, Sky, Sunset]
  2  2024-07-14  aes 0.902  faces  0 (kids 0)  4032x3024  IMG_4610.HEIC  5E6F7A8B-...  Lahaina, Maui  [Beach, Sunset]
  3  2019-03-02  aes 0.877  faces  0 (kids 0)  4032x3024  IMG_0931.HEIC  9C0D1E2F-...  Wailea, Maui  [Beach, Palm Tree]

$ photogrep match old-slide-photo.jpg
old-slide-photo.jpg
  match    0.97  2025-04-18  IMG_2210.HEIC  Honolulu, Oahu
           3F4A5B6C-7D8E-9F01-2345-6789ABCDEF01

Everything runs locally. photogrep never writes to your library and makes no network requests; Photos.app itself downloads iCloud originals when you export.

Install

Requires macOS 27 or later and Python 3.10 or later.

uv tool install photogrep               # or: pipx install photogrep
brew install euda1mon1a/tap/photogrep   # Homebrew

Your terminal needs Full Disk Access (System Settings > Privacy & Security) to read the Photos library. export and show also ask once for permission to control Photos.

Commands

Filters combine with AND. Repeating a flag (two --labels) means either one. match takes the same filters.

Flag Matches
--label scene and event labels: Beach, Hike, Dog, Birthday, Graduation
--place place names at any level: a park, neighborhood, city, region, state
--person people you have named in Photos
--album album and memory titles
--text words read from the image (all must appear)
--near LAT,LON,KM taken within KM of a point
--since / --until year range
--min-faces / --max-faces face count
--min-adults / --min-kids / --max-kids faces by apparent age
--landscape / --portrait orientation

Terms match case-insensitively, and part of a term is enough (--label grad finds Graduation). Results are ranked by Apple's aesthetic score. Add --json for scripts, --sheet out.jpg for a contact sheet, or --export DIR to save full-resolution copies.

photogrep search --near 37.7694,-122.4862,1 --min-adults 6 --since 2024 --sheet groups.jpg
photogrep search --text menu --album Travel --json

match

Finds the library photo an image came from: a crop, a screenshot of part of it, a resized copy from a slide deck or a chat. Add --in DIR to also look through a folder of image files, or --no-library to search only folders.

photogrep match screenshot.png --top 3
photogrep match from-the-deck.jpg --in ~/Downloads

Scores run up to 1.0. A match scores 0.90 or higher; 0.80 to 0.90 is reported as possible. The exit status is 0 when every image matched and 1 otherwise, like grep. Photos without a readable preview are counted and reported as not compared, never as a non-match. Photos from shared albums and Shared with You are searched too, and labeled as such.

How well it works, measured on a real library of about 64,000 photos: 198 random crops (42% to 100% of the original's width, resized, recompressed as JPEG) were each looked up among 598 photos. The source ranked first for all 198; 192 scored as a match and 5 as possible. No other photo scored 0.90; the highest was 0.83. A full pass over the 64,000-photo library takes about 90 seconds on Apple silicon.

If you know roughly where or when the photo is from, add search filters (--since 2024 --place Paris) to compare against fewer photos; it runs proportionally faster.

The image needs to cover at least 40% of the original's width. Mirrored, rotated or heavily edited copies will not match, and an image with almost no detail (a patch of sky) is refused rather than guessed at. Burst shots and duplicates of the source can score high, and should.

labels

Lists the terms Photos has indexed, with how many photos carry each one. Use it to find out what to search for.

photogrep labels --kind scene --top 30
photogrep labels --like park

export and show

photogrep export 3F4A5B6C --to ~/Desktop/picks        # a unique UUID prefix is enough
photogrep export 3F4A5B6C --to ~/Desktop/picks --originals
photogrep show 3F4A5B6C                                # opens Photos on that picture

Export goes through Photos.app, which downloads the original from iCloud if needed. Without --originals you get the current version with your edits applied.

Scripts and AI agents

photogrep is meant to be driven by other programs, AI coding agents included.

  • --json on search, match and labels prints results on stdout. Counts and progress go to stderr.
  • Exit status follows grep: 0 when something was found, 1 when nothing was (no photos, no match, no labels), 2 for an error, with the message on stderr.
  • Nothing prompts. Two macOS permissions need a person once: Full Disk Access for the app running photogrep (Terminal, or the agent's host app), and the first export or show asks to control Photos.
  • match takes about 90 seconds per image on a 64,000-photo library, which can outlast a tool-call timeout. Narrow it with filters, or pass several images in one call, which costs less than separate calls.
  • Ask photogrep labels --like WORD for the vocabulary first. Photos says "Graduations", not "graduation ceremony".

How it works

Each run makes a copy-on-write clone of Photos.sqlite and the search index leo.sqlite (with their WAL files) into a temporary folder. On APFS the clone is instant and takes no extra space. All queries run against the clone.

  • Photos.sqlite (ZASSET, ZDETECTEDFACE, ZADDITIONALASSETATTRIBUTES) supplies dates, locations, sizes, aesthetic scores, faces and original filenames.
  • database/search/leo.sqlite is the search index that replaced psi.sqlite in macOS 27. items.identifier is the asset UUID and items.lexeme_ids is a little-endian uint32 array of ids into lexicon(lexeme_id, category, content).
  • Every photo has a small preview under resources/derivatives/masters/, even when the original lives only in iCloud. Contact sheets and match use those previews.
  • match runs two passes. The coarse pass slides a 64-pixel grayscale thumbnail of your image across every preview at seven scales (normalized cross-correlation, computed with FFTs) and keeps the 25 best. The fine pass rechecks those at 160 pixels, on both the image and its edges, so large shapes and smooth gradients cannot make unrelated photos look alike. It uses up to four worker processes.

Caveats

These parts of the Photos database are undocumented, so treat them as observations, not a contract:

  • Lexicon categories, as seen on macOS 27.0.1: 4000 scene labels, 4090 events, 4120 words in the image, 2060 points of interest, 2070 and 2100 neighborhoods, 2090 cities and named places, 2110 regions, 2130 and 2140 states, 2210 bodies of water, 3000 people, 7010 albums, 7020 memories.
  • Children are counted from ZDETECTEDFACE.ZAGETYPE values 1 and 2, which look like babies and children. That reading is inferred.
  • Labels, faces and text only exist after Photos has finished analyzing the library, which happens while the Mac is idle and plugged in.
  • A future macOS update can change any of this.

Development

uv sync
uv run pytest

The tests build a small synthetic library, so they run anywhere.

License

MIT

Metadata

Release files for photogrep 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for photogrep 0.1.1
File Size Uploaded
photogrep-0.1.1.tar.gz 22.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for photogrep 0.1.1
File Interpreter ABI Platform
photogrep-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 44.1 kB

Release files / photogrep-0.1.1.tar.gz

Download URL photogrep-0.1.1.tar.gz
Size 22.6 kB
Tags Source
SHA-256 checksum
How to use checksums
b28747e151369301c34d7df48f07cbc094395964e5c7e101daff1f07814cf9bc
BLAKE2b-256 checksum
How to use checksums
088711e4f80741f55fb84b9247bd8376a643c3a35c7c9b208b2829d7bff4f884
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / photogrep-0.1.1-py3-none-any.whl

Download URL photogrep-0.1.1-py3-none-any.whl
Size 21.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9634cc663e5df012f885d411773ba490444eec3aae8d6c6e4cdf0e277808eb31
BLAKE2b-256 checksum
How to use checksums
74430a8ea8972671ef7ffc589c46659897b693bd5abcc8c379a17cb777718101
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page