diff --git a/CHANGELOG.md b/CHANGELOG.md index caec5c8..fb1031d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`Color.rgb_float`**: New property returning the canonical color as float sRGB components in `[0, 1]`, plus a `Color.from_srgb_float(...)` constructor for building colors from continuous (non-quantized) centroids. +- **Honest color attribute names**: `Color.frequency` (relative cluster weight + in `[0, 1]`, summing to 1), `Color.alpha` (raw 0–255 channel), and + `Color.opacity` (the same value as the old `.weight`, in `[0, 1]`). These + replace `.freq`, `.a`, and `.weight` respectively. +- **String acceptance for `mode` and `colorspace`**: both now accept an enum + member, its value, or its case-insensitive name (e.g. `mode="KM"`, + `mode="KMeans"`). ### Changed @@ -42,6 +49,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Spatial dimensions are no longer needed since extractors reshape by the array's actual length. Custom extractors implementing the protocol must update their signature accordingly. +- **CLI option names aligned with the library**: `--palette-size` (canonical; + `--n` kept as an alias) and `--max-workers` (canonical; `--num-threads` kept + as an alias). + +### Deprecated + +- **`Color.freq`, `Color.weight`, `Color.a`**: replaced by `Color.frequency`, + `Color.opacity`, and `Color.alpha` respectively. The old names still work for + one release and now emit a `DeprecationWarning`. The CLI's `--n` and + `--num-threads` remain as deprecated aliases of `--palette-size` and + `--max-workers`. ### Removed diff --git a/README.md b/README.md index 1d60e2b..8d25b9f 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,10 @@ pylette image.jpg pylette *.jpg --export-json --output results/ # Extract 8 colors in HSV colorspace with structured export -pylette photo.png --n 8 --colorspace hsv --export-json --output colors.json +pylette photo.png --palette-size 8 --colorspace hsv --export-json --output colors.json # Batch process with parallel processing and table display -pylette images/*.png --n 6 --num-threads 4 +pylette images/*.png --palette-size 6 --max-workers 4 ``` **Example Output:** @@ -104,7 +104,7 @@ pylette *.png --export-json --output results/ --no-stdout ```bash # Use different extraction algorithms -pylette image.jpg --mode MedianCut --n 6 +pylette image.jpg --mode MedianCut --palette-size 6 # Handle transparent images pylette logo.png --alpha-mask-threshold 128 @@ -128,7 +128,7 @@ for color in palette.colors: print(f"RGB: {color.rgb}") print(f"Hex: {color.hex}") print(f"HSV: {color.hsv}") - print(f"Frequency: {color.freq:.2%}") + print(f"Frequency: {color.frequency:.2%}") # Export to structured JSON palette.to_json(filename='palette.json', colorspace='hsv') @@ -258,14 +258,14 @@ Arguments: IMAGE_SOURCES... Images, URLs, or directories to process [required] Options: - --mode [KMeans|MedianCut] Extraction algorithm [default: KMeans] - --n INTEGER Number of colors to extract [default: 5] + --mode [KMeans|MedianCut|OKLab] Extraction algorithm [default: KMeans] + --palette-size, --n INTEGER Number of colors to extract [default: 5] --sort-by [frequency|luminance] Sort colors by [default: luminance] --colorspace [rgb|hsv|hls] Color space [default: rgb] --export-json Export to JSON format --output PATH Output file or directory for JSON export --alpha-mask-threshold [0-255] Alpha threshold for transparency - --num-threads INTEGER Parallel processing threads + --max-workers, --num-threads INTEGER Parallel processing threads --display-colors Show palette images --no-stdout Suppress table output --help Show help message diff --git a/docs/index.md b/docs/index.md index f000030..5c0791a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -63,10 +63,10 @@ Extract palettes from images using simple commands: pylette *.jpg --export-json --output results/ # Extract 8 colors in HSV colorspace with structured export - pylette photo.png --n 8 --colorspace hsv --export-json --output colors.json + pylette photo.png --palette-size 8 --colorspace hsv --export-json --output colors.json # Batch process with parallel processing and table display - pylette images/*.png --n 6 --num-threads 4 + pylette images/*.png --palette-size 6 --max-workers 4 ``` **Example Output:** @@ -100,7 +100,7 @@ For programmatic usage and advanced workflows: print(f"RGB: {color.rgb}") print(f"Hex: {color.hex}") print(f"HSV: {color.hsv}") - print(f"Frequency: {color.freq:.2%}") + print(f"Frequency: {color.frequency:.2%}") # Export to structured JSON palette.to_json(filename='palette.json', colorspace='hsv') diff --git a/pylette/cmd.py b/pylette/cmd.py index 842af7c..06b68c9 100644 --- a/pylette/cmd.py +++ b/pylette/cmd.py @@ -26,7 +26,9 @@ def main( List[str], typer.Argument(help="A list of paths / directories / URLs pointing to images.") ], # These can be paths or URLs mode: ExtractionMethod = ExtractionMethod.KM, - n: int = 5, + palette_size: int = typer.Option( + 5, "--palette-size", "--n", help="Number of colors to extract. (--n is a deprecated alias.)" + ), sort_by: SortBy = SortBy.luminance, stdout: bool = True, out_filename: pathlib.Path | None = None, @@ -38,8 +40,12 @@ def main( max=255, help="Alpha threshold for transparent image masking (0-255). Pixels with alpha below this value are excluded.", ), - num_threads: int | None = typer.Option( - None, min=1, help="Number of threads used for batch extraction of color palettes" + max_workers: int | None = typer.Option( + None, + "--max-workers", + "--num-threads", + min=1, + help="Number of worker threads for batch extraction. (--num-threads is a deprecated alias.)", ), export_json: bool = typer.Option(False, "--export-json", help="Export palettes to JSON format"), output: pathlib.Path | None = typer.Option( @@ -55,7 +61,7 @@ def main( raise typer.Exit(1) # Set up progress bar for CLI - with PyletteProgress(palette_size=n) as progress: + with PyletteProgress(palette_size=palette_size) as progress: task_id = progress.add_task("Extracting colors...", total=len(image_sources)) def progress_callback(task_number: int, result: BatchResult): @@ -71,11 +77,11 @@ def progress_callback(task_number: int, result: BatchResult): results = batch_extract_colors( images=image_sources, - palette_size=n, + palette_size=palette_size, sort_mode=sort_by.value, mode=mode, alpha_mask_threshold=alpha_mask_threshold, - max_workers=num_threads, + max_workers=max_workers, progress_callback=progress_callback, ) @@ -181,7 +187,7 @@ def display_palette_summary(successful_results: list[BatchResult], colorspace: C # Add color rows for color in palette.colors: hex_color = color.hex - frequency = f"{color.freq:.1%}" + frequency = f"{color.frequency:.1%}" # Get colorspace values color_values = color.get_colors(colorspace) @@ -219,3 +225,7 @@ def print_extraction_summary(successful: list[BatchResult], failed: list[BatchRe def main_typer() -> None: pylette_app() + + +if __name__ == "__main__": + main_typer() diff --git a/pylette/src/color.py b/pylette/src/color.py index fbc3bee..d9caed9 100644 --- a/pylette/src/color.py +++ b/pylette/src/color.py @@ -1,8 +1,9 @@ import colorsys +import warnings import numpy as np -from pylette.src.types import ColorSpace +from pylette.src.types import ColorSpace, coerce_to_enum # Weights for calculating luminance luminance_weights = np.array([0.2126, 0.7152, 0.0722]) @@ -37,7 +38,7 @@ def __init__(self, rgba: tuple[int, ...], frequency: float): r, g, b, alpha = (int(round(float(v))) for v in rgba) self._srgb: tuple[float, float, float] = (r / 255.0, g / 255.0, b / 255.0) self._alpha: float = alpha / 255.0 - self.freq: float = frequency + self.frequency: float = frequency @classmethod def from_srgb_float( @@ -66,7 +67,7 @@ def from_srgb_float( r, g, b = srgb obj._srgb = (_clamp_unit(r), _clamp_unit(g), _clamp_unit(b)) obj._alpha = _clamp_unit(alpha) - obj.freq = frequency + obj.frequency = frequency return obj @property @@ -91,15 +92,25 @@ def rgb(self) -> tuple[int, int, int]: return (int(round(r * 255.0)), int(round(g * 255.0)), int(round(b * 255.0))) @property - def a(self) -> int: + def alpha(self) -> int: """ - The alpha channel as an 8-bit value. + The alpha channel as a raw 8-bit value (matches :attr:`rgba`). Returns: int: Alpha as a plain Python int in [0, 255]. """ return int(round(self._alpha * 255.0)) + @property + def opacity(self) -> float: + """ + The alpha channel as a fraction (opacity) in ``[0, 1]``. + + Returns: + float: Opacity in [0, 1]. + """ + return self._alpha + @property def rgba(self) -> tuple[int, int, int, int]: """ @@ -109,17 +120,41 @@ def rgba(self) -> tuple[int, int, int, int]: tuple[int, int, int, int]: (r, g, b, a) as plain Python ints in [0, 255]. """ r, g, b = self.rgb - return (r, g, b, self.a) + return (r, g, b, self.alpha) + + @property + def a(self) -> int: + """Deprecated alias for :attr:`alpha`.""" + warnings.warn( + "Color.a is deprecated and will be removed; use Color.alpha instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.alpha @property def weight(self) -> float: - """ - The alpha channel as a fraction in ``[0, 1]``. + """Deprecated alias for :attr:`opacity`. - Returns: - float: Alpha in [0, 1]. + The name is misleading: in a palette context "weight" reads as relative + importance (frequency), but it holds opacity. Use :attr:`opacity`. """ - return self._alpha + warnings.warn( + "Color.weight is deprecated and will be removed; use Color.opacity instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.opacity + + @property + def freq(self) -> float: + """Deprecated alias for :attr:`frequency`.""" + warnings.warn( + "Color.freq is deprecated and will be removed; use Color.frequency instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.frequency def display(self, w: int = 50, h: int = 50) -> None: """ @@ -145,18 +180,20 @@ def __lt__(self, other: "Color") -> bool: Returns: bool: True if the frequency of this color is less than the frequency of the other color, False otherwise. """ - return self.freq < other.freq + return self.frequency < other.frequency - def get_colors(self, colorspace: ColorSpace = ColorSpace.RGB) -> tuple[int, ...] | tuple[float, ...]: + def get_colors(self, colorspace: ColorSpace | str = ColorSpace.RGB) -> tuple[int, ...] | tuple[float, ...]: """ Returns the color values in the specified color space. Parameters: - colorspace (ColorSpace): The color space to use. + colorspace (ColorSpace | str): The color space to use (enum member, + its value, or case-insensitive name). Returns: tuple[int, ...] | tuple[float, ...]: The color values in the specified color space. """ + colorspace = coerce_to_enum(colorspace, ColorSpace) colors = {ColorSpace.RGB: self.rgb, ColorSpace.HSV: self.hsv, ColorSpace.HLS: self.hls} return colors[colorspace] diff --git a/pylette/src/color_extraction.py b/pylette/src/color_extraction.py index dcc2b1a..ee8054c 100644 --- a/pylette/src/color_extraction.py +++ b/pylette/src/color_extraction.py @@ -21,6 +21,7 @@ PILImage, ProcessingStats, SourceType, + coerce_to_enum, ) @@ -88,7 +89,7 @@ def batch_extract_colors( images: Sequence[ImageInput], palette_size: int = 5, resize: bool = True, - mode: ExtractionMethod = ExtractionMethod.KM, + mode: ExtractionMethod | str = ExtractionMethod.KM, sort_mode: Literal["luminance", "frequency"] | None = None, alpha_mask_threshold: int | None = None, max_workers: int | None = None, @@ -141,7 +142,7 @@ def extract_colors( image: ImageInput, palette_size: int = 5, resize: bool = True, - mode: ExtractionMethod = ExtractionMethod.KM, + mode: ExtractionMethod | str = ExtractionMethod.KM, sort_mode: Literal["luminance", "frequency"] | None = None, alpha_mask_threshold: int | None = None, ) -> Palette: @@ -168,6 +169,8 @@ def extract_colors( start_time = time.time() + mode = coerce_to_enum(mode, ExtractionMethod) + source_type = _get_source_type_from_image_input(image) # Normalize input to PIL Image and convert to RGBA img_obj = _normalize_image_input(image) diff --git a/pylette/src/extractors/registry.py b/pylette/src/extractors/registry.py index 754ac6a..94578b5 100644 --- a/pylette/src/extractors/registry.py +++ b/pylette/src/extractors/registry.py @@ -5,7 +5,7 @@ from typing import Callable, TypeVar from pylette.src.extractors.protocol import ColorExtractor -from pylette.src.types import ExtractionMethod +from pylette.src.types import ExtractionMethod, coerce_to_enum _REGISTRY: dict[ExtractionMethod, ColorExtractor] = {} _E = TypeVar("_E", bound=ColorExtractor) @@ -40,12 +40,7 @@ def get_extractor(method: ExtractionMethod | str) -> ColorExtractor: ValueError: If ``method`` is not a known method or has no registered extractor. """ - if not isinstance(method, ExtractionMethod): - try: - method = ExtractionMethod(method) - except ValueError: - valid = ", ".join(sorted(m.value for m in ExtractionMethod)) - raise ValueError(f"Unknown extraction method {method}. Valid methods: {valid}.") from None + method = coerce_to_enum(method, ExtractionMethod) try: return _REGISTRY[method] diff --git a/pylette/src/palette.py b/pylette/src/palette.py index 540fa5b..aa5115a 100644 --- a/pylette/src/palette.py +++ b/pylette/src/palette.py @@ -4,7 +4,15 @@ from PIL import Image from pylette.src.color import Color -from pylette.src.types import ColorSpace, ExtractionParams, ImageInfo, PaletteMetaData, ProcessingStats, SourceType +from pylette.src.types import ( + ColorSpace, + ExtractionParams, + ImageInfo, + PaletteMetaData, + ProcessingStats, + SourceType, + coerce_to_enum, +) class Palette: @@ -17,7 +25,7 @@ def __init__(self, colors: list[Color], metadata: PaletteMetaData | None = None) """ self.colors = colors - self.frequencies = [c.freq for c in colors] + self.frequencies = [c.frequency for c in colors] self.number_of_colors = len(colors) self.metadata = metadata @@ -116,7 +124,7 @@ def random_color(self, N: int, mode: str = "frequency") -> list[Color]: def to_json( self, filename: str | None = None, - colorspace: ColorSpace = ColorSpace.RGB, + colorspace: ColorSpace | str = ColorSpace.RGB, include_metadata: bool = True, ) -> dict[str, object] | None: """ @@ -124,13 +132,16 @@ def to_json( Parameters: filename (str | None): File to save to. If None, returns the dictionary. - colorspace (Literal["rgb", "hsv", "hls"]): Color space to use. + colorspace (ColorSpace | str): Color space to use (enum member, its + value, or case-insensitive name). include_metadata (bool): Whether to include palette metadata. Returns: dict | None: The palette data as a dictionary if filename is None. """ + colorspace = coerce_to_enum(colorspace, ColorSpace) + # Build the palette data palette_data: dict[str, object] = { "colors": [], @@ -143,7 +154,7 @@ def to_json( for color in self.colors: color_values = color.get_colors(colorspace) color_data: dict[str, object] = { - "frequency": float(color.freq), + "frequency": float(color.frequency), } # Add colorspace-specific field @@ -197,7 +208,7 @@ def to_json( def export( self, filename: str, - colorspace: ColorSpace = ColorSpace.RGB, + colorspace: ColorSpace | str = ColorSpace.RGB, include_metadata: bool = True, ) -> None: """ @@ -205,7 +216,8 @@ def export( Parameters: filename (str): File to save to (extension will be added automatically if not present). - colorspace (ColorSpace): Color space to use. + colorspace (ColorSpace | str): Color space to use (enum member, its + value, or case-insensitive name). include_metadata (bool): Whether to include metadata. """ @@ -216,7 +228,7 @@ def export( self.to_json(filename=filename, colorspace=colorspace, include_metadata=include_metadata) def __str__(self): - return "".join(["({}, {}, {}, {}) \n".format(c.rgb[0], c.rgb[1], c.rgb[2], c.freq) for c in self.colors]) + return "".join(["({}, {}, {}, {}) \n".format(c.rgb[0], c.rgb[1], c.rgb[2], c.frequency) for c in self.colors]) # Convenient metadata accessors @property diff --git a/pylette/src/types.py b/pylette/src/types.py index e072932..dbbb815 100644 --- a/pylette/src/types.py +++ b/pylette/src/types.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Any, Protocol, TypeAlias, TypedDict +from typing import TYPE_CHECKING, Any, Protocol, TypeAlias, TypedDict, TypeVar import numpy as np from cv2.typing import MatLike @@ -65,6 +65,35 @@ class ColorSpace(str, Enum): HLS = "hls" +_EnumT = TypeVar("_EnumT", bound=Enum) + + +def coerce_to_enum(value: "_EnumT | str", enum_cls: type[_EnumT]) -> _EnumT: + """Coerce ``value`` to a member of ``enum_cls``. + + Accepts an existing member, the member's value, or its (case-insensitive) + name. This is the single place that turns user-facing strings into enums + (e.g. ``mode`` and ``colorspace``), replacing acceptance scattered across the + registry, ``extract_colors``, and JSON export. + + Raises: + ValueError: If ``value`` matches no member of ``enum_cls``. + """ + if isinstance(value, enum_cls): + return value + if isinstance(value, str): + try: + return enum_cls(value) + except ValueError: + pass + try: + return enum_cls[value.upper()] + except KeyError: + pass + valid = ", ".join(f"{m.name}/{m.value}" for m in enum_cls) + raise ValueError(f"Unknown {enum_cls.__name__} {value!r}. Valid options: {valid}.") + + # PaletteMetaData Types class SourceType(str, Enum): FILE_PATH = "file_path" diff --git a/smoke_test.py b/smoke_test.py index 6491bd0..e22fd8a 100644 --- a/smoke_test.py +++ b/smoke_test.py @@ -16,6 +16,20 @@ import pylette +def make_test_image(w: int = 64, h: int = 64) -> Image.Image: + """Build a deterministic multi-color image. + + A solid-color image has only one distinct color, so clustering extractors + (KMeans, OKLab) collapse to a single swatch and emit ConvergenceWarnings, + while MedianCut pads to ``palette_size`` with duplicates. A gradient gives + every method enough distinct colors to return a full, meaningful palette. + """ + pixels = [((x * 4) % 256, (y * 4) % 256, ((x + y) * 2) % 256) for y in range(h) for x in range(w)] + img = Image.new("RGB", (w, h)) + img.putdata(pixels) + return img + + def test_library_import(): """Test that the library can be imported successfully.""" print("✓ Testing library import...") @@ -28,12 +42,11 @@ def test_basic_color_extraction(): """Test basic color extraction functionality.""" print("✓ Testing basic color extraction...") - # Create a simple test image - test_img = Image.new("RGB", (100, 100), color="red") + # Create a multi-color test image so extraction returns a full palette + test_img = make_test_image() palette = pylette.extract_colors(test_img, palette_size=5) - assert len(palette) <= 5, f"Palette size should not exceed 5, got {len(palette)}" - assert len(palette) > 0, "Should extract at least one color" + assert len(palette) == 5, f"Expected 5 colors from a multi-color image, got {len(palette)}" print(f" Extracted {len(palette)} colors successfully") @@ -44,12 +57,12 @@ def test_cli_functionality(): # Create a temporary test image with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: - test_img = Image.new("RGB", (50, 50), color="blue") + test_img = make_test_image() test_img.save(tmp.name) # Test CLI command result = subprocess.run( - [sys.executable, "-m", "pylette.cmd", tmp.name, "--palette_size", "3"], capture_output=True, text=True + [sys.executable, "-m", "pylette.cmd", tmp.name, "--palette-size", "3"], capture_output=True, text=True ) if result.returncode != 0: @@ -62,27 +75,27 @@ def test_cli_functionality(): def test_extraction_methods(): - """Test different extraction methods.""" + """Test every extraction method registered in the registry.""" print("✓ Testing different extraction methods...") - test_img = Image.new("RGB", (50, 50), color="green") + from pylette.src.extractors import available_methods + + test_img = make_test_image() - # Test K-means extractor - kmeans_palette = pylette.extract_colors(test_img, palette_size=3, mode=pylette.types.ExtractionMethod.KM) - assert len(kmeans_palette) <= 3, "K-means should respect palette size" - print(f" K-means extracted {len(kmeans_palette)} colors") + methods = available_methods() + assert methods, "Registry should expose at least one extraction method" - # Test Median cut extractor - mediancut_palette = pylette.extract_colors(test_img, palette_size=3, mode=pylette.types.ExtractionMethod.MC) - assert len(mediancut_palette) <= 3, "Median cut should respect palette size" - print(f" Median cut extracted {len(mediancut_palette)} colors") + for method in methods: + palette = pylette.extract_colors(test_img, palette_size=3, mode=method) + assert len(palette) == 3, f"{method.value} should extract 3 colors from a multi-color image, got {len(palette)}" + print(f" {method.value} extracted {len(palette)} colors") def test_json_export(): """Test JSON export functionality.""" print("✓ Testing JSON export...") - test_img = Image.new("RGB", (50, 50), color="purple") + test_img = make_test_image() palette = pylette.extract_colors(test_img, palette_size=2) # Test JSON export @@ -93,7 +106,7 @@ def test_json_export(): with open(tmp.name, "r") as f: data = json.load(f) assert "colors" in data, "JSON should contain 'colors' key" - assert len(data["colors"]) <= 2, f"Should have at most 2 colors, got {len(data['colors'])}" + assert len(data["colors"]) == 2, f"Should have 2 colors, got {len(data['colors'])}" # Clean up Path(tmp.name).unlink() diff --git a/tests/integration/test_color_representation.py b/tests/integration/test_color_representation.py index 5561b4f..b6f2570 100644 --- a/tests/integration/test_color_representation.py +++ b/tests/integration/test_color_representation.py @@ -80,5 +80,31 @@ def test_eight_bit_constructor_matches_legacy_hsv() -> None: def test_rgba_and_alpha_are_plain_ints() -> None: color = Color(rgba=(10, 20, 30, 128), frequency=0.5) assert color.rgba == (10, 20, 30, 128) - assert isinstance(color.a, int) - assert color.weight == pytest.approx(128 / 255) + assert isinstance(color.alpha, int) + assert color.alpha == 128 + assert color.opacity == pytest.approx(128 / 255) + + +def test_alpha_and_opacity_are_derived() -> None: + """`.alpha` is the raw 0-255 channel; `.opacity` is the [0, 1] float (P2a).""" + color = Color.from_srgb_float((0.1, 0.2, 0.3), frequency=0.5, alpha=0.5) + assert color.opacity == pytest.approx(0.5) + assert color.alpha == 128 # round(0.5 * 255) + assert color.rgba[3] == color.alpha + + +def test_frequency_is_canonical() -> None: + color = Color(rgba=(10, 20, 30, 255), frequency=0.25) + assert color.frequency == 0.25 + + +@pytest.mark.parametrize( + "deprecated_attr, canonical_attr", + [("freq", "frequency"), ("weight", "opacity"), ("a", "alpha")], +) +def test_deprecated_aliases_warn_and_still_work(deprecated_attr: str, canonical_attr: str) -> None: + """`.freq`, `.weight`, `.a` remain functional for one release with a warning.""" + color = Color(rgba=(10, 20, 30, 128), frequency=0.5) + with pytest.warns(DeprecationWarning): + deprecated_value = getattr(color, deprecated_attr) + assert deprecated_value == getattr(color, canonical_attr) diff --git a/tests/integration/test_colorspaces.py b/tests/integration/test_colorspaces.py index 59e341c..b88086d 100644 --- a/tests/integration/test_colorspaces.py +++ b/tests/integration/test_colorspaces.py @@ -50,16 +50,16 @@ def test_palette_invariants_with_image_path( f"Expected {palette_size} colors in palette, got {palette.number_of_colors}" ) assert len(palette.colors) == palette_size, f"Expected {palette_size} colors in palette, got {len(palette.colors)}" - assert palette.colors[0].freq >= palette.colors[-1].freq, ( + assert palette.colors[0].frequency >= palette.colors[-1].frequency, ( "Expected colors to be sorted by frequency in descending order" ) - assert palette.colors[0].freq > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" - assert palette.colors[0].freq <= 1.0, ( + assert palette.colors[0].frequency > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" + assert palette.colors[0].frequency <= 1.0, ( "Expected the most frequent color to have a frequency less than or equal to 1.0" ) assert_approx_equal( - sum(c.freq for c in palette.colors), + sum(c.frequency for c in palette.colors), 1.0, err_msg="Expected the sum of all frequencies to be 1.0", ) @@ -85,16 +85,16 @@ def test_palette_invariants_with_image_pathlike( f"Expected {palette_size} colors in palette, got {palette.number_of_colors}" ) assert len(palette.colors) == palette_size, f"Expected {palette_size} colors in palette, got {len(palette.colors)}" - assert palette.colors[0].freq >= palette.colors[-1].freq, ( + assert palette.colors[0].frequency >= palette.colors[-1].frequency, ( "Expected colors to be sorted by frequency in descending order" ) - assert palette.colors[0].freq > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" - assert palette.colors[0].freq <= 1.0, ( + assert palette.colors[0].frequency > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" + assert palette.colors[0].frequency <= 1.0, ( "Expected the most frequent color to have a frequency less than or equal to 1.0" ) assert_approx_equal( - sum(c.freq for c in palette.colors), + sum(c.frequency for c in palette.colors), 1.0, err_msg="Expected the sum of all frequencies to be 1.0", ) @@ -120,16 +120,16 @@ def test_palette_invariants_with_image_bytes( f"Expected {palette_size} colors in palette, got {palette.number_of_colors}" ) assert len(palette.colors) == palette_size, f"Expected {palette_size} colors in palette, got {len(palette.colors)}" - assert palette.colors[0].freq >= palette.colors[-1].freq, ( + assert palette.colors[0].frequency >= palette.colors[-1].frequency, ( "Expected colors to be sorted by frequency in descending order" ) - assert palette.colors[0].freq > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" - assert palette.colors[0].freq <= 1.0, ( + assert palette.colors[0].frequency > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" + assert palette.colors[0].frequency <= 1.0, ( "Expected the most frequent color to have a frequency less than or equal to 1.0" ) assert_approx_equal( - sum(c.freq for c in palette.colors), + sum(c.frequency for c in palette.colors), 1.0, err_msg="Expected the sum of all frequencies to be 1.0", ) @@ -152,16 +152,16 @@ def test_palette_invariants_with_PIL_image( f"Expected {palette_size} colors in palette, got {palette.number_of_colors}" ) assert len(palette.colors) == palette_size, f"Expected {palette_size} colors in palette, got {len(palette.colors)}" - assert palette.colors[0].freq >= palette.colors[-1].freq, ( + assert palette.colors[0].frequency >= palette.colors[-1].frequency, ( "Expected colors to be sorted by frequency in descending order" ) - assert palette.colors[0].freq > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" - assert palette.colors[0].freq <= 1.0, ( + assert palette.colors[0].frequency > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" + assert palette.colors[0].frequency <= 1.0, ( "Expected the most frequent color to have a frequency less than or equal to 1.0" ) assert_approx_equal( - sum(c.freq for c in palette.colors), + sum(c.frequency for c in palette.colors), 1.0, err_msg="Expected the sum of all frequencies to be 1.0", ) @@ -184,16 +184,16 @@ def test_palette_invariants_with_opencv( f"Expected {palette_size} colors in palette, got {palette.number_of_colors}" ) assert len(palette.colors) == palette_size, f"Expected {palette_size} colors in palette, got {len(palette.colors)}" - assert palette.colors[0].freq >= palette.colors[-1].freq, ( + assert palette.colors[0].frequency >= palette.colors[-1].frequency, ( "Expected colors to be sorted by frequency in descending order" ) - assert palette.colors[0].freq > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" - assert palette.colors[0].freq <= 1.0, ( + assert palette.colors[0].frequency > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" + assert palette.colors[0].frequency <= 1.0, ( "Expected the most frequent color to have a frequency less than or equal to 1.0" ) assert_approx_equal( - sum(c.freq for c in palette.colors), + sum(c.frequency for c in palette.colors), 1.0, err_msg="Expected the sum of all frequencies to be 1.0", ) @@ -216,16 +216,16 @@ def test_palette_invariants_with_image_url( f"Expected {palette_size} colors in palette, got {palette.number_of_colors}" ) assert len(palette.colors) == palette_size, f"Expected {palette_size} colors in palette, got {len(palette.colors)}" - assert palette.colors[0].freq >= palette.colors[-1].freq, ( + assert palette.colors[0].frequency >= palette.colors[-1].frequency, ( "Expected colors to be sorted by frequency in descending order" ) - assert palette.colors[0].freq > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" - assert palette.colors[0].freq <= 1.0, ( + assert palette.colors[0].frequency > 0.0, "Expected the most frequent color to have a frequency greater than 0.0" + assert palette.colors[0].frequency <= 1.0, ( "Expected the most frequent color to have a frequency less than or equal to 1.0" ) assert_approx_equal( - sum(c.freq for c in palette.colors), + sum(c.frequency for c in palette.colors), 1.0, err_msg="Expected the sum of all frequencies to be 1.0", ) @@ -274,8 +274,8 @@ def test_color_extraction_deterministic_kmeans( sort_mode=sort_mode, ) for c1, c2 in zip(palette1.colors, palette2.colors): - r, g, b, freq = c1.rgb[0], c1.rgb[1], c1.rgb[2], c1.freq - r2, g2, b2, freq2 = c2.rgb[0], c2.rgb[1], c2.rgb[2], c2.freq + r, g, b, freq = c1.rgb[0], c1.rgb[1], c1.rgb[2], c1.frequency + r2, g2, b2, freq2 = c2.rgb[0], c2.rgb[1], c2.rgb[2], c2.frequency assert r == r2, f"Expected r1 == r2, got {r} != {r2}" assert g == g2, f"Expected g1 == g2, got {g} != {g2}"