Skip to content

About

Facetwork domain: group cycling photos → per-rider enhanced portraits (YOLO detect + SAM cutout + Real-ESRGAN + GFPGAN/CodeFormer + face grouping + 4x6 framing; HEIC/RAW in)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

35 Commits

Folders and files

Repository files navigation

fwh_peloton

Split group cycling photos into per-rider enhanced portraits. A peloton is the pack of riders — this finds each one and gives you a focused, sharpened picture of them, then helps you size, organize, and cull the results.

group photo → detect riders (YOLO person+bicycle) → crop each (person ∪ bike)
            → dehaze + auto-brighten → keep NATIVE detail (upscale only when a crop
              is smaller than the target — a sharp high-res crop is left as-is;
              Real-ESRGAN would over-smooth it) → [SAM cutout] → [face-restore]
            → a tight _single (+ optional wider _context that also captures the
              surrounding riders) → out/<stem>_riderNN[.jpg | .tif]

Feature specifications

Per-feature specs live in docs/ — one document per capability, each grounded in the FFL docstrings, handlers, and _peloton_tools code.

Spec What it covers
portrait-pipeline Flagship. Group photo → one enhanced portrait per rider (detect → crop → dehaze/brighten → upscale → face-restore); 16-bit TIFF path; per-photo fleet fan-out.
detection YOLO person+bicycle detection, person↔bike pairing, require_bike, mocks.
cropping-framing-cutouts Box geometry, fixed-aspect/print-size framing, SAM cutouts.
enhancement Dehaze, auto-brighten, upscale (Real-ESRGAN → Lanczos), face-restore (GFPGAN/CodeFormer), 16-bit variants.
format-conversion RAW/TIFF/JPEG → TIFF/JPEG at any resolution; adaptive multi-threaded engine; tiffs-to-jpegs.
copy-tree Parallel, restart-safe recursive directory copy.
grouping-and-culling Same-rider face grouping, quality scoring, blur culling.
ffl-workflows The FFL domain layer — facets, workflows, handlers, entry point.
storage-and-cache Local/S3(MinIO) storage backend + sidecar cache (present but not yet wired).

Full index: docs/README.md.

Two enhancement fixes matter for high-resolution sources (e.g. 45 MP DSLR RAW):

  • Detail-preserving. Each rider crop off a big frame is already high-res, so the Real-ESRGAN 4× upscaler (a low-res super-resolver that denoises) is skipped unless the crop is genuinely smaller than the target — running it then downscaling visibly softens a sharp crop. --upscale-mode auto (default) / never / always.
  • Dehaze. These frames come off the camera flat (lifted blacks, muted colour); a per-image black/white-point stretch + mild contrast/colour removes the veil. On by default (--no-dehaze to disable).

Output: --out-format jpg (default, 8-bit) or tiff (lossless 16-bit — decodes RAW at 16-bit and does the tonal math in 16-bit so a heavy brighten/dehaze stretch stays banding-free; the archival master, large files). Keep the TIFFs as masters and derive shareable JPEGs on the side with tiffs-to-jpegs (a separate --in-dir/--out-dir step, no re-processing).

A Facetwork domain package following the tools/handlers pattern. This ships the reusable library + CLI tools (src/peloton/tools/); the FFL handlers/workflow are the next phase.

Input formats: JPEG/PNG/WebP/TIFF/BMP, iPhone HEIC/HEIF (pillow-heif, core), and camera RAW — .nef (Nikon), .cr2/.cr3 (Canon), .arw (Sony), .dng, .raf, .orf, .rw2, … (rawpy/LibRaw, .[raw] extra). A folder may mix any of these — each file is decoded by its own format.

Tools

Tool Does
detect-riders Detect each cyclist → JSON of person/bicycle boxes
crop-riders Detect + crop each rider (no enhance)
enhance-image Upscale + face-restore a single image
process-photo The pipeline: one photo → per-rider portraits (+ optional fixed size / SAM cutout)
batch-photos Run the pipeline over a whole directory (reuses models; running manifest.json)
group-riders Cluster the crops by face → one rider_NNN/ folder per person, best-shot first
cull-blurry Score focus; move out-of-focus photos to _unfocused/
tiffs-to-jpegs Derive shareable 8-bit JPEGs from a directory of 16-bit TIFF masters (separate step)
convert-photos Convert RAW/TIFF/JPEG → TIFF or JPEG at any resolution (--format/--resize), recursive + adaptive-parallel. (nef-to-tif is a RAW→TIFF alias.)
copy-tree Parallel recursive directory copy — mirror a tree, restart-safe (--workers)

Every tool: JSON on stdout, logs on stderr, --use-mock (offline, no models), --log-level. Full reference + the graceful-degradation backends, --frame/--aspect/--size framing, and --segment cutouts: src/peloton/tools/README.md.

Quick start

pip install -e '.[detect,enhance]'          # models; core alone runs degraded
# offline smoke (no models/network):
python src/peloton/tools/process_photo.py --image group.jpg --out-dir out/ --use-mock

# a folder → a tight _single + a wider _context per rider, cleaned up, native
# detail, lossless 16-bit TIFF (drop --out-format for 8-bit JPEG):
python src/peloton/tools/batch_photos.py --in-dir photos/ --out-dir out/ \
    --require-bike --context --auto-brighten --out-format tiff

# or a tight portrait + a print-ready 4×6 (1200×1800) per rider:
python src/peloton/tools/batch_photos.py --in-dir photos/ --out-dir out/ \
    --require-bike --frame both --size 1200x1800

# organize + cull:
python src/peloton/tools/group_riders.py --in-dir out/ --out-dir out/by_rider
python src/peloton/tools/cull_blurry.py  --in-dir photos/ --min-sharpness 900

