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]))")