From 6d660f48c36fe9a818bc62e99ae2b9ac7895038f Mon Sep 17 00:00:00 2001 From: mrbwburns <> Date: Sat, 20 Jan 2024 17:15:03 +0100 Subject: [PATCH] finally got the hang of bw/colour image handling --- inkycal/modules/inky_image.py | 250 ++++++++++++------------- inkycal/modules/inkycal_fullweather.py | 9 +- inkycal/modules/inkycal_image.py | 51 ++--- 3 files changed, 148 insertions(+), 162 deletions(-) diff --git a/inkycal/modules/inky_image.py b/inkycal/modules/inky_image.py index 1580c1a..d259e71 100755 --- a/inkycal/modules/inky_image.py +++ b/inkycal/modules/inky_image.py @@ -7,9 +7,10 @@ Copyright by aceinnolab """ import logging import os +from typing import Literal -import PIL import numpy +import PIL import requests from PIL import Image @@ -17,8 +18,7 @@ logger = logging.getLogger(__name__) class Inkyimage: - """Custom Imgae class written for commonly used image operations. - """ + """Custom Imgae class written for commonly used image operations.""" def __init__(self, image=None): """Initialize InkyImage module""" @@ -27,9 +27,9 @@ class Inkyimage: self.image = image # give an OK message - logger.info(f'{__name__} loaded') + logger.info(f"{__name__} loaded") - def load(self, path:str) -> None: + def load(self, path: str) -> None: """loads an image from a URL or filepath. Args: @@ -45,54 +45,54 @@ class Inkyimage: """ # Try to open the image if it exists and is an image file try: - if path.startswith('http'): - logger.info('loading image from URL') + if path.startswith("http"): + logger.info("loading image from URL") image = Image.open(requests.get(path, stream=True).raw) else: - logger.info('loading image from local path') + logger.info("loading image from local path") image = Image.open(path) except FileNotFoundError: - logger.error('No image file found', exc_info=True) - raise Exception(f'Your file could not be found. Please check the filepath: {path}') + logger.error("No image file found", exc_info=True) + raise Exception(f"Your file could not be found. Please check the filepath: {path}") except OSError: - logger.error('Invalid Image file provided', exc_info=True) - raise Exception('Please check if the path points to an image file.') + logger.error("Invalid Image file provided", exc_info=True) + raise Exception("Please check if the path points to an image file.") - logger.info(f'width: {image.width}, height: {image.height}') + logger.info(f"width: {image.width}, height: {image.height}") - image.convert(mode='RGBA') # convert to a more suitable format + image.convert(mode="RGBA") # convert to a more suitable format self.image = image - logger.info('loaded Image') + logger.info("loaded Image") def clear(self): """Removes currently saved image if present.""" if self.image: self.image = None - logger.info('cleared previous image') + logger.info("cleared previous image") def _preview(self): """Preview the image on gpicview (only works on Rapsbian with Desktop)""" if self._image_loaded(): - path = '/home/pi/Desktop/' - self.image.save(path + 'temp.png') - os.system("gpicview " + path + 'temp.png') - os.system('rm ' + path + 'temp.png') + path = "/home/pi/Desktop/" + self.image.save(path + "temp.png") + os.system("gpicview " + path + "temp.png") + os.system("rm " + path + "temp.png") @staticmethod def preview(image): """Previews an image on gpicview (only works on Rapsbian with Desktop).""" - path = '~/temp' - image.save(path + '/temp.png') - os.system("gpicview " + path + '/temp.png') - os.system('rm ' + path + '/temp.png') + path = "~/temp" + image.save(path + "/temp.png") + os.system("gpicview " + path + "/temp.png") + os.system("rm " + path + "/temp.png") def _image_loaded(self): """returns True if image was loaded""" if self.image: return True else: - logger.error('image not loaded') + logger.error("image not loaded") return False def flip(self, angle): @@ -105,12 +105,12 @@ class Inkyimage: image = self.image if not angle % 90 == 0: - logger.error('Angle must be a multiple of 90') + logger.error("Angle must be a multiple of 90") return image = image.rotate(angle, expand=True) self.image = image - logger.info(f'flipped image by {angle} degrees') + logger.info(f"flipped image by {angle} degrees") def autoflip(self, layout: str) -> None: """flips the image automatically to the given layout. @@ -129,17 +129,17 @@ class Inkyimage: if self._image_loaded(): image = self.image - if layout == 'horizontal': + if layout == "horizontal": if image.height > image.width: - logger.info('image width greater than image height, flipping') + logger.info("image width greater than image height, flipping") image = image.rotate(90, expand=True) - elif layout == 'vertical': + elif layout == "vertical": if image.width > image.height: - logger.info('image width greater than image height, flipping') + logger.info("image width greater than image height, flipping") image = image.rotate(90, expand=True) else: - logger.error('layout not supported') + logger.error("layout not supported") return self.image = image @@ -153,26 +153,26 @@ class Inkyimage: image = self.image if len(image.getbands()) == 4: - logger.info('removing alpha channel') - bg = Image.new('RGBA', (image.width, image.height), 'white') + logger.info("removing alpha channel") + bg = Image.new("RGBA", (image.width, image.height), "white") im = Image.alpha_composite(bg, image) self.image.paste(im, (0, 0)) - logger.info('removed transparency') + logger.info("removed transparency") def resize(self, width=None, height=None): """Resize an image to desired width or height""" if self._image_loaded(): if not width and not height: - logger.error('no height of width specified') + logger.error("no height of width specified") return image = self.image if width: initial_width = image.width - wpercent = (width / float(image.width)) + wpercent = width / float(image.width) hsize = int((float(image.height) * float(wpercent))) image = image.resize((width, hsize), Image.LANCZOS) logger.info(f"resized image from {initial_width} to {image.width}") @@ -180,7 +180,7 @@ class Inkyimage: if height: initial_height = image.height - hpercent = (height / float(image.height)) + hpercent = height / float(image.height) wsize = int(float(image.width) * float(hpercent)) image = image.resize((wsize, height), Image.LANCZOS) logger.info(f"resized image from {initial_height} to {image.height}") @@ -203,131 +203,129 @@ class Inkyimage: def clear_white(img): """Replace all white pixels from image with transparent pixels""" - x = numpy.asarray(img.convert('RGBA')).copy() + x = numpy.asarray(img.convert("RGBA")).copy() x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8) return Image.fromarray(x) image2 = clear_white(image2) image1.paste(image2, (0, 0), image2) - logger.info('merged given images into one') + logger.info("merged given images into one") return image1 - def to_palette(self, palette, dither=True) -> (PIL.Image, PIL.Image): - """Maps an image to a given colour palette. - Maps each pixel from the image to a colour from the palette. +def image_to_palette( + image: Image, palette: Literal = ["bwr", "bwy", "bw", "16gray"], dither: bool = True +) -> (PIL.Image, PIL.Image): + """Maps an image to a given colour palette. - Args: - - palette: A supported token. (see below) - - dither:->bool. Use dithering? Set to `False` for solid colour fills. + Maps each pixel from the image to a colour from the palette. - Returns: - - two images: one for the coloured band and one for the black band. + Args: + - palette: A supported token. (see below) + - dither:->bool. Use dithering? Set to `False` for solid colour fills. - Raises: - - ValueError if palette token is not supported + Returns: + - two images: one for the coloured band and one for the black band. - Supported palette tokens: + Raises: + - ValueError if palette token is not supported - >>> 'bwr' # black-white-red - >>> 'bwy' # black-white-yellow - >>> 'bw' # black-white - >>> '16gray' # 16 shades of gray - """ - # Check if an image is loaded - if self._image_loaded(): - image = self.image.convert('RGB') - else: - raise FileNotFoundError + Supported palette tokens: - if palette == 'bwr': - # black-white-red palette - pal = [255, 255, 255, 0, 0, 0, 255, 0, 0] + >>> 'bwr' # black-white-red + >>> 'bwy' # black-white-yellow + >>> 'bw' # black-white + >>> '16gray' # 16 shades of gray + """ - elif palette == 'bwy': - # black-white-yellow palette - pal = [255, 255, 255, 0, 0, 0, 255, 255, 0] + if palette == "bwr": + # black-white-red palette + pal = [255, 255, 255, 0, 0, 0, 255, 0, 0] - elif palette == 'bw': - pal = None - elif palette == '16gray': - pal = [x for x in range(0, 256, 16)] * 3 - pal.sort() + elif palette == "bwy": + # black-white-yellow palette + pal = [255, 255, 255, 0, 0, 0, 255, 255, 0] - else: - logger.error('The given palette is unsupported.') - raise ValueError('The given palette is not supported.') + elif palette == "bw": + pal = None + elif palette == "16gray": + pal = [x for x in range(0, 256, 16)] * 3 + pal.sort() - if pal: - # The palette needs to have 256 colors, for this, the black-colour - # is added until the - colours = len(pal) // 3 - # print(f'The palette has {colours} colours') + else: + logger.error("The given palette is unsupported.") + raise ValueError("The given palette is not supported.") - if 256 % colours != 0: - # print('Filling palette with black') - pal += (256 % colours) * [0, 0, 0] + if pal: + # The palette needs to have 256 colors, for this, the black-colour + # is added until the + colours = len(pal) // 3 + # print(f'The palette has {colours} colours') - # print(pal) - colours = len(pal) // 3 - # print(f'The palette now has {colours} colours') + if 256 % colours != 0: + # print('Filling palette with black') + pal += (256 % colours) * [0, 0, 0] - # Create a dummy image to be used as a palette - palette_im = Image.new('P', (1, 1)) + # print(pal) + colours = len(pal) // 3 + # print(f'The palette now has {colours} colours') - # Attach the created palette. The palette should have 256 colours - # equivalent to 768 integers - palette_im.putpalette(pal * (256 // colours)) + # Create a dummy image to be used as a palette + palette_im = Image.new("P", (1, 1)) - # Quantize the image to given palette - quantized_im = image.quantize(palette=palette_im, dither=dither) - quantized_im = quantized_im.convert('RGB') + # Attach the created palette. The palette should have 256 colours + # equivalent to 768 integers + palette_im.putpalette(pal * (256 // colours)) - # get rgb of the non-black-white colour from the palette - rgb = [pal[x:x + 3] for x in range(0, len(pal), 3)] - rgb = [col for col in rgb if col != [0, 0, 0] and col != [255, 255, 255]][0] - r_col, g_col, b_col = rgb - # print(f'r:{r_col} g:{g_col} b:{b_col}') + # Quantize the image to given palette + quantized_im = image.quantize(palette=palette_im, dither=dither) + quantized_im = quantized_im.convert("RGB") - # Create an image buffer for black pixels - buffer1 = numpy.array(quantized_im) + # get rgb of the non-black-white colour from the palette + rgb = [pal[x : x + 3] for x in range(0, len(pal), 3)] + rgb = [col for col in rgb if col != [0, 0, 0] and col != [255, 255, 255]][0] + r_col, g_col, b_col = rgb + # print(f'r:{r_col} g:{g_col} b:{b_col}') - # Get RGB values of each pixel - r, g, b = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] + # Create an image buffer for black pixels + buffer1 = numpy.array(quantized_im) - # convert coloured pixels to white - buffer1[numpy.logical_and(r == r_col, g == g_col)] = [255, 255, 255] + # Get RGB values of each pixel + r, g, b = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] - # reconstruct image for black-band - im_black = Image.fromarray(buffer1) + # convert coloured pixels to white + buffer1[numpy.logical_and(r == r_col, g == g_col)] = [255, 255, 255] - # Create a buffer for coloured pixels - buffer2 = numpy.array(quantized_im) + # reconstruct image for black-band + im_black = Image.fromarray(buffer1) - # Get RGB values of each pixel - r, g, b = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] + # Create a buffer for coloured pixels + buffer2 = numpy.array(quantized_im) - # convert black pixels to white - buffer2[numpy.logical_and(r == 0, g == 0)] = [255, 255, 255] + # Get RGB values of each pixel + r, g, b = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] - # convert non-white pixels to black - buffer2[numpy.logical_and(g == g_col, b == 0)] = [0, 0, 0] + # convert black pixels to white + buffer2[numpy.logical_and(r == 0, g == 0)] = [255, 255, 255] - # reconstruct image for colour-band - im_colour = Image.fromarray(buffer2) + # convert non-white pixels to black + buffer2[numpy.logical_and(g == g_col, b == 0)] = [0, 0, 0] - # self.preview(im_black) - # self.preview(im_colour) + # reconstruct image for colour-band + im_colour = Image.fromarray(buffer2) - else: - im_black = image.convert('1', dither=dither) - im_colour = Image.new(mode='1', size=im_black.size, color='white') + # self.preview(im_black) + # self.preview(im_colour) - logger.info('mapped image to specified palette') + else: + im_black = image.convert("1", dither=dither) + im_colour = Image.new(mode="1", size=im_black.size, color="white") - return im_black, im_colour + logger.info("mapped image to specified palette") + + return im_black, im_colour -if __name__ == '__main__': - print(f'running {__name__} in standalone/debug mode') +if __name__ == "__main__": + print(f"running {__name__} in standalone/debug mode") diff --git a/inkycal/modules/inkycal_fullweather.py b/inkycal/modules/inkycal_fullweather.py index 5165cc2..699a2e7 100644 --- a/inkycal/modules/inkycal_fullweather.py +++ b/inkycal/modules/inkycal_fullweather.py @@ -24,6 +24,7 @@ from inkycal.custom.functions import fonts from inkycal.custom.functions import internet_available from inkycal.custom.functions import top_level from inkycal.custom.inkycal_exceptions import NetworkNotReachableError +from inkycal.modules.inky_image import image_to_palette from inkycal.modules.template import inkycal_module logger = logging.getLogger(__name__) @@ -631,12 +632,14 @@ class Fullweather(inkycal_module): self.image = self.image.rotate(90, expand=True) # TODO: only for debugging, remove this: - # self.image.save("./openweather_full.png") + self.image.save("./openweather_full.png") logger.info("Fullscreen weather forecast generated successfully.") + # Convert images according to specified palette + im_black, im_colour = image_to_palette(image=self.image, palette="bwr", dither=True) + # Return the images ready for the display - # tbh, I have no idea why I need to return two separate images here - return self.image, self.image + return im_black, im_colour def get_font(self, style, size): # Returns the TrueType font object with the given characteristics diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index aed26e0..ea2a7a9 100755 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -2,8 +2,8 @@ Inkycal Image Module Copyright by aceinnolab """ - from inkycal.custom import * +from inkycal.modules.inky_image import image_to_palette from inkycal.modules.inky_image import Inkyimage as Images from inkycal.modules.template import inkycal_module @@ -11,36 +11,21 @@ logger = logging.getLogger(__name__) class Inkyimage(inkycal_module): - """Displays an image from URL or local path - """ + """Displays an image from URL or local path""" name = "Inkycal Image - show an image from a URL or local path" requires = { - "path": { "label": "Path to a local folder, e.g. /home/pi/Desktop/images. " - "Only PNG and JPG/JPEG images are used for the slideshow." + "Only PNG and JPG/JPEG images are used for the slideshow." }, - - "palette": { - "label": "Which palette should be used for converting images?", - "options": ["bw", "bwr", "bwy"] - } - + "palette": {"label": "Which palette should be used for converting images?", "options": ["bw", "bwr", "bwy"]}, } optional = { - - "autoflip": { - "label": "Should the image be flipped automatically?", - "options": [True, False] - }, - - "orientation": { - "label": "Please select the desired orientation", - "options": ["vertical", "horizontal"] - } + "autoflip": {"label": "Should the image be flipped automatically?", "options": [True, False]}, + "orientation": {"label": "Please select the desired orientation", "options": ["vertical", "horizontal"]}, } def __init__(self, config): @@ -48,24 +33,24 @@ class Inkyimage(inkycal_module): super().__init__(config) - config = config['config'] + config = config["config"] # required parameters for param in self.requires: if not param in config: - raise Exception(f'config is missing {param}') + raise Exception(f"config is missing {param}") # optional parameters - self.path = config['path'] - self.palette = config['palette'] - self.autoflip = config['autoflip'] - self.orientation = config['orientation'] + self.path = config["path"] + self.palette = config["palette"] + self.autoflip = config["autoflip"] + self.orientation = config["orientation"] self.dither = True - if 'dither' in config and config["dither"] == False: + if "dither" in config and config["dither"] == False: self.dither = False # give an OK message - print(f'{__name__} loaded') + print(f"{__name__} loaded") def generate_image(self): """Generate image for this module""" @@ -75,7 +60,7 @@ class Inkyimage(inkycal_module): im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.info(f"Image size: {im_size}") # initialize custom image class im = Images() @@ -94,7 +79,7 @@ class Inkyimage(inkycal_module): im.resize(width=im_width, height=im_height) # convert images according to specified palette - im_black, im_colour = im.to_palette(self.palette, self.dither) + im_black, im_colour = image_to_palette(image=im, palette=self.palette, dither=self.dither) # with the images now send, clear the current image im.clear() @@ -103,5 +88,5 @@ class Inkyimage(inkycal_module): return im_black, im_colour -if __name__ == '__main__': - print(f'running {__name__} in standalone/debug mode') +if __name__ == "__main__": + print(f"running {__name__} in standalone/debug mode")