CLI · commit e50a993

lenslab

Photography habits analyzer from EXIF: focal lengths, settings & gear advice

Download lenslab.zip
Quickstart
unzip lenslab.zip && cd lenslab
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

lenslab

lenslab reads the EXIF of your photo library and tells you what you actually shoot: which focal lengths you use, whether your shutter speeds risk blur, how high you push ISO for your sensor size, when you shoot relative to the sun, and which lens is gathering dust. It ends with plain-English gear advice such as "60% of zoom shots at 24-28mm, consider a 28mm prime", and writes a self-contained HTML report with SVG charts.

Features

Install

uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'   # dev adds pytest and piexif (sample generator)
# optional HEIC support: uv pip install -p .venv/bin/python -e '.[heic]'

Quickstart

.venv/bin/python examples/generate_library.py            # 500 synthetic photos in examples/library
.venv/bin/lenslab summary examples/library
.venv/bin/lenslab report examples/library -o out/report.html
.venv/bin/lenslab export examples/library -o out/shots.csv
.venv/bin/lenslab compare examples/library --camera "6D" --camera "X-E4"

The generated library has two cameras (Canon EOS 6D Mark II full frame, Fujifilm X-E4 APS-C), four lenses (two zooms, two primes; the Fuji 35mm is deliberately abandoned after Sept 2025), six trips plus home shooting, GPS on most trip shots, ratings through XMP sidecars, and about 8% RAW-like TIFF files (.cr2, .dng). A pre-rendered report is in examples/sample-report.html.

Usage

All commands take LIBRARY (a directory, scanned recursively, or a single file) and these options: --cache FILE (default ~/.cache/lenslab/cache.sqlite), --no-cache, --workers N, --stab-stops N (default 3), --fallback-location LAT,LON (sun position for photos without GPS).

Command Purpose
scan LIBRARY Fill the cache, list photos per camera
summary LIBRARY [--gap-hours H] [--unused-days D] Terminal summary and advice
report LIBRARY -o FILE [--title T] Self-contained HTML report
export LIBRARY -o FILE CSV with one row per photo (adds shake flag and light phase)
compare LIBRARY --camera A --camera B Side-by-side metrics; names are case-insensitive substrings
sample DIR [--count N] [--seed S] Generate a synthetic library

Real output of lenslab summary examples/library:

500 photos (500 from cache, 0 scanned, 0 unreadable)
───────────────── lenslab  500 photos, 2024-10-09 → 2026-08-31 ─────────────────
Cameras: Canon EOS 6D Mark II, Fujifilm X-E4
           Focal length (35mm equiv.)            
