1

Filtre les couleurs ignorées et aligne les palettes

This commit is contained in:
Richard Dern 2025-12-02 15:07:35 +01:00
parent 7b6045941f
commit 812fd4a862
10 changed files with 66 additions and 2 deletions

View File

@ -164,6 +164,7 @@ Le script lit `data/intermediate/parts_filtered.csv` et `data/final/stats.csv` (
2. `python -m scripts.plot_colors_grid`
Le script lit `data/intermediate/parts_filtered.csv` et `data/raw/colors.csv`, puis génère deux visuels : `figures/step12/colors_grid.png` pour l'ensemble des pièces (rechanges incluses) et `figures/step12/colors_grid_minifigs.png` pour la seule palette des minifigs. Les couleurs sont triées perceptuellement et mises en scène sur une grille hexagonale.
Les codes couleurs 0033B2 et 05131D sont ignorés pour ne pas polluer les palettes.
### Étape 13 : palette de couleurs par set

View File

@ -9,6 +9,7 @@ from matplotlib.lines import Line2D
from lib.filesystem import ensure_parent_dir
from lib.color_sort import lab_sort_key, sort_hex_colors_lab
from lib.rebrickable.color_ignores import is_ignored_color_rgb
from lib.rebrickable.parts_inventory import normalize_boolean
from lib.rebrickable.stats import read_rows
@ -30,6 +31,8 @@ def load_used_colors(parts_path: Path, colors_path: Path, minifig_only: bool = F
colors_lookup = {(row["rgb"], normalize_boolean(row["is_trans"])): row["name"] for row in read_rows(colors_path)}
totals: Dict[Tuple[str, str], int] = {}
for row in rows:
if is_ignored_color_rgb(row["color_rgb"]):
continue
if minifig_only and row.get("is_minifig_part") != "true":
continue
if not minifig_only and row.get("is_minifig_part") == "true":

View File

@ -0,0 +1,8 @@
"""Couleurs à exclure des palettes."""
IGNORED_COLOR_RGB = {"0033b2", "05131d"}
def is_ignored_color_rgb(rgb: str) -> bool:
"""Retourne vrai si le code couleur doit être ignoré."""
return rgb.strip().lower() in IGNORED_COLOR_RGB

View File

@ -5,6 +5,7 @@ from pathlib import Path
from typing import Dict, Iterable, List, Tuple
from lib.filesystem import ensure_parent_dir
from lib.rebrickable.color_ignores import is_ignored_color_rgb
def load_parts(parts_path: Path) -> List[dict]:
@ -28,6 +29,8 @@ def aggregate_colors_by_set(parts: Iterable[dict], colors_lookup: Dict[Tuple[str
"""Agrège les quantités par set et par couleur."""
totals: Dict[Tuple[str, str, str, str, str], dict] = {}
for row in parts:
if is_ignored_color_rgb(row["color_rgb"]):
continue
key = (row["set_num"], row["set_id"], row["year"], row["color_rgb"], row["is_translucent"])
existing = totals.get(key)
if existing is None:

View File

@ -5,6 +5,7 @@ from pathlib import Path
from typing import Dict, Iterable, List, Set, Tuple
from lib.rebrickable.colors_by_set import build_colors_lookup
from lib.rebrickable.color_ignores import is_ignored_color_rgb
from lib.rebrickable.stats import read_rows
@ -39,6 +40,8 @@ def aggregate_head_colors_by_set(
continue
if row["is_spare"] == "true":
continue
if is_ignored_color_rgb(row["color_rgb"]):
continue
key = (row["set_num"], row["set_id"], row["year"], row["color_rgb"])
existing = aggregates.get(key)
if existing is None:

View File

@ -5,7 +5,10 @@ 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
@ -24,6 +27,39 @@ def load_sets_enriched(path: Path) -> Dict[str, dict]:
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)
@ -31,6 +67,8 @@ def build_top_colors_by_set(rows: Iterable[dict], sets_lookup: Dict[str, dict],
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:
@ -49,7 +87,9 @@ def build_top_colors_by_set(rows: Iterable[dict], sets_lookup: Dict[str, dict],
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"]))
for rank, color_row in enumerate(sorted_rows[:top_n], start=1):
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"],
@ -62,7 +102,7 @@ def build_top_colors_by_set(rows: Iterable[dict], sets_lookup: Dict[str, dict],
"quantity_non_minifig": str(color_row["quantity"]),
}
)
results.sort(key=lambda r: (int(r["year"]), r["name"], r["set_num"], int(r["rank"])))
results.sort(key=lambda r: (int(r["year"]), r["set_num"], r["name"], int(r["rank"])))
return results

View File

@ -42,6 +42,7 @@ def test_aggregate_colors_by_set(tmp_path: Path) -> None:
["3002", "FFFFFF", "false", "1000-1", "1000", "2020", "1", "true", "false"],
["3003", "000000", "true", "1000-1", "1000", "2020", "4", "false", "true"],
["4001", "FFFFFF", "false", "2000-1", "2000", "2021", "3", "false", "true"],
["4002", "0033B2", "false", "2000-1", "2000", "2021", "5", "false", "false"],
],
)
write_csv(
@ -50,6 +51,7 @@ def test_aggregate_colors_by_set(tmp_path: Path) -> None:
[
["1", "White", "FFFFFF", "False", "0", "0", "0", "0"],
["2", "Trans-Black", "000000", "True", "0", "0", "0", "0"],
["3", "Ignored Blue", "0033B2", "False", "0", "0", "0", "0"],
],
)

View File

@ -42,6 +42,7 @@ def test_plot_colors_grid(tmp_path: Path) -> None:
["3001", "FFFFFF", "false", "1000", "2", "false"],
["3002", "000000", "true", "1000", "5", "false"],
["3003", "FF0000", "false", "1000", "1", "true"],
["3004", "0033B2", "false", "1000", "10", "false"],
],
)
write_csv(

View File

@ -49,6 +49,7 @@ def test_extract_minifig_heads_and_colors(tmp_path: Path) -> None:
["3001", "FFFFFF", "false", "1000-1", "1000", "2020", "2", "false", "false"],
["3626b", "FFE1BD", "false", "2000-1", "2000", "2021", "2", "false", "true"],
["3626b", "FFE1BD", "false", "2000-1", "2000", "2021", "1", "true", "true"],
["3626b", "0033B2", "false", "2000-1", "2000", "2021", "1", "false", "true"],
],
)
write_csv(
@ -67,6 +68,7 @@ def test_extract_minifig_heads_and_colors(tmp_path: Path) -> None:
["1", "Light Flesh", "FFE1BD", "False", "0", "0", "0", "0"],
["2", "Medium Dark Flesh", "E7B68F", "False", "0", "0", "0", "0"],
["3", "White", "FFFFFF", "False", "0", "0", "0", "0"],
["4", "Ignored Blue", "0033B2", "False", "0", "0", "0", "0"],
],
)

View File

@ -22,6 +22,7 @@ def test_build_top_colors_by_set_selects_top5_non_minifig(tmp_path: Path) -> Non
"123-1,123,2020,444444,false,Green,2,2,0,2\n"
"123-1,123,2020,555555,false,Yellow,1,1,0,1\n"
"123-1,123,2020,666666,false,Pink,1,1,0,1\n"
"123-1,123,2020,0033B2,false,Ignored Blue,50,50,0,50\n"
"124-1,124,2021,aaaaaa,false,Gray,4,4,4,0\n",
)
sets_path = tmp_path / "sets_enriched.csv"