Skip to content

krites cull

Description

Judge a shoot: analyse every frame and assign a keep, maybe or reject verdict with reasons. For each frame krites measures model-free quality signals, focus (variance of the Laplacian) and exposure (blown-highlight and crushed-shadow clipping), and resolves a verdict through the cull profile. Hard gates reject, soft floors demote to maybe, and a clean frame is a keep. Near-duplicate bursts are then grouped by perceptual hash: the best frame is kept and the rest demoted to maybe, tagged with the burst they belong to.

krites proposes and you decide: a verdict is a label written to .krites/verdicts.yaml, never a file operation. Originals are never touched.

Each cull also writes an analysis cache (.krites/analysis.yaml) holding the raw per-frame signals, and on every verdict records the machine's proposed ruling alongside your final one. That history is what a learning loop would train on; it also makes verdicts explainable and re-culls fast.

Re-culling reuses that cache, so tuning a threshold and re-culling is near-instant on a big shoot. The exact conditions for reuse, and the ones that force a fresh pass, are in The analysis cache.

The shoot must already be registered with krites ingest.

Usage

krites cull [flags]

The shoot is taken from --shoot, or the current working directory if omitted.

Flags

Flag Description Default
--shoot Shoot directory to cull the current directory
--profile Assert which profile this shoot uses (unset: no assertion)
--reanalyze Recompute analysis, ignoring the cache false

Plus the global flags on every command, including --output text|json.

--profile does not select a profile

There is only one profile, wedding-default, and --profile cannot switch to another. It is an assertion: krites resolves the shoot's profile from the config cascade, then checks the name you passed matches, and fails if it does not.

unknown profile "portrait" — this shoot uses "wedding-default"

Left unset, which is the default, no assertion is made and the shoot's profile is used. Its thresholds are the cull.profile.* keys, which cascade global-to-shoot; see Tune a cull profile.

Examples

# Cull the shoot in the current directory
cd ~/shoots/smith-wedding && krites cull

# Or target it by path, with JSON output
krites cull --shoot ~/shoots/smith-wedding --output json

Text output:

Shoot "smith-wedding" — 4 frames
  keep 1   maybe 0   reject 3

  keep   01-sharp.jpg
  reject 02-flat.jpg  — out of focus (sharpness 0 below 50)
  reject 03-dark.jpg  — out of focus (sharpness 0 below 50); crushed shadows (100% over 30%)
  reject 04-bright.jpg  — out of focus (sharpness 0 below 50); blown highlights (100% over 10%)

With face analysis on, the cull also judges eyes open or closed. The wedding-default profile carries an eye gate, and the worst face's eye-open probability drives the verdict:

  • a likely blink (below eye_open_soft, seed 0.50) demotes a frame to maybe with a subject blinking … reason;
  • eye_open_hard is disabled by default (0), so a closed-eye frame is never auto-rejected: it may be the only record of a moment, so the call is yours. Raise it to opt into auto-reject;
  • only faces clearing min_face_box (seed 0.10 of the frame's shorter side) count, so a blinking guest in the far background doesn't demote a portrait;
  • within a near-duplicate burst, an open-eyed frame is kept over a sharper blink.

Face analysis runs local ONNX models with no cgo, and it is off by default. Turn it on with one key:

face:
  enabled: true

krites fetches and verifies the models and the ONNX Runtime library on first use; there's no path to set. By default a detector (YuNet) finds the faces and MediaPipe's FaceMesh reads the eyes. The alternatives, the calibration anchors and the licence of each model are in Enable face/eye analysis and the configuration reference.

With it on, the cull demotes blinks to maybe, as subject blinking (N% eye-open, below 50%), and records min_eye_open per frame. Off, the cull runs model-free (no faces, so no eye penalty). The gate, the signal projection and the burst ranking are pure Go and WASM-safe; only the native adapter is gated behind config.

Expression signal: smile and facing the camera

With face analysis on, the same pass also reads smile and facing the camera (a head-pose estimate, not iris gaze) per face, as probabilities in [0,1], worst face across the frame like eyes. Under the default mediapipe strategy the smile comes from a trained blendshape model; under the ear fallback it is a geometric mouth-curvature estimate.

By default they rank bursts and are recorded; they don't reject. The wedding-default profile ships them verdict-neutral (smile_soft = 0, facing_soft = 0), because a solemn ceremony face or a candid glance away is normal at a wedding, not a defect. What they do by default:

  • within a near-duplicate burst, among open-eyed frames the better-smiling frame is preferred, then the one more facing the camera. The ranking chain is eyes open, then smile, then facing, then aesthetic, then sharpness, and a demotion names the tier that decided it (kept the smiling frame);
  • every frame records min_smile and min_facing in the analysis cache, alongside sharpness and eyes, for the learner.

Opt into demote-to-maybe by raising the soft floors in the profile (smile_soft, facing_soft, and frown_soft under mediapipe).

The facing anchors, and the smile anchors used by the ear fallback, are provisional: seeded, not yet calibrated against a real-frame distribution the way the eye anchors were. That is exactly why expression ships verdict-neutral. An uncalibrated estimator can inform a ranking but can't move a verdict.

Aesthetic signal (deferred)

The cull carries an aesthetic ranking tier and an aesthetic cache slot, but no scorer ships yet. The whole-frame "is this a strong photograph" model is deferred to Phase 4, where it would be built as a CLIP model plus an aesthetic head, so the learning loop gets a reusable per-frame embedding from the same pass. Until then the aesthetic tier is inert, the ranking skips it, and aesthetic_floor stays 0; it is never a hard gate by design.

Notes and current limits

  • wedding-default is the only profile; its focus, exposure and eye thresholds are starting points you tune. A profile catalog command (krites profile …) is specified and not built.
  • Re-culls reuse the analysis cache: tuning a threshold and re-culling re-resolves verdicts from the cached signals, with no re-decode and no model pass. --reanalyze forces a full recompute. The cache is invalidated automatically when min_face_box changes, when eye detection is turned on or off, and when any face setting that shapes a measurement changes (the detector, the strategy, a calibration anchor). It does not check a file's modification time, so replacing an original in place after a cull needs --reanalyze.
  • Face analysis runs on the CPU by default. On Apple Silicon, face.execution_provider: coreml uses CoreML and falls back to the CPU if it isn't available.
  • RAW frames are culled on the JPEG preview embedded in them; krites doesn't decode RAW sensor data.