Commercial RAW Photo Culling Engine (Shoot Intel)
A 7-phase CLI pipeline that ingests commercial editorial shoots, filtering unusable RAW frames locally via Pillow pixel math before making multimodal vision API calls.
Manual Review of RAW Frames
Commercial editorial shoots generate thousands of frames per session. Photographers fire continuous high-speed bursts to capture a single fleeting expression, dynamic model turn, or product splash. When the memory cards dump onto studio storage, an assistant or lead photographer spends 2 to 4 hours manually flagging blurry shots, isolating blink frames, and separating candidates from discards.
Blindly dumping 1,200 45-megapixel RAW files (~42GB) into multimodal vision APIs like Claude 3.5 Sonnet costs over $12.00 in token fees per session, saturates studio bandwidth, and wastes API spend rating frames that were out-of-focus before they even left the camera sensor.
Filtering by Blur and Exposure
The core architectural insight: optical sharpness and exposure balance are deterministic physical properties. A 3x3 Laplacian edge convolution and a 256-bin luminance histogram can classify whether a frame is blurry, underexposed, or clipped in less than 4 milliseconds on local CPU.
No language model should ever be asked: “Is this photo in focus?” It costs money, introduces latency, and cannot match the mathematical precision of variance over pixel gradients.
Filter aggressively at the local edge with local operations. Only promote the top 2 sharpest candidates from each temporal burst cluster to multimodal vision evaluation.
By enforcing local rejection gates, 62% of raw frames are resolved on local disk with zero network requests. Studio API spend drops from ~$12.00 to ~$4.20 per shoot session, while photographer review time collapses from 3 hours to 20 minutes.
Local Pre-Flight Culling to Lightroom XMP
Shoot Intelligence executes a 7-phase CLI pipeline: it scans media cards for RAW files, extracts sub-second EXIF timestamps and embedded thumbnail previews, runs Laplacian blur variance (< 120.0 rejected locally) and 256-bin histogram checks, clusters remaining frames into bursts within a 2.0-second temporal window, routes the top candidates to Claude 3.5 Sonnet for expression scoring, and exports standard Adobe XMP sidecar files directly alongside the RAW binaries.
The original RAW files are treated as strictly read-only. When the photographer opens Lightroom Classic, star ratings (1 to 5) and color labels (Green for KEEP, Red for REJECT) appear instantly upon folder synchronization.
{ "session_dir": "/Volumes/EOS_R5/DCIM/100CANON", "total_frames": 1148, "local_rejected_blur": 524, "local_rejected_exposure": 188, "burst_clusters_formed": 84, "sent_to_vision_api": 252, "api_spend_usd": 4.18, "xmp_sidecars_written": 1148, "cull_duration_seconds": 118.4 }
7-Phase Local Computer Vision & Multimodal Culling Pipeline
Each phase is isolated with defined inputs, outputs, and strict invariants. Local CPU filters gate cloud API calls so multimodal vision is only invoked on viable candidates. Click any phase card to inspect its implementation, input state, and output state.
RAW Ingest ($0.00) → Laplacian Blur ($0.00) → Exposure Screening ($0.00) → Burst Cluster ($0.00) → Pre-API Gate ($0.00) → Claude Vision (~$4.20) → XMP Export ($0.00)
Phase 01 — RAW Ingestion
Recursively scans camera media cards for RAW files (.CR3, .ARW, .NEF, .DNG). Extracts EXIF capture timestamps with sub-second precision, shutter speeds, ISO, lens focal length, and embedded thumbnail previews.
Technical Architecture Decisions
Laplacian Variance as the Local Sharpness Gate
Convolving an image thumbnail with a 3x3 Laplacian kernel computes the second spatial derivative of pixel intensity. In focus, high-contrast edge gradients produce massive variance across the image matrix. Out of focus, edge blur smooths the gradients and variance collapses toward zero.
Shoot Intelligence computes ImageStat.Stat(thumb.filter(LAPLACIAN_KERNEL)).var[0] in pure Pillow. If the variance is below 120.0, the frame is tagged REJECT_BLUR locally. Zero cloud API calls. Under 4 milliseconds per frame.
from PIL import Image, ImageFilter, ImageStat LAPLACIAN_3x3 = ImageFilter.Kernel((3, 3), [0, 1, 0, 1, -4, 1, 0, 1, 0], scale=1, offset=0) BLUR_THRESHOLD = 120.0 def phase_blur_variance(thumb: Image.Image) -> float: # Pure local convolution — zero OpenCV binary dependency required gray = thumb.convert("L") edges = gray.filter(LAPLACIAN_3x3) stat = ImageStat.Stat(edges) return stat.var[0] # Variance across high-frequency edge gradients
Temporal Burst Clustering with a 2.0-Second Sliding Window
Commercial photographers shoot in high-speed bursts: 6 to 12 frames in 2 seconds of the exact same setup. Sending every frame in a burst to a vision model wastes money evaluating negligible micro-variations.
The clustering algorithm groups consecutive frames whose EXIF capture timestamps differ by ≤ 2.0 seconds. When a gap exceeding 2.0 seconds is encountered, the current cluster closes and a new one begins. Within each cluster, frames are ranked by sharpness, and only the top 2 candidates advance to Claude Vision.
def phase_cluster_bursts(assets: list[RawAsset], delta_sec: float = 2.0) -> list[BurstCluster]: if not assets: return [] clusters = [] current = [assets[0]] for next_asset in assets[1:]: gap = (next_asset.timestamp - current[-1].timestamp).total_seconds() if gap <= delta_sec: current.append(next_asset) else: clusters.append(BurstCluster(members=current)) current = [next_asset] clusters.append(BurstCluster(members=current)) return clusters
Lightroom-Compatible XMP Sidecars (Zero RAW File Mutation)
In commercial studio environments, touching or modifying original RAW binaries (.CR3, .ARW, .DNG) violates archival contracts and risks file corruption. Camera media must remain bit-for-bit identical to the sensor dump.
Shoot Intelligence generates standard Adobe XML sidecars (DSC_4825.xmp) alongside each RAW frame. It writes standard xmp:Rating (1 to 5) and photoshop:ColorLabel (Green, Yellow, Red) tags. When the photographer imports the folder into Lightroom Classic, all cull flags appear instantly with zero import plugins needed.
def phase_generate_xmp(asset: RawAsset) -> Path: xmp_path = asset.path.with_suffix(".xmp") xmp_content = render_xmp_template(asset.final_star_rating, asset.color_label) xmp_path.write_text(xmp_content, encoding="utf-8") return xmp_path
Run the Full Suite Locally in Under 3 Seconds
The full test suite runs offline against synthesized mock RAW EXIF records. Zero API keys, zero network calls, zero token spend required to verify the implementation.