1

Collage des pièces imprimées exclusives et variante sans impressions

This commit is contained in:
Richard Dern 2025-12-03 17:37:01 +01:00
parent a474e57694
commit a6e89bf6ef
5 changed files with 117 additions and 5 deletions

View File

@ -6,7 +6,7 @@ from typing import List
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnnotationBbox, OffsetImage from matplotlib.offsetbox import AnnotationBbox, OffsetImage
from PIL import Image from PIL import Image, ImageDraw, ImageFont
from lib.filesystem import ensure_parent_dir from lib.filesystem import ensure_parent_dir
@ -21,6 +21,22 @@ def load_part_rarity(path: Path) -> List[dict]:
return rows 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: def format_label(row: dict) -> str:
"""Formate létiquette de laxe vertical.""" """Formate létiquette de laxe vertical."""
return f"{row['part_num']}{row['part_name']}" return f"{row['part_num']}{row['part_name']}"
@ -84,3 +100,56 @@ def plot_part_rarity(
ensure_parent_dir(destination_path) ensure_parent_dir(destination_path)
fig.savefig(destination_path, dpi=150) fig.savefig(destination_path, dpi=150)
plt.close(fig) 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")

View File

@ -126,7 +126,8 @@ def build_part_rarity(
other_quantity = other_usage.get(part_num, 0) other_quantity = other_usage.get(part_num, 0)
total_quantity = entry["quantity"] + other_quantity total_quantity = entry["quantity"] + other_quantity
sample_set_num = sorted(entry["set_numbers"])[0] sample_set_num = sorted(entry["set_numbers"])[0]
sample_set_id = filtered_sets[sample_set_num]["set_id"] sample_set_row = filtered_sets[sample_set_num]
sample_set_id = sample_set_row["set_id"]
rows.append( rows.append(
{ {
"part_num": part_num, "part_num": part_num,
@ -135,6 +136,7 @@ def build_part_rarity(
"part_category": categories[part["part_cat_id"]], "part_category": categories[part["part_cat_id"]],
"sample_set_num": sample_set_num, "sample_set_num": sample_set_num,
"sample_set_id": sample_set_id, "sample_set_id": sample_set_id,
"sample_set_year": sample_set_row["year"],
"filtered_quantity": str(entry["quantity"]), "filtered_quantity": str(entry["quantity"]),
"filtered_set_count": str(len(entry["set_numbers"])), "filtered_set_count": str(len(entry["set_numbers"])),
"other_sets_quantity": str(other_quantity), "other_sets_quantity": str(other_quantity),
@ -156,6 +158,7 @@ def write_part_rarity(destination_path: Path, rows: Sequence[dict]) -> None:
"part_category", "part_category",
"sample_set_num", "sample_set_num",
"sample_set_id", "sample_set_id",
"sample_set_year",
"filtered_quantity", "filtered_quantity",
"filtered_set_count", "filtered_set_count",
"other_sets_quantity", "other_sets_quantity",

View File

@ -2,7 +2,7 @@
from pathlib import Path from pathlib import Path
from lib.plots.part_rarity import plot_part_rarity from lib.plots.part_rarity import plot_part_rarity, plot_printed_exclusive_parts
PART_RARITY_TOP_PATH = Path("data/intermediate/part_rarity_exclusive.csv") PART_RARITY_TOP_PATH = Path("data/intermediate/part_rarity_exclusive.csv")
@ -10,12 +10,15 @@ DESTINATION_PATH = Path("figures/step34/part_rarity.png")
RESOURCES_DIR = Path("figures/rebrickable") RESOURCES_DIR = Path("figures/rebrickable")
PART_RARITY_NO_PRINT_PATH = Path("data/intermediate/part_rarity_exclusive_no_print.csv") PART_RARITY_NO_PRINT_PATH = Path("data/intermediate/part_rarity_exclusive_no_print.csv")
DESTINATION_NO_PRINT = Path("figures/step34/part_rarity_no_print.png") DESTINATION_NO_PRINT = Path("figures/step34/part_rarity_no_print.png")
PART_RARITY_FULL_PATH = Path("data/intermediate/part_rarity.csv")
DESTINATION_PRINTED_COLLAGE = Path("figures/step34/printed_exclusive_parts.png")
def main() -> None: def main() -> None:
"""Charge le top des pièces rares et produit le graphique illustré.""" """Charge le top des pièces rares et produit le graphique illustré."""
plot_part_rarity(PART_RARITY_TOP_PATH, DESTINATION_PATH, resources_dir=RESOURCES_DIR) plot_part_rarity(PART_RARITY_TOP_PATH, DESTINATION_PATH, resources_dir=RESOURCES_DIR)
plot_part_rarity(PART_RARITY_NO_PRINT_PATH, DESTINATION_NO_PRINT, resources_dir=RESOURCES_DIR) plot_part_rarity(PART_RARITY_NO_PRINT_PATH, DESTINATION_NO_PRINT, resources_dir=RESOURCES_DIR)
plot_printed_exclusive_parts(PART_RARITY_FULL_PATH, DESTINATION_PRINTED_COLLAGE, resources_dir=RESOURCES_DIR)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -117,6 +117,7 @@ def test_build_part_rarity_counts_spares_and_ignores_categories(tmp_path: Path)
"part_category": "Bricks", "part_category": "Bricks",
"sample_set_num": "2000-1", "sample_set_num": "2000-1",
"sample_set_id": "2000", "sample_set_id": "2000",
"sample_set_year": "2021",
"filtered_quantity": "1", "filtered_quantity": "1",
"filtered_set_count": "1", "filtered_set_count": "1",
"other_sets_quantity": "0", "other_sets_quantity": "0",
@ -130,6 +131,7 @@ def test_build_part_rarity_counts_spares_and_ignores_categories(tmp_path: Path)
"part_category": "Bricks", "part_category": "Bricks",
"sample_set_num": "1000-1", "sample_set_num": "1000-1",
"sample_set_id": "1000", "sample_set_id": "1000",
"sample_set_year": "2020",
"filtered_quantity": "3", "filtered_quantity": "3",
"filtered_set_count": "2", "filtered_set_count": "2",
"other_sets_quantity": "3", "other_sets_quantity": "3",
@ -143,6 +145,7 @@ def test_build_part_rarity_counts_spares_and_ignores_categories(tmp_path: Path)
"part_category": "Large Buildable Figures", "part_category": "Large Buildable Figures",
"sample_set_num": "2000-1", "sample_set_num": "2000-1",
"sample_set_id": "2000", "sample_set_id": "2000",
"sample_set_year": "2021",
"filtered_quantity": "2", "filtered_quantity": "2",
"filtered_set_count": "1", "filtered_set_count": "1",
"other_sets_quantity": "4", "other_sets_quantity": "4",
@ -175,6 +178,7 @@ def test_write_part_rarity_outputs_csv(tmp_path: Path) -> None:
"part_category": "Bricks", "part_category": "Bricks",
"sample_set_num": "123-1", "sample_set_num": "123-1",
"sample_set_id": "123", "sample_set_id": "123",
"sample_set_year": "2020",
"filtered_quantity": "3", "filtered_quantity": "3",
"filtered_set_count": "2", "filtered_set_count": "2",
"other_sets_quantity": "3", "other_sets_quantity": "3",
@ -188,7 +192,7 @@ def test_write_part_rarity_outputs_csv(tmp_path: Path) -> None:
assert destination.exists() assert destination.exists()
content = destination.read_text().strip().splitlines() content = destination.read_text().strip().splitlines()
assert content[0] == ( assert content[0] == (
"part_num,part_name,part_cat_id,part_category,sample_set_num,sample_set_id,filtered_quantity,filtered_set_count," "part_num,part_name,part_cat_id,part_category,sample_set_num,sample_set_id,sample_set_year,filtered_quantity,filtered_set_count,"
"other_sets_quantity,catalog_total_quantity,filtered_share" "other_sets_quantity,catalog_total_quantity,filtered_share"
) )
assert content[1] == "p1,Brick 1x1,1,Bricks,123-1,123,3,2,3,6,0.5000" assert content[1] == "p1,Brick 1x1,1,Bricks,123-1,123,2020,3,2,3,6,0.5000"

View File

@ -0,0 +1,33 @@
"""Tests du collage des pièces imprimées exclusives."""
import matplotlib
from pathlib import Path
from PIL import Image
from lib.plots.part_rarity import plot_printed_exclusive_parts
matplotlib.use("Agg")
def test_plot_printed_exclusive_parts(tmp_path: Path) -> None:
"""Génère un collage des pièces imprimées exclusives avec images locales."""
data_path = tmp_path / "part_rarity.csv"
resources_dir = tmp_path / "figures" / "rebrickable"
resources_dir.mkdir(parents=True)
(resources_dir / "1000" / "rare_parts").mkdir(parents=True)
(resources_dir / "2000" / "rare_parts").mkdir(parents=True)
Image.new("RGB", (60, 40), color=(255, 0, 0)).save(resources_dir / "1000" / "rare_parts" / "p1.jpg")
Image.new("RGB", (60, 40), color=(0, 255, 0)).save(resources_dir / "2000" / "rare_parts" / "p2.jpg")
data_path.write_text(
"part_num,part_name,part_cat_id,part_category,sample_set_num,sample_set_id,sample_set_year,filtered_quantity,filtered_set_count,other_sets_quantity,catalog_total_quantity,filtered_share\n"
"p1,Slope print,1,Bricks,1000-1,1000,2020,3,2,0,3,1.0000\n"
"p2,Tile print,1,Bricks,2000-1,2000,2021,2,1,0,2,1.0000\n"
"p3,Tile plain,1,Bricks,2000-1,2000,2021,2,1,0,2,1.0000\n"
)
destination = tmp_path / "figures" / "step34" / "printed_exclusive_parts.png"
plot_printed_exclusive_parts(data_path, destination, resources_dir=resources_dir, columns=2)
assert destination.exists()
assert destination.stat().st_size > 0