lookbook

lookbook — distill image pools into reference sets for personalized model training.

Public facade. The package is split across:

  • lookbook.base — Protocols and core types (Annotation, Manifest)

  • lookbook.store — Repository pattern (Stores) over dol

  • lookbook.refs — ImageRef implementations

  • lookbook.manifest — Manifest helpers

  • lookbook.registry — Plugin registries (scorers, filters, embedders, selectors)

  • lookbook.pipeline — Pipeline orchestrator

  • lookbook.scorers — Per-image scorer modules (registered on import)

  • lookbook.selectors — Selector modules (registered on import)

  • lookbook.io — Ingest, export

Plugin registries are accessed as lookbook.registry.scorers etc. (not lookbook.scorers, which is the submodule that holds scorers).

class lookbook.Annotation(image_id: str, metric_id: str, value: Any, config_hash: str = '', cost_tier: int = 0, timestamp: datetime = <factory>, backend: str = '')[source]

A single annotation produced by a Scorer or Embedder for one image.

The (image_id, metric_id) pair is the manifest key. config_hash is the cache key — re-running with the same config yields a cache hit; changing a threshold or model invalidates.

class lookbook.BytesImageRef(payload: bytes, image_id: str = '', metadata: Mapping[str, ~typing.Any]=<factory>)[source]

An image stored as raw bytes in memory.

local_path(*, cache_dir: str | None = None) str[source]

Materialize the bytes into a file (cached by image_id).

class lookbook.Embedder(*args, **kwargs)[source]

Computes a vector representation in some named embedding space.

class lookbook.Filter(*args, **kwargs)[source]

Predicate over annotations that decides whether to keep an image.

class lookbook.IdentitySimilarity(metric_id: str = 'identity_similarity', cost_tier: int = 2, requires: tuple = (), backend: str = 'lookbook:identity_similarity', reference_embedding: Any | None = None, reference_image: Any | None = None, reference_images: Sequence[Any] | None = None, embedder: EmbedderSpec = 'arcface', threshold: float = 0.85, aggregation: str = 'max', normalization: str = 'rescale')[source]

Score a candidate image’s identity-likeness to a locked reference.

Unlike the single-image scorers, this one is stateful by reference: it holds the reference embedding(s) and compares each candidate against them. Construct one per locked subject, then score every generation.

Construction (provide exactly one of the reference inputs):

  • reference_embedding — a precomputed vector (1-D) or pool (2-D / sequence of vectors). Cheapest; the reference is never re-embedded.

  • reference_image — a single ImageRef (or anything the injected embedder accepts), embedded once at construction and cached.

  • reference_images — a pool of refs, each embedded once.

Parameters (keyword-only past the embedder seam):

  • embedder — registry name ("arcface" default), an Embedder object, or a bare embed_fn(ref) -> vector callable. Injectable so tests pass a fake embedder and non-face subjects pass "clip" / "dinov2".

  • threshold — advisory pass/fail cutoff on the [0, 1] scale.

  • aggregation"max" (default) | "mean" | "min" over the reference pool.

  • normalization — cosine → [0, 1] map; see DFLT_NORMALIZATION.

The score(ref, manifest) method returns a JSON-able dict (see SimilarityResult.as_dict()) so it persists through the manifest’s default codec. Use compare_to_reference() for a one-call facade that returns the typed SimilarityResult.

compare(ref: ImageRef) SimilarityResult[source]

Compare one candidate ref against the reference; typed result.

score(ref: ImageRef, manifest: MutableMapping[Tuple[str, str], Annotation]) dict[source]

Scorer-protocol entry point — returns the JSON-able result dict.

class lookbook.ImageRef(*args, **kwargs)[source]

Anything that knows its identity and how to be opened lazily.

class lookbook.InteractiveDecision(*, keep: tuple[str, ...] = (), reject: tuple[str, ...] = (), stop: bool = False)[source]

One round’s verdict from the caller.

keep

``image_id``s the caller wants to lock into the final selection. They’re added immediately, removed from the candidate pool, and not shown again.

Type:

tuple[str, …]

reject

``image_id``s the caller never wants to see again in this run.

Type:

tuple[str, …]

stop

When True, the loop terminates after applying this round’s keep/reject and returns whatever is in the confirmed-kept set so far (clipped to k).

Type:

bool

class lookbook.PathImageRef(path: str, image_id: str = '', metadata: Mapping[str, ~typing.Any]=<factory>)[source]

An image referenced by filesystem path.

local_path(*, cache_dir: str | None = None) str[source]

Return the existing on-disk path. cache_dir is ignored.

