color_tools.image.basic

Basic image analysis functions for color_tools.

This module provides general-purpose image analysis utilities, separate from the HueForge-specific tools in analysis.py.

Functions in this module require Pillow and numpy:

pip install color-match-tools[image]

Example:

>>> from color_tools.image import count_unique_colors, get_color_histogram, get_dominant_color
>>>
>>> # Count colors in an image
>>> total = count_unique_colors("photo.jpg")
>>> print(f"Image contains {total} unique colors")
Image contains 42,387 unique colors
>>>
>>> # Get color histogram
>>> histogram = get_color_histogram("photo.jpg")
>>> print(f"Red pixels: {histogram.get((255, 0, 0), 0)}")
Red pixels: 1523
>>>
>>> # Get dominant color
>>> dominant = get_dominant_color("photo.jpg")
>>> print(f"Most common color: RGB{dominant}")
Most common color: RGB(240, 235, 230)
>>>
>>> # Check if image is indexed
>>> if is_indexed_mode("icon.png"):
...     print("Image uses a color palette")
Image uses a color palette
color_tools.image.basic.count_unique_colors(image_path)[source]

Count the total number of unique RGB colors in an image.

Uses numpy for efficient counting of unique color combinations. The image is converted to RGB mode before counting (alpha channel ignored).

Parameters:

image_path (str | Path) – Path to the image file

Return type:

int

Returns:

Number of unique RGB colors (integer)

Raises:

Example

>>> count_unique_colors("photo.jpg")
42387
>>> # Indexed images (GIF, PNG with palette) show palette size
>>> count_unique_colors("icon.gif")
256
>>> # Solid color image
>>> count_unique_colors("red_square.png")
1

Note

For indexed color images (mode ‘P’), this counts unique colors in the converted RGB image, not the palette size. Use is_indexed_mode() to check if an image uses a palette.

color_tools.image.basic.is_indexed_mode(image_path)[source]

Check if an image uses indexed color mode (palette-based).

Indexed color images (mode ‘P’) store pixel values as indices into a color palette, rather than direct RGB values. This is common for: - GIF images (max 256 colors) - PNG images with palettes - Some BMP images

Parameters:

image_path (str | Path) – Path to the image file

Return type:

bool

Returns:

True if image is in indexed mode (‘P’), False otherwise

Raises:

Example

>>> is_indexed_mode("photo.jpg")
False
>>> is_indexed_mode("icon.gif")
True
>>> is_indexed_mode("logo.png")  # Depends on PNG type
True  # If PNG uses palette

Note

PIL/Pillow mode codes: - ‘P’: Palette-based (indexed color) - ‘RGB’: Direct RGB color - ‘RGBA’: RGB with alpha channel - ‘L’: Grayscale - ‘1’: Binary (black and white)

color_tools.image.basic.get_color_histogram(image_path)[source]

Get histogram mapping RGB colors to their pixel counts.

Returns a dictionary where keys are RGB tuples and values are the number of pixels with that color. Uses numpy for efficient histogram calculation.

Parameters:

image_path (str | Path) – Path to the image file

Return type:

dict[tuple[int, int, int], int]

Returns:

Dictionary mapping (R, G, B) tuples to pixel counts

Raises:

Example

>>> histogram = get_color_histogram("photo.jpg")
>>> histogram[(255, 0, 0)]  # Count of pure red pixels
1523
>>> # Find most common color
>>> most_common = max(histogram.items(), key=lambda x: x[1])
>>> print(f"Color: {most_common[0]}, Count: {most_common[1]}")
Color: (240, 235, 230), Count: 15042
>>> # Get all colors sorted by frequency
>>> sorted_colors = sorted(histogram.items(), key=lambda x: x[1], reverse=True)
>>> for color, count in sorted_colors[:5]:
...     print(f"RGB{color}: {count} pixels")
RGB(240, 235, 230): 15042 pixels
RGB(235, 230, 225): 12834 pixels
...

Note

For images with many colors, the histogram can be large. Consider using count_unique_colors() if you only need the count.

color_tools.image.basic.get_dominant_color(image_path)[source]

Get the most common (dominant) color in an image.

Returns the single RGB color that appears most frequently in the image. This is equivalent to finding the mode of the color distribution.

Parameters:

image_path (str | Path) – Path to the image file

Return type:

tuple[int, int, int]

Returns:

RGB tuple (R, G, B) of the most common color

Raises:

Example

>>> dominant = get_dominant_color("photo.jpg")
>>> print(f"Dominant color: RGB{dominant}")
Dominant color: RGB(240, 235, 230)
>>> # Use with nearest color matching
>>> from color_tools import Palette
>>> palette = Palette.load_default()
>>> color_record, distance = palette.nearest_color(dominant)
>>> print(f"Closest CSS color: {color_record.name}")
Closest CSS color: seashell

Note

For images with many unique colors, this uses the histogram approach which may be memory-intensive. For very large images, consider downsampling first using Pillow’s thumbnail() method.

color_tools.image.basic.analyze_brightness(image_path)[source]

