/ Kinetic_POV / convert / convert.py
convert.py
  1  # SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  # Image converter script for POV LED poi project.  Reads one or more images
  6  # as input, generates tables which can be copied-and-pasted or redirected
  7  # to a .h file, e.g.:
  8  #
  9  # $ python convert.py image1.gif image2.png > graphics.h
 10  #
 11  # Ideal image dimensions are determined by hardware setup, e.g. LED poi
 12  # project uses 16 LEDs, so image height should match.  Width is limited
 13  # by AVR PROGMEM capacity -- very limited on Trinket!
 14  #
 15  # Adafruit invests time and resources providing this open source code,
 16  # please support Adafruit and open-source hardware by purchasing
 17  # products from Adafruit!
 18  #
 19  # Written by Phil Burgess / Paint Your Dragon for Adafruit Industries.
 20  # MIT license, all text above must be included in any redistribution.
 21  # See 'COPYING' file for additional notes.
 22  # --------------------------------------------------------------------------
 23  
 24  from PIL import Image
 25  import sys
 26  
 27  # Establish peak and average current limits - a function of battery
 28  # capacity and desired run time.
 29  
 30  # These you can edit to match your build:
 31  batterySize    = 150  # Battery capacity, in milliamp-hours (mAh)
 32  runTime        = 1.1  # Est. max run time, in hours (longer = dimmer LEDs)
 33  parallelStrips = 2    # Same data is issued to this many LED strips
 34  
 35  # These probably don't need editing:
 36  mcuCurrent     = 20   # Est. current used by microcontrolled board (mA)
 37  wireLimit      = 1500 # Ampacity of battery wires (est 26 gauge) (milliamps)
 38  
 39  # Estimate average and peak LED currents, within some safety thresholds:
 40  if(runTime < 1.0): runTime = 1.0        # Don't exceed 1C rate from battery
 41  cl = batterySize - mcuCurrent * runTime # After MCU, charge left for LEDs
 42  if cl < 0: cl = 0                       # Must be non-negative
 43  avgC = cl / runTime / parallelStrips
 44  if avgC > wireLimit: avgC = wireLimit   # Don't exceed battery wire ampacity
 45  peakC = avgC * 2.2                      # Battery+wires OK w/brief peaks
 46  
 47  bR    = 1.0       # Can adjust
 48  bG    = 1.0       # color balance
 49  bB    = 1.0       # for whiter whites!
 50  gamma = 2.7       # For more linear-ish perceived brightness
 51  
 52  # Current estimates are averages measured from strip on LiPoly cell
 53  mA0   =  1.3      # LED current when off (driver logic still needs some)
 54  mAR   = 15.2 * bR # + current for 100% red
 55  mAG   =  8.7 * bG # + current for 100% green
 56  mAB   =  8.0 * bB # + current for 100% blue
 57  
 58  # --------------------------------------------------------------------------
 59  
 60  cols     = 0 # Current column number in output
 61  byteNum  = 0
 62  numBytes = 0
 63  
 64  def writeByte(n):
 65  	global cols, byteNum, numBytes
 66  
 67  	cols += 1                      # Increment column #
 68  	if cols >= 8:                  # If max column exceeded...
 69  		print                  # end current line
 70  		sys.stdout.write("  ") # and start new one
 71  		cols = 0               # Reset counter
 72  	sys.stdout.write("{0:#0{1}X}".format(n, 4))
 73  	byteNum += 1
 74  	if byteNum < numBytes:
 75  		sys.stdout.write(",")
 76  		if cols < 7:
 77  			sys.stdout.write(" ")
 78  
 79  # --------------------------------------------------------------------------
 80  
 81  numLEDs = 0
 82  images  = []
 83  
 84  # Initial pass loads each image & tracks tallest size overall
 85  
 86  for name in sys.argv[1:]: # For each image passed to script...
 87  	image        = Image.open(name)
 88  	image.pixels = image.load()
 89  	# Determine if image is truecolor vs. colormapped.
 90  	image.colors = image.getcolors(256)
 91  	if image.colors == None:
 92  		image.numColors = 257 # Image is truecolor
 93  	else:
 94  		# If 256 colors or less, that doesn't necessarily mean
 95  		# it's a non-truecolor image yet, just that it has few
 96  		# colors.  Check the image type and if it's truecolor or
 97  		# similar, convert the image to a paletted mode so it can
 98  		# be more efficiently stored.  Since there are few colors,
 99  		# this operation is lossless.
