Ajoute les frises de têtes et enrichit les étiquettes
@@ -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`
|
||||
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 :
|
||||
|
||||
- `data/intermediate/minifig_character_sets.csv` : apparitions des personnages avec set, identifiant de set, année et fig_num.
|
||||
- `figures/step32/minifig_characters/{personnage}.png` : frise horizontale par personnage, composée des visuels de minifigs dans l’ordre chronologique, annotés avec l’année et le numéro de set. Les minifigs dont l’image n’est pas disponible sont remplacées par un rectangle neutre pour matérialiser le manque.
|
||||
- `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 l’ordre chronologique, annotés avec l’année, le numéro de set (avec `*` si possédé) et l’identifiant de minifig. Les minifigs dont l’image n’est 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 l’identifiant de la minifig (`fig-*`) et un astérisque à côté du set (`set_num*`) lorsqu’il est présent dans la collection.
|
||||
|
||||
BIN
figures/step32/minifig_heads/Alan_Grant.png
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
figures/step32/minifig_heads/Allison_Miles.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
figures/step32/minifig_heads/Atwater.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
figures/step32/minifig_heads/Barry.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
figures/step32/minifig_heads/Ben.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
figures/step32/minifig_heads/Brooklynn.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
figures/step32/minifig_heads/Claire_Dearing.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
figures/step32/minifig_heads/Danny_Nedermeyer.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
figures/step32/minifig_heads/Darius.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
figures/step32/minifig_heads/Dennis_Nedry.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
figures/step32/minifig_heads/Duncan_Kincaid.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
figures/step32/minifig_heads/Eli_Mills.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
figures/step32/minifig_heads/Ellie_Sattler.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
figures/step32/minifig_heads/Franklin_Webb.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
figures/step32/minifig_heads/Gray_Mitchell.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
figures/step32/minifig_heads/Gunnar_Eversol.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
figures/step32/minifig_heads/Henry_Loomis.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
figures/step32/minifig_heads/Henry_Wu.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
figures/step32/minifig_heads/Hudson_Harper.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
figures/step32/minifig_heads/Ian_Malcolm.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
figures/step32/minifig_heads/Isabella_Delgado.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
figures/step32/minifig_heads/John_Hammond.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
figures/step32/minifig_heads/Kayla_Watts.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
figures/step32/minifig_heads/Ken_Wheatley.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
figures/step32/minifig_heads/Kenji.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
figures/step32/minifig_heads/LeClerc.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
figures/step32/minifig_heads/Lex_Murphy.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
figures/step32/minifig_heads/Maisie_Lockwood.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
figures/step32/minifig_heads/Martin_Krebs.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
figures/step32/minifig_heads/Owen_Grady.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
figures/step32/minifig_heads/Rainn_DeLaCourt.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
figures/step32/minifig_heads/Ray_Arnold.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
figures/step32/minifig_heads/Reuben_Delgado.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
figures/step32/minifig_heads/Robert_Muldoon.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
figures/step32/minifig_heads/Sammy.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
figures/step32/minifig_heads/Simon_Masrani.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
figures/step32/minifig_heads/Sinjin_Prescott.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
figures/step32/minifig_heads/Soyona_Santos.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
figures/step32/minifig_heads/Teresa_Delgado.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
figures/step32/minifig_heads/Tim_Murphy.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
figures/step32/minifig_heads/Vic_Hoskins.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
figures/step32/minifig_heads/Xavier_Dobbs.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
figures/step32/minifig_heads/Yaz.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
figures/step32/minifig_heads/Zach_Mitchell.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
figures/step32/minifig_heads/Zia_Rodriguez.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
figures/step32/minifig_heads/Zora_Bennett.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
@@ -57,6 +57,7 @@ def build_character_collage(
|
||||
destination_dir: Path,
|
||||
font: ImageFont.ImageFont,
|
||||
missing_paths: Set[str] | None = None,
|
||||
image_filename: str = "minifig.jpg",
|
||||
image_height: int = 260,
|
||||
label_height: int = 44,
|
||||
spacing: int = 28,
|
||||
@@ -66,7 +67,7 @@ def build_character_collage(
|
||||
missing = missing_paths or set()
|
||||
cells: List[Image.Image] = []
|
||||
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 ""
|
||||
label = f"{row['year']} - {row['set_num']}{owned} ({row['fig_num']})"
|
||||
if str(image_path) in missing:
|
||||
@@ -92,6 +93,7 @@ def build_character_collages(
|
||||
resources_dir: Path,
|
||||
destination_dir: Path,
|
||||
missing_paths: Set[str] | None = None,
|
||||
image_filename: str = "minifig.jpg",
|
||||
image_height: int = 260,
|
||||
label_height: int = 44,
|
||||
spacing: int = 28,
|
||||
@@ -108,6 +110,7 @@ def build_character_collages(
|
||||
destination_dir,
|
||||
font,
|
||||
missing_paths=missing_paths,
|
||||
image_filename=image_filename,
|
||||
image_height=image_height,
|
||||
label_height=label_height,
|
||||
spacing=spacing,
|
||||
|
||||
@@ -86,13 +86,18 @@ def group_by_character(rows: Iterable[dict]) -> Dict[str, List[dict]]:
|
||||
|
||||
def load_missing_minifigs(path: Path) -> set[str]:
|
||||
"""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()
|
||||
with path.open() as csv_file:
|
||||
reader = csv.DictReader(csv_file)
|
||||
for row in reader:
|
||||
if row["status"] != "missing":
|
||||
continue
|
||||
if not row["path"].endswith("minifig.jpg"):
|
||||
if not row["path"].endswith(filename):
|
||||
continue
|
||||
missing.add(row["path"])
|
||||
return missing
|
||||
|
||||
43
scripts/plot_minifig_head_collages.py
Normal 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()
|
||||
@@ -85,6 +85,7 @@ def test_build_character_sets_and_collages(tmp_path: Path) -> None:
|
||||
resources_dir,
|
||||
destination_dir,
|
||||
missing_paths=missing_minifigs,
|
||||
image_filename="minifig.jpg",
|
||||
image_height=20,
|
||||
label_height=10,
|
||||
spacing=2,
|
||||
@@ -98,3 +99,28 @@ def test_build_character_sets_and_collages(tmp_path: Path) -> None:
|
||||
assert bob.size == (42, 30)
|
||||
assert claire.size == (20, 30)
|
||||
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)
|
||||
|
||||