1

Ajoute l’étape 28 des palettes perceptuelles

This commit is contained in:
2025-12-02 15:50:41 +01:00
parent 74f8fa57e1
commit 909a1eae71
6 changed files with 383 additions and 2 deletions

View File

@@ -0,0 +1,143 @@
"""Construction de palettes perceptuelles (top 5) par set hors minifigs."""
from pathlib import Path
from typing import Dict, Iterable, List, Sequence, Set
from lib.rebrickable.set_color_swatches import color_display_key, load_sets_enriched, parse_rgb_hex, hue_bucket
from lib.rebrickable.stats import read_rows
def load_colors_by_set(path: Path) -> List[dict]:
"""Charge colors_by_set.csv."""
return read_rows(path)
def compute_shares(rows: Iterable[dict]) -> Dict[str, List[dict]]:
"""Calcule les parts relatives de couleurs hors minifigs pour chaque set."""
by_set: Dict[str, List[dict]] = {}
totals: Dict[str, int] = {}
for row in rows:
quantity = int(row["quantity_non_minifig"])
if quantity <= 0:
continue
set_num = row["set_num"]
totals[set_num] = totals.get(set_num, 0) + quantity
current = by_set.get(set_num)
if current is None:
by_set[set_num] = [row]
else:
current.append(row)
shares: Dict[str, List[dict]] = {}
for set_num, color_rows in by_set.items():
total = totals.get(set_num, 0)
if total == 0:
continue
shares[set_num] = []
for row in color_rows:
share = int(row["quantity_non_minifig"]) / total
shares[set_num].append(
{
"set_num": row["set_num"],
"set_id": row["set_id"],
"name": row.get("name", ""),
"year": row["year"],
"color_rgb": row["color_rgb"],
"color_name": row["color_name"],
"quantity_non_minifig": row["quantity_non_minifig"],
"share_non_minifig": f"{share:.5f}",
}
)
return shares
def select_diverse_palette(rows: List[dict], top_n: int) -> List[dict]:
"""Sélectionne une palette diversifiée : priorité à la part et à la variété de teinte."""
sorted_by_share = sorted(rows, key=lambda r: (-float(r["share_non_minifig"]), r["color_name"]))
selected: List[dict] = []
buckets_used: Set[int] = set()
for row in sorted_by_share:
r, g, b = parse_rgb_hex(row["color_rgb"])
h, _s, _v = __import__("colorsys").rgb_to_hsv(r, g, b)
bucket = hue_bucket(h * 360.0)
if bucket in buckets_used:
continue
selected.append(row)
buckets_used.add(bucket)
if len(selected) == top_n:
break
if len(selected) < top_n:
for row in sorted_by_share:
if row in selected:
continue
selected.append(row)
if len(selected) == top_n:
break
while len(selected) < top_n:
selected.append(
{
"set_num": rows[0]["set_num"] if rows else "",
"set_id": rows[0]["set_id"] if rows else "",
"name": rows[0]["name"] if rows else "",
"year": rows[0]["year"] if rows else "",
"color_rgb": "",
"color_name": "N/A",
"quantity_non_minifig": "0",
"share_non_minifig": "0",
}
)
ordered = sorted(selected, key=color_display_key)
for rank, row in enumerate(ordered, start=1):
row["rank"] = str(rank)
return ordered[:top_n]
def build_perceptual_swatches(rows: Iterable[dict], sets_lookup: Dict[str, dict], top_n: int = 5) -> List[dict]:
"""Construit les palettes perceptuelles (parts relatives + diversité de teinte)."""
shares = compute_shares(rows)
swatches: List[dict] = []
for set_num, color_rows in shares.items():
set_meta = sets_lookup.get(set_num)
if set_meta is None:
continue
selected = select_diverse_palette(color_rows, top_n)
for row in selected:
swatches.append(
{
"set_num": set_num,
"set_id": set_meta["set_id"],
"name": set_meta["name"],
"year": str(set_meta["year"]),
"rank": row["rank"],
"color_rgb": row["color_rgb"],
"color_name": row["color_name"],
"share_non_minifig": row["share_non_minifig"],
"quantity_non_minifig": row["quantity_non_minifig"],
}
)
swatches.sort(key=lambda r: (int(r["year"]), r["set_num"], int(r["rank"])))
return swatches
def write_perceptual_swatches(path: Path, rows: Sequence[dict]) -> None:
"""Écrit le CSV des palettes perceptuelles."""
from lib.filesystem import ensure_parent_dir
ensure_parent_dir(path)
fieldnames = [
"set_num",
"set_id",
"name",
"year",
"rank",
"color_rgb",
"color_name",
"share_non_minifig",
"quantity_non_minifig",
]
with path.open("w", newline="") as csv_file:
import csv
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
for row in rows:
writer.writerow(row)