Source code for imgwrench.commands.colorfix

"""Fix colors of images by stretching their channel histograms to full
range."""

import click
from PIL import Image
import numpy as np

from ..param import COLOR


DEFAULT_LEVEL = 0.01


def _quantiles_iter(img, level):
    assert img.mode == "RGB"
    assert level > 0 and level < 1
    h = img.histogram()
    for i in range(3):
        channel_h = h[i * 256 : (i + 1) * 256]
        n_pixels = sum(channel_h)
        low = int(level * n_pixels)
        high = int((1 - level) * n_pixels) + 1
        s = 0
        searching_low = True
        for i, p in enumerate(channel_h):
            s += p
            if searching_low:
                if s >= low:
                    yield i
                    searching_low = False
                    continue
            else:
                if s >= high:
                    yield i
                    break


[docs]def quantiles(img, level=DEFAULT_LEVEL): """Compute high and low quantiles to the given level""" r_low, r_high, g_low, g_high, b_low, b_high = list(_quantiles_iter(img, level)) return (r_low, r_high), (g_low, g_high), (b_low, b_high)
[docs]def colorfix_quantiles(img, level=DEFAULT_LEVEL): """Fix colors by stretching channel histograms between given quantiles to full range.""" channel_quantiles = quantiles(img, level) return stretch_histogram(img, channel_quantiles)
[docs]def colorfix_fixed_cutoff(img, lower_cutoff, upper_cutoff): """Fix colors by stretching channel histograms between given cutoff colors to full range.""" cutoffs = list(zip(lower_cutoff, upper_cutoff)) return stretch_histogram(img, cutoffs)
def _inner_cutoffs(first_cutoffs, second_cutoffs): for first, second in zip(first_cutoffs, second_cutoffs): yield max(first[0], second[0]), min(first[1], second[1])
[docs]def colorfix_quantiles_fixed_cutoff(img, level, lower_cutoff, upper_cutoff): """Fix colors by stretching channel histogram between inner values of given quantiles and cutoff colors to full range.""" channel_quantiles = quantiles(img, level) cutoffs = list(zip(lower_cutoff, upper_cutoff)) combined = list(_inner_cutoffs(channel_quantiles, cutoffs)) return stretch_histogram(img, combined)
[docs]def stretch_histogram(img, cutoffs): """Stretch channel histograms between given cutoffs to full range.""" # convert PIL image to numpy array for processing arr = np.array(img).astype(np.int16) # type int16 is required to prevent stretched colors from overflowing # arr.shape = (height, width, channel) # iterate over all three color channels (red, green, blue) for idx_channel in range(3): low, high = cutoffs[idx_channel] assert low >= 0, ( "low value for channel {} is {}, but must be 0 " "or larger" ).format(idx_channel, low) assert high <= 255, ( "high value for channel {} is {}, but must be 255" " or less" ).format(idx_channel, high) if low == 0 and high == 255: continue # no stretching required, save some cpu cycles channel = arr[:, :, idx_channel] # stretch colors betweem low and high to full range stretched = (channel - low) / (high - low) * 256 stretched = stretched.astype(np.int16) # cut off anything that has been scaled under or over full range channel[:, :] = np.maximum(np.minimum(stretched, 255), 0) # convert back to uint8 (lossless after range cutoff) # ... and to PIL image return Image.fromarray(arr.astype(np.uint8))
QUANTILES = "quantiles" FIXED_CUTOFF = "fixed-cutoff" QUANTILES_FIXED_CUTOFF = "quantiles-fixed-cutoff" @click.command(name="colorfix") @click.option( "-m", "--method", type=click.Choice( [QUANTILES, FIXED_CUTOFF, QUANTILES_FIXED_CUTOFF], case_sensitive=False ), default="quantiles-fixed-cutoff", show_default=True, help="algorithm method to use; quantiles stretches all channel " "histograms between the quantiles specified by --alpha; " "fixed-cutoff stretches channels between the cutoffs " "specified by --lower-cutoff and --upper-cutoff; " "quantiles-fixed-cutoff combines the two methods and " 'applies the "stronger" of both cutoffs (i.e. the higher ' "value of lower cutoffs and lower value of upper cutoffs)", ) @click.option( "-a", "--alpha", type=click.FLOAT, default=DEFAULT_LEVEL, show_default=True, help="quantile (low and high) to be clipped to minimum " "and maximum color; relevant for --method=quantiles " "and --method=quantiles-fixed-cutoff", ) @click.option( "-l", "--lower-cutoff", type=COLOR, default="rgb(127,0,0)", show_default=True, help="lower cutoff as a color name, hex value " "or in rgb(...) function form; " "relevant for --method=fixed-cutoff " "and --method=quantiles-fixed-cutoff", ) @click.option( "-u", "--upper-cutoff", type=COLOR, default="white", show_default=True, help="lower cutoff as a color name, hex value " "or in rgb(...) function form; " "relevant for --method=fixed-cutoff " "and --method=quantiles-fixed-cutoff", ) def cli_colorfix(method, alpha, lower_cutoff, upper_cutoff): """Fix colors by stretching channel histograms to full range.""" click.echo("Initializing colorfix with parameters {}".format(locals())) def _colorfix(images): for info, image in images: if method == QUANTILES: yield info, colorfix_quantiles(image, alpha) elif method == FIXED_CUTOFF: yield info, colorfix_fixed_cutoff(image, lower_cutoff, upper_cutoff) elif method == QUANTILES_FIXED_CUTOFF: yield info, colorfix_quantiles_fixed_cutoff( image, alpha, lower_cutoff, upper_cutoff ) else: raise NotImplementedError("{} not implemented".format(method)) return _colorfix