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`
|
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`
|
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 :
|
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.
|
- `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_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_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
|
### Étape 33 : réutilisation des têtes de minifigs dans le catalogue
|
||||||
|
|
||||||
|
|||||||
@ -121,3 +121,41 @@ def build_character_collages(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return generated
|
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 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.minifig_character_sets import build_character_sets, group_by_character
|
||||||
from lib.rebrickable.resources import sanitize_name
|
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"
|
head_image = resources_dir / "1000" / sanitize_name("Alice") / "head.jpg"
|
||||||
create_image(head_image, (10, 120, 60), (8, 12))
|
create_image(head_image, (10, 120, 60), (8, 12))
|
||||||
head_collages = build_character_collages(
|
head_collages = build_character_collages(
|
||||||
group_by_character(character_sets),
|
{"Bob": group_by_character(character_sets)["Bob"]},
|
||||||
resources_dir,
|
resources_dir,
|
||||||
heads_destination,
|
heads_destination,
|
||||||
missing_paths=head_missing,
|
missing_paths=head_missing,
|
||||||
@ -123,8 +123,23 @@ def test_build_character_sets_and_collages(tmp_path: Path) -> None:
|
|||||||
label_height=8,
|
label_height=8,
|
||||||
spacing=2,
|
spacing=2,
|
||||||
)
|
)
|
||||||
assert len(head_collages) == 3
|
assert len(head_collages) == 1
|
||||||
alice_head = Image.open(heads_destination / f"{sanitize_name('Alice')}.png")
|
|
||||||
bob_head = Image.open(heads_destination / f"{sanitize_name('Bob')}.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)
|
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