Analyze image brightness characteristics.

Calculates the mean brightness of the image in grayscale and provides an assessment based on standard thresholds.

Parameters:

image_path (str | Path) – Path to the image file

Returns:

  • ‘mean_brightness’: Mean brightness value (0-255 scale)

  • ’assessment’: Human-readable assessment (‘dark’|’normal’|’bright’)

Return type:

Dictionary with

Raises:

Example

>>> result = analyze_brightness("photo.jpg")
>>> print(f"Brightness: {result['mean_brightness']:.1f} ({result['assessment']})")
Brightness: 127.3 (normal)
>>> # Dark image
>>> result = analyze_brightness("dark_photo.jpg")
>>> print(result)
{'mean_brightness': 45.2, 'assessment': 'dark'}

Note

Brightness thresholds: - Dark: mean < THRESHOLD_DARK_IMAGE (60) - Bright: mean > THRESHOLD_BRIGHT_IMAGE (195) - Normal: THRESHOLD_DARK_IMAGE ≤ mean ≤ THRESHOLD_BRIGHT_IMAGE

color_tools.image.basic.analyze_contrast(image_path)[source]

Analyze image contrast using standard deviation of pixel values.

Higher standard deviation indicates more contrast (wider range of brightness values). Lower standard deviation indicates less contrast (more uniform brightness).

Parameters:

image_path (str | Path) – Path to the image file

Returns:

  • ‘contrast_std’: Standard deviation of brightness values

  • ’assessment’: Human-readable assessment (‘low’|’normal’)

Return type:

Dictionary with

Raises:

Example

>>> result = analyze_contrast("photo.jpg")
>>> print(f"Contrast: {result['contrast_std']:.1f} ({result['assessment']})")
Contrast: 62.4 (normal)
>>> # Low contrast image
>>> result = analyze_contrast("flat_image.jpg")
>>> print(result)
{'contrast_std': 25.3, 'assessment': 'low'}

Note

Contrast threshold: - Low contrast: std < THRESHOLD_LOW_CONTRAST (40) - Normal contrast: std ≥ THRESHOLD_LOW_CONTRAST

color_tools.image.basic.analyze_noise_level(image_path, crop_size=512, noise_threshold=2.0)[source]

Estimate noise level using scikit-image restoration.estimate_sigma().

Analyzes a center crop of the image to estimate noise sigma. This method is effective for detecting sensor noise, compression artifacts, and other forms of image degradation.

Parameters:
  • image_path (str | Path) – Path to the image file

  • crop_size (int) – Size of center crop to analyze (default: 512px)

  • noise_threshold (float) – Threshold for noise assessment (default: THRESHOLD_NOISE_SIGMA)

Returns:

  • ‘noise_sigma’: Estimated noise standard deviation

  • ’assessment’: Human-readable assessment (‘clean’|’noisy’)

Return type:

Dictionary with

Raises:

Example

>>> result = analyze_noise_level("photo.jpg")
>>> print(f"Noise: {result['noise_sigma']:.2f} ({result['assessment']})")
Noise: 1.23 (clean)
>>> # Noisy image
>>> result = analyze_noise_level("noisy_photo.jpg")
>>> print(result)
{'noise_sigma': 3.45, 'assessment': 'noisy'}

Note

  • Uses center crop to avoid edge effects

  • Estimates noise in RGB channels and averages

  • Noise threshold: sigma > THRESHOLD_NOISE_SIGMA (2.0) = noisy

  • Fallback: Returns 0.0 if estimation fails

color_tools.image.basic.analyze_dynamic_range(image_path)[source]

Analyze dynamic range and tonal distribution of an image.

Examines the full range of brightness values used and provides suggestions for gamma correction based on the tonal distribution.

Parameters:

image_path (str | Path) – Path to the image file

Returns:

  • ‘min_value’: Minimum brightness value (0-255)

  • ’max_value’: Maximum brightness value (0-255)

  • ’range’: Dynamic range (max - min)

  • ’mean_brightness’: Mean brightness for gamma assessment

  • ’range_assessment’: Assessment of dynamic range usage (‘full’|’limited’)

  • ’gamma_suggestion’: Suggested gamma adjustment for tonal balance

Return type:

Dictionary with

Raises:

Example

>>> result = analyze_dynamic_range("photo.jpg")
>>> print(f"Range: {result['range']} ({result['range_assessment']})")
>>> print(f"Gamma suggestion: {result['gamma_suggestion']}")
Range: 248 (full)
Gamma suggestion: Normal (mean balanced)
>>> # Limited range image
>>> result = analyze_dynamic_range("flat_image.jpg")
>>> print(result)
{'min_value': 45, 'max_value': 198, 'range': 153, 'mean_brightness': 89.2,
 'range_assessment': 'limited', 'gamma_suggestion': 'Decrease (<1.0) to boost midtones'}

