Filtre les collages globaux sur les personnages multi-variations
This commit is contained in:
parent
99d47bec6a
commit
cdc39cdfa0
@ -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 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, annotés avec l’anné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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
41
scripts/plot_minifig_collage_grids.py
Normal file
41
scripts/plot_minifig_collage_grids.py
Normal 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()
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user