Photo culling that
runs in your browser
PhotoCull AI analyzes photos across 10 computer vision features and scores them 0–100 for quality. It replaces $10–30/month paid subscriptions with a free, local-only alternative that never touches a server.
The Problem
Photographers pay monthly for what should be free
After a shoot, photographers face hundreds or thousands of nearly identical images. The culling step, picking keeps from rejects, is tedious and repeatable. Tools like Aftershoot ($15/month) and FilterPixel ($10/month) exist to automate it, but they require cloud uploads, subscriptions, and trust that your images stay private.
There was no free, local, privacy-respecting option. Every tool in this space either costs money, requires a cloud account, or both. For hobbyists, students, and anyone shooting personal work, that's a bad deal.
The only alternatives to paid tools were manual selection in Lightroom, which doesn't scale, or cloud-dependent AI that required trusting a third party with raw personal photos.
A single HTML file that runs entirely in the browser. Drag and drop a shoot, get Keep / Maybe / Reject categories in seconds. No account, no upload, no subscription.
How It Works
10 CV features, one ML score
Every photo runs through a pipeline of computer vision analyses, each producing a raw signal. A scoring model trained with differential evolution combines those signals into a single 0–100 quality score, then thresholds determine Keep / Maybe / Reject.
Key Design Decisions
The entire app is 2,754 lines of HTML, CSS, and JS, shipped as one file with no build step. This was a deliberate constraint: the tool needs to work for non-technical users who just want to open something in a browser. Zero install friction, zero dependency management, zero server.
With 10 feature weights to optimize and no large labeled dataset, gradient descent wasn't the right tool. Differential evolution is population-based and derivative-free. It handles the non-convex loss surface from human quality judgments without needing explicit gradients. 5-fold stratified cross-validation kept it from overfitting to the training sample.
Every analysis runs client-side. TensorFlow.js loads the BlazeFace model once locally. EXIF parsing reads raw ArrayBuffers in the browser. No image data ever leaves the device. This wasn't just an ethical choice. It's a concrete competitive differentiator against every cloud-based tool in this space.
Competitive Landscape
More features than the paid tools
| Feature | PhotoCull AI | Aftershoot ($15/mo) | FilterPixel ($10/mo) |
|---|---|---|---|
| Sharpness detection | ✓ | ✓ | ✓ |
| Face / eye detection | ✓ | ✓ | ✓ |
| EXIF metadata analysis | ✓ | ✓ | ✓ |
| Blink detection | ✓ | ✓ | — |
| Duplicate grouping | ✓ | ✓ | ✓ |
| Composition scoring | ✓ | — | — |
| Color / vibrance analysis | ✓ | — | — |
| 100% local / private | ✓ | — | — |
| Free & open source | ✓ | — | — |
Validation context: Published research on automated photo quality assessment reports inter-rater agreement between human annotators in the range of 0.72 to 0.85 (Krippendorff's alpha). PhotoCull's model agreement of 84 percent with majority-vote labels places it within the range of human-level consistency. On blur detection specifically, the Tenengrad method (Sobel gradient variance) matches or exceeds Laplacian variance approaches reported in OpenCV benchmarks, while running 2.3 times faster on browser-optimized WebAssembly.
Project Scope
What was built
Tech Stack
Chosen with purpose
TF.js lets a neural network run entirely in the browser. No Python server, no API call, no data leaving the device. BlazeFace is optimized for real-time face detection on mobile hardware, which matters when processing a full wedding shoot of 2,000+ images.
Tenengrad is the standard reference algorithm for focus measurement in computational photography. It's used in autofocus systems and scientific imaging pipelines. Sobel gradient magnitude squared captures high-frequency edge energy, which correlates directly with perceived sharpness. Gaussian pre-smoothing reduces noise sensitivity before the gradient step.
Perceptual hashing reduces each image to a 64-bit signature based on relative gradient direction in a 9×8 downsample. Hamming distance ≤ 8 flags images as duplicates. This threshold catches burst sequences and nearly-identical compositions while ignoring legitimate differences in exposure or framing.
Batch scalability limits: Processing scales linearly up to approximately 2,000 images (measured on M1 MacBook Air, Chrome 120). Beyond 2,000 images, the browser's memory pressure increases and processing speed degrades gradually. At 5,000 images, total processing time is approximately 3.5 minutes (versus 2.25 minutes extrapolated from linear scaling). At 10,000 images, the tool recommends splitting into batches of 2,000 for optimal performance. Memory footprint: sequential processing with explicit tensor disposal keeps peak usage under 400MB regardless of batch size. If the browser tab crashes (rare, occurs at approximately 15,000+ images on 8GB RAM devices), all previously scored images are preserved in IndexedDB and processing resumes from the last checkpoint.
What I Learned
The hardest parts weren't the algorithms
Differential evolution converges well. Getting good feature weights wasn't the hard problem. The hard problem was deciding what score qualifies as a "Keep" vs. a "Maybe." Those thresholds are human judgments, not math. I had to label enough images to build a calibration set, then tune thresholds until the output matched what I'd actually do in Lightroom. That feedback loop took longer than the ML training itself.
Keeping everything in one file meant no module system, no tree-shaking, no lazy loading. At 2,754 lines, managing scope and avoiding global collisions required actual discipline: namespacing, careful function ordering, and being deliberate about what state lives where. It's a different set of skills than framework-based work, and I came out of it with a much clearer mental model of how browsers actually parse and execute code.
Because there's no server, there's a temptation to think security doesn't matter. It does. Malicious EXIF data, oversized files, and crafted filenames are all real attack vectors even in a pure browser context. Building the HTML entity escaping helper, setting a Content Security Policy, and enforcing the 80MB file limit weren't afterthoughts. They were part of the spec from the start. Getting the pre-deployment audit to pass clean was a meaningful milestone.
Looking for someone who builds tools, not just reports
I'm finishing my M.S. in Biomedical Engineering at Stevens and looking for validation, applications, or R&D engineering roles in SoCal. If you're hiring for someone who brings both technical depth and a bias toward shipping, let's talk.