class ThresholdProcessor(Processor): """ Abstract base class for a processor using a uint8_t threshold matrix """ # This is used in instakit.processors.halftone __slots__ = tuplize('threshold_matrix') LO_TUP = tuplize(0) HI_TUP = tuplize(255) def __init__(self, threshold=128.0): """ Initialize with a threshold value between 0 and 255 """ self.threshold_matrix = int(threshold) * self.LO_TUP + \ (256-int(threshold)) * self.HI_TUP
class Posterize(Processor): """ Reduce the number of bits (1 to 8) per channel """ __slots__ = tuplize('bits') def __init__(self, bits=4): self.bits = min(max(1, bits), 8) def process(self, image): return ImageOps.posterize(image, bits=self.bits)
class EnhanceNop(ABC, metaclass=Slotted): __slots__ = tuplize('image') def __init__(self, image=None): self.image = image def adjust(self, *args, **kwargs): return self.image
class Solarize(Processor): """ Invert all pixel values above an 8-bit threshold """ __slots__ = tuplize('threshold') def __init__(self, threshold=128): self.threshold = min(max(1, threshold), 255) def process(self, image): return ImageOps.solarize(image, threshold=self.threshold)
class Equalize(Processor): """ Apply a non-linear mapping to the image, via histogram """ __slots__ = tuplize('mask') def __init__(self, mask=None): self.mask = hasattr(mask, 'copy') and mask.copy() or mask def process(self, image): return ImageOps.equalize(image, mask=self.mask)
class AutoContrast(Processor): """ Normalize contrast throughout the image, via histogram """ __slots__ = tuplize('cutoff', 'ignore') def __init__(self, cutoff=0, ignore=None): self.cutoff, self.ignore = cutoff, ignore def process(self, image): return ImageOps.autocontrast(image, cutoff=self.cutoff, ignore=self.ignore)
class Pipe(Sequence): """ A static linear pipeline of processors to be applied en masse. Derived from a `pilkit` class: `pilkit.processors.base.ProcessorPipeline` """ __slots__ = tuplize('tuple') @classmethod def base_type(cls): return tuple @wraps(tuple.__init__) def __init__(self, *args): self.tuple = tuplize(*args) def iterate(self): yield from self.tuple @wraps(tuple.__len__) def __len__(self): return len(self.tuple) @wraps(tuple.__contains__) def __contains__(self, value): return value in self.tuple @wraps(tuple.__getitem__) def __getitem__(self, idx): return self.tuple[idx] @wraps(tuple.index) def index(self, value): return self.tuple.index(value) def last(self): if not bool(self): raise IndexError("pipe is empty") return self.tuple[-1] def process(self, image): for processor in self.iterate(): image = processor.process(image) return image def __eq__(self, other): if not isinstance(other, (type(self), type(self).base_type())): return NotImplemented return super(Pipe, self).__eq__(other)
class Adjustment(Processor): """ Base type for image adjustment processors """ __slots__ = tuplize('value') def __init__(self, value=1.0): """ Initialize the adjustment with a float value """ self.value = value @abstract def adjust(self, image): """ Adjust the image, using the float value with which the adjustment was first initialized """ ... def process(self, image): return (self.value == 1.0) and image or self.adjust(image)
class Pipeline(MutableSequence): """ A mutable linear pipeline of processors to be applied en masse. Derived from a `pilkit` class: `pilkit.processors.base.ProcessorPipeline` """ __slots__ = tuplize('list') @classmethod def base_type(cls): return list @wraps(list.__init__) def __init__(self, *args): base_type = type(self).base_type() if len(args) == 0: self.list = base_type() if len(args) == 1: target = args[0] if type(target) is type(self): self.list = copy(target.list) elif type(target) is base_type: self.list = copy(target) elif type(target) in (tuple, set, frozenset): self.list = base_type([*target]) elif type(target) in (dict, defaultdict, OrderedDict): self.list = base_type([*sorted(target).values()]) elif hasattr(target, 'iterate'): self.list = base_type([*target.iterate()]) elif hasattr(target, '__iter__'): self.list = base_type([*target]) else: self.list = base_type([target]) else: self.list = base_type([*args]) def iterate(self): yield from self.list @wraps(list.__len__) def __len__(self): return len(self.list) @wraps(list.__contains__) def __contains__(self, value): return value in self.list @wraps(list.__getitem__) def __getitem__(self, idx): return self.list[idx] @wraps(list.__setitem__) def __setitem__(self, idx, value): if value in (None, NOOp): value = NOOp() self.list[idx] = value @wraps(list.__delitem__) def __delitem__(self, idx): del self.list[idx] @wraps(list.index) def index(self, value): return self.list.index(value) @wraps(list.append) def append(self, value): self.list.append(value) @wraps(list.extend) def extend(self, iterable): self.list.extend(iterable) def pop(self, idx=-1): """ Remove and return item at `idx` (default last). Raises IndexError if list is empty or `idx` is out of range. See list.pop(…) for details. """ self.list.pop(idx) def last(self): if not bool(self): raise IndexError("pipe is empty") return self.list[-1] def process(self, image): for processor in self.iterate(): image = processor.process(image) return image def __eq__(self, other): if not isinstance(other, (type(self), type(self).base_type())): return NotImplemented return super(Pipe, self).__eq__(other)
def __init__(self, *args): self.tuple = tuplize(*args)
class BandFork(Fork): """ BandFork is a processor container -- a processor that applies other processors. BandFork acts selectively on the individual bands of input image data, either: - applying a band-specific processor instance, or - applying a default processor factory successively across all bands. BandFork’s interface is closely aligned with Python’s mutable-mapping API‡ -- with which most programmers are no doubt quite familiar: • Ex. 1: apply Atkinson dithering to each of an RGB images’ bands: >>> from instakit.utils.pipeline import BandFork >>> from instakit.processors.halftone import Atkinson >>> BandFork(Atkinson).process(my_image) • Ex. 2: apply Atkinson dithering to only the green band: >>> from instakit.utils.pipeline import BandFork >>> from instakit.processors.halftone import Atkinson >>> bfork = BandFork(None) >>> bfork['G'] = Atkinson() >>> bfork.process(my_image) BandFork inherits from `instakit.abc.Fork`, which itself is not just an Instakit Processor. The Fork ABC implements the required methods of an Instakit Processor Container†, through which it furnishes an interface to individual bands -- also generally known as channels, per the language of the relevant Photoshop UI elements -- of image data. † q.v. the `instakit.abc` module source code supra. ‡ q.v. the `collections.abc` module, and the `MutableMapping` abstract base class within, supra. """ __slots__ = tuplize('mode_t') def __init__(self, processor_factory, *args, **kwargs): """ Initialize a BandFork instance, using the given callable value for `processor_factory` and any band-appropriate keyword-arguments, e.g. `(R=MyProcessor, G=MyOtherProcessor, B=None)` """ # Call `super(…)`, passing `processor_factory`: super(BandFork, self).__init__(processor_factory, *args, **kwargs) # Reset `self.mode_t` if a new mode was specified -- # N.B. we can’t use the “self.mode” property during “__init__(…)”: self.mode_t = kwargs.pop('mode', Mode.RGB) @property def mode(self): return self.mode_t @mode.setter def mode(self, value): if value is None: return if type(value) in string_types: value = Mode.for_string(value) if Mode.is_mode(value): # if value is not self.mode_t: self.set_mode_t(value) else: raise TypeError("invalid mode type: %s (%s)" % (type(value), value)) def set_mode_t(self, value): self.mode_t = value # DOUBLE SHADOW!! @property def band_labels(self): return self.mode.bands def iterate(self): yield from (self[band_label] for band_label in self.band_labels) def split(self, image): return self.mode.process(image).split() def compose(self, *bands): return self.mode.merge(*bands) def process(self, image): processed = [] for processor, band in zip(self.iterate(), self.split(image)): processed.append(processor.process(band)) return self.compose(*processed)