Ajoute les frises de têtes et enrichit les étiquettes
This commit is contained in:
parent
4d37323c15
commit
43062b7ed4
@ -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 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.
|
- `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.
|
- 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.
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
43
scripts/plot_minifig_head_collages.py
Normal file
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,
|
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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user