# derive shareable JPEGs from the 16-bit TIFF masters (separate, idempotent step):
python src/peloton/tools/tiffs_to_jpegs.py --in-dir out/ --out-dir out_jpg/

# convert RAW/TIFF/JPEG → TIFF or JPEG, any resolution (convert-photos):
python src/peloton/tools/convert_photos.py --image shot.NEF --out-dir out/                    # RAW → 16-bit TIFF, full res
python src/peloton/tools/convert_photos.py --image master.tif --out-dir out/ --format jpeg --resize 2048  # TIF → JPEG, long edge 2048
python src/peloton/tools/convert_photos.py --in-dir jpgs/ --out-dir tifs/ --from jpg          # JPEG → TIFF
# a whole tree, mirroring structure (RAW → JPEG @ 3000px), resumable + adaptive-parallel:
python src/peloton/tools/convert_photos.py --in-dir shoots/ --out-dir out/ --recursive --format jpeg --resize 3000 --resume
#   --format tif|jpeg · --quality N · --resize N|WxH|50% · --from raw|any|<ext list>
#   --workers auto (default): sizes to free CPUs, ramps up on headroom, backs off on saturation

Run as an FFL workflow

The tools are also exposed as a Facetwork domain (facetwork.domains entry point + handlers/ + ffl/peloton.ffl), so the pipeline runs on the runtime / fleet.

Event facets (image data flows by reference — file/MinIO paths):

  • peloton.Portraits.ProcessPhoto(image_path, out_dir, …) → (n_riders, outputs)
  • peloton.Ingest.ConvertRaw(image_path, out_dir, highlight_mode) → (output)
  • peloton.Ingest.ConvertTree(in_dir, out_dir, out_format, resize, from_sel, …) → (converted, skipped, failed) — multi-threaded whole-directory/tree convert (RAW/TIFF/JPEG → TIFF/JPEG)
  • peloton.Ingest.CopyTree(src, dst, workers) → (copied, skipped, failed) — multi-threaded recursive copy
  • peloton.Ingest.ListImages(in_dir) → (paths, count)

Workflows: ProcessOnePhoto, ProcessBatch(paths, out_dir) (fan out per photo), ConvertBatch(paths, out_dir) (fleet fan-out, one task/file), ConvertDir(in_dir, out_dir, …) (one step, multi-threaded), CopyDir(src, dst).

The multi-threaded conversion/copy engine (adaptive --workers auto — sizes to free CPUs, ramps up on headroom, backs off on saturation) is shared by the CLIs (convert-photos, copy-tree) and these handlers.

pip install -e '.[detect,enhance,raw,domain]'        # domain = the facetwork runtime
facetwork compile src/peloton/ffl/peloton.ffl --check # validate the FFL
python -m facetwork.domains --seed peloton            # register handlers + seed the flows

FFL at a glance

A step is name = Facet(args), later steps reference earlier ones as step.field, and andThen foreach fans the per-photo work out across the fleet:

namespace my.peloton {

    use peloton.Ingest
    use peloton.Portraits

    /** Enumerate a directory, then process every photo in parallel. */
    workflow ProcessDir(in_dir: String, out_dir: String) => (count: Long) andThen {

        listed = peloton.Ingest.ListImages(in_dir = $.in_dir) andThen foreach p in $.paths {

            done = peloton.Portraits.ProcessPhoto(image_path = $.p, out_dir = $$.out_dir)

            yield ProcessDir(count = 1)
        }
    }
}
fw ffl run --primary my.ffl --library src/peloton/ffl/peloton.ffl \
  --workflow my.peloton.ProcessDir \
  --inputs '{"in_dir": "/data/raw", "out_dir": "/data/out"}'

📖 docs/ffl-examples.md — the full example gallery: fan-out from a facet list vs a CLI list, two-stage convert-then-process, catch so one corrupt file doesn't kill the batch, when guards, call-time mixins for long conversions, and reusing the shipped workflows. Every snippet there is compile-checked.

Extras (optional, lazy-imported — the pipeline degrades gracefully without them)

Extra Enables
detect YOLO rider detection + SAM cutouts (ultralytics/torch)
enhance Real-ESRGAN upscale + GFPGAN/CodeFormer face-restore (spandrel/gfpgan)
recognize Same-rider grouping (insightface/onnxruntime)
raw Camera RAW decode (rawpy/LibRaw)
s3 S3/MinIO storage (boto3)
domain Run as an FFL workflow on the Facetwork runtime (facetwork)

Model weights auto-cache to ~/.cache/peloton/weights. Without a backend, upscale falls back to Lanczos and face-restore to passthrough; each run records which backend actually ran.

Layout

src/peloton/
  tools/
    detect_riders crop_riders enhance_image process_photo batch_photos
    group_riders  cull_blurry tiffs_to_jpegs convert_photos copy_tree  (+ .sh wrappers)
    _peloton_tools/  images crop detect segment enhance quality recognize
                     pipeline copytree peloton_mocks sidecar storage
  ffl/peloton.ffl    event facets + workflows
  handlers/          ingest/ (list/convert/convert-tree/copy-tree) + portraits/ + shared/ shim
tests/               offline suite (59 tests, no network/models via --use-mock)

Tests

pip install pytest pillow && pytest tests/ -q     # all offline

Caveats

  • Face grouping is imperfect on helmeted/sunglassed cyclists (weak embeddings); --threshold tunes precision/recall. Bib-number OCR would identify riders better.
  • NEF/RAW decode is validated end-to-end on real Nikon .NEF (8-bit and 16-bit), including a 1442-photo 45 MP run.

About

Facetwork domain: group cycling photos → per-rider enhanced portraits (YOLO detect + SAM cutout + Real-ESRGAN + GFPGAN/CodeFormer + face grouping + 4x6 framing; HEIC/RAW in)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages