Skip to content

Commit b08249e

Browse files
authored
Merge pull request #641 from hchargois/numpy-everywhere
Use Numpy for all revisions
2 parents 9946dcf + fdb9b58 commit b08249e

File tree

51 files changed

+23639
-133
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+23639
-133
lines changed

library/lcd/lcd_comm_rev_a.py

+4-38
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
from typing import Optional
2222

2323
from serial.tools.list_ports import comports
24-
import numpy as np
2524

2625
from library.lcd.lcd_comm import *
26+
from library.lcd.serialize import image_to_RGB565, chunked
2727
from library.log import logger
2828

2929

@@ -173,32 +173,6 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT):
173173
byteBuffer[10] = (height & 255)
174174
self.serial_write(bytes(byteBuffer))
175175

176-
@staticmethod
177-
def imageToRGB565LE(image: Image.Image):
178-
if image.mode not in ["RGB", "RGBA"]:
179-
# we need the first 3 channels to be R, G and B
180-
image = image.convert("RGB")
181-
182-
rgb = np.asarray(image)
183-
184-
# flatten the first 2 dimensions (width and height) into a single stream
185-
# of RGB pixels
186-
rgb = rgb.reshape((image.size[1] * image.size[0], -1))
187-
188-
# extract R, G, B channels and promote them to 16 bits
189-
r = rgb[:, 0].astype(np.uint16)
190-
g = rgb[:, 1].astype(np.uint16)
191-
b = rgb[:, 2].astype(np.uint16)
192-
193-
# construct RGB565
194-
r = (r >> 3)
195-
g = (g >> 2)
196-
b = (b >> 3)
197-
rgb565 = (r << 11) | (g << 5) | b
198-
199-
# serialize to little-endian
200-
return rgb565.astype('<u2').tobytes()
201-
202176
def DisplayPILImage(
203177
self,
204178
image: Image.Image,
@@ -232,20 +206,12 @@ def DisplayPILImage(
232206
(x0, y0) = (x, y)
233207
(x1, y1) = (x + image_width - 1, y + image_height - 1)
234208

235-
rgb565le = self.imageToRGB565LE(image)
209+
rgb565le = image_to_RGB565(image, "little")
236210

237211
self.SendCommand(Command.DISPLAY_BITMAP, x0, y0, x1, y1)
238212

239213
# Lock queue mutex then queue all the requests for the image data
240214
with self.update_queue_mutex:
241-
242215
# Send image data by multiple of "display width" bytes
243-
start = 0
244-
end = width * 8
245-
while end <= len(rgb565le):
246-
self.SendLine(rgb565le[start:end])
247-
start, end = end, end + width * 8
248-
249-
# Write last line if needed
250-
if start != len(rgb565le):
251-
self.SendLine(rgb565le[start:])
216+
for chunk in chunked(rgb565le, width * 8):
217+
self.SendLine(chunk)

library/lcd/lcd_comm_rev_b.py

+13-30
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
# You should have received a copy of the GNU General Public License
1818
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1919

20-
import struct
21-
2220
from serial.tools.list_ports import comports
2321

2422
from library.lcd.lcd_comm import *
23+
from library.lcd.serialize import image_to_RGB565, chunked
2524
from library.log import logger
2625

2726

@@ -194,6 +193,13 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT):
194193
else:
195194
self.SendCommand(Command.SET_ORIENTATION, payload=[OrientationValueRevB.ORIENTATION_LANDSCAPE])
196195

196+
def serialize_image(self, image: Image.Image, height: int, width: int) -> bytes:
197+
if image.width != width or image.height != height:
198+
image = image.crop((0, 0, width, height))
199+
if self.orientation == Orientation.REVERSE_PORTRAIT or self.orientation == Orientation.REVERSE_LANDSCAPE:
200+
image = image.rotate(180)
201+
return image_to_RGB565(image, "big")
202+
197203
def DisplayPILImage(
198204
self,
199205
image: Image.Image,
@@ -231,34 +237,11 @@ def DisplayPILImage(
231237
(y0 >> 8) & 255, y0 & 255,
232238
(x1 >> 8) & 255, x1 & 255,
233239
(y1 >> 8) & 255, y1 & 255])
234-
pix = image.load()
235-
line = bytes()
240+
241+
rgb565be = self.serialize_image(image, image_height, image_width)
236242

237243
# Lock queue mutex then queue all the requests for the image data
238244
with self.update_queue_mutex:
239-
for h in range(image_height):
240-
for w in range(image_width):
241-
if self.orientation == Orientation.PORTRAIT or self.orientation == Orientation.LANDSCAPE:
242-
R = pix[w, h][0] >> 3
243-
G = pix[w, h][1] >> 2
244-
B = pix[w, h][2] >> 3
245-
else:
246-
# Manage reverse orientations from software, because display does not manage it
247-
R = pix[image_width - w - 1, image_height - h - 1][0] >> 3
248-
G = pix[image_width - w - 1, image_height - h - 1][1] >> 2
249-
B = pix[image_width - w - 1, image_height - h - 1][2] >> 3
250-
251-
# Color information is 0bRRRRRGGGGGGBBBBB
252-
# Revision A: Encode in Little-Endian (native x86/ARM encoding)
253-
# Revition B: Encode in Big-Endian
254-
rgb = (R << 11) | (G << 5) | B
255-
line += struct.pack('>H', rgb)
256-
257-
# Send image data by multiple of "display width" bytes
258-
if len(line) >= self.get_width() * 8:
259-
self.SendLine(line)
260-
line = bytes()
261-
262-
# Write last line if needed
263-
if len(line) > 0:
264-
self.SendLine(line)
245+
# Send image data by multiple of "display width" bytes
246+
for chunk in chunked(rgb565be, self.get_width() * 8):
247+
self.SendLine(chunk)

library/lcd/lcd_comm_rev_c.py

+32-37
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
import time
2323
from enum import Enum
2424
from math import ceil
25-
from typing import Optional
25+
from typing import Optional, Tuple
2626

2727
import serial
2828
from PIL import Image
2929
from serial.tools.list_ports import comports
3030

3131
from library.lcd.lcd_comm import Orientation, LcdComm
32+
from library.lcd.serialize import image_to_BGRA, image_to_BGR, chunked
3233
from library.log import logger
3334

3435

@@ -282,6 +283,9 @@ def DisplayPILImage(
282283
if image.size[0] > self.get_width():
283284
image_width = self.get_width()
284285

286+
if image_width != image.size[0] or image_height != image.size[1]:
287+
image = image.crop((0, 0, image_width, image_height))
288+
285289
assert x <= self.get_width(), 'Image X coordinate must be <= display width'
286290
assert y <= self.get_height(), 'Image Y coordinate must be <= display height'
287291
assert image_height > 0, 'Image height must be > 0'
@@ -293,76 +297,67 @@ def DisplayPILImage(
293297
self._send_command(Command.START_DISPLAY_BITMAP, padding=Padding.START_DISPLAY_BITMAP)
294298
self._send_command(Command.DISPLAY_BITMAP)
295299
self._send_command(Command.SEND_PAYLOAD,
296-
payload=bytearray(self._generate_full_image(image, self.orientation)),
300+
payload=bytearray(self._generate_full_image(image)),
297301
readsize=1024)
298302
self._send_command(Command.QUERY_STATUS, readsize=1024)
299303
else:
300304
with self.update_queue_mutex:
301-
img, pyd = self._generate_update_image(image, x, y, Count.Start, Command.UPDATE_BITMAP,
302-
self.orientation)
305+
img, pyd = self._generate_update_image(image, x, y, Count.Start, Command.UPDATE_BITMAP)
303306
self._send_command(Command.SEND_PAYLOAD, payload=pyd)
304307
self._send_command(Command.SEND_PAYLOAD, payload=img)
305308
self._send_command(Command.QUERY_STATUS, readsize=1024)
306309
Count.Start += 1
307310

308-
@staticmethod
309-
def _generate_full_image(image: Image.Image, orientation: Orientation = Orientation.PORTRAIT):
310-
if orientation == Orientation.PORTRAIT:
311+
def _generate_full_image(self, image: Image.Image) -> bytes:
312+
if self.orientation == Orientation.PORTRAIT:
311313
image = image.rotate(90, expand=True)
312-
elif orientation == Orientation.REVERSE_PORTRAIT:
314+
elif self.orientation == Orientation.REVERSE_PORTRAIT:
313315
image = image.rotate(270, expand=True)
314-
elif orientation == Orientation.REVERSE_LANDSCAPE:
316+
elif self.orientation == Orientation.REVERSE_LANDSCAPE:
315317
image = image.rotate(180)
316318

317-
image_data = image.convert("RGBA").load()
318-
image_ret = ''
319-
for y in range(image.height):
320-
for x in range(image.width):
321-
pixel = image_data[x, y]
322-
image_ret += f'{pixel[2]:02x}{pixel[1]:02x}{pixel[0]:02x}{pixel[3]:02x}'
319+
bgra_data = image_to_BGRA(image)
323320

324-
hex_data = bytearray.fromhex(image_ret)
325-
return b'\x00'.join(hex_data[i:i + 249] for i in range(0, len(hex_data), 249))
321+
return b'\x00'.join(chunked(bgra_data, 249))
326322

327-
def _generate_update_image(self, image, x, y, count, cmd: Optional[Command] = None,
328-
orientation: Orientation = Orientation.PORTRAIT):
323+
def _generate_update_image(
324+
self, image: Image.Image, x: int, y: int, count: int, cmd: Optional[Command] = None
325+
) -> Tuple[bytearray, bytearray]:
329326
x0, y0 = x, y
330327

331-
if orientation == Orientation.PORTRAIT:
328+
if self.orientation == Orientation.PORTRAIT:
332329
image = image.rotate(90, expand=True)
333330
x0 = self.get_width() - x - image.height
334-
elif orientation == Orientation.REVERSE_PORTRAIT:
331+
elif self.orientation == Orientation.REVERSE_PORTRAIT:
335332
image = image.rotate(270, expand=True)
336333
y0 = self.get_height() - y - image.width
337-
elif orientation == Orientation.REVERSE_LANDSCAPE:
334+
elif self.orientation == Orientation.REVERSE_LANDSCAPE:
338335
image = image.rotate(180, expand=True)
339336
y0 = self.get_width() - x - image.width
340337
x0 = self.get_height() - y - image.height
341-
elif orientation == Orientation.LANDSCAPE:
338+
elif self.orientation == Orientation.LANDSCAPE:
342339
x0, y0 = y, x
343340

344-
img_raw_data = []
345-
image_data = image.convert("RGBA").load()
346-
for h in range(image.height):
347-
img_raw_data.append(f'{((x0 + h) * self.display_height) + y0:06x}{image.width:04x}')
348-
for w in range(image.width):
349-
current_pixel = image_data[w, h]
350-
img_raw_data.append(f'{current_pixel[2]:02x}{current_pixel[1]:02x}{current_pixel[0]:02x}')
341+
img_raw_data = bytearray()
342+
bgr_data = image_to_BGR(image)
343+
for h, line in enumerate(chunked(bgr_data, image.width * 3)):
344+
img_raw_data += int(((x0 + h) * self.display_height) + y0).to_bytes(3, "big")
345+
img_raw_data += int(image.width).to_bytes(2, "big")
346+
img_raw_data += line
351347

352-
image_msg = ''.join(img_raw_data)
353-
image_size = f'{int((len(image_msg) / 2) + 2):06x}' # The +2 is for the "ef69" that will be added later.
348+
image_size = int(len(img_raw_data) + 2).to_bytes(3, "big") # The +2 is for the "ef69" that will be added later.
354349

355350
# logger.debug("Render Count: {}".format(count))
356351
payload = bytearray()
357352

358353
if cmd:
359354
payload.extend(cmd.value)
360-
payload.extend(bytearray.fromhex(image_size))
355+
payload.extend(image_size)
361356
payload.extend(Padding.NULL.value * 3)
362357
payload.extend(count.to_bytes(4, 'big'))
363358

364-
if len(image_msg) > 500:
365-
image_msg = '00'.join(image_msg[i:i + 498] for i in range(0, len(image_msg), 498))
366-
image_msg += 'ef69'
359+
if len(img_raw_data) > 250:
360+
img_raw_data = bytearray(b'\x00').join(chunked(bytes(img_raw_data), 249))
361+
img_raw_data += b'\xef\x69'
367362

368-
return bytearray.fromhex(image_msg), payload
363+
return img_raw_data, payload

library/lcd/lcd_comm_rev_d.py

+10-28
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
# You should have received a copy of the GNU General Public License
1717
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

19-
import struct
2019
from enum import Enum
2120

2221
from serial.tools.list_ports import comports
2322

2423
from library.lcd.lcd_comm import *
24+
from library.lcd.serialize import image_to_RGB565, chunked
2525
from library.log import logger
2626

2727

@@ -109,7 +109,7 @@ def SetBrightness(self, level: int = 25):
109109
# Convert our brightness % to an absolute value.
110110
converted_level = level * 5
111111

112-
level_bytes = bytearray(converted_level.to_bytes(2))
112+
level_bytes = bytearray(converted_level.to_bytes(2, "big"))
113113

114114
# Send the command twice because sometimes it is not applied...
115115
self.SendCommand(cmd=Command.SETBL, payload=level_bytes)
@@ -166,40 +166,22 @@ def DisplayPILImage(
166166
image_width, image_height = image_height, image_width
167167

168168
# Send bitmap size
169-
image_data = bytearray(x0.to_bytes(2))
170-
image_data += bytearray(x1.to_bytes(2))
171-
image_data += bytearray(y0.to_bytes(2))
172-
image_data += bytearray(y1.to_bytes(2))
169+
image_data = bytearray()
170+
image_data += x0.to_bytes(2, "big")
171+
image_data += x1.to_bytes(2, "big")
172+
image_data += y0.to_bytes(2, "big")
173+
image_data += y1.to_bytes(2, "big")
173174
self.SendCommand(cmd=Command.BLOCKWRITE, payload=image_data)
174175

175176
# Prepare bitmap data transmission
176177
self.SendCommand(Command.INTOPICMODE)
177178

178-
pix = image.load()
179-
line = bytes([80])
179+
rgb565be = image_to_RGB565(image, "big")
180180

181181
# Lock queue mutex then queue all the requests for the image data
182182
with self.update_queue_mutex:
183-
for h in range(image_height):
184-
for w in range(image_width):
185-
R = pix[w, h][0] >> 3
186-
G = pix[w, h][1] >> 2
187-
B = pix[w, h][2] >> 3
188-
189-
# Color information is 0bRRRRRGGGGGGBBBBB
190-
# Revision A: Encode in Little-Endian (native x86/ARM encoding)
191-
# Revition B: Encode in Big-Endian
192-
rgb = (R << 11) | (G << 5) | B
193-
line += struct.pack('>H', rgb)
194-
195-
# Send image data by multiple of 64 bytes + 1 command byte
196-
if len(line) >= 65:
197-
self.SendLine(line[0:64])
198-
line = bytes([80]) + line[64:]
199-
200-
# Write last line if needed
201-
if len(line) > 0:
202-
self.SendLine(line)
183+
for chunk in chunked(rgb565be, 63):
184+
self.SendLine(b"\x50" + chunk)
203185

204186
# Indicate the complete bitmap has been transmitted
205187
self.SendCommand(Command.OUTPICMODE)

library/lcd/serialize.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Iterator, Literal
2+
3+
import numpy as np
4+
from PIL import Image
5+
6+
7+
def chunked(data: bytes, chunk_size: int) -> Iterator[bytes]:
8+
for i in range(0, len(data), chunk_size):
9+
yield data[i : i + chunk_size]
10+
11+
12+
def image_to_RGB565(image: Image.Image, endianness: Literal["big", "little"]) -> bytes:
13+
if image.mode not in ["RGB", "RGBA"]:
14+
# we need the first 3 channels to be R, G and B
15+
image = image.convert("RGB")
16+
17+
rgb = np.asarray(image)
18+
19+
# flatten the first 2 dimensions (width and height) into a single stream
20+
# of RGB pixels
21+
rgb = rgb.reshape((image.size[1] * image.size[0], -1))
22+
23+
# extract R, G, B channels and promote them to 16 bits
24+
r = rgb[:, 0].astype(np.uint16)
25+
g = rgb[:, 1].astype(np.uint16)
26+
b = rgb[:, 2].astype(np.uint16)
27+
28+
# construct RGB565
29+
r = r >> 3
30+
g = g >> 2
31+
b = b >> 3
32+
rgb565 = (r << 11) | (g << 5) | b
33+
34+
# serialize to the correct endianness
35+
if endianness == "big":
36+
typ = ">u2"
37+
else:
38+
typ = "<u2"
39+
return rgb565.astype(typ).tobytes()
40+
41+
42+
def image_to_BGR(image: Image.Image) -> bytes:
43+
if image.mode not in ["RGB", "RGBA"]:
44+
# we need the first 3 channels to be R, G and B
45+
image = image.convert("RGB")
46+
rgb = np.asarray(image)
47+
# same as rgb[:, :, [2, 1, 0]] but faster
48+
bgr = np.take(rgb, (2, 1, 0), axis=-1)
49+
return bgr.tobytes()
50+
51+
52+
def image_to_BGRA(image: Image.Image) -> bytes:
53+
if image.mode != "RGBA":
54+
image = image.convert("RGBA")
55+
rgba = np.asarray(image)
56+
# same as rgba[:, :, [2, 1, 0, 3]] but faster
57+
bgra = np.take(rgba, (2, 1, 0, 3), axis=-1)
58+
return bgra.tobytes()

tests/__init__.py

Whitespace-only changes.

tests/library/__init__.py

Whitespace-only changes.

tests/library/lcd/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)