Source code for trojai.datagen.instagram_xforms
from trojai.datagen.image_entity import GenericImageEntity, ImageEntity
from trojai.datagen.transform_interface import ImageTransform
from trojai.datagen.image_conversion_utils import normalization_to_rgb, normalization_from_rgb
import wand.image
import wand.color
import wand.drawing
import numpy as np
from numpy.random import RandomState
import cv2
import math
from abc import abstractmethod
from io import BytesIO
import logging
logger = logging.getLogger(__name__)
[docs]class FilterXForm(ImageTransform):
"""
Create filter xform, if no channel order is specified it is assumed to be in BGR order (opencv default),
this refers only to the first 3 channels of input data as the alpha channel is handled independently
"""
def __init__(self, channel_order: str = 'BGR', pre_normalize: bool = True, post_normalize: bool = True):
self.valid_channel_orders = {'BGR', 'RGB'}
if channel_order not in self.valid_channel_orders:
raise ValueError("Unknown channel order specified for %s!"
" Valid options are %s!" % (self.__repr__(), str(self.valid_channel_orders)))
self.channel_order = channel_order
self.pre_normalize = pre_normalize
self.post_normalize = post_normalize
"""
Abstract class containing wand interface functionality common to all filter transforms
"""
[docs] @abstractmethod
def filter(self, image: wand.image.Image) -> wand.image.Image:
"""
subclass defined function to be called by do
:param image: wand Image to be filtered
:return: filtered wand Image
"""
pass
[docs] def do(self, input_obj: ImageEntity, random_state_obj: RandomState) -> ImageEntity:
"""
Compresses 3-channel image input image as a specified filetype and stores in memory,
passes to into wand and applies filter, stores filtered image as specified filetype again in memory,
which is then decompressed back into 3-channel image
:param input_obj: entity to be transformed
:param random_state_obj: object to hold random state and enable reproducibility
:return:new entity with transform applied
"""
data = input_obj.get_data()
original_n_chan = data.shape[2]
rgb_data, alpha_ch = normalization_to_rgb(data, self.pre_normalize, self.__repr__())
logger.debug("%s is treating input as %s!" % (self.__repr__(), self.channel_order))
if self.channel_order == 'RGB':
rgb_data = cv2.cvtColor(rgb_data, cv2.COLOR_RGB2BGR)
form = '.bmp'
success, buffer = cv2.imencode(form, rgb_data)
# faster than numpy tobytes method
input_stream = BytesIO(buffer)
with wand.image.Image(blob=input_stream.getvalue()) as wand_image:
filtered_wand_image = self.filter(wand_image)
output_stream = BytesIO()
filtered_wand_image.save(output_stream)
rgb_filtered_data = cv2.imdecode(np.frombuffer(output_stream.getbuffer(), np.uint8), 1)
if self.channel_order == 'RGB':
rgb_filtered_data = cv2.cvtColor(rgb_filtered_data, cv2.COLOR_BGR2RGB)
filtered_data = normalization_from_rgb(rgb_filtered_data, alpha_ch, self.post_normalize,
original_n_chan, self.__repr__())
return GenericImageEntity(filtered_data, input_obj.get_mask())
def _colortone(self, image: wand.image.Image, color: str, dst_percent: int, invert: bool) -> None:
"""
tones either white or black values in image to the provided color,
intensity of toning depends on dst_percent
:param image: provided image
:param color: color to tone image
:param dst_percent: percentage of image pixel value to include when blending with provided color,
0 is unchanged, 100 is completely colored in
:param invert: if True blacks are modified, if False whites are modified
:return:
"""
mask_src = image.clone()
mask_src.colorspace = 'gray'
if invert:
mask_src.negate()
mask_src.alpha_channel = 'copy'
src = image.clone()
src.colorize(wand.color.Color(color), wand.color.Color('#FFFFFF'))
src.composite_channel('alpha', mask_src, 'copy_alpha')
image.composite_channel('default_channels', src, 'blend',
arguments=str(dst_percent) + "," + str(100 - dst_percent))
def _vignette(self, image: wand.image.Image, color_1: str = 'none', color_2: str = 'black',
crop_factor: float = 1.5) -> None:
"""
applies fading from color_1 to color_2 in radial gradient pattern on given image
:param image: provided image
:param color_1: center color
:param color_2: edge color
:param crop_factor: size of radial gradient pattern, which is then cropped and combined with image,
larger values include more of color_1, smaller values include more of color_2
:return: None
"""
crop_x = math.floor(image.width * crop_factor)
crop_y = math.floor(image.height * crop_factor)
src = wand.image.Image()
src.pseudo(width=crop_x, height=crop_y, pseudo='radial-gradient:' + color_1 + '-' + color_2)
src.crop(0, 0, width=image.width, height=image.height, gravity='center')
src.reset_coords()
image.composite_channel('default_channels', src, 'multiply')
image.merge_layers('flatten')
[docs]class GothamFilterXForm(FilterXForm):
"""
Class implementing Instagram's Gotham filter
"""
[docs] def filter(self, image: wand.image.Image) -> wand.image.Image:
"""
modified from https://github.com/acoomans/instagram-filters/blob/master/instagram_filters/filters/gotham.py
:param image: provided image
:return: new filtered image
"""
filtered_image = image.clone()
filtered_image.modulate(120, 10, 100)
filtered_image.colorize(wand.color.Color('#222b6d'), wand.color.Color('#333333'))
filtered_image.gamma(.9)
filtered_image.sigmoidal_contrast(True, 3, .5 * filtered_image.quantum_range)
filtered_image.sigmoidal_contrast(True, 3, .5 * filtered_image.quantum_range)
return filtered_image
[docs]class NashvilleFilterXForm(FilterXForm):
"""
Class implementing Instagram's Nashville filter
"""
[docs] def filter(self, image: wand.image.Image) -> wand.image.Image:
"""
modified from https://github.com/acoomans/instagram-filters/blob/master/instagram_filters/filters/nashville.py
:param image:
:return: new filtered image
"""
filtered_image = image.clone()
self._colortone(filtered_image, '#222b6d', 50, True)
self._colortone(filtered_image, '#f7daae', 50, False)
filtered_image.sigmoidal_contrast(True, 3, .5 * filtered_image.quantum_range)
filtered_image.modulate(100, 150, 100)
filtered_image.auto_gamma()
return filtered_image
[docs]class KelvinFilterXForm(FilterXForm):
"""
Class implementing Instagram's Kelvin filter
"""
[docs] def filter(self, image: wand.image.Image) -> wand.image.Image:
"""
modified from https://github.com/acoomans/instagram-filters/blob/master/instagram_filters/filters/kelvin.py
:param image: provided image
:return: new filtered image
"""
filtered_image = image.clone()
filtered_image.auto_gamma()
filtered_image.modulate(120, 50, 100)
with wand.drawing.Drawing() as draw:
draw.fill_color = '#FF9900'
draw.fill_opacity = 0.2
draw.rectangle(left=0, top=0, width=filtered_image.width, height=filtered_image.height)
draw(filtered_image)
return filtered_image
[docs]class LomoFilterXForm(FilterXForm):
"""
Class implementing Instagram's Lomo filter
"""
[docs] def filter(self, image: wand.image.Image) -> wand.image.Image:
"""
modified from https://github.com/acoomans/instagram-filters/blob/master/instagram_filters/filters/lomo.py
:param image: provided image
:return: new filtered image
"""
filtered_image = image.clone()
filtered_image.level(.5, channel="R")
filtered_image.level(.5, channel="G")
self._vignette(filtered_image)
return filtered_image
[docs]class ToasterXForm(FilterXForm):
"""
Class implementing Instagram's Toaster filter
"""
[docs] def filter(self, image: wand.image.Image) -> wand.image.Image:
"""
modified from https://github.com/acoomans/instagram-filters/blob/master/instagram_filters/filters/toaster.py
:param image: provided image
:return: new filtered image
"""
filtered_image = image.clone()
self._colortone(filtered_image, '#330000', 50, True)
filtered_image.modulate(150, 80, 100)
filtered_image.gamma(1.2)
filtered_image.sigmoidal_contrast(True, 3, .5 * filtered_image.quantum_range)
filtered_image.sigmoidal_contrast(True, 3, .5 * filtered_image.quantum_range)
self._vignette(filtered_image, 'none', 'LavenderBlush3')
self._vignette(filtered_image, '#ff9966', 'none')
return filtered_image