1

Filtre les collages globaux sur les personnages multi-variations

This commit is contained in:
Richard Dern 2025-12-03 23:30:31 +01:00
parent 99d47bec6a
commit cdc39cdfa0
4 changed files with 102 additions and 5 deletions

View File

@ -361,12 +361,15 @@ 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`
4. `python -m scripts.plot_minifig_collage_grids`
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, 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, 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, annotés avec lannée, le set (avec `*` si possédé) et le `part_num` de la tête.
- `figures/step32/minifig_characters_all.png` : superposition verticale des frises minifigs pour les personnages ayant au moins deux variations.
- `figures/step32/minifig_heads_all.png` : superposition verticale des frises têtes pour les personnages ayant au moins deux variations.
### Étape 33 : réutilisation des têtes de minifigs dans le catalogue

View File

@ -121,3 +121,41 @@ def build_character_collages(
)
)
return generated
def filter_characters_with_variations(
grouped_entries: Dict[str, Sequence[dict]],
min_variations: int = 2,
) -> Dict[str, Sequence[dict]]:
"""Conserve uniquement les personnages ayant un nombre minimal de variations (fig_num distincts)."""
filtered: Dict[str, Sequence[dict]] = {}
for character, entries in grouped_entries.items():
variations = {entry["fig_num"] for entry in entries}
if len(variations) >= min_variations:
filtered[character] = entries
return filtered
def stack_collages(
source_dir: Path,
destination_path: Path,
spacing: int = 18,
allowed_stems: Set[str] | None = None,
) -> None:
"""Superpose verticalement toutes les frises présentes dans un dossier."""
images: List[Image.Image] = []
for path in sorted(source_dir.glob("*.png")):
if allowed_stems is not None and path.stem not in allowed_stems:
continue
images.append(Image.open(path).convert("RGB"))
if not images:
return
max_width = max(image.width for image in images)
total_height = sum(image.height for image in images) + spacing * (len(images) - 1)
canvas = Image.new("RGB", (max_width, total_height), (255, 255, 255))
y = 0
for image in images:
canvas.paste(image, (0, y))
y += image.height + spacing
ensure_parent_dir(destination_path)
canvas.save(destination_path)

View File

@ -0,0 +1,41 @@
"""Assemble des grilles verticales filtrées sur les personnages multi-variations."""
from pathlib import Path
from lib.plots.minifig_character_collages import filter_characters_with_variations, stack_collages
from lib.rebrickable.minifig_character_sets import (
build_character_sets,
group_by_character,
load_minifigs_by_set,
load_sets,
)
from lib.rebrickable.resources import sanitize_name
MINIFIGS_BY_SET_PATH = Path("data/intermediate/minifigs_by_set.csv")
SETS_ENRICHED_PATH = Path("data/intermediate/sets_enriched.csv")
CHARACTERS_DIR = Path("figures/step32/minifig_characters")
HEADS_DIR = Path("figures/step32/minifig_heads")
CHARACTERS_DEST = Path("figures/step32/minifig_characters_all.png")
HEADS_DEST = Path("figures/step32/minifig_heads_all.png")
EXCLUDED_CHARACTERS = ["Figurant"]
def main() -> None:
"""Superpose les frises de personnages ayant au moins deux variations."""
minifigs = load_minifigs_by_set(MINIFIGS_BY_SET_PATH)
sets_lookup = load_sets(SETS_ENRICHED_PATH)
character_sets = build_character_sets(minifigs, sets_lookup, excluded_characters=EXCLUDED_CHARACTERS)
grouped = group_by_character(character_sets)
filtered = filter_characters_with_variations(grouped, min_variations=2)
keep = {sanitize_name(name) for name in filtered.keys()}
for source_dir, destination in (
(CHARACTERS_DIR, CHARACTERS_DEST),
(HEADS_DIR, HEADS_DEST),
):
stack_collages(source_dir, destination, spacing=12, allowed_stems=keep)
if __name__ == "__main__":
main()

View File

@ -4,7 +4,7 @@ from pathlib import Path
from PIL import Image
from lib.plots.minifig_character_collages import build_character_collages
from lib.plots.minifig_character_collages import build_character_collages, stack_collages
from lib.rebrickable.minifig_character_sets import build_character_sets, group_by_character
from lib.rebrickable.resources import sanitize_name
@ -114,7 +114,7 @@ def test_build_character_sets_and_collages(tmp_path: Path) -> None:
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),
{"Bob": group_by_character(character_sets)["Bob"]},
resources_dir,
heads_destination,
missing_paths=head_missing,
@ -123,8 +123,23 @@ def test_build_character_sets_and_collages(tmp_path: Path) -> None:
label_height=8,
spacing=2,
)
assert len(head_collages) == 3
alice_head = Image.open(heads_destination / f"{sanitize_name('Alice')}.png")
assert len(head_collages) == 1
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)
def test_stack_collages_with_filter(tmp_path: Path) -> None:
"""Assemble une grille verticale en filtrant les frises autorisées."""
source_dir = tmp_path / "collages"
source_dir.mkdir()
img1 = source_dir / "Alice.png"
img2 = source_dir / "Bob.png"
Image.new("RGB", (10, 6), (100, 100, 100)).save(img1)
Image.new("RGB", (14, 8), (120, 120, 120)).save(img2)
destination = tmp_path / "stacked.png"
stack_collages(source_dir, destination, spacing=2, allowed_stems={"Bob"})
result = Image.open(destination)
assert result.size == (14, 8)
assert result.getpixel((0, 0)) == (120, 120, 120)