Skip to content

Commit 179b83d

Browse files
Various speedups thx to Jeff & Scott!
1 parent 0cfeb31 commit 179b83d

2 files changed

Lines changed: 29 additions & 103 deletions

File tree

CLUE_Light_Painter/bmp2led.py

Lines changed: 27 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -143,23 +143,24 @@ def scandir(self, path):
143143
return valid_list
144144

145145

146-
def read_row(self, row, num_bytes):
146+
def read_row(self, row, dest):
147147
"""
148148
Read one row of pixels from BMP file, clipped to minimum of BMP
149149
image width or LED strip length.
150150
Arguments:
151-
row (int): index of row to read (0 to (image height - 1))
152-
Returns: ulab ndarray (uint8 type) containing pixel data in
153-
BMP-native order (B,G,R per pixel), no need to reorder to DotStar
154-
order until later.
151+
row (int) : Index of row to read (0 to (img height-1)).
152+
dest (uint8 ndarray) : Destination buffer. After reading, buffer
153+
contains pixel data in BMP-native order
154+
(B,G,R per pixel), no need to reorder to
155+
DotStar order until later.
155156
"""
156157
# 'flip' logic is intentionally backwards from typical BMP loader,
157158
# this makes BMP image prep an easy 90 degree CCW rotation.
158159
if not self.bmp_specs.flip:
159160
row = self.bmp_specs.height - 1 - row
160161
self.bmp_file.seek(self.bmp_specs.image_offset +
161162
row * self.bmp_specs.row_size)
162-
return ulab.array(self.bmp_file.read(num_bytes), dtype=ulab.uint8)
163+
self.bmp_file.readinto(dest)
163164

164165

