Projects

Here's where I post some of my work.

Quick Python Thumbnail Generator

Posted 2021-01-13

I suppose you could call this a meta-post because I made this small program for the purpose of developing this site.

Early on, I wanted to make sure that this site was lightweight without looking like it was from the 90s. When I was preparing the gallery, I noticed that the artwork would be about 83 MB, all downloaded to view one page. Now that's not a problem if you're like me sitting at home with a 500 Mbps connection, but doing that sort of thing is an easy way to get mobile users with limited data to hate you. That's where this script comes in.

For this program, I used Python 3.2, OpenCV 4.2.0, and tkinter (not sure of the version for that).

The quick summary of it is that it uses tkinter to select a directory to import images from and then select another to output images. Technically this is not nessacary and you could just hardcode the directory or use command-line arguments. I'm just extra like that. The program takes inventory of all the images in the import directory and creates a list of the file names for output. The output name is generally the same but with "_thumb" appended and the file type forced as a JPEG. Following that, it looks for thumbnail files in the set that already exist so as not to process those again. After all that is done, each source image is opened up. If it's a GIF, it is opened as a video and only the first frame is used. The program looks for the longer dimension and scales it down to the maximum (400px in my case) while maintaining aspect ratio. Finally, the newly resized image is saved in the export directory as a JPG.

By using thumbnails, the total amount of data to download decreases from 82 MB to 1.39 MB. If someone wants to view in the original quality, they can click on it to download the original. It also works well with the Hoverzoom browser extension.

Anyways, that's enough talking for me. Here is the code. I'm sure people can optimize this or use different libraries like PIL, but perfect is the enemy of good.

Code

          from tkinter import filedialog
          from os import listdir
          from os.path import isfile, join, exists
          import cv2

          max_width = 400;
          max_height = 400;

          import_dir = filedialog.askdirectory();
          print(import_dir)
          export_dir = filedialog.askdirectory();
          print(export_dir)

          import_file_list = [f for f in listdir(import_dir) if isfile(join(import_dir, f))]
          export_file_list = [''+f[0:f.rfind('.')]+'_thumb.jpg' for f in import_file_list]

          import_file_list = [''+import_dir+'/'+f for f in import_file_list]
          export_file_list = [''+export_dir+'/'+f for f in export_file_list]

          thumb_checker = [True if exists(f) else False for f in export_file_list]
          print(thumb_checker)

          final_import_file_list = []
          final_export_file_list = []
          for i in range(len(import_file_list)):
              if not thumb_checker[i]:
                  final_import_file_list.append(import_file_list[i])
                  final_export_file_list.append(export_file_list[i])

          for i in range(len(final_import_file_list)):
              import_path = final_import_file_list[i]
              export_path = final_export_file_list[i]
              input_img = []
              if import_path[import_path.rfind('.')::].lower() == '.gif':
                  gif = cv2.VideoCapture(import_path)
                  ret, frame = gif.read()
                  input_img = frame;
                  gif.release()
              else:
                  input_img = cv2.imread(import_path)
              height, width = input_img.shape[:2]
              scale_factor = 1.0
              if width >= height and width > max_width:
                  scale_factor = max_width/width
              elif height > max_height:
                  scale_factor = max_height/height
              dim = (int(width*scale_factor), int(height*scale_factor))
              output_img = cv2.resize(input_img, dim, interpolation = cv2.INTER_AREA)
              cv2.imwrite(export_path, output_img)
              print('Wrote ' + export_path + ' with dims: ' + str(dim))

My convoluted approach to addressing WS2812 RGB LEDs

Posted 2021-01-13

Back in 2016/17, I wanted a system of controlling a set of WS2812B RGB LEDs for a project I was working on. I was doing this on a PIC18 microcontroller and I wasn't really looking to port over existing Arduino libraries to this system, so I did some searching online and set up this system. Later on, when I wanted to control an LED strip with an Arduino, I just copied the code over and made some slight changes.

