Add the colour palette: white-shade filter and palette extraction

MIL-001 tasks 1 to 6.

- Pin the toolchain and tool configuration in pyproject.toml: Python 3.13,
  colorgram.py 1.2.0, and ruff, mypy (strict) and pytest as dev tools.
- Add is_white_shade and remove_white_shades (threshold 240) with tests that
  pin the boundary at 239 and 240.
- Add extract_palette, which reads the reference image with colorgram and drops
  white shades, with tests on synthetic images and on the reference image.
- Add the reference image assets/20260524_132700.jpg and a note on it.
- Record the code review against QC-PY-001 as RC-005 (verdict Go).

Closes #1
Closes #2
Closes #3
Closes #4
Closes #5
Closes #6

Task: MIL-001#1
Task: MIL-001#2
Task: MIL-001#3
Task: MIL-001#4
Task: MIL-001#5
Task: MIL-001#6
This commit is contained in:
2026-10-07 23:49:38 +08:00
parent b823d1f405
commit b7119ef907
7 changed files with 295 additions and 1 deletions
+43
View File
@@ -0,0 +1,43 @@
"""Hirst-style spot painting drawn with turtle."""
from collections.abc import Iterable
from pathlib import Path
import colorgram
type Colour = tuple[int, int, int]
"""A colour as red, green and blue values from 0 to 255."""
REFERENCE_IMAGE_PATH = (
Path(__file__).resolve().parent.parent / "assets" / "20260524_132700.jpg"
)
# More than a Hirst painting has colours; colorgram returns fewer when the image
# has fewer, so the palette is never padded.
EXTRACTED_COLOUR_COUNT = 30
# A colour counts as a white shade when red, green and blue are all at or above
# this value: such dots are invisible on the white background (SC3 of BC-001).
WHITE_THRESHOLD = 240
def is_white_shade(colour: Colour) -> bool:
"""Return whether red, green and blue are all at or above the threshold."""
return all(channel >= WHITE_THRESHOLD for channel in colour)
def remove_white_shades(colours: Iterable[Colour]) -> list[Colour]:
"""Return the colours without white shades, in their original order."""
return [colour for colour in colours if not is_white_shade(colour)]
def extract_palette(
image_path: Path, colour_count: int = EXTRACTED_COLOUR_COUNT
) -> list[Colour]:
"""Return the image's colours without white shades, most common first.
Raises FileNotFoundError when the image does not exist.
"""
extracted = colorgram.extract(str(image_path), colour_count)
colours = [(found.rgb.r, found.rgb.g, found.rgb.b) for found in extracted]
return remove_white_shades(colours)