100  		if (image.mode != '1' and image.mode != 'L' and
101  		  image.mode != 'P'):
102  			image = image.convert("P", palette="ADAPTIVE")
103  			image.pixels = image.load()
104  			image.colors = image.getcolors(256)
105  		# image.colors is an unsorted list of tuples where each
106  		# item is a pixel count and a color palette index.
107  		# Unused palette indices (0 pixels) are not in list,
108  		# so its length tells us the unique color count...
109  		image.numColors = len(image.colors)
110  		# The image & palette aren't necessarily optimally packed,
111  		# e.g. might have a 216-color 'web safe' palette but only
112  		# use a handful of colors.  In order to reduce the palette
113  		# storage requirements, only the colors in use will be
114  		# output.  The pixel indices in the image must be remapped
115  		# to this new palette sequence...
116  		remap = [0] * 256
117  		for c in range(image.numColors): # For each color used...
118  			# The original color index (image.colors[c][1])
119  			# is reassigned to a sequential 'packed' index (c):
120  			remap[image.colors[c][1]] = c
121  		# Every pixel in image is then remapped through this table:
122  		for y in range(image.size[1]):
123  			for x in range(image.size[0]):
124  				image.pixels[x, y] = remap[image.pixels[x, y]]
125  		# The color palette associated with the image is still in
126  		# its unpacked/unoptimal order; image pixel values no longer
127  		# point to correct entries.  This is OK and we'll compensate
128  		# for it later in the code.
129  	image.name = name
130  	image.bph  = image.size[1] # Byte-padded height (tweaked below)
131  	images.append(image)
132  
133  	# 1- and 4-bit images are padded to the next byte boundary.
134  	# Image size not fully validated - on purpose - in case of quick
135  	# test with an existing (but non-optimal) file.  If too big or too
136  	# small for the LED strip, just wastes some PROGMEM space or some
137  	# LEDs will be lit wrong, usually no biggie.
138  	if image.numColors <= 2:    # 1 bit/pixel, use 8-pixel blocks
139  		if image.bph & 7: image.bph += 8 - (image.bph & 7)
140  	elif image.numColors <= 16: # 4 bits/pixel, use 2-pixel blocks
141  		if image.bph & 1: image.bph += 1
142  
143  	if image.bph > numLEDs: numLEDs = image.bph
144  
145  print ("// Don't edit this file!  It's software-generated.")
146  print ("// See convert.py script instead.")
147  print()
148  print ("#define PALETTE1  0")
149  print ("#define PALETTE4  1")
150  print ("#define PALETTE8  2")
151  print ("#define TRUECOLOR 3")
152  print()
153  print ("#define NUM_LEDS %d" % numLEDs)
154  print()
155  
156  # Second pass estimates current of each column, then peak & overall average
157  
158  for imgNum, image in enumerate(images): # For each image in list...
159  	sys.stdout.write("// %s%s\n\n" % (image.name,
160  	  ' '.ljust(73 - len(image.name),'-')))
161  	if image.numColors <= 256:
162  		# Palette optimization requires some weird shenanigans...
163  		# first, make a duplicate image where width=image.numColors
164  		# and height=1.  This will have the same color palette as
165  		# the original image, which may contain many unused entries.
166  		lut = image.resize((image.numColors, 1))
167  		lut.pixels = lut.load()
168  		# The image.colors[] list contains the original palette
169  		# indices of the colors actually in use.  Draw one pixel
170  		# into the 'lut' image for each color index in use, in the
171  		# order they appear in the color list...
172  		for x in range(image.numColors):
173  			lut.pixels[x, 0] = image.colors[x][1]
174  		# ...then convert the lut image to RGB format to provide a
175  		# list of (R,G,B) values representing the packed color list.
176  		lut = list(lut.convert("RGB").getdata())
177  
178  		# Estimate current for each element of palette:
179  		paletteCurrent = []
180  		for i in range(image.numColors):
181  			paletteCurrent.append(mA0 +
182  			  pow((lut[i][0] / 255.0), gamma) * mAR +
183  			  pow((lut[i][1] / 255.0), gamma) * mAG +
184  			  pow((lut[i][2] / 255.0), gamma) * mAB)
185  
186  	# Estimate peak and average current for each column of image
187  	colMaxC = 0.0 # Maximum column current
188  	colAvgC = 0.0 # Average column current
189  	for x in range(image.size[0]): # For each row...
190  		mA = 0.0 # Sum current of each pixel's palette entry
191  		for y in range(image.size[1]):
192  			if image.numColors <= 256:
193  				mA += paletteCurrent[image.pixels[x, y]]
194  			else:
195  				mA += (mA0 +
196  				  pow((image.pixels[x, y][0] / 255.0),
197  				  gamma) * mAR +
198  				  pow((image.pixels[x, y][1] / 255.0),
199  				  gamma) * mAG +
200  				  pow((image.pixels[x, y][2] / 255.0),
201  				  gamma) * mAB)
202  		colAvgC += mA                 # Accumulate average (div later)
203  		if mA > colMaxC: colMaxC = mA # Monitor peak
204  	colAvgC /= image.size[0] # Sum div into average
205  
206  	s1 = peakC / colMaxC   # Scaling factor for peak current constraint
207  	s2 = avgC  / colAvgC   # Scaling factor for average current constraint
208  	if s2 < s1:  s1 = s2   # Use smaller of two (so both constraints met),
209  	if s1 > 1.0: s1 = 1.0  # but never increase brightness
210  
211  	s1 *= 255.0   # (0.0-1.0) -> (0.0-255.0)
212  	bR1 = bR * s1 # Scale color balance values
213  	bG1 = bG * s1
214  	bB1 = bB * s1
215  
216  	p        = 0 # Current pixel number in image
217  	cols     = 7 # Force wrap on 1st output
218  	byteNum  = 0
219  
220  	if image.numColors <= 256:
221  		# Output gamma- and brightness-adjusted color palette:
222  		print ("const uint8_t PROGMEM palette%02d[][3] = {" % imgNum)
223  		for i in range(image.numColors):
224  			sys.stdout.write("  { %3d, %3d, %3d }" % (
225  			  int(pow((lut[i][0]/255.0),gamma)*bR1+0.5),
226  			  int(pow((lut[i][1]/255.0),gamma)*bG1+0.5),
227  			  int(pow((lut[i][2]/255.0),gamma)*bB1+0.5)))
228  			if i < (image.numColors - 1): print (",")
229  		print (" };")
230  		print()
231  
232  		sys.stdout.write(
233  		  "const uint8_t PROGMEM pixels%02d[] = {" % imgNum)
234  
235  		if image.numColors <= 2:
236  			numBytes = image.size[0] * numLEDs / 8
237  		elif image.numColors <= 16:
238  			numBytes = image.size[0] * numLEDs / 2
239  		elif image.numColors <= 256:
240  			numBytes = image.size[0] * numLEDs
241  		else:
242  			numBytes = image.size[0] * numLEDs * 3
243  
244  		for x in range(image.size[0]):
245  			if image.numColors <= 2:
246  				for y in range(0, numLEDs, 8):
247  					sum = 0
248  					for bit in range(8):
249  						y1 = y + bit
250  						if y1 < image.size[1]:
251  							sum += (
252  							  image.pixels[x,
253  							  y1] << bit)
254  					writeByte(sum)
255  			elif image.numColors <= 16:
256  				for y in range(0, numLEDs, 2):
257  					if y < image.size[1]:
258  						p1 = image.pixels[x, y]
259  					else:
260  						p1 = 0
261  					if (y + 1) < image.size[1]:
262  						p2 = image.pixels[x, y + 1]
263  					else:
264  						p2 = 0
265  					writeByte(p1 * 16 + p2)
266  			elif image.numColors <= 256:
267  				for y in range(numLEDs):
268  					if y < image.size[1]:
269  						writeByte(image.pixels[x, y])
270  					else:
271  						writeByte(0)
272  			else:
273  				for y in range(numLEDs):
274  					if y < image.size[1]:
275  						writeByte(image.pixels[x, y][0])
276  						writeByte(image.pixels[x, y][1])
277  						writeByte(image.pixels[x, y][2])
278  					else:
279  						writeByte(0)
280  						writeByte(0)
281  						writeByte(0)
282  
283  	else:
284  		# Perform gamma- and brightness-adjustment on pixel data
285  		sys.stdout.write(
286  		  "const uint8_t PROGMEM pixels%02d[] = {" % imgNum)
287  		numBytes = image.size[0] * numLEDs * 3
288  
289  		for x in range(image.size[0]):
290  			for y in range(numLEDs):
291  				if y < image.size[1]:
292  					writeByte(int(pow((
293  					  image.pixels[x, y][0] / 255.0),
294  					  gamma) * bR1 + 0.5))
295  					writeByte(int(pow((
296  					  image.pixels[x, y][1] / 255.0),
297  					  gamma) * bG1 + 0.5))
298  					writeByte(int(pow((
299  					  image.pixels[x, y][2] / 255.0),
300  					  gamma) * bB1 + 0.5))
301  				else:
302  					writeByte(0)
303  					writeByte(0)
304  					writeByte(0)
305  
306  	print (" };") # end pixels[] array
307  	print()
308  
309  # Last pass, print table of images...
310  
311  print ("typedef struct {")
312  print ("  uint8_t        type;    // PALETTE[1,4,8] or TRUECOLOR")
313  print ("  line_t         lines;   // Length of image (in scanlines")
314  print ("  const uint8_t *palette; // -> PROGMEM color table (NULL if truecolor")
315  print ("  const uint8_t *pixels;  // -> Pixel data in PROGMEM")
316  print ("} image;")
317  print()
318  print ("const image PROGMEM images[] = {")
319  
320  for imgNum, image in enumerate(images): # For each image in list...
321  	sys.stdout.write("  { ")
322  	if image.numColors <= 2:
323  		sys.stdout.write("PALETTE1 , ")
324  	elif image.numColors <= 16:
325  		sys.stdout.write("PALETTE4 , ")
326  	elif image.numColors <= 256:
327  		sys.stdout.write("PALETTE8 , ")
328  	else:
329  		sys.stdout.write("TRUECOLOR, ")
330  
331  	sys.stdout.write(" %3d, " % image.size[0])
332  
333  	if image.numColors <= 256:
334  		sys.stdout.write("(const uint8_t *)palette%02d, " % imgNum)
335  	else:
336  		sys.stdout.write("NULL                      , ")
337  
338  	sys.stdout.write("pixels%02d }" % imgNum)
339  
340  	if imgNum < len(images) - 1:
341  		print(",")
342  	else:
343  		print
344  
345  print ("};")
346  print()
347  print ("#define NUM_IMAGES (sizeof(images) / sizeof(images[0]))")