The WS2812B is an addressable RGB LED. Essentially, how they work is that they have a data input (DIN) and data output (DOUT) pin. With just one unit, the DIN pin would be connected to your controlling device (an Arduino UNO in my case). Working with more units, you daisy chain them by connecting the DOUT pin of one unit to the DIN pin of the next one. Rinse and repeat for the rest of the set.

To set the colour of an LED, you must send 24 bits of data; 8 bits for each colour channel. For sending a single bit, its ideal to consider it as sending a PWM signal with a period of 1.25µs. In that context, you use a duty cycle of roughly 66% (two thirds) for a 1 bit and 33% (one third) for a 0 bit. Don't worry too much about getting perfect timing. There is some tolerance to this. The order to send the bits in is GBR, with the most significant bit first. The first 24 bits you send will control the first LED. Each subsequent set will control the next LED in the daisy chain. Once you've reached the last LED, you must wait 50µs before you can start again.

I recommend checking the datasheet for the official reference.

Now I know there are definitely libraries that handle the control of these LEDs, but I didn't want to do any of that.

This is my system for sending 1 and 0 bits.

Code

                #define DO_ON_BIT   asm("sbi 11,7\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("cbi 11,7\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t");

                #define DO_OFF_BIT  asm("sbi 11,7\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("cbi 11,7\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t"); \
                                    asm("nop\n\t");
              

Yeah, its pretty stupid, but consider this: assuming you disable interrupts, the timing of this stays pretty consistent. This method makes use of inline assembly instructions.

The NOP instruction is for "no-operation", so basically do nothing for one instruction cycle.
The SBI instruction is to set a bit to 1. The first argument specifies the register (and ill get to that) while the latter argument determines the specific bit.
The CBI instruction is identical to SBI except that it sets the bit to 0.

In this specific use case, I wanted to control the LEDs with digital pin 7. So I have to look for the register and pin that correspond to it.
I used this reference and saw that digital pin 7 corresponds to PD7.
PD7 corresponds to PORTD (port D), bit 7. Now the libraries for the Arduino Uno actually has macros for PORTD so you could just use that in the SBI and CBI instructions. If you want to avoid macros, however, you have to go into the datasheet for the microcontroller, the ATmega168.
On page 99 and 100, you can find section 14.4 which has the references for the registers that control the digital inputs and outputs. PORTD has a hex address of 0x0B which is 11 in decimal. PD7 is in bit 7.
With that information gathered, I can control digital pin 7 with sbi 11,7 and cbi 11,7.

With that out of the way, we face the task of daisy chaining these bit instructions. Here's my function for controlling a single LED.

Code

      void writeWS2812(byte r, byte g, byte b, bool singleFlag=false) {
        cli();
        int j;
        for(j=0; j<0x8; j++) {
            if((g<<j)&B10000000) {DO_ON_BIT}
            else {DO_OFF_BIT}
          }
        for(j=0; j<0x8; j++) {
          if((r<<j)&B10000000) {DO_ON_BIT}
          else {DO_OFF_BIT}
        }
        for(j=0; j<0x8; j++) {
          if((b<<j)&B10000000) {DO_ON_BIT}
          else {DO_OFF_BIT}
        }
        if(singleFlag) sei();
      }
    

The cli() function is used to disable interrupts. sei() re-enables them. The singleFlag argument is set as false by default. It's only set true if controlling just 1 LED. All it changes is that it calls sei().
When controlling many LEDs, I use a byte buffer of 3x the number of LEDs. So I would declare something like byte chans[30*3]; for 30 LEDs.
I would fill up the buffer with the colour data that I want and then I would call this single-line for loop:
for(int i = 0; i < 30; i++) writeWS2812(chans[(3*i)+0], chans[(3*i)+1], chans[(3*i)+2]);
At the end of that, I would follow up with an sei() call and a delayMicroseconds() call of at least 50µs.

And thats about it!