Why it exists
Product photography for a multi-SKU catalog. Dozens of new SKUs per week. Each photo needs background removed, square-cropped, scaled to 1000×1000, optionally stamped with the SKU filename. Doing this by hand in Photoshop is death.
Built originally for an internal team that ships hundreds of new product photos per week. Open-sourced because the pattern is generic — anyone running a catalog with manual photo-prep grind can use it as-is or adapt it.
The pipeline
- Background removal —
rembgwith thebirefnet-generalmodel. Filename prefixsmallsskips this step (pile shots, bulk product fills the frame already). - Bulk detection — if
rembgkept >85% coverage (no clear subject) OR <5% coverage (over-removed), falls back to the original. Saves the "pile of nugs" case. - Component cleanup — keeps everything connected to the main subject. Strips only small distant fragments (alpha > 30 threshold).
- Smart resize — crops to subject bounding box, scales to 900×900, centers on a 1000×1000 transparent canvas with a 50px border.
- Outputs three folders —
pendingProducts/(clean square PNG, catalog-ready),edited/(same image + SKU filename banner),original/(source files moved here, nothing destroyed). - Webhook ping (optional) — POST to a configured URL with API-key header on batch completion. Drives downstream Odoo / Shopify / CMS sync.
What makes it different
The hard part wasn't calling rembg — it was the heuristic for "did the background-removal step actually work, or did it make things worse?" That came out of pairing with Claude on real failure cases. Two patterns broke naive bg-removal:
- Pile shots — no clear subject,
rembgwould over-remove and produce empty PNGs. - Scattered subjects — small fragments (loose nugs around a jar) got stripped as noise.
The coverage-percentage fallback turned a manual triage step into one heuristic. The component-cleanup threshold preserved scatter while still stripping JPEG artifacts.
Photo type matrix
| Type | Example filename | Behavior |
|---|---|---|
| Single subject | jar-001.jpg | Full pipeline: bg removal + resize + banner |
| Bulk / pile | smalls-batch-3.jpg | Skip bg removal (filename prefix), resize only |
| Scattered subject | jar-with-scatter.jpg | Full pipeline, keeps scattered pieces (cleanup tuned) |
| Over-removed by rembg | (auto-detected) | Falls back to original |
Stack
Native macOS Tkinter UI — no Electron, no web frontend, no 200MB Chromium runtime. Standalone .app built with PyInstaller (spec bundles font, model paths, icon). Settings live in ~/Library/Application Support/CombinedProcessor/settings.json, outside the repo by design.
Architecture
photoEditor/
├── combined_processor.py # Core pipeline: rembg → cleanup → resize → banner
├── network_utils.py # Webhook HTTP client
├── tk_app/
│ ├── app.py # Tkinter GUI
│ └── settings.py # JSON settings persistence
├── build_app.sh # PyInstaller build script (→ dist/PhotoEditor.app)
├── PhotoEditor.icns # App icon
├── PhotoEditor.spec # PyInstaller spec (bundled font, model)
├── Inter-Bold.ttf # Banner font
└── requirements.txt # 37 deps
Configuration
Default folder
Where the file picker opens by default.
Webhook URL
POSTed to on batch completion — e.g. https://hooks.example.com/photos-done.
API key + header
Sent as the value of a configurable header (default: Authorization).
Logs
Rolling debug logs under ~/Library/Logs/CombinedProcessor/.
Install
End users (Mac, no Python required)
Download the standalone .app bundle and follow the macOS Gatekeeper walkthrough in INSTALL.md. The app is unsigned (no Apple Developer Program enrollment), so first launch needs Privacy & Security → Open Anyway. One-time setup; subsequent launches are normal.
Developers (build from source)
git clone https://github.com/jaded423/photoEditor.git
cd photoEditor
# Requires Python 3.13
./build_app.sh # → dist/PhotoEditor.app
./build_app.sh --clean # clean build artifacts first
# Or run directly without packaging
./run_python.sh
AI-assisted, end-to-end
Built through iterative dialogue with Claude. Same approach behind everything on this site — see the philosophy for more, or what I'm building right now.