class lookbook.Pipeline(scorers: list[Scorer] = <factory>, embedders: list[Embedder] = <factory>, filters: list[Filter] = <factory>, selector: Selector | None = None, diagnose_clusters: int = 0)[source]

An ordered set of scorers + embedders + filters + a final selector.

run(candidates: Iterable[ImageRef], *, k: int, stores: Stores | None = None, constraints: Mapping[str, Any] | None = None, run_id: str | None = None) RunResult[source]

Score, embed, filter, and select.

stores defaults to in-memory. Pass get_stores() for the user’s app data folder defaults.

class lookbook.RunResult(run_id: str, kept: list[~lookbook.base.ImageRef], candidates: list[~lookbook.base.ImageRef], selector_id: str, scorer_ids: list[str], started_at: ~datetime.datetime, finished_at: ~datetime.datetime, report: dict = <factory>)[source]

The result of running a pipeline.

class lookbook.Scorer(*args, **kwargs)[source]

Computes one annotation per image. Idempotent w.r.t. config_hash.

class lookbook.Selector(*args, **kwargs)[source]

Chooses a subset of size up-to-K from candidates given the manifest.

class lookbook.SimilarityResult(identity_cosine: float, score: float, passed: bool, threshold: float, per_reference: tuple[float, ...]=<factory>, aggregation: str = 'max', n_references: int = 0)[source]

Outcome of comparing a candidate against a (possibly pooled) reference.

identity_cosine

The aggregated raw cosine in [-1, 1] (pre-normalization). The identity_cosine name is kept for the supervisor’s vocabulary even when the embedder is CLIP/DINOv2 — it is the cosine in whatever space the embedder defines.

Type:

float

score

The aggregated similarity normalized to [0, 1] (1 = same identity). This is what the gate compares against threshold.

Type:

float

passed

score >= threshold. Advisory only — the consumer decides whether to act on it.

Type:

bool

threshold

The threshold used for passed (on the [0, 1] scale).

Type:

float

per_reference

The per-reference normalized similarities before aggregation. Length is the size of the reference pool. Lets callers see which locked view matched (e.g. argmax for aggregation="max").

Type:

tuple[float, …]

aggregation

The aggregation applied over per_reference.

Type:

str

n_references

Size of the reference pool.

Type:

int

as_dict() dict[source]

JSON-able view (what the scorer writes to the manifest).

class lookbook.Stores(images: MutableMapping, manifest: MutableMapping, runs: MutableMapping, embeddings: MutableMapping, root: str | None = None)[source]

Bundle of all stores lookbook uses for persistence.

class lookbook.UrlImageRef(url: str, image_id: str = '', metadata: Mapping[str, ~typing.Any]=<factory>, _cached: bytes | None = None)[source]

An image referenced by URL.

The bytes are fetched on first access and cached on the instance.

local_path(*, cache_dir: str | None = None) str[source]

Download (once) and return the local path.

lookbook.compare_to_reference(reference_image: Any, candidate_image: Any, *, embedder: EmbedderSpec = 'arcface', threshold: float = 0.85, aggregation: str = 'max', normalization: str = 'rescale') SimilarityResult[source]

Compare a candidate to a reference in one call → SimilarityResult.

The supervisor’s headline verb. reference_image may be a single image ref or a pool (any sequence of refs) — the pool is embedded once and folded by aggregation. candidate_image is a single ref.

All inputs are whatever the injected embedder accepts — typically an ImageRef, but with a fake/callable embedder they can be anything (tests pass plain ids).

Parameters:
  • embedder – Registry name ("arcface" default for faces; "clip" / "dinov2" for scenes/architecture), an Embedder object, or a bare embed_fn(x) -> vector callable.

  • threshold – Advisory pass/fail cutoff on the [0, 1] scale (1 = same identity).

  • aggregation – How to fold a reference pool — "max" (default), "mean", "min".

  • normalization – Cosine → [0, 1] map — "rescale" (default) or "clamp".

Returns:

.score (∈ [0, 1]), .passed (advisory), .identity_cosine (raw cosine), and the per-reference breakdown.

Return type:

SimilarityResult

lookbook.curate(source, *, k: int = 20, scorer_ids: Sequence[str | tuple] = ('random_score',), embedder_ids: Sequence[str | tuple] = (), filter_ids: Sequence[str | tuple] = (), selector_id: str | tuple = 'top_k', diagnose_clusters: int = 0, stores: Stores | None = None, constraints: Mapping[str, Any] | None = None) RunResult[source]

High-level facade: ingest a source, run a pipeline, return the result.

