1

Ajoute les frises de têtes et enrichit les étiquettes

This commit is contained in:
Richard Dern 2025-12-02 22:19:12 +01:00
parent 4d37323c15
commit 43062b7ed4
5 changed files with 83 additions and 4 deletions

View File

@ -346,9 +346,11 @@ Les requêtes API sont dédoublonnées, espacées (fair-use) et mises en cache d
1. `source .venv/bin/activate` 1. `source .venv/bin/activate`
2. `python -m scripts.plot_minifig_character_collages` 2. `python -m scripts.plot_minifig_character_collages`
3. `python -m scripts.plot_minifig_head_collages`
Le script lit `data/intermediate/minifigs_by_set.csv`, `data/intermediate/sets_enriched.csv` et les ressources téléchargées dans `figures/rebrickable`. Il regroupe chaque personnage connu (hors figurants) avec les sets associés et leur année de commercialisation, pour produire : Le script lit `data/intermediate/minifigs_by_set.csv`, `data/intermediate/sets_enriched.csv` et les ressources téléchargées dans `figures/rebrickable`. Il regroupe chaque personnage connu (hors figurants) avec les sets associés et leur année de commercialisation, pour produire :
- `data/intermediate/minifig_character_sets.csv` : apparitions des personnages avec set, identifiant de set, année et fig_num. - `data/intermediate/minifig_character_sets.csv` : apparitions des personnages avec set, identifiant de set, année, possession et fig_num.
- `figures/step32/minifig_characters/{personnage}.png` : frise horizontale par personnage, composée des visuels de minifigs dans lordre chronologique, annotés avec lannée et le numéro de set. Les minifigs dont limage nest pas disponible sont remplacées par un rectangle neutre pour matérialiser le manque. - `figures/step32/minifig_characters/{personnage}.png` : frise horizontale par personnage, composée des visuels de minifigs dans lordre chronologique, annotés avec lannée, le numéro de set (avec `*` si possédé) et lidentifiant de minifig. Les minifigs dont limage nest pas disponible sont remplacées par un rectangle neutre pour matérialiser le manque.
- `figures/step32/minifig_heads/{personnage}.png` : même principe mais en utilisant les visuels de têtes (`head.jpg`) pour chaque apparition.
- Les étiquettes affichent aussi lidentifiant de la minifig (`fig-*`) et un astérisque à côté du set (`set_num*`) lorsquil est présent dans la collection. - Les étiquettes affichent aussi lidentifiant de la minifig (`fig-*`) et un astérisque à côté du set (`set_num*`) lorsquil est présent dans la collection.

View File

