You've already forked etude_lego_jurassic_world
Reviens à l’état fbf20e2 sans palettes par set
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
"""Préparation des palettes dominantes par set (hors minifigs)."""
|
||||
|
||||
import csv
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Sequence
|
||||
|
||||
import colorsys
|
||||
|
||||
from lib.filesystem import ensure_parent_dir
|
||||
from lib.rebrickable.color_ignores import is_ignored_color_rgb
|
||||
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 load_sets_enriched(path: Path) -> Dict[str, dict]:
|
||||
"""Indexe nom et année par set_num."""
|
||||
lookup: Dict[str, dict] = {}
|
||||
with path.open() as csv_file:
|
||||
reader = csv.DictReader(csv_file)
|
||||
for row in reader:
|
||||
lookup[row["set_num"]] = {"name": row["name"], "year": int(row["year"]), "set_id": row["set_id"]}
|
||||
return lookup
|
||||
|
||||
|
||||
def parse_rgb_hex(value: str) -> tuple[float, float, float]:
|
||||
"""Parse un code hexadécimal (RRGGBB) en composantes 0-1."""
|
||||
normalized = value.strip()
|
||||
if len(normalized) != 6:
|
||||
return (0.0, 0.0, 0.0)
|
||||
r = int(normalized[0:2], 16) / 255.0
|
||||
g = int(normalized[2:4], 16) / 255.0
|
||||
b = int(normalized[4:6], 16) / 255.0
|
||||
return (r, g, b)
|
||||
|
||||
|
||||
def hue_bucket(degrees: float) -> int:
|
||||
"""Regroupe les teintes en grandes familles pour l'affichage."""
|
||||
if degrees >= 330 or degrees < 30:
|
||||
return 0 # rouge
|
||||
if degrees < 90:
|
||||
return 1 # jaune / orangé
|
||||
if degrees < 150:
|
||||
return 2 # vert
|
||||
if degrees < 270:
|
||||
return 3 # bleu
|
||||
return 4 # violet
|
||||
|
||||
|
||||
def color_display_key(row: dict) -> tuple[float, int, float, str]:
|
||||
"""Clé de tri visuelle : teinte regroupée d'abord, puis luminosité."""
|
||||
r, g, b = parse_rgb_hex(row["color_rgb"])
|
||||
h, _s, v = colorsys.rgb_to_hsv(r, g, b)
|
||||
degrees = h * 360.0
|
||||
bucket = hue_bucket(degrees)
|
||||
return (bucket, degrees, v, row["color_name"])
|
||||
|
||||
|
||||
def build_top_colors_by_set(rows: Iterable[dict], sets_lookup: Dict[str, dict], top_n: int = 5) -> List[dict]:
|
||||
"""Sélectionne les top couleurs hors minifigs pour chaque set."""
|
||||
colors_by_set: Dict[str, List[dict]] = defaultdict(list)
|
||||
for row in rows:
|
||||
quantity = int(row["quantity_non_minifig"])
|
||||
if quantity <= 0:
|
||||
continue
|
||||
if is_ignored_color_rgb(row["color_rgb"]):
|
||||
continue
|
||||
set_num = row["set_num"]
|
||||
set_meta = sets_lookup.get(set_num)
|
||||
if set_meta is None:
|
||||
continue
|
||||
colors_by_set[set_num].append(
|
||||
{
|
||||
"set_num": set_num,
|
||||
"set_id": row["set_id"],
|
||||
"year": set_meta["year"],
|
||||
"name": set_meta["name"],
|
||||
"color_rgb": row["color_rgb"],
|
||||
"color_name": row["color_name"],
|
||||
"quantity": quantity,
|
||||
}
|
||||
)
|
||||
results: List[dict] = []
|
||||
for set_num, color_rows in colors_by_set.items():
|
||||
sorted_rows = sorted(color_rows, key=lambda r: (-r["quantity"], r["color_name"]))
|
||||
selected = sorted_rows[:top_n]
|
||||
ordered = sorted(selected, key=color_display_key)
|
||||
for rank, color_row in enumerate(ordered, start=1):
|
||||
results.append(
|
||||
{
|
||||
"set_num": color_row["set_num"],
|
||||
"set_id": color_row["set_id"],
|
||||
"name": color_row["name"],
|
||||
"year": str(color_row["year"]),
|
||||
"rank": str(rank),
|
||||
"color_rgb": color_row["color_rgb"],
|
||||
"color_name": color_row["color_name"],
|
||||
"quantity_non_minifig": str(color_row["quantity"]),
|
||||
}
|
||||
)
|
||||
results.sort(key=lambda r: (int(r["year"]), r["set_num"], r["name"], int(r["rank"])))
|
||||
return results
|
||||
|
||||
|
||||
def write_top_colors(path: Path, rows: Sequence[dict]) -> None:
|
||||
"""Écrit le CSV des couleurs dominantes par set."""
|
||||
ensure_parent_dir(path)
|
||||
fieldnames = [
|
||||
"set_num",
|
||||
"set_id",
|
||||
"name",
|
||||
"year",
|
||||
"rank",
|
||||
"color_rgb",
|
||||
"color_name",
|
||||
"quantity_non_minifig",
|
||||
]
|
||||
with path.open("w", newline="") as csv_file:
|
||||
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for row in rows:
|
||||
writer.writerow(row)
|
||||
@@ -1,143 +0,0 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user