Each plugin id may be either a string (“blur”) or a (name, kwargs) tuple ((“blur”, {“max_side”: 256})) to override the default config.

diagnose_clusters > 0 runs cluster-coverage diagnosis after selection and writes the result into the report’s notes.

lookbook.curate_for_character(source, *, k: int = 1, face_detector: str | tuple = 'insightface', stores: Stores | None = None, constraints: Mapping[str, Any] | None = None) RunResult[source]

Curate the best reference image(s) of a known character from a pool.

Opinionated facade over curate(), tuned for the “pick the reference image of this one character” job (IP-adapter conditioning, model-sheet seeding): every image is scored for resolution, sharpness, exposure and face quality, then the top k are taken by the composite face_quality metric.

Identity is not scored — the pool is assumed to already be one character, so what matters is which frame shows them most usably: in focus, well exposed, face clearly visible and well sized.

face_detector names the registered face-box scorer:

  • "insightface" — real RetinaFace detection; needs pip install lookbook[person]. The default.

  • "mock_face" — the deterministic centred-box detector, for tests and demos without the ML dependency.

Images with no detected face score 0 and sort last; the pool is never filtered down to empty, so a small or awkward pool still yields a best-effort pick.

lookbook.curate_for_environment(source, *, k: int = 1, stores: Stores | None = None, constraints: Mapping[str, Any] | None = None) RunResult[source]

Curate the best reference image(s) for a non-person subject.

Opinionated facade over curate() for pools where face detection is moot — environment plates, prop references, style boards. Each image is scored for resolution, sharpness and exposure, folded into the composite technical_quality metric, and the top k are taken.

Unlike curate_for_character() this needs no ML dependency — the scorers run on Pillow + numpy (cv2 is used for sharpness when present, with a numpy fallback otherwise).

lookbook.curate_interactive(source, *, on_decision: Callable[[list[ImageRef], dict[str, Any]], InteractiveDecision] | Sequence[InteractiveDecision], k: int = 20, present: int = 8, max_rounds: int = 20, scorer_ids: Sequence[Any] = ('random_score',), embedder_ids: Sequence[Any] = (), filter_ids: Sequence[Any] = (), selector_id: Any = 'top_k', stores: Stores | None = None, constraints: Mapping[str, Any] | None = None) RunResult[source]

Run the pipeline in rounds, taking keep/reject decisions per round.

Args mirror lookbook.curate() plus:

Parameters:
  • on_decision – A callable or a pre-recorded sequence of InteractiveDecision. The callable receives the round’s top candidates and an info dict {round, n_kept_so_far, n_pool_remaining}.

  • present – How many candidates to surface per round.

  • max_rounds – Hard cap on iterations; a defensive guard against broken decision callables that never stop.

Returns:

A RunResult whose kept list is the confirmed set (in the order they were kept). candidates carries the same as the final round’s pre-selection candidate pool.

lookbook.get_stores(*, root: str | None = None, images_store: MutableMapping | None = None, manifest_store: MutableMapping | None = None, runs_store: MutableMapping | None = None, embeddings: MutableMapping | None = None) Stores[source]

Build the lookbook Stores bundle.

All arguments are optional. When root is None and no stores are supplied, the user’s app data folder is used. Pass dict() for any slot to keep that store in memory (useful for tests).

>>> stores = get_stores(
...     images_store={}, manifest_store={}, runs_store={}, embeddings={}
... )
>>> stores.manifest is not None
True
lookbook.ingest(source: str | PathLike | Iterable[ImageRef]) list[ImageRef][source]

Materialize a source into a list of ImageRefs.

>>> import tempfile, os
>>> with tempfile.TemporaryDirectory() as d:
...     for name in ['a.jpg', 'b.png', 'note.txt']:
...         open(os.path.join(d, name), 'wb').close()
...     refs = ingest(d)
>>> sorted(os.path.basename(r.path) for r in refs)
['a.jpg', 'b.png']
lookbook.score(ref: ImageRef | str, *, metric_id: str, stores: Stores | None = None) Any[source]

Score one image against one metric. Returns the bare value.

Caches into the manifest so repeated calls are free.

lookbook.to_local_path(ref, *, cache_dir: str | None = None) str[source]

Return a filesystem path where the bytes of ref can be read.

Works on any object that exposes .local_path(cache_dir) — i.e. every concrete ImageRef shipped with lookbook. Provided as a free function so downstream code doesn’t need to know which subtype it has.

lookbook.value_of(manifest: MutableMapping[Tuple[str, str], Annotation], image_id: str, metric_id: str, default: Any = None) Any[source]

Return the bare value of an annotation (not the wrapper), or default.