Raspberry Pi, GfxLCD and image displaying

During the work on GUI, I had to add a new function, ability to load and display an image. First, this post was a part of bigger one but I think it should be a post on its own.
I tried to display numbers but we have no ability to display a text. So I thought about using images and that’s why we are here:)

GfxLCD and image

To handle images we will use PIL package. As I’m working currently on RPi with ILI display,  we will first add image support to it. We should be able to read image size, convert colours to RGB, set area and write pixels.

    def draw_image(self, pos_x, pos_y, image):
        """draw a PIL image"""
        image_file = image.convert('RGB')
        width, height = image_file.size
        self._set_area(
            pos_x,
            pos_y,
            pos_x + width - 1,
            pos_y + height - 1
        )
        for r, g, b in list(image_file.getdata()):
            self.color = (r, g, b)
            self.driver.data(self._converted_color(), None)

And usage:

import RPi.GPIO
from gfxlcd.driver.ili9325.gpio import GPIO as ILIGPIO
from gfxlcd.driver.ili9325.ili9325 import ILI9325
RPi.GPIO.setmode(RPi.GPIO.BCM)
from PIL import Image

lcd_tft = ILI9325(240, 320, ILIGPIO())
lcd_tft.init()
image_file = Image.open("colourwheel.png")
lcd_tft.draw_image(20, 50, image_file)

Pictures of the display are rather poor but something can be seen:

This works on ILI but what about SSD and NJU? They have only one colour. Hopefully, PIL will convert it to B&W.

Switched to second RPi with SSD and NJU. we can create the image displaying on them.
My first thought was to convert it to B&W and display. Good idea but fails what shows a test script:

lcd_oled = SSD1306(128, 64, SPI())
lcd_oled.init()
lcd_oled.auto_flush = False

image_file = Image.open("dsp2017_101_64.png")
image_file = image_file.convert("1")
width, height = image_file.size

print(list(image_file.getdata()))
x=0
y=0
for stream in list(image_file.getdata()):
    if stream:
        lcd_oled.draw_pixel(x, y)
    x += 1
    if x > width - 1:
        x = 0
        y += 1

Highly pixelized image, so this one is big no. What next? We could write our own algorithm. First, convert to greyscale and then use a threshold to detect 1 and 0. But this gives us another problem, we need a variable that works only in B&W mode. We could assign to threshold a fixed value but this is wrong, sometimes we would have to adjust it.
So I’m gonna move option to Pixel class and it will be available everywhere but only Page will use it.
We need to initialize it:

self.options['threshold'] = 50

And we cannot forget about setter and getter.

    @property
    def threshold(self):
        """get threshold for B&W conversion"""
        return self.options['threshold']

    @threshold.setter
    def threshold(self, threshold):
        """set B&W threshold for conversion """
        self.options['threshold'] = threshold

And finally, a drawing function in Page

    def draw_image(self, pos_x, pos_y, image):
        """draw a PIL image"""
        image_file = image.convert('L')
        width, height = image_file.size
        offset_x = 0
        offset_y = 0
        for stream in list(image_file.getdata()):
            if stream > self.options['threshold']:
                self.draw_pixel(pos_x + offset_x, pos_y + offset_y)
            offset_x += 1
            if offset_x > width - 1:
                offset_x = 0
                offset_y += 1

The effect is quite good:

What else can be useful? Maybe we should add transparency? A new variable that will be treated as non-existent colour. Hmm, it may be quite good. If we have numbers with the background we will just set background colour as transparent. I like it!
And maybe not just one value but the array of values? Or whatever:) Single value or array. Yes, this sounds good.

    def draw_image(self, pos_x, pos_y, image):
        """draw a PIL image"""
        image_file = image.convert('L')
        width, height = image_file.size
        offset_x = 0
        offset_y = 0
        print(list(image_file.getdata()))
        for stream in list(image_file.getdata()):
            if stream > self.options['threshold'] and not self._is_transparent(stream):
                self.draw_pixel(pos_x + offset_x, pos_y + offset_y)
            offset_x += 1
            if offset_x > width - 1:
                offset_x = 0
                offset_y += 1

    def _is_transparent(self, color):
        """check if color is a transparency color"""
        if type(self.options['transparency_color']) == int and color == self.options['transparency_color']:
            return True
        elif type(self.options['transparency_color']) == list and color in self.options['transparency_color']:
            return True

        return False

And demo script:

lcd_oled = SSD1306(128, 64, SPI())
lcd_oled.init()
lcd_oled.auto_flush = False

image_file = Image.open("assets/dsp2017_101_64.png")

lcd_oled.threshold = 50

lcd_oled.threshold = 0
lcd_oled.transparency_color = [110, 57] #110 #[110, 57]
print (type(lcd_oled.transparency_color))
# lcd_oled.threshold = 255

lcd_oled.draw_image(10, 0, image_file)

lcd_oled.flush(True)

The result:

Ok, back to ILI and transparency.

Used same method and big fail:) I forgot that we cannot skip any pixel because we are working in the selected area. We need to find a command that will move to next position or something similar. Back to documentation.

Damm, no such command. What to do now?
Maybe there is a way but it is quite troublesome. We could try and reassign area on the fly. When we detect a transparent pixel we will spawn a new temporary area, drawing from current column + 1, current row to the end of row. And keep in mind new area that we will assign after reaching the end of temporary one. The new area would be from next row, column zero to the end of required area.
Sounds complicated but let’s try.

 def draw_image(self, pos_x, pos_y, image):
        """draw a PIL image"""
        image_file = image.convert('RGB')
        width, height = image_file.size
        self._set_area(
            pos_x,
            pos_y,
            pos_x + width - 1,
            pos_y + height - 1
        )
        row = 0
        col = 0
        area = None
        for red, green, blue in list(image_file.getdata()):
            if self._is_transparent((red, green, blue)):
                area = (pos_x, pos_y + row + 1,  pos_x + width - 1, pos_y + height - 1)
                self._set_area(pos_x + col + 1, pos_y + row, pos_x + width - 1, pos_y + row)
            else:
                self.color = (red, green, blue)
                self.driver.data(self._converted_color(), None)

            col += 1
            if col > width - 1:
                col = 0
                row += 1
                if area is not None:
                    self._set_area(*area)
                    area = None

    def _is_transparent(self, color):
        """check if color is a transparency color"""
        if self.options['transparency_color'] is None:
            return False
        elif type(self.options['transparency_color'][0]) == int and color == self.options['transparency_color']:
            return True
        elif type(self.options['transparency_color'][0]) == list and color in self.options['transparency_color']:
            return True

        return False

The effect is quite nice:

But the speed of rendering is affected by the transparency. See that with each transparent pixel we set a new area. Maybe we can improve it a little? How? Do not set new area with each detection of transparency but rather calculate with each and set when writing the pixel.

            if self._is_transparent((red, green, blue)):
                area = (pos_x, pos_y + row + 1,  pos_x + width - 1, pos_y + height - 1)
                temporary_area = (pos_x + col + 1, pos_y + row, pos_x + width - 1, pos_y + row)
            else:
                if temporary_area is not None:
                    self._set_area(*temporary_area)
                    temporary_area = None
                self.color = (red, green, blue)
                self.driver.data(self._converted_color(), None)

The performance has increased more that twice!

Summary

We can draw images on our displays. This is awesome. It even supports transparency to some level:) Not perfectly, what shows the cover image.
I hope we have all bricks to create the Control Node.

Advertisements

2 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s