Spaces:
Runtime error
Runtime error
| """A library for describing and applying affine transforms to PIL images.""" | |
| import numpy as np | |
| import PIL.Image | |
| class RGBTransform(object): | |
| """A description of an affine transformation to an RGB image. | |
| This class is immutable. | |
| Methods correspond to matrix left-multiplication/post-application: | |
| for example, | |
| RGBTransform().multiply_with(some_color).desaturate() | |
| describes a transformation where the multiplication takes place first. | |
| Use rgbt.applied_to(image) to return a converted copy of the given image. | |
| For example: | |
| grayish = RGBTransform.desaturate(factor=0.5).applied_to(some_image) | |
| """ | |
| def __init__(self, matrix=None): | |
| self._matrix = matrix if matrix is not None else np.eye(4) | |
| def _then(self, operation): | |
| return RGBTransform(np.dot(_embed44(operation), self._matrix)) | |
| def desaturate(self, factor=1.0, weights=(0.299, 0.587, 0.114)): | |
| """Desaturate an image by the given amount. | |
| A factor of 1.0 will make the image completely gray; | |
| a factor of 0.0 will leave the image unchanged. | |
| The weights represent the relative contributions of each channel. | |
| They should be a 1-by-3 array-like object (tuple, list, np.array). | |
| In most cases, their values should sum to 1.0 | |
| (otherwise, the transformation will cause the image | |
| to get lighter or darker). | |
| """ | |
| weights = _to_rgb(weights, "weights") | |
| # tile: [wr, wg, wb] ==> [[wr, wg, wb], [wr, wg, wb], [wr, wg, wb]] | |
| desaturated_component = factor * np.tile(weights, (3, 1)) | |
| saturated_component = (1 - factor) * np.eye(3) | |
| operation = desaturated_component + saturated_component | |
| return self._then(operation) | |
| def multiply_with(self, base_color, factor=1.0): | |
| """Multiply an image by a constant base color. | |
| The base color should be a 1-by-3 array-like object | |
| representing an RGB color in [0, 255]^3 space. | |
| For example, to multiply with orange, | |
| the transformation | |
| RGBTransform().multiply_with((255, 127, 0)) | |
| might be used. | |
| The factor controls the strength of the multiplication. | |
| A factor of 1.0 represents straight multiplication; | |
| other values will be linearly interpolated between | |
| the identity (0.0) and the straight multiplication (1.0). | |
| """ | |
| component_vector = _to_rgb(base_color, "base_color") / 255.0 | |
| new_component = factor * np.diag(component_vector) | |
| old_component = (1 - factor) * np.eye(3) | |
| operation = new_component + old_component | |
| return self._then(operation) | |
| def mix_with(self, base_color, factor=1.0): | |
| """Mix an image by a constant base color. | |
| The base color should be a 1-by-3 array-like object | |
| representing an RGB color in [0, 255]^3 space. | |
| For example, to mix with orange, | |
| the transformation | |
| RGBTransform().mix_with((255, 127, 0)) | |
| might be used. | |
| The factor controls the strength of the color to be added. | |
| If the factor is 1.0, all pixels will be exactly the new color; | |
| if it is 0.0, the pixels will be unchanged. | |
| """ | |
| base_color = _to_rgb(base_color, "base_color") | |
| operation = _embed44((1 - factor) * np.eye(3)) | |
| operation[:3, 3] = factor * base_color | |
| return self._then(operation) | |
| def get_matrix(self): | |
| """Get the underlying 3-by-4 matrix for this affine transform.""" | |
| return self._matrix[:3, :] | |
| def applied_to(self, image): | |
| """Apply this transformation to a copy of the given RGB* image. | |
| The image should be a PIL image with at least three channels. | |
| Specifically, the RGB and RGBA modes are both supported, but L is not. | |
| Any channels past the first three will pass through unchanged. | |
| The original image will not be modified; | |
| a new image of the same mode and dimensions will be returned. | |
| """ | |
| # PIL.Image.convert wants the matrix as a flattened 12-tuple. | |
| # (The docs claim that they want a 16-tuple, but this is wrong; | |
| # cf. _imaging.c:767 in the PIL 1.1.7 source.) | |
| matrix = tuple(self.get_matrix().flatten()) | |
| channel_names = image.getbands() | |
| channel_count = len(channel_names) | |
| if channel_count < 3: | |
| raise ValueError("Image must have at least three channels!") | |
| elif channel_count == 3: | |
| return image.convert('RGB', matrix) | |
| else: | |
| # Probably an RGBA image. | |
| # Operate on the first three channels (assuming RGB), | |
| # and tack any others back on at the end. | |
| channels = list(image.split()) | |
| rgb = PIL.Image.merge('RGB', channels[:3]) | |
| transformed = rgb.convert('RGB', matrix) | |
| new_channels = transformed.split() | |
| channels[:3] = new_channels | |
| return PIL.Image.merge(''.join(channel_names), channels) | |
| def applied_to_pixel(self, color): | |
| """Apply this transformation to a single RGB* pixel. | |
| In general, you want to apply a transformation to an entire image. | |
| But in the special case where you know that the image is all one color, | |
| you can save cycles by just applying the transformation to that color | |
| and then constructing an image of the desired size. | |
| For example, in the result of the following code, | |
| image1 and image2 should be identical: | |
| rgbt = create_some_rgb_tranform() | |
| white = (255, 255, 255) | |
| size = (100, 100) | |
| image1 = rgbt.applied_to(PIL.Image.new("RGB", size, white)) | |
| image2 = PIL.Image.new("RGB", size, rgbt.applied_to_pixel(white)) | |
| The construction of image2 will be faster for two reasons: | |
| first, only one PIL image is created; and | |
| second, the transformation is only applied once. | |
| The input must have at least three channels; | |
| the first three channels will be interpreted as RGB, | |
| and any other channels will pass through unchanged. | |
| To match the behavior of PIL, | |
| the values of the resulting pixel will be rounded (not truncated!) | |
| to the nearest whole number. | |
| """ | |
| color = tuple(color) | |
| channel_count = len(color) | |
| extra_channels = tuple() | |
| if channel_count < 3: | |
| raise ValueError("Pixel must have at least three channels!") | |
| elif channel_count > 3: | |
| color, extra_channels = color[:3], color[3:] | |
| color_vector = np.array(color + (1, )).reshape(4, 1) | |
| result_vector = np.dot(self._matrix, color_vector) | |
| result = result_vector.flatten()[:3] | |
| full_result = tuple(result) + extra_channels | |
| rounded = tuple(int(round(x)) for x in full_result) | |
| return rounded | |
| def _embed44(matrix): | |
| """Embed a 4-by-4 or smaller matrix in the upper-left of I_4.""" | |
| result = np.eye(4) | |
| r, c = matrix.shape | |
| result[:r, :c] = matrix | |
| return result | |
| def _to_rgb(thing, name="input"): | |
| """Convert an array-like object to a 1-by-3 numpy array, or fail.""" | |
| thing = np.array(thing) | |
| assert thing.shape == (3, ), ( | |
| "Expected %r to be a length-3 array-like object, but found shape %s" % | |
| (name, thing.shape)) | |
| return thing |