finally got the hang of bw/colour image handling
This commit is contained in:
		| @@ -7,9 +7,10 @@ Copyright by aceinnolab | |||||||
| """ | """ | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
|  | from typing import Literal | ||||||
|  |  | ||||||
| import PIL |  | ||||||
| import numpy | import numpy | ||||||
|  | import PIL | ||||||
| import requests | import requests | ||||||
| from PIL import Image | from PIL import Image | ||||||
|  |  | ||||||
| @@ -17,8 +18,7 @@ logger = logging.getLogger(__name__) | |||||||
|  |  | ||||||
|  |  | ||||||
| class Inkyimage: | 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): |     def __init__(self, image=None): | ||||||
|         """Initialize InkyImage module""" |         """Initialize InkyImage module""" | ||||||
| @@ -27,7 +27,7 @@ class Inkyimage: | |||||||
|         self.image = image |         self.image = image | ||||||
|  |  | ||||||
|         # give an OK message |         # 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. |         """loads an image from a URL or filepath. | ||||||
| @@ -45,54 +45,54 @@ class Inkyimage: | |||||||
|         """ |         """ | ||||||
|         # Try to open the image if it exists and is an image file |         # Try to open the image if it exists and is an image file | ||||||
|         try: |         try: | ||||||
|             if path.startswith('http'): |             if path.startswith("http"): | ||||||
|                 logger.info('loading image from URL') |                 logger.info("loading image from URL") | ||||||
|                 image = Image.open(requests.get(path, stream=True).raw) |                 image = Image.open(requests.get(path, stream=True).raw) | ||||||
|             else: |             else: | ||||||
|                 logger.info('loading image from local path') |                 logger.info("loading image from local path") | ||||||
|                 image = Image.open(path) |                 image = Image.open(path) | ||||||
|         except FileNotFoundError: |         except FileNotFoundError: | ||||||
|             logger.error('No image file found', exc_info=True) |             logger.error("No image file found", exc_info=True) | ||||||
|             raise Exception(f'Your file could not be found. Please check the filepath: {path}') |             raise Exception(f"Your file could not be found. Please check the filepath: {path}") | ||||||
|  |  | ||||||
|         except OSError: |         except OSError: | ||||||
|             logger.error('Invalid Image file provided', exc_info=True) |             logger.error("Invalid Image file provided", exc_info=True) | ||||||
|             raise Exception('Please check if the path points to an image file.') |             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 |         self.image = image | ||||||
|         logger.info('loaded Image') |         logger.info("loaded Image") | ||||||
|  |  | ||||||
|     def clear(self): |     def clear(self): | ||||||
|         """Removes currently saved image if present.""" |         """Removes currently saved image if present.""" | ||||||
|         if self.image: |         if self.image: | ||||||
|             self.image = None |             self.image = None | ||||||
|             logger.info('cleared previous image') |             logger.info("cleared previous image") | ||||||
|  |  | ||||||
|     def _preview(self): |     def _preview(self): | ||||||
|         """Preview the image on gpicview (only works on Rapsbian with Desktop)""" |         """Preview the image on gpicview (only works on Rapsbian with Desktop)""" | ||||||
|         if self._image_loaded(): |         if self._image_loaded(): | ||||||
|             path = '/home/pi/Desktop/' |             path = "/home/pi/Desktop/" | ||||||
|             self.image.save(path + 'temp.png') |             self.image.save(path + "temp.png") | ||||||
|             os.system("gpicview " + path + 'temp.png') |             os.system("gpicview " + path + "temp.png") | ||||||
|             os.system('rm ' + path + 'temp.png') |             os.system("rm " + path + "temp.png") | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def preview(image): |     def preview(image): | ||||||
|         """Previews an image on gpicview (only works on Rapsbian with Desktop).""" |         """Previews an image on gpicview (only works on Rapsbian with Desktop).""" | ||||||
|         path = '~/temp' |         path = "~/temp" | ||||||
|         image.save(path + '/temp.png') |         image.save(path + "/temp.png") | ||||||
|         os.system("gpicview " + path + '/temp.png') |         os.system("gpicview " + path + "/temp.png") | ||||||
|         os.system('rm ' + path + '/temp.png') |         os.system("rm " + path + "/temp.png") | ||||||
|  |  | ||||||
|     def _image_loaded(self): |     def _image_loaded(self): | ||||||
|         """returns True if image was loaded""" |         """returns True if image was loaded""" | ||||||
|         if self.image: |         if self.image: | ||||||
|             return True |             return True | ||||||
|         else: |         else: | ||||||
|             logger.error('image not loaded') |             logger.error("image not loaded") | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
|     def flip(self, angle): |     def flip(self, angle): | ||||||
| @@ -105,12 +105,12 @@ class Inkyimage: | |||||||
|  |  | ||||||
|             image = self.image |             image = self.image | ||||||
|             if not angle % 90 == 0: |             if not angle % 90 == 0: | ||||||
|                 logger.error('Angle must be a multiple of 90') |                 logger.error("Angle must be a multiple of 90") | ||||||
|                 return |                 return | ||||||
|  |  | ||||||
|             image = image.rotate(angle, expand=True) |             image = image.rotate(angle, expand=True) | ||||||
|             self.image = image |             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: |     def autoflip(self, layout: str) -> None: | ||||||
|         """flips the image automatically to the given layout. |         """flips the image automatically to the given layout. | ||||||
| @@ -129,17 +129,17 @@ class Inkyimage: | |||||||
|         if self._image_loaded(): |         if self._image_loaded(): | ||||||
|  |  | ||||||
|             image = self.image |             image = self.image | ||||||
|             if layout == 'horizontal': |             if layout == "horizontal": | ||||||
|                 if image.height > image.width: |                 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) |                     image = image.rotate(90, expand=True) | ||||||
|  |  | ||||||
|             elif layout == 'vertical': |             elif layout == "vertical": | ||||||
|                 if image.width > image.height: |                 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) |                     image = image.rotate(90, expand=True) | ||||||
|             else: |             else: | ||||||
|                 logger.error('layout not supported') |                 logger.error("layout not supported") | ||||||
|                 return |                 return | ||||||
|             self.image = image |             self.image = image | ||||||
|  |  | ||||||
| @@ -153,26 +153,26 @@ class Inkyimage: | |||||||
|             image = self.image |             image = self.image | ||||||
|  |  | ||||||
|             if len(image.getbands()) == 4: |             if len(image.getbands()) == 4: | ||||||
|                 logger.info('removing alpha channel') |                 logger.info("removing alpha channel") | ||||||
|                 bg = Image.new('RGBA', (image.width, image.height), 'white') |                 bg = Image.new("RGBA", (image.width, image.height), "white") | ||||||
|                 im = Image.alpha_composite(bg, image) |                 im = Image.alpha_composite(bg, image) | ||||||
|  |  | ||||||
|                 self.image.paste(im, (0, 0)) |                 self.image.paste(im, (0, 0)) | ||||||
|                 logger.info('removed transparency') |                 logger.info("removed transparency") | ||||||
|  |  | ||||||
|     def resize(self, width=None, height=None): |     def resize(self, width=None, height=None): | ||||||
|         """Resize an image to desired width or height""" |         """Resize an image to desired width or height""" | ||||||
|         if self._image_loaded(): |         if self._image_loaded(): | ||||||
|  |  | ||||||
|             if not width and not height: |             if not width and not height: | ||||||
|                 logger.error('no height of width specified') |                 logger.error("no height of width specified") | ||||||
|                 return |                 return | ||||||
|  |  | ||||||
|             image = self.image |             image = self.image | ||||||
|  |  | ||||||
|             if width: |             if width: | ||||||
|                 initial_width = image.width |                 initial_width = image.width | ||||||
|                 wpercent = (width / float(image.width)) |                 wpercent = width / float(image.width) | ||||||
|                 hsize = int((float(image.height) * float(wpercent))) |                 hsize = int((float(image.height) * float(wpercent))) | ||||||
|                 image = image.resize((width, hsize), Image.LANCZOS) |                 image = image.resize((width, hsize), Image.LANCZOS) | ||||||
|                 logger.info(f"resized image from {initial_width} to {image.width}") |                 logger.info(f"resized image from {initial_width} to {image.width}") | ||||||
| @@ -180,7 +180,7 @@ class Inkyimage: | |||||||
|  |  | ||||||
|             if height: |             if height: | ||||||
|                 initial_height = image.height |                 initial_height = image.height | ||||||
|                 hpercent = (height / float(image.height)) |                 hpercent = height / float(image.height) | ||||||
|                 wsize = int(float(image.width) * float(hpercent)) |                 wsize = int(float(image.width) * float(hpercent)) | ||||||
|                 image = image.resize((wsize, height), Image.LANCZOS) |                 image = image.resize((wsize, height), Image.LANCZOS) | ||||||
|                 logger.info(f"resized image from {initial_height} to {image.height}") |                 logger.info(f"resized image from {initial_height} to {image.height}") | ||||||
| @@ -203,17 +203,20 @@ class Inkyimage: | |||||||
|  |  | ||||||
|         def clear_white(img): |         def clear_white(img): | ||||||
|             """Replace all white pixels from image with transparent pixels""" |             """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) |             x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8) | ||||||
|             return Image.fromarray(x) |             return Image.fromarray(x) | ||||||
|  |  | ||||||
|         image2 = clear_white(image2) |         image2 = clear_white(image2) | ||||||
|         image1.paste(image2, (0, 0), image2) |         image1.paste(image2, (0, 0), image2) | ||||||
|         logger.info('merged given images into one') |         logger.info("merged given images into one") | ||||||
|  |  | ||||||
|         return image1 |         return image1 | ||||||
|  |  | ||||||
|     def to_palette(self, palette, dither=True) -> (PIL.Image, PIL.Image): |  | ||||||
|  | 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. |     """Maps an image to a given colour palette. | ||||||
|  |  | ||||||
|     Maps each pixel from the image to a colour from the palette. |     Maps each pixel from the image to a colour from the palette. | ||||||
| @@ -235,29 +238,24 @@ class Inkyimage: | |||||||
|     >>> 'bw'  # black-white |     >>> 'bw'  # black-white | ||||||
|     >>> '16gray' # 16 shades of gray |     >>> '16gray' # 16 shades of gray | ||||||
|     """ |     """ | ||||||
|         # Check if an image is loaded |  | ||||||
|         if self._image_loaded(): |  | ||||||
|             image = self.image.convert('RGB') |  | ||||||
|         else: |  | ||||||
|             raise FileNotFoundError |  | ||||||
|  |  | ||||||
|         if palette == 'bwr': |     if palette == "bwr": | ||||||
|         # black-white-red palette |         # black-white-red palette | ||||||
|         pal = [255, 255, 255, 0, 0, 0, 255, 0, 0] |         pal = [255, 255, 255, 0, 0, 0, 255, 0, 0] | ||||||
|  |  | ||||||
|         elif palette == 'bwy': |     elif palette == "bwy": | ||||||
|         # black-white-yellow palette |         # black-white-yellow palette | ||||||
|         pal = [255, 255, 255, 0, 0, 0, 255, 255, 0] |         pal = [255, 255, 255, 0, 0, 0, 255, 255, 0] | ||||||
|  |  | ||||||
|         elif palette == 'bw': |     elif palette == "bw": | ||||||
|         pal = None |         pal = None | ||||||
|         elif palette == '16gray': |     elif palette == "16gray": | ||||||
|         pal = [x for x in range(0, 256, 16)] * 3 |         pal = [x for x in range(0, 256, 16)] * 3 | ||||||
|         pal.sort() |         pal.sort() | ||||||
|  |  | ||||||
|     else: |     else: | ||||||
|             logger.error('The given palette is unsupported.') |         logger.error("The given palette is unsupported.") | ||||||
|             raise ValueError('The given palette is not supported.') |         raise ValueError("The given palette is not supported.") | ||||||
|  |  | ||||||
|     if pal: |     if pal: | ||||||
|         # The palette needs to have 256 colors, for this, the black-colour |         # The palette needs to have 256 colors, for this, the black-colour | ||||||
| @@ -274,7 +272,7 @@ class Inkyimage: | |||||||
|         # print(f'The palette now has {colours} colours') |         # print(f'The palette now has {colours} colours') | ||||||
|  |  | ||||||
|         # Create a dummy image to be used as a palette |         # Create a dummy image to be used as a palette | ||||||
|             palette_im = Image.new('P', (1, 1)) |         palette_im = Image.new("P", (1, 1)) | ||||||
|  |  | ||||||
|         # Attach the created palette. The palette should have 256 colours |         # Attach the created palette. The palette should have 256 colours | ||||||
|         # equivalent to 768 integers |         # equivalent to 768 integers | ||||||
| @@ -282,7 +280,7 @@ class Inkyimage: | |||||||
|  |  | ||||||
|         # Quantize the image to given palette |         # Quantize the image to given palette | ||||||
|         quantized_im = image.quantize(palette=palette_im, dither=dither) |         quantized_im = image.quantize(palette=palette_im, dither=dither) | ||||||
|             quantized_im = quantized_im.convert('RGB') |         quantized_im = quantized_im.convert("RGB") | ||||||
|  |  | ||||||
|         # get rgb of the non-black-white colour from the palette |         # 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 = [pal[x : x + 3] for x in range(0, len(pal), 3)] | ||||||
| @@ -321,13 +319,13 @@ class Inkyimage: | |||||||
|         # self.preview(im_colour) |         # self.preview(im_colour) | ||||||
|  |  | ||||||
|     else: |     else: | ||||||
|             im_black = image.convert('1', dither=dither) |         im_black = image.convert("1", dither=dither) | ||||||
|             im_colour = Image.new(mode='1', size=im_black.size, color='white') |         im_colour = Image.new(mode="1", size=im_black.size, color="white") | ||||||
|  |  | ||||||
|         logger.info('mapped image to specified palette') |     logger.info("mapped image to specified palette") | ||||||
|  |  | ||||||
|     return im_black, im_colour |     return im_black, im_colour | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == "__main__": | ||||||
|     print(f'running {__name__} in standalone/debug mode') |     print(f"running {__name__} in standalone/debug mode") | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ from inkycal.custom.functions import fonts | |||||||
| from inkycal.custom.functions import internet_available | from inkycal.custom.functions import internet_available | ||||||
| from inkycal.custom.functions import top_level | from inkycal.custom.functions import top_level | ||||||
| from inkycal.custom.inkycal_exceptions import NetworkNotReachableError | from inkycal.custom.inkycal_exceptions import NetworkNotReachableError | ||||||
|  | from inkycal.modules.inky_image import image_to_palette | ||||||
| from inkycal.modules.template import inkycal_module | from inkycal.modules.template import inkycal_module | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @@ -631,12 +632,14 @@ class Fullweather(inkycal_module): | |||||||
|             self.image = self.image.rotate(90, expand=True) |             self.image = self.image.rotate(90, expand=True) | ||||||
|  |  | ||||||
|         # TODO: only for debugging, remove this: |         # 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.") |         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 |         # Return the images ready for the display | ||||||
|         # tbh, I have no idea why I need to return two separate images here |         return im_black, im_colour | ||||||
|         return self.image, self.image |  | ||||||
|  |  | ||||||
|     def get_font(self, style, size): |     def get_font(self, style, size): | ||||||
|         # Returns the TrueType font object with the given characteristics |         # Returns the TrueType font object with the given characteristics | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ | |||||||
| Inkycal Image Module | Inkycal Image Module | ||||||
| Copyright by aceinnolab | Copyright by aceinnolab | ||||||
| """ | """ | ||||||
|  |  | ||||||
| from inkycal.custom import * | 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.inky_image import Inkyimage as Images | ||||||
| from inkycal.modules.template import inkycal_module | from inkycal.modules.template import inkycal_module | ||||||
|  |  | ||||||
| @@ -11,36 +11,21 @@ logger = logging.getLogger(__name__) | |||||||
|  |  | ||||||
|  |  | ||||||
| class Inkyimage(inkycal_module): | 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" |     name = "Inkycal Image - show an image from a URL or local path" | ||||||
|  |  | ||||||
|     requires = { |     requires = { | ||||||
|  |  | ||||||
|         "path": { |         "path": { | ||||||
|             "label": "Path to a local folder, e.g. /home/pi/Desktop/images. " |             "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 = { |     optional = { | ||||||
|  |         "autoflip": {"label": "Should the image be flipped automatically?", "options": [True, False]}, | ||||||
|         "autoflip": { |         "orientation": {"label": "Please select the desired orientation", "options": ["vertical", "horizontal"]}, | ||||||
|             "label": "Should the image be flipped automatically?", |  | ||||||
|             "options": [True, False] |  | ||||||
|         }, |  | ||||||
|  |  | ||||||
|         "orientation": { |  | ||||||
|             "label": "Please select the desired orientation", |  | ||||||
|             "options": ["vertical", "horizontal"] |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     def __init__(self, config): |     def __init__(self, config): | ||||||
| @@ -48,24 +33,24 @@ class Inkyimage(inkycal_module): | |||||||
|  |  | ||||||
|         super().__init__(config) |         super().__init__(config) | ||||||
|  |  | ||||||
|         config = config['config'] |         config = config["config"] | ||||||
|  |  | ||||||
|         # required parameters |         # required parameters | ||||||
|         for param in self.requires: |         for param in self.requires: | ||||||
|             if not param in config: |             if not param in config: | ||||||
|                 raise Exception(f'config is missing {param}') |                 raise Exception(f"config is missing {param}") | ||||||
|  |  | ||||||
|         # optional parameters |         # optional parameters | ||||||
|         self.path = config['path'] |         self.path = config["path"] | ||||||
|         self.palette = config['palette'] |         self.palette = config["palette"] | ||||||
|         self.autoflip = config['autoflip'] |         self.autoflip = config["autoflip"] | ||||||
|         self.orientation = config['orientation'] |         self.orientation = config["orientation"] | ||||||
|         self.dither = True |         self.dither = True | ||||||
|         if 'dither' in config and config["dither"] == False: |         if "dither" in config and config["dither"] == False: | ||||||
|             self.dither = False |             self.dither = False | ||||||
|  |  | ||||||
|         # give an OK message |         # give an OK message | ||||||
|         print(f'{__name__} loaded') |         print(f"{__name__} loaded") | ||||||
|  |  | ||||||
|     def generate_image(self): |     def generate_image(self): | ||||||
|         """Generate image for this module""" |         """Generate image for this module""" | ||||||
| @@ -75,7 +60,7 @@ class Inkyimage(inkycal_module): | |||||||
|         im_height = int(self.height - (2 * self.padding_top)) |         im_height = int(self.height - (2 * self.padding_top)) | ||||||
|         im_size = im_width, im_height |         im_size = im_width, im_height | ||||||
|  |  | ||||||
|         logger.info(f'Image size: {im_size}') |         logger.info(f"Image size: {im_size}") | ||||||
|  |  | ||||||
|         # initialize custom image class |         # initialize custom image class | ||||||
|         im = Images() |         im = Images() | ||||||
| @@ -94,7 +79,7 @@ class Inkyimage(inkycal_module): | |||||||
|         im.resize(width=im_width, height=im_height) |         im.resize(width=im_width, height=im_height) | ||||||
|  |  | ||||||
|         # convert images according to specified palette |         # 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 |         # with the images now send, clear the current image | ||||||
|         im.clear() |         im.clear() | ||||||
| @@ -103,5 +88,5 @@ class Inkyimage(inkycal_module): | |||||||
|         return im_black, im_colour |         return im_black, im_colour | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == "__main__": | ||||||
|     print(f'running {__name__} in standalone/debug mode') |     print(f"running {__name__} in standalone/debug mode") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 mrbwburns
					mrbwburns