165166
# pylint: disable=too-many-arguments, too-many-locals
@@ -209,14 +210,11 @@ def process(self, input_filename, output_filename, rows,
209210
# It's formed just like valid strip data (with header, per-pixel
210211
# start markers and footer), with colors all '0' to start...these
211212
# will be filled later.
212-
dotstar_buffer = bytearray([0] * 4 +
213-
[255, 0, 0, 0] * self.num_pixels +
214-
[255] * ((self.num_pixels + 15) // 16))
213+
dotstar_buffer = ulab.array([0] * 4 +
214+
[255, 0, 0, 0] * self.num_pixels +
215+
[255] * ((self.num_pixels + 15) // 16),
216+
dtype=ulab.uint8)
215217
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-
#reorder = [0] * dotstar_row_size
220218

221219
# Output rows are held in RAM and periodically written,
222220
# marginally faster than writing each row separately.
@@ -251,46 +249,14 @@ def process(self, input_filename, output_filename, rows,
251249
# Constrain bytes-to-read to pixel strip length
252250
clipped_width = min(self.bmp_specs.width, self.num_pixels)
253251
row_bytes = 3 * clipped_width
254-
# Compute reorder list here (needs row bytes to work)
255252

256253
# Each output row is interpolated from two BMP rows,
257254
# we'll call them 'a' and 'b' here.
258-
row_a_data, row_b_data = None, None
255+
row_a_data = ulab.zeros(row_bytes, dtype=ulab.uint8)
256+
row_b_data = ulab.zeros(row_bytes, dtype=ulab.uint8)
259257
prev_row_a_index, prev_row_b_index = None, None
260258

261259
with open(output_filename, 'wb') as led_file:
262-
# Determine remapping indices from BMP's always-BGR
263-
# pixel byte order to DotStar's variable order
264-
# (contained in self.red_index, green_index, blue_index).
265-
# I'm sure there's better ways but have a headache.
266-
# This is ONLY needed if using the first of two
267-
# benchmarked methods later (or something similar to it).
268-
# There's really only six possible orders, I could make a list
269-
# if self.blue_index is 0: # BXX DotStar
270-
# offset_0 = 0 # DotStar byte 0 is BMP byte 0 (B)
271-
# if self.green_index is 1: # BGR
272-
# offset_1 = 1 # DotStar byte 1 is BMP byte 1 (G)
273-
# offset_2 = 2 # DotStar byte 2 is BMP byte 2 (R)
274-
# else: # BRG
275-
# offset_1 = 2 # DotStar byte 1 is BMP byte 2 (R)
276-
# offset_2 = 1 # DotStar byte 2 is BMP byte 1 (G)
277-
# elif self.green_index is 0: # GXX DotStar
278-
# offset_0 = 1 # DotStar byte 0 is BMP byte 1 (G)
279-
# if self.blue_index is 1: # GBR
280-
# offset_1 = 0 # DotStar byte 1 is BMP byte 0 (B)
281-
# offset_2 = 2 # DotStar byte 2 is BMP byte 2 (R)
282-
# else: # GRB
283-
# offset_1 = 2 # DotStar byte 1 is BMP byte 2 (R)
284-
# offset_2 = 0 # DotStar byte 2 is BMP byte 0 (B)
285-
# else: # RXX DotStar
286-
# offset_0 = 2 # DotStar byte 0 is BMP byte 2 (R)
287-
# if self.green_index is 1: # RGB
288-
# offset_1 = 1 # DotStar byte 1 is BMP byte 1 (G)
289-
# offset_2 = 0 # DotStar byte 2 is BMP byte 0 (R)
290-
# else: # RBG
291-
# offset_1 = 0 # DotStar byte 1 is BMP byte 0 (R)
292-
# offset_2 = 1 # DotStar byte 2 is BMP byte 1 (G)
293-
294260
# To avoid continually appending to output file (a slow
295261
# operation), seek to where the end of the file would
296262
# be, write a nonsense byte there, then seek back to
@@ -324,15 +290,15 @@ def process(self, input_filename, output_filename, rows,
324290
# (else do another interp/dither with existing data)
325291
if row_a_index != prev_row_a_index:
326292
# If we've advanced exactly one row, reassign
327-
# old 'b' data to 'a' row, else read new 'a'.
293+
# old 'b' data to 'a' row (swap, so buffers
294+
# remain distinct), else read new 'a'.
328295
if row_a_index == prev_row_b_index:
329-
row_a_data = row_b_data
296+
row_a_data, row_b_data = row_b_data, row_a_data
330297
else:
331-
row_a_data = self.read_row(row_a_index,
332-
row_bytes)
298+
self.read_row(row_a_index, row_a_data)
333299
# Read new 'b' data on any row change
334-
row_b_data = self.read_row(row_b_index,
335-
row_bytes)
300+
self.read_row(row_b_index, row_b_data)
301+
336302
prev_row_a_index = row_a_index
337303
prev_row_b_index = row_b_index
338304
time1 += (monotonic() - row_start_time)
@@ -390,55 +356,15 @@ def process(self, input_filename, output_filename, rows,
390356
# Reorder data from BGR to DotStar color order,
391357
# allowing for header and start-of-pixel markers
392358
# in the DotStar data.
393-
394-
# Benchmarking two approaches here...first uses a
395-
# zipped list working from the ndarray (because
396-
# CircuitPython bytearrays don't allow step-by-3),
397-
# converting to a bytearray before write.
398-
# This needs the 3 offset_* variables from earlier.
399-
400-
# for dot_idx, color in enumerate(
401-
# list(zip(got[offset_0::3],
402-
# got[offset_1::3],
403-
# got[offset_2::3]))):
404-
# dot_pos = 5 + dot_idx * 4
405-
# dotstar_list[dot_pos:dot_pos + 3] = color
406-
# output_buffer[output_position:output_position +
407-
# dotstar_row_size] = bytearray(
408-
# dotstar_list)
409-
410-
# Other approach, 'got' is converted from uint8
411-
# ndarray to bytearray (seems a bit faster) and then
412-
# a brute-force walkthrough loop...
413-
bgr = bytearray(got)
414-
for column in range(clipped_width):
415-
bmp_pos = column * 3
416-
dotstar_pos = 5 + column * 4
417-
dotstar_buffer[dotstar_pos +
418-
self.blue_index] = bgr[bmp_pos]
419-
dotstar_buffer[dotstar_pos +
420-
self.green_index] = bgr[bmp_pos + 1]
421-
dotstar_buffer[dotstar_pos +
422-
self.red_index] = bgr[bmp_pos + 2]
359+
dotstar_buffer[5 + self.blue_index:
360+
5 + 4 * clipped_width:4] = got[0::3]
361+
dotstar_buffer[5 + self.green_index:
362+
5 + 4 * clipped_width:4] = got[1::3]
363+
dotstar_buffer[5 + self.red_index:
364+
5 + 4 * clipped_width:4] = got[2::3]
423365
output_buffer[output_position:output_position +
424-
dotstar_row_size] = dotstar_buffer
425-
# Performance of the two is pretty similar.
426-
# Walkthrough loop seems a twee faster but then
427-
# has a negative effect on ulab performance, maybe
428-
# memory-management related?
429-
430-
# And a third, using a reordering table...
431-
# This doesn't actually work yet because the
432-
# reorder table hasn't been computed.
433-
# Two extra items (0 and 255) are appended for
434-
# use by headers/footers/etc. Can't directly append
435-
# to ndarray, so we bytearray-ify it first.
436-
#got = bytearray(got) + bytearray([0, 255])
437-
#output_buffer[output_position:output_position +
438-
# dotstar_row_size] = bytearray(
439-
# got[i] for i in reorder)
440-
# Initial testing (with a nonsense reorder table)
441-
# doesn't suggest it's any faster.
366+
dotstar_row_size] = memoryview(
367+
dotstar_buffer)
442368

443369
time3 += (monotonic() - row_start_time)
444370

CLUE_Light_Painter/code.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,9 @@ def load_image(self):
201201
# pylint: disable=eval-used
202202
# (It's cool, is a 'trusted string' in the code)
203203
duration = eval(TIMES[self.time]) # Playback time in seconds
204-
# The 0.85 here is an empirical guesstimate; playback is ever-so-
204+
# The 0.9 here is an empirical guesstimate; playback is ever-so-
205205
# slightly slower than benchmark speed due to button testing.
206-
rows = int(duration * self.rows_per_second * 0.85 + 0.5)
206+
rows = int(duration * self.rows_per_second * 0.9 + 0.5)
207207
# Remap brightness from 0.0-1.0 to 15-100%
208208
brightness = 0.15 + self.brightness * 0.85
209209
try:

0 commit comments

Comments
 (0)