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

View File

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

View File

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