┏━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ mm     ┃ share ┃                              ┃
┡━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 24–28  │   28% │ ███████████████████          │
│ 28–35  │   17% │ ████████████                 │
│ 35–50  │    4% │ ███                          │
│ 50–70  │   40% │ ████████████████████████████ │
│ 70–100 │   10% │ ███████                      │
└────────┴───────┴──────────────────────────────┘
Aperture  median f/5.6, most used f/4, 8% at f/2 or wider, 12% at f/11+
Shake risk  15/481 handheld shots flagged (3%; 9% ignoring stabilisation)
  20250405_0301.jpg 52mm @ 1/4 (3.7 stops too slow)
  20241022_0045.jpg 50mm @ 1/4 (3.6 stops too slow)
  20250408_0346.jpg 50mm @ 1/4 (3.6 stops too slow)
                          ISO vs sensor size                           
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┳━━━━━━┳━━━━━━━┓
┃ Camera               ┃ Sensor     ┃ Comfort ┃ Median ┃ P90  ┃ Above ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━╇━━━━━━╇━━━━━━━┩
│ Canon EOS 6D Mark II │ full frame │ 6400    │ 400    │ 3200 │ 3%    │
│ Fujifilm X-E4        │ APS-C      │ 3200    │ 400    │ 3200 │ 8%    │
└──────────────────────┴────────────┴─────────┴────────┴──────┴───────┘
Light  golden hour 14%, blue hour 5%, night 17%, daylight 64% (349 located 
shots)
Keepers  146/294 rated shots are 4+ stars (50%); 59% of the library is rated
                             Lens usage                              
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Lens                     ┃ Shots ┃ Share ┃ Last used  ┃ Status    ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ EF24-70mm f/4L IS USM    │ 225   │ 45%   │ 2026-08-31 │ active    │
│ XF18-55mmF2.8-4 R LM OIS │ 167   │ 33%   │ 2026-08-31 │ active    │
│ XF35mmF1.4 R             │ 55    │ 11%   │ 2025-08-29 │ idle 367d │
│ EF50mm f/1.8 STM         │ 53    │ 11%   │ 2026-08-31 │ active    │
└──────────────────────────┴───────┴───────┴────────────┴───────────┘
                   Trips (43 sessions, gap > 36h)                    
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ From       ┃ To         ┃ Days ┃ Shots ┃ Top lens                 ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 2024-11-08 │ 2024-11-11 │ 4    │ 36    │ EF24-70mm f/4L IS USM    │
│ 2025-04-03 │ 2025-04-08 │ 6    │ 60    │ EF24-70mm f/4L IS USM    │
│ 2025-07-12 │ 2025-07-18 │ 7    │ 81    │ EF24-70mm f/4L IS USM    │
│ 2025-10-04 │ 2025-10-07 │ 4    │ 47    │ EF24-70mm f/4L IS USM    │
│ 2025-12-07 │ 2025-12-08 │ 2    │ 14    │ XF18-55mmF2.8-4 R LM OIS │
│ 2026-02-20 │ 2026-02-24 │ 5    │ 53    │ XF18-55mmF2.8-4 R LM OIS │
│ 2026-07-02 │ 2026-07-03 │ 2    │ 10    │ XF18-55mmF2.8-4 R LM OIS │
│ 2026-08-14 │ 2026-08-16 │ 3    │ 50    │ XF18-55mmF2.8-4 R LM OIS │
└────────────┴────────────┴──────┴───────┴──────────────────────────┘
──────────────────────────────────── Advice ────────────────────────────────────
• 60% of Canon EOS 6D Mark II zoom shots at 24–28mm (35mm equiv.) → consider a 
28mm prime
• 54% of Fujifilm X-E4 zoom shots at 27–30mm (35mm equiv.) → consider a 28mm 
prime (about 19mm on this camera)
• 3% of handheld shots may show camera shake after allowing 3 stops for 
stabilisation (9% by the plain 1/focal rule)
• 8% of Fujifilm X-E4 shots are above ISO 3200 (APS-C comfort limit)
• 14% of 349 located shots were taken in golden hour
• XF35mmF1.4 R has sat unused for 367 days (last used 2025-08-29)
• Keeper rate (rating >= 4): 50% of 294 rated shots

Real output of lenslab compare examples/library --camera "6D" --camera "X-E4" (first part):

500 photos (500 from cache, 0 scanned, 0 unreadable)
                              Camera comparison                               
┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃                       ┃    Canon EOS 6D Mark II ┃            Fujifilm X-E4 ┃
┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Sensor                │      full frame (1.00x) │            APS-C (1.50x) │
│ Shots                 │                     278 │                      222 │
│ Active                │ 2024-10-09 → 2026-08-31 │  2024-10-09 → 2026-08-31 │
│ Lenses                │                       2 │                        2 │
│ Top lens              │   EF24-70mm f/4L IS USM │ XF18-55mmF2.8-4 R LM OIS │
│ Median focal (equiv.) │                    35mm │                     52mm │
│ Busiest band          │                 50–70mm │                  50–70mm │
│ Median aperture       │                   f/5.6 │                      f/4 │
│ Wide open (<= f/2)    │                      4% │                      14% │
│ Median ISO            │                     400 │                      400 │
│ ISO above comfort     │                      3% │                       8% │
│ Shake risk            │                      3% │                       3% │
│ Keeper rate           │                     56% │                      41% │
│ Golden-hour share     │                     12% │                      15% │

report

Configuration

There are no config files. Tunables are command-line flags (above). Environment: XDG_CACHE_HOME moves the default cache. Crop factors, sensor classes, ISO comfort limits and stabilisation patterns live in lenslab/gear.py; extend CROP_TABLE for cameras it does not know (unknown bodies are assumed full frame and crop_source says assumed in the CSV).

Architecture

Module Role
exif.py TIFF IFD parser, JPEG/TIFF/CR3/HEIC containers, flat field extraction
gear.py Crop-factor table, sensor classes, ISO limits, stabilisation heuristics
models.py Shot record and construction (35mm equivalent resolution)
scanner.py File discovery, process pool, SQLite cache
xmp.py Sidecar ratings
sun.py Sun elevation (NOAA algorithm), light phases
analytics.py All statistics, recommendations, trips, comparison, CSV rows
report.py HTML and SVG rendering
cli.py Click commands and rich terminal output
samplegen.py Synthetic library generator

Algorithms and worked examples are in docs/algorithms.md.

Tests

.venv/bin/pytest -q

Limitations