Note

  • Full range threshold: range ≥ THRESHOLD_FULL_DYNAMIC_RANGE (216, 85% of 0-255 spectrum)

  • Gamma suggestions based on mean brightness: - Mean < GAMMA_DARK_THRESHOLD (100): Decrease gamma to boost midtones - Mean > GAMMA_BRIGHT_THRESHOLD (200): Increase gamma to suppress midtones - GAMMA_DARK_THRESHOLD ≤ mean ≤ GAMMA_BRIGHT_THRESHOLD: Normal/balanced

color_tools.image.basic.transform_image(image_path, transform_func, preserve_alpha=True, output_path=None)[source]

Apply a color transformation function to every pixel of an image.

This is the core function that handles image loading, pixel iteration, transformation application, and optional saving. It’s used by both CVD simulation and palette quantization functions.

Parameters:
  • image_path (Path | str) – Path to input image file

  • transform_func (Callable[[tuple[int, int, int]], tuple[int, int, int]]) – Function that takes RGB tuple (r, g, b) and returns RGB tuple

  • preserve_alpha (bool) – If True, preserve alpha channel in RGBA images

  • output_path (Path | str | None) – Optional path to save transformed image

Return type:

Image

Returns:

PIL Image with transformed colors

Raises:

Example

>>> # Define a transformation (invert colors)
>>> def invert_rgb(rgb):
...     r, g, b = rgb
...     return (255-r, 255-g, 255-b)
>>>
>>> # Apply to image
>>> transformed = transform_image("photo.jpg", invert_rgb)
>>> transformed.save("inverted.jpg")
color_tools.image.basic.simulate_cvd_image(image_path, deficiency_type, output_path=None)[source]

Simulate color vision deficiency for an entire image.

This shows how an image would appear to someone with a specific type of color blindness. Useful for testing image accessibility.

Parameters:
  • image_path (Path | str) – Path to input image

  • deficiency_type (str) – Type of CVD to simulate - ‘protanopia’ or ‘protan’: Red-blind - ‘deuteranopia’ or ‘deutan’: Green-blind - ‘tritanopia’ or ‘tritan’: Blue-blind

  • output_path (Path | str | None) – Optional path to save simulated image

Return type:

Image

Returns:

PIL Image showing CVD simulation

Example

>>> # See how image appears to someone with deuteranopia
>>> sim_image = simulate_cvd_image("colorful.jpg", "deuteranopia")
>>> sim_image.save("deuteranopia_sim.jpg")
>>>
>>> # Test accessibility of an infographic
>>> simulate_cvd_image("chart.png", "protanopia", "chart_protan.png")
color_tools.image.basic.correct_cvd_image(image_path, deficiency_type, output_path=None)[source]

Apply color vision deficiency correction to an entire image.

This shifts colors to improve discriminability for individuals with color blindness. The corrected image should be viewed by people with the specified deficiency type.

Parameters:
  • image_path (Path | str) – Path to input image

  • deficiency_type (str) – Type of CVD to correct for - ‘protanopia’ or ‘protan’: Red-blind - ‘deuteranopia’ or ‘deutan’: Green-blind - ‘tritanopia’ or ‘tritan’: Blue-blind

  • output_path (Path | str | None) – Optional path to save corrected image

Return type:

Image

Returns:

PIL Image with CVD correction applied

Example

>>> # Enhance image for deuteranopia viewers
>>> corrected = correct_cvd_image("chart.jpg", "deuteranopia")
>>> corrected.save("chart_deutan_enhanced.jpg")
color_tools.image.basic.quantize_image_to_palette(image_path, palette_name, metric='de2000', dither=False, output_path=None)[source]

Convert an image to use only colors from a specified palette.

This maps each pixel to the nearest color in the target palette using perceptually-accurate color distance metrics. Perfect for creating retro-style graphics or testing designs with limited color sets.

Parameters:
  • image_path (Path | str) – Path to input image

  • palette_name (str) – Name of palette to use: - Built-in palettes: ‘cga4’, ‘ega16’, ‘ega64’, ‘vga’, ‘web’, ‘gameboy’ - User palettes: ‘user-mycustom’ (files in data/user/palettes/ must have ‘user-’ prefix) - User palettes do not override built-in palettes (separate namespaces)

  • metric (str) – Color distance metric for matching: - ‘de2000’: CIEDE2000 (most perceptually accurate) - ‘de94’: CIE94 (good balance) - ‘de76’: CIE76 (simple LAB distance) - ‘cmc’: CMC l:c (textile industry standard) - ‘euclidean’: Simple RGB distance (fastest) - ‘hsl_euclidean’: HSL distance with hue wraparound

  • dither (bool) – Apply Floyd-Steinberg dithering to reduce banding

  • output_path (Path | str | None) – Optional path to save quantized image

Return type:

Image

Returns:

PIL Image using only palette colors

Example

>>> # Convert photo to CGA 4-color palette
>>> retro = quantize_image_to_palette("photo.jpg", "cga4")
>>> retro.save("retro_cga.png")
>>>
>>> # Create EGA-style artwork with dithering
>>> quantize_image_to_palette(
...     "artwork.png",
...     "ega16",
...     metric="de2000",
...     dither=True,
...     output_path="ega_dithered.png"
... )