22BMP-to-DotStar-ready-bytearrays.
33"""
44
5+ from time import monotonic
56# pylint: disable=import-error
67import os
78import math
89import ulab
910
11+ BUFFER_ROWS = 32
12+
1013class 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