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`
|
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.
|
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
|
### É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.filesystem import ensure_parent_dir
|
||||||
from lib.color_sort import lab_sort_key, sort_hex_colors_lab
|
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.parts_inventory import normalize_boolean
|
||||||
from lib.rebrickable.stats import read_rows
|
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)}
|
colors_lookup = {(row["rgb"], normalize_boolean(row["is_trans"])): row["name"] for row in read_rows(colors_path)}
|
||||||
totals: Dict[Tuple[str, str], int] = {}
|
totals: Dict[Tuple[str, str], int] = {}
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
if is_ignored_color_rgb(row["color_rgb"]):
|
||||||
|
continue
|
||||||
if minifig_only and row.get("is_minifig_part") != "true":
|
if minifig_only and row.get("is_minifig_part") != "true":
|
||||||
continue
|
continue
|
||||||
if not minifig_only and row.get("is_minifig_part") == "true":
|
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 typing import Dict, Iterable, List, Tuple
|
||||||
|
|
||||||
from lib.filesystem import ensure_parent_dir
|
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]:
|
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."""
|
"""Agrège les quantités par set et par couleur."""
|
||||||
totals: Dict[Tuple[str, str, str, str, str], dict] = {}
|
totals: Dict[Tuple[str, str, str, str, str], dict] = {}
|
||||||
for row in parts:
|
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"])
|
key = (row["set_num"], row["set_id"], row["year"], row["color_rgb"], row["is_translucent"])
|
||||||
existing = totals.get(key)
|
existing = totals.get(key)
|
||||||
if existing is None:
|
if existing is None:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
from typing import Dict, Iterable, List, Set, Tuple
|
from typing import Dict, Iterable, List, Set, Tuple
|
||||||
|
|
||||||
from lib.rebrickable.colors_by_set import build_colors_lookup
|
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
|
from lib.rebrickable.stats import read_rows
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +40,8 @@ def aggregate_head_colors_by_set(
|
|||||||
continue
|
continue
|
||||||
if row["is_spare"] == "true":
|
if row["is_spare"] == "true":
|
||||||
continue
|
continue
|
||||||
|
if is_ignored_color_rgb(row["color_rgb"]):
|
||||||
|
continue
|
||||||
key = (row["set_num"], row["set_id"], row["year"], row["color_rgb"])
|
key = (row["set_num"], row["set_id"], row["year"], row["color_rgb"])
|
||||||
existing = aggregates.get(key)
|
existing = aggregates.get(key)
|
||||||
if existing is None:
|
if existing is None:
|
||||||
|
|||||||
@ -5,7 +5,10 @@ from collections import defaultdict
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, List, Sequence
|
from typing import Dict, Iterable, List, Sequence
|
||||||
|
|
||||||
|
import colorsys
|
||||||
|
|
||||||
from lib.filesystem import ensure_parent_dir
|
from lib.filesystem import ensure_parent_dir
|
||||||
|
from lib.rebrickable.color_ignores import is_ignored_color_rgb
|
||||||
from lib.rebrickable.stats import read_rows
|
from lib.rebrickable.stats import read_rows
|
||||||
|
|
||||||
|
|
||||||
@ -24,6 +27,39 @@ def load_sets_enriched(path: Path) -> Dict[str, dict]:
|
|||||||
return lookup
|
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]:
|
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."""
|
"""Sélectionne les top couleurs hors minifigs pour chaque set."""
|
||||||
colors_by_set: Dict[str, List[dict]] = defaultdict(list)
|
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"])
|
quantity = int(row["quantity_non_minifig"])
|
||||||
if quantity <= 0:
|
if quantity <= 0:
|
||||||
continue
|
continue
|
||||||
|
if is_ignored_color_rgb(row["color_rgb"]):
|
||||||
|
continue
|
||||||
set_num = row["set_num"]
|
set_num = row["set_num"]
|
||||||
set_meta = sets_lookup.get(set_num)
|
set_meta = sets_lookup.get(set_num)
|
||||||
if set_meta is None:
|
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] = []
|
results: List[dict] = []
|
||||||
for set_num, color_rows in colors_by_set.items():
|
for set_num, color_rows in colors_by_set.items():
|
||||||
sorted_rows = sorted(color_rows, key=lambda r: (-r["quantity"], r["color_name"]))
|
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(
|
results.append(
|
||||||
{
|
{
|
||||||
"set_num": color_row["set_num"],
|
"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"]),
|
"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
|
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"],
|
["3002", "FFFFFF", "false", "1000-1", "1000", "2020", "1", "true", "false"],
|
||||||
["3003", "000000", "true", "1000-1", "1000", "2020", "4", "false", "true"],
|
["3003", "000000", "true", "1000-1", "1000", "2020", "4", "false", "true"],
|
||||||
["4001", "FFFFFF", "false", "2000-1", "2000", "2021", "3", "false", "true"],
|
["4001", "FFFFFF", "false", "2000-1", "2000", "2021", "3", "false", "true"],
|
||||||
|
["4002", "0033B2", "false", "2000-1", "2000", "2021", "5", "false", "false"],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
write_csv(
|
write_csv(
|
||||||
@ -50,6 +51,7 @@ def test_aggregate_colors_by_set(tmp_path: Path) -> None:
|
|||||||
[
|
[
|
||||||
["1", "White", "FFFFFF", "False", "0", "0", "0", "0"],
|
["1", "White", "FFFFFF", "False", "0", "0", "0", "0"],
|
||||||
["2", "Trans-Black", "000000", "True", "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"],
|
["3001", "FFFFFF", "false", "1000", "2", "false"],
|
||||||
["3002", "000000", "true", "1000", "5", "false"],
|
["3002", "000000", "true", "1000", "5", "false"],
|
||||||
["3003", "FF0000", "false", "1000", "1", "true"],
|
["3003", "FF0000", "false", "1000", "1", "true"],
|
||||||
|
["3004", "0033B2", "false", "1000", "10", "false"],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
write_csv(
|
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"],
|
["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", "2", "false", "true"],
|
||||||
["3626b", "FFE1BD", "false", "2000-1", "2000", "2021", "1", "true", "true"],
|
["3626b", "FFE1BD", "false", "2000-1", "2000", "2021", "1", "true", "true"],
|
||||||
|
["3626b", "0033B2", "false", "2000-1", "2000", "2021", "1", "false", "true"],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
write_csv(
|
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"],
|
["1", "Light Flesh", "FFE1BD", "False", "0", "0", "0", "0"],
|
||||||
["2", "Medium Dark Flesh", "E7B68F", "False", "0", "0", "0", "0"],
|
["2", "Medium Dark Flesh", "E7B68F", "False", "0", "0", "0", "0"],
|
||||||
["3", "White", "FFFFFF", "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,444444,false,Green,2,2,0,2\n"
|
||||||
"123-1,123,2020,555555,false,Yellow,1,1,0,1\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,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",
|
"124-1,124,2021,aaaaaa,false,Gray,4,4,4,0\n",
|
||||||
)
|
)
|
||||||
sets_path = tmp_path / "sets_enriched.csv"
|
sets_path = tmp_path / "sets_enriched.csv"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user