finally got the hang of bw/colour image handling

This commit is contained in:
mrbwburns 2024-01-20 17:15:03 +01:00
parent c515da04f0
commit 6d660f48c3
3 changed files with 148 additions and 162 deletions

View File

@ -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,9 +27,9 @@ 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.
Args: Args:
@ -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,10 +280,10 @@ 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)]
rgb = [col for col in rgb if col != [0, 0, 0] and col != [255, 255, 255]][0] rgb = [col for col in rgb if col != [0, 0, 0] and col != [255, 255, 255]][0]
r_col, g_col, b_col = rgb r_col, g_col, b_col = rgb
# print(f'r:{r_col} g:{g_col} b:{b_col}') # print(f'r:{r_col} g:{g_col} b:{b_col}')
@ -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")

View File

@ -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

View File

@ -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")