Skip to content
Open Source · Deployed

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.

Role Solo Developer
Timeline March 2026
Type Open Source / CV & ML Tool
License MIT

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 gap
Pay monthly or do it by hand

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.

The solution
Browser-native CV/ML, zero backend

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.

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.

Scoring Pipeline
Photos Drag & drop JPEG/PNG
EXIF Parser ISO · Aperture · Shutter · Focal length
Tenengrad Sobel gradient → sharpness score
BlazeFace TF.js · Face + eye detection
Blink detect Eye region luminance variance
dHash 64-bit perceptual hash · duplicate grouping
Composition Rule of thirds · headroom · horizon
Color analysis HSL · vibrance · color cast
ML Score Diff. evolution · 5-fold CV
Keep / Maybe / Reject 0–100 quality score
Single-file architecture

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.

Differential evolution for model training

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.

Privacy as a feature, not a footnote

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.

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.

What was built

2,754
lines of HTML/CSS/JS
10
CV features analyzed
1
file, zero build step
0
backend dependencies

Chosen with purpose

TensorFlow.js BlazeFace Tenengrad Sharpness dHash Perceptual Hashing Differential Evolution 5-fold Cross-Validation Inline EXIF Parser Sobel Gradient Vanilla JS Single-File HTML
TensorFlow.js + BlazeFace

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 sharpness (Sobel gradient magnitude²)

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.

dHash for duplicate detection

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.

The hardest parts weren't the algorithms

01
The model training loop was the easy part. Calibrating the thresholds was where the real work lived.

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.

02
Single-file architecture is a real constraint, not just a party trick.

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.

03
Security in a client-side app still requires real engineering.

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.