Skip to content

Commit 5a95408

Browse files
Sundry speed and image quality improvements
1 parent 8ead623 commit 5a95408

2 files changed

Lines changed: 177 additions & 52 deletions

File tree

CLUE_Light_Painter/bmp2led.py

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
BMP-to-DotStar-ready-bytearrays.
33
"""
44

5+
from time import monotonic
56
# pylint: disable=import-error
67
import os
78
import math
89
import ulab
910

11+
BUFFER_ROWS = 32
12+
1013
class BMPError(Exception):
1114
"""Used for raising errors in the BMP2LED Class."""
1215
pass
@@ -210,6 +213,14 @@ def process(self, input_filename, output_filename, rows,
210213
[255, 0, 0, 0] * self.num_pixels +
211214
[255] * ((self.num_pixels + 15) // 16))
212215
dotstar_row_size = len(dotstar_buffer)
216+
# Operation performed later requires a list, not a bytearray.
217+
# Make a copy, keeping the same values.
218+
# dotstar_list = list(dotstar_buffer)
219+
220+
# Output rows are held in RAM and periodically written,
221+
# marginally faster than writing each row separately.
222+
output_buffer = bytearray(BUFFER_ROWS * dotstar_row_size)
223+
output_position = 0
213224

214225
# Delete old temporary file, if any
215226
try:
@@ -246,17 +257,56 @@ def process(self, input_filename, output_filename, rows,
246257
prev_row_a_index, prev_row_b_index = None, None
247258

248259
with open(output_filename, 'wb') as led_file:
260+
# Determine remapping indices from BMP's always-BGR
261+
# pixel byte order to DotStar's variable order
262+
# (contained in self.red_index, green_index, blue_index).
263+
# I'm sure there's better ways but have a headache.
264+
# This is ONLY needed if using the first of two
265+
# benchmarked methods later (or something similar to it).
266+
# if self.blue_index is 0: # BXX DotStar
267+
# offset_0 = 0 # DotStar byte 0 is BMP byte 0 (B)
268+
# if self.green_index is 1: # BGR
269+
# offset_1 = 1 # DotStar byte 1 is BMP byte 1 (G)
270+
# offset_2 = 2 # DotStar byte 2 is BMP byte 2 (R)
271+
# else: # BRG
272+
# offset_1 = 2 # DotStar byte 1 is BMP byte 2 (R)
273+
# offset_2 = 1 # DotStar byte 2 is BMP byte 1 (G)
274+
# elif self.green_index is 0: # GXX DotStar
275+
# offset_0 = 1 # DotStar byte 0 is BMP byte 1 (G)
276+
# if self.blue_index is 1: # GBR
277+
# offset_1 = 0 # DotStar byte 1 is BMP byte 0 (B)
278+
# offset_2 = 2 # DotStar byte 2 is BMP byte 2 (R)
279+
# else: # GRB
280+
# offset_1 = 2 # DotStar byte 1 is BMP byte 2 (R)
281+
# offset_2 = 0 # DotStar byte 2 is BMP byte 0 (B)
282+
# else: # RXX DotStar
283+
# offset_0 = 2 # DotStar byte 0 is BMP byte 2 (R)
284+
# if self.green_index is 1: # RGB
285+
# offset_1 = 1 # DotStar byte 1 is BMP byte 1 (G)
286+
# offset_2 = 0 # DotStar byte 2 is BMP byte 0 (R)
287+
# else: # RBG
288+
# offset_1 = 0 # DotStar byte 1 is BMP byte 0 (R)
289+
# offset_2 = 1 # DotStar byte 2 is BMP byte 1 (G)
290+
291+
# To avoid continually appending to output file (a slow
292+
# operation), seek to where the end of the file would
293+
# be, write a nonsense byte there, then seek back to
294+
# the beginning. Significant improvement!
295+
led_file.seek((dotstar_row_size * rows) - 1)
296+
led_file.write(b'\0')
297+
led_file.seek(0)
249298
err = 0
299+
time1, time2, time3, time4 = 0, 0, 0, 0
300+
start_time = monotonic()
250301
for row in range(rows): # For each output row...
251-
position = row / (rows - 1) # 0.0 to 1.0
252-
if callback:
253-
callback(position)
302+
row_start_time = monotonic()
254303

255304
# Scale position into pixel space...
256305
if loop: # 0 to <image height
257306
position = self.bmp_specs.height * row / rows
258307
else: # 0 to last row.0
259-
position *= self.bmp_specs.height - 1
308+
position = (row / (rows - 1) *
309+
(self.bmp_specs.height - 1))
260310

261311
# Separate absolute position into several values:
262312
# integer 'a' and 'b' row indices, floating 'a' and
@@ -282,6 +332,7 @@ def process(self, input_filename, output_filename, rows,
282332
row_bytes)
283333
prev_row_a_index = row_a_index
284334
prev_row_b_index = row_b_index
335+
time1 += (monotonic() - row_start_time)
285336

286337
# Pixel values are stored as bytes from 0-255.
287338
# Gamma correction requires floats from 0.0 to 1.0.
@@ -332,21 +383,65 @@ def process(self, input_filename, output_filename, rows,
332383
# will be used on subsequent rows.
333384
err = err - err_bits
334385

386+
time2 += (monotonic() - row_start_time)
335387
# Reorder data from BGR to DotStar color order,
336388
# allowing for header and start-of-pixel markers
337389
# in the DotStar data.
390+
391+
# Benchmarking two approaches here...first uses a
392+
# zipped list working from the ndarray (because
393+
# CircuitPython bytearrays don't allow step-by-3),
394+
# converting to a bytearray before write.
395+
# This needs the 3 offset_* variables from earlier.
396+
397+
# for dot_idx, color in enumerate(
398+
# list(zip(got[offset_0::3],
399+
# got[offset_1::3],
400+
# got[offset_2::3]))):
401+
# dot_pos = 5 + dot_idx * 4
402+
# dotstar_list[dot_pos:dot_pos + 3] = color
403+
# output_buffer[output_position:output_position +
404+
# dotstar_row_size] = bytearray(
405+
# dotstar_list)
406+
407+
# Other approach, 'got' is converted from uint8
408+
# ndarray to bytearray (seems a bit faster) and then
409+
# a brute-force walkthrough loop...
410+
bgr = bytearray(got)
338411
for column in range(clipped_width):
339412
bmp_pos = column * 3
340413
dotstar_pos = 5 + column * 4
341-
bgr = got[bmp_pos:bmp_pos + 3]
342414
dotstar_buffer[dotstar_pos +
343-
self.blue_index] = bgr[0]
415+
self.blue_index] = bgr[bmp_pos]
344416
dotstar_buffer[dotstar_pos +
345-
self.green_index] = bgr[1]
417+
self.green_index] = bgr[bmp_pos + 1]
346418
dotstar_buffer[dotstar_pos +
347-
self.red_index] = bgr[2]
348-
349-
led_file.write(dotstar_buffer)
419+
self.red_index] = bgr[bmp_pos + 2]
420+
output_buffer[output_position:output_position +
421+
dotstar_row_size] = dotstar_buffer
422+
# Performance of the two is pretty similar.
423+
# Walkthrough loop seems a twee faster but then
424+
# has a negative effect on ulab performance, maybe
425+
# memory-management related?
426+
427+
time3 += (monotonic() - row_start_time)
428+
429+
# Add converted data to output buffer.
430+
# Periodically write when full.
431+
output_position += dotstar_row_size
432+
if output_position >= len(output_buffer):
433+
led_file.write(output_buffer)
434+
if callback:
435+
callback(row / (rows - 1))
436+
output_position = 0
437+
438+
time4 += (monotonic() - row_start_time)
439+
440+
# Write any remaining buffered data
441+
if output_position:
442+
led_file.write(output_buffer[:output_position])
443+
if callback:
444+
callback(1.0)
350445

351446
# If not looping, add an 'all off' row of LED data
352447
# at end to ensure last row timing is consistent.
@@ -358,6 +453,15 @@ def process(self, input_filename, output_filename, rows,
358453
[255] *
359454
((self.num_pixels + 15) //
360455
16)))
456+
print('Total time', monotonic() - start_time)
457+
time4 -= time3
458+
time3 -= time2
459+
time2 -= time1
460+
print(rows, 'rows')
461+
print('BMP-reading time', time1)
462+
print('ulab time', time2)
463+
print('Reordering time', time3)
464+
print('File-writing time', time4)
361465

362466
#print("Loaded OK!")
363467
return rows

0 commit comments

Comments
 (0)