Culling a fight card the same night
Code and schematic on GitHub: github.com/nickcupo/photo-pipeline
A fight card is 1,500 to 2,000 frames and the promotion wants images that night. Culling by hand is what makes that impossible: at 80 px in a preview, a blink, a mouth caught mid-word and a smear of motion blur all look like a face, and you find out which is which at 100% at two in the morning.
So the culling runs on the frames’ real pixels, and the editing starts from a preset the software wrote for each scene.
Three passes
Focus, on the subject. The embedded JPEG is pulled out of each RAW. YuNet finds the faces, YOLOX finds bodies, and sharpness is measured on that box only, because a whole-frame score picks the tack-sharp cage fence over the fighters behind it every time. Bursts are grouped by capture time and the clearly soft frames in each go.
Every face, at full resolution. Every RAW is decoded at the sensor’s full resolution, four at a time, and every face is judged on those pixels: sharpness on the band across the eyes, motion blur as the anisotropy of the gradients inside the face, blink, smile and jaw from MediaPipe’s blendshapes, expression from CLIP on the face crop, exposure on the camera’s own rendering, and an SFace embedding so the output says who is in each frame. The worst main face decides: one blink in a couple is a blink. Two faces touching are a kiss, and shut eyes are the picture there, so that rule is lifted.
Quality. CLIP through the LAION aesthetic head for light and composition, zero-shot moments (strike, takedown, hand raise, walkout), same-picture grouping where CLIP similarity and a perceptual hash must agree, and the best of each group is the pick.
The preset per scene
The frames cluster into scenes: weigh-ins under fluorescents, the cage under the LED rig, the walkout in the dark. For each one the software reads what it is (CLIP zero-shot over eighteen subjects) and what the light is (twelve lighting situations, checked against the camera’s white balance from the RAW, the cast on the near-neutral pixels and the clipping), applies a rule table, and writes a DxO PhotoLab preset. Fluorescent gets neutralised; golden hour and neon keep their cast, because there the cast is the picture. Faces are lifted to a target, capped where the global slider would fog the room, with the rest noted for a control point.
With one more flag it writes a PhotoLab sidecar per frame carrying that preset and the cull’s star rating, so the folder opens edited and rated. Filter three stars and up, look, export.
What it took
The face stage was calibrated on a night shoot where the first version of the sharpness metric rejected 171 of 291 frames, including the best ones. Contrast normalisation was the fix: a dim face and a bright one have to be measured on the same scale. The motion metric false-fired on a light tube beside a face until saturated pixels were masked. A ceiling lamp detected as a face was quietly vetoing the kiss frame until a “this is a lamp” prompt threw it out.
What it still does not do: judge sharpness on anything without a human face. Fur, gloves and backs get the preview-resolution check only. Three attempts at a general blur detector, hand-made metrics, CLIP prompts, and a probe trained on synthetic blur, all failed to tell a blurred dog from a sharp one on real frames. It needs labelled data and an animal-face model, and it is not built.
The code is public at github.com/nickcupo/photo-pipeline, with a Mac app on its releases page and, in docs/ML.md, every model and threshold and the training still to do.
Everything above is in the repository: github.com/nickcupo/photo-pipeline
Tagged photography, python, opencv, clip, mma.