1

156 lines
5.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Visualisation des pièces les plus rares observées dans les sets filtrés."""
import csv
from pathlib import Path
from typing import List
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, OffsetImage
from PIL import Image, ImageDraw, ImageFont
from lib.filesystem import ensure_parent_dir
def load_part_rarity(path: Path) -> List[dict]:
"""Charge le CSV des pièces rares."""
rows: List[dict] = []
with path.open() as csv_file:
reader = csv.DictReader(csv_file)
for row in reader:
rows.append(row)
return rows
def select_printed_exclusive(rows: List[dict], resources_dir: Path) -> List[dict]:
"""Filtre les pièces imprimées exclusives aux sets filtrés disposant d'une image locale."""
filtered: List[dict] = []
for row in rows:
if row.get("other_sets_quantity", "0") != "0":
continue
if "print" not in row["part_name"].lower():
continue
image_path = resources_dir / row.get("sample_set_id", "") / "rare_parts" / f"{row['part_num']}.jpg"
if not image_path.exists():
continue
filtered.append(row)
filtered.sort(key=lambda r: (r["part_name"], r["part_num"]))
return filtered
def format_label(row: dict) -> str:
"""Formate létiquette de laxe vertical."""
return f"{row['part_num']}{row['part_name']}"
def load_part_image(row: dict, resources_dir: Path) -> Image.Image | None:
"""Charge l'image associée à une pièce si elle est disponible."""
path = resources_dir / row["sample_set_id"] / "rare_parts" / f"{row['part_num']}.jpg"
if not path.exists():
return None
return Image.open(path)
def plot_part_rarity(
path: Path,
destination_path: Path,
resources_dir: Path = Path("figures/rebrickable"),
show_images: bool = True,
) -> None:
"""Trace un bar chart horizontal des pièces les plus rares avec leurs visuels."""
rows = load_part_rarity(path)
selected = rows
labels = [format_label(row) for row in selected]
filtered_counts = [int(row["filtered_quantity"]) for row in selected]
other_counts = [int(row["other_sets_quantity"]) for row in selected]
positions = list(range(len(selected)))
fig, ax = plt.subplots(figsize=(13, 0.55 * len(selected) + 1.4))
ax.barh(positions, filtered_counts, color="#1f78b4", label="Sets filtrés")
ax.barh(positions, other_counts, left=filtered_counts, color="#b2df8a", label="Autres sets")
ax.set_yticks(positions)
ax.set_yticklabels(labels)
ax.set_xlabel("Occurrences de la pièce (rechanges incluses)")
ax.grid(axis="x", linestyle="--", alpha=0.4)
ax.legend()
if show_images:
max_count = max((f + o) for f, o in zip(filtered_counts, other_counts)) if selected else 0
pad = max_count * 0.15 if max_count > 0 else 1.0
ax.set_xlim(left=-pad, right=max_count + pad * 0.3)
for row, pos in zip(selected, positions):
image = load_part_image(row, resources_dir)
if image is None:
continue
target_height = 28
ratio = target_height / image.height
resized = image.resize((int(image.width * ratio), target_height))
imagebox = OffsetImage(resized)
ab = AnnotationBbox(
imagebox,
(-pad * 0.45, pos),
xycoords=("data", "data"),
box_alignment=(0.5, 0.5),
frameon=False,
)
ax.add_artist(ab)
fig.subplots_adjust(left=0.42)
fig.tight_layout()
ensure_parent_dir(destination_path)
fig.savefig(destination_path, dpi=150)
plt.close(fig)
def plot_printed_exclusive_parts(
path: Path,
destination_path: Path,
resources_dir: Path = Path("figures/rebrickable"),
columns: int = 5,
) -> None:
"""Assemble les images des pièces imprimées exclusives aux sets filtrés."""
rows = load_part_rarity(path)
selected = select_printed_exclusive(rows, resources_dir)
selected.sort(key=lambda r: (int(r.get("sample_set_year", "9999") or 9999), r["sample_set_num"], r["part_num"]))
if not selected:
return
images: List[Image.Image] = []
labels: List[str] = []
for row in selected:
image_path = resources_dir / row["sample_set_id"] / "rare_parts" / f"{row['part_num']}.jpg"
img = Image.open(image_path).convert("RGBA")
max_side = 180
ratio = min(max_side / img.width, max_side / img.height, 1.0)
if ratio < 1.0:
img = img.resize((int(img.width * ratio), int(img.height * ratio)))
images.append(img)
labels.append(f"{row.get('sample_set_year', '')}{row['sample_set_num']}")
columns = max(1, columns)
rows_count = (len(images) + columns - 1) // columns
cell_width = 220
font = ImageFont.load_default()
draw_temp = ImageDraw.Draw(Image.new("RGB", (10, 10)))
def measure(text: str) -> tuple[int, int]:
bbox = draw_temp.textbbox((0, 0), text, font=font)
return bbox[2] - bbox[0], bbox[3] - bbox[1]
text_height = max(measure(label)[1] for label in labels)
cell_height = 190 + text_height + 14
width = columns * cell_width
height = rows_count * cell_height
canvas = Image.new("RGBA", (width, height), (255, 255, 255, 255))
draw = ImageDraw.Draw(canvas)
for index, (img, label) in enumerate(zip(images, labels)):
col = index % columns
row_idx = index // columns
x = col * cell_width + (cell_width - img.width) // 2
y = row_idx * cell_height + 8
canvas.paste(img, (x, y), img)
text_width, _ = measure(label)
text_x = col * cell_width + (cell_width - text_width) // 2
text_y = y + img.height + 6
draw.text((text_x, text_y), label, fill="#111111", font=font)
ensure_parent_dir(destination_path)
canvas.convert("RGB").save(destination_path, "PNG")