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:
- Return type:
- Returns:
Number of unique RGB colors (integer)
- Raises:
ImportError – If Pillow or numpy is not installed
FileNotFoundError – If image file doesn’t exist
IOError – If image file cannot be opened
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:
- Return type:
- Returns:
True if image is in indexed mode (‘P’), False otherwise
- Raises:
ImportError – If Pillow is not installed
FileNotFoundError – If image file doesn’t exist
IOError – If image file cannot be opened
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:
- Return type:
- Returns:
Dictionary mapping (R, G, B) tuples to pixel counts
- Raises:
ImportError – If Pillow or numpy is not installed
FileNotFoundError – If image file doesn’t exist
IOError – If image file cannot be opened
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:
- Return type:
- Returns:
RGB tuple (R, G, B) of the most common color
- Raises:
ImportError – If Pillow or numpy is not installed
FileNotFoundError – If image file doesn’t exist
IOError – If image file cannot be opened
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:
- Returns:
‘mean_brightness’: Mean brightness value (0-255 scale)
’assessment’: Human-readable assessment (‘dark’|’normal’|’bright’)
- Return type:
Dictionary with
- Raises:
ImportError – If Pillow or numpy is not installed
FileNotFoundError – If image file doesn’t exist
IOError – If image file cannot be opened
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:
- Returns:
‘contrast_std’: Standard deviation of brightness values
’assessment’: Human-readable assessment (‘low’|’normal’)
- Return type:
Dictionary with
- Raises:
ImportError – If Pillow or numpy is not installed
FileNotFoundError – If image file doesn’t exist
IOError – If image file cannot be opened
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:
- Returns:
‘noise_sigma’: Estimated noise standard deviation
’assessment’: Human-readable assessment (‘clean’|’noisy’)
- Return type:
Dictionary with
- Raises:
ImportError – If Pillow, numpy, or scikit-image is not installed
FileNotFoundError – If image file doesn’t exist
IOError – If image file cannot be opened
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:
- 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:
ImportError – If Pillow or numpy is not installed
FileNotFoundError – If image file doesn’t exist
IOError – If image file cannot be opened
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:
- Return type:
- Returns:
PIL Image with transformed colors
- Raises:
ImportError – If Pillow is not installed
FileNotFoundError – If input image doesn’t exist
ValueError – If image format is unsupported
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:
- Return type:
- 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:
- Return type:
- 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:
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 wraparounddither (
bool) – Apply Floyd-Steinberg dithering to reduce bandingoutput_path (
Path|str|None) – Optional path to save quantized image
- Return type:
- 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" ... )