@ -57,6 +57,7 @@ def build_character_collage(
destination_dir: Path, destination_dir: Path,
font: ImageFont.ImageFont, font: ImageFont.ImageFont,
missing_paths: Set[str] | None = None, missing_paths: Set[str] | None = None,
image_filename: str = "minifig.jpg",
image_height: int = 260, image_height: int = 260,
label_height: int = 44, label_height: int = 44,
spacing: int = 28, spacing: int = 28,
@ -66,7 +67,7 @@ def build_character_collage(
missing = missing_paths or set() missing = missing_paths or set()
cells: List[Image.Image] = [] cells: List[Image.Image] = []
for row in entries: for row in entries:
image_path = resources_dir / row["set_id"] / sanitized / "minifig.jpg" image_path = resources_dir / row["set_id"] / sanitized / image_filename
owned = "*" if row.get("in_collection", "").lower() == "true" else "" owned = "*" if row.get("in_collection", "").lower() == "true" else ""
label = f"{row['year']} - {row['set_num']}{owned} ({row['fig_num']})" label = f"{row['year']} - {row['set_num']}{owned} ({row['fig_num']})"
if str(image_path) in missing: if str(image_path) in missing:
@ -92,6 +93,7 @@ def build_character_collages(
resources_dir: Path, resources_dir: Path,
destination_dir: Path, destination_dir: Path,
missing_paths: Set[str] | None = None, missing_paths: Set[str] | None = None,
image_filename: str = "minifig.jpg",
image_height: int = 260, image_height: int = 260,
label_height: int = 44, label_height: int = 44,
spacing: int = 28, spacing: int = 28,
@ -108,6 +110,7 @@ def build_character_collages(
destination_dir, destination_dir,
font, font,
missing_paths=missing_paths, missing_paths=missing_paths,
image_filename=image_filename,
image_height=image_height, image_height=image_height,
label_height=label_height, label_height=label_height,
spacing=spacing, spacing=spacing,

View File

@ -86,13 +86,18 @@ def group_by_character(rows: Iterable[dict]) -> Dict[str, List[dict]]:
def load_missing_minifigs(path: Path) -> set[str]: def load_missing_minifigs(path: Path) -> set[str]:
"""Charge les ressources minifigs introuvables consignées dans le log de téléchargement.""" """Charge les ressources minifigs introuvables consignées dans le log de téléchargement."""
return load_missing_resources(path, "minifig.jpg")
def load_missing_resources(path: Path, filename: str) -> set[str]:
"""Charge les ressources introuvables d'un type donné consignées dans le log de téléchargement."""
missing: set[str] = set() missing: set[str] = set()
with path.open() as csv_file: with path.open() as csv_file:
reader = csv.DictReader(csv_file) reader = csv.DictReader(csv_file)
for row in reader: for row in reader:
if row["status"] != "missing": if row["status"] != "missing":
continue continue
if not row["path"].endswith("minifig.jpg"): if not row["path"].endswith(filename):
continue continue
missing.add(row["path"]) missing.add(row["path"])
return missing return missing

View File

@ -0,0 +1,43 @@
"""Construit les frises chronologiques des têtes de minifigs par personnage."""
from pathlib import Path
from lib.plots.minifig_character_collages import build_character_collages
from lib.rebrickable.minifig_character_sets import (
build_character_sets,
group_by_character,
load_minifigs_by_set,
load_missing_resources,
load_sets,
write_character_sets,
)
MINIFIGS_BY_SET_PATH = Path("data/intermediate/minifigs_by_set.csv")
SETS_ENRICHED_PATH = Path("data/intermediate/sets_enriched.csv")
RESOURCES_DIR = Path("figures/rebrickable")
DESTINATION_DIR = Path("figures/step32/minifig_heads")
CHARACTER_SETS_PATH = Path("data/intermediate/minifig_character_sets.csv")
DOWNLOAD_LOG_PATH = Path("data/intermediate/resources_download_log.csv")
EXCLUDED_CHARACTERS = ["Figurant"]
def main() -> None:
"""Assemble les apparitions des personnages et génère les frises de têtes associées."""
minifigs_by_set = load_minifigs_by_set(MINIFIGS_BY_SET_PATH)
sets_lookup = load_sets(SETS_ENRICHED_PATH)
character_sets = build_character_sets(minifigs_by_set, sets_lookup, excluded_characters=EXCLUDED_CHARACTERS)
write_character_sets(CHARACTER_SETS_PATH, character_sets)
missing_heads = load_missing_resources(DOWNLOAD_LOG_PATH, "head.jpg")
grouped = group_by_character(character_sets)
build_character_collages(
grouped,
RESOURCES_DIR,
DESTINATION_DIR,
missing_paths=missing_heads,
image_filename="head.jpg",
)
if __name__ == "__main__":
main()

View File

@ -85,6 +85,7 @@ def test_build_character_sets_and_collages(tmp_path: Path) -> None:
resources_dir, resources_dir,
destination_dir, destination_dir,
missing_paths=missing_minifigs, missing_paths=missing_minifigs,
image_filename="minifig.jpg",
image_height=20, image_height=20,
label_height=10, label_height=10,
spacing=2, spacing=2,
@ -98,3 +99,28 @@ def test_build_character_sets_and_collages(tmp_path: Path) -> None:
assert bob.size == (42, 30) assert bob.size == (42, 30)
assert claire.size == (20, 30) assert claire.size == (20, 30)
assert claire.getpixel((5, 10)) == (220, 220, 220) assert claire.getpixel((5, 10)) == (220, 220, 220)
# Variante sur les têtes : utilise un autre fichier et un autre manque.
heads_destination = tmp_path / "output_heads"
head_missing = {
str(resources_dir / "1001" / sanitize_name("Bob") / "head.jpg"),
str(resources_dir / "1002" / sanitize_name("Bob") / "head.jpg"),
str(resources_dir / "1004" / sanitize_name("Claire Dearing") / "head.jpg"),
}
head_image = resources_dir / "1000" / sanitize_name("Alice") / "head.jpg"
create_image(head_image, (10, 120, 60), (8, 12))
head_collages = build_character_collages(
group_by_character(character_sets),
resources_dir,
heads_destination,
missing_paths=head_missing,
image_filename="head.jpg",
image_height=16,
label_height=8,
spacing=2,
)
assert len(head_collages) == 3
alice_head = Image.open(heads_destination / f"{sanitize_name('Alice')}.png")
bob_head = Image.open(heads_destination / f"{sanitize_name('Bob')}.png")
assert alice_head.size[1] == 24
assert bob_head.getpixel((3, 8)) == (220, 220, 220)