Filtre les couleurs ignorées et aligne les palettes
This commit is contained in:
parent
7b6045941f
commit
812fd4a862
@ -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
|
||||
|
||||
|
||||
@ -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":
|
||||
|
||||
8
lib/rebrickable/color_ignores.py
Normal file
8
lib/rebrickable/color_ignores.py
Normal 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
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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"],
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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"],
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user