Convert list of integers to list of bits Python

Viewed 100

I am currently trying to feed video data to a laser (we use the laser for communications). The laser works through modulation, where we have 2 states total, equivalent to 0 and 1. Hence, in order for me to feed the laser the video data, I first need to convert it to bits. My frames, which I get from a webcam with openCV, are represented by 2D arrays which contain 8-bit integers to get a greyscale image. Currently, I am transforming these arrays as follows:

if __name__ == '__main__':
video = Video()

frame = video.getFrameBits()

Where the Video class is defined as:

class Video:
# scale_percent: percent of original size of frame
def __init__(self, scale_percent=100):
    self.cap = cv2.VideoCapture(0)

    # Get one frame to figure out sizing constraints
    _, frame = self.cap.read()

    width = int(frame.shape[1] * scale_percent / 100)
    height = int(frame.shape[0] * scale_percent / 100)
    self.dim = (width, height)

# color: If true show color frames. Not yet implemented
def getFrame(self, color=False):
    _, frame = self.cap.read()

    frame = cv2.resize(frame, self.dim, interpolation=cv2.INTER_AREA)

    if not color:
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        return gray

def getFrameBits(self):
    frame = self.getFrame()

    for row in frame:
        for pixel in row:
            frame_bits.append(intToBits(pixel))
    
    return frame_bits

And the int to bits function as:

def intToBits(x):
    send = str(bin(x).lstrip('0b'))
    send = send.zfill(8)

    return send

The reason I am using the intToBits function is because I would like to be able to take this array that I called frame, and feed it directly into the laser. With the current implementation, the leading zeros are not truncated from the array. So I get an output like: [10010101,10010100,10010101,10010111,10010110,10010101,10010100,10010001,10010001,01011000,...]

The problem with this whole code is that it runs much too slow on the micro-controller I have available. It takes about 5 seconds to get a single frame, which is rather abysmal. My first thought is to get rid of the nested for loop in getFrameBits, as follows:

frame_bits = [intToBits(pixel) for row in frame for pixel in row]

This did improve the time, but I would like to see if it can be improved further. We are still taking about 1 second to get the frame, which is better, but we are expecting a larger sampling rate.

My next idea would be to code this in C and run it in Python, but I am not too familiar with C. So whilst I would be willing to do this, I wanted to make sure it was the correct direction to take.

Are there some more ways I can optimize this code?

Thanks!

1 Answers

A bit of bit-masking should do the trick:

Along with a wee bit of vectorization - no need to write the C code in case where the under-the-hood built-in code of numpy, which is written in C already, has your back.

import numpy as np
import cProfile, pstats, io

input_size = 1000000
#since I don't have your image, I made do with a random sample of 8bit numbers.
test_input = np.random.randint(0, 256, input_size)
#to check that we get the correct result, set input_size to 2
#and uncomment the line below
#test_input = [255, 7]

#your function for the speed comparison purposes
def intToBits(x):
    send = str(bin(x).lstrip('0b'))
    send = send.zfill(8)
    return send

#note, that in this case x is the whole array, not just one number
#to make the full use of the vectorization
#also the output is not a bitfield, but a string
#the > 0 at the end is to convert the result into booleans.
#strictly speaking it isn't necessary if you are fine with 0 1 integers.
def binary_repr(x):
    return(
    np.dstack((
    np.bitwise_and(x, 0b10000000) >> 7,
    np.bitwise_and(x, 0b1000000) >> 6,
    np.bitwise_and(x, 0b100000) >> 5,
    np.bitwise_and(x, 0b10000) >> 4,
    np.bitwise_and(x, 0b1000) >> 3,
    np.bitwise_and(x, 0b100) >> 2,
    np.bitwise_and(x, 0b10) >> 1,
    np.bitwise_and(x, 0b1)
    )).flatten() > 0)

#starting the profiler.
pr = cProfile.Profile()
pr.enable()

#the two computations that we want to compare
a = []
for i in range(input_size):
    a.append(intToBits(test_input[i]))
print(a)
b = binary_repr(test_input)
print(b)

#the comparison
sortby = 'cumulative'
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
print(s.getvalue())

For the completeness sake, the profiler results:

  ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.577    0.000    0.920    0.000 (intToBits)
        2    0.195    0.098    0.195    0.098 {built-in method builtins.print}
  1000000    0.125    0.000    0.125    0.000 {method 'lstrip' of 'str' objects}
  1000000    0.119    0.000    0.119    0.000 {built-in method builtins.bin}
  1000000    0.099    0.000    0.099    0.000 {method 'zfill' of 'str' objects}
  1000008    0.082    0.000    0.082    0.000 {method 'append' of 'list' objects}
        1    0.030    0.030    0.062    0.062 (binary_repr)

As you can see, even generating the data is taking more time than the switch to the bitwise representation. While you will need to modify this a little to fit into your code since the output format is a bit different - one large array instead of an array of arrays - it should be well worth it.

Related