pythonimagenumpyopencvbits-per-pixel

4 bit per pixel image from binary file in Python with Numpy and CV2?


Suppose I want to represent binary data as a black and white image, with only sixteen distinct levels for the gray values for each pixel so that each two adjacent pixels (lengthwise) represent a single byte. How can I do this? If, for example, I use the following:

import numpy as np
path = r'mybinaryfile.bin'
bin_data = np.fromfile(path, dtype='uint8')
scalar = 20
width = int(1800/scalar)
height = int(1000/scalar)
for jj in range(50):
  wid = int(width*height)
  img = bin_data[int(jj*wid):int((jj+1)*wid)].reshape(height,width)
  final_img = cv2.resize(img, (scalar*width, scalar*height), interpolation = cv2.INTER_NEAREST)
  fn = f'tmp/output_{jj}.png'
  cv2.imwrite(fn, final_img)

I can create a sequence of PNG files that represent the binary file, with each 20 by 20 block of pixels representing a single byte. However, this creates too many unique values for the grays (256), so I need to reduce it to fewer (16). How can I "split" each pixel into two pixels with 16 distinct gray levels (4 bpp, rather than 8) instead?

Using 4 bpp rather than 8 bpp should double the number of image files since I'm keeping the resolution the same but doubling the number of pixels I use to represent a byte (2 pixels per byte rather than 1).


Solution

  • I have understood that you want to take an 8-bit number and split the upper four bits and the lower four bits.

    This can be done with a couple of bitwise operations.

    def split_octet(data):
        """
        For each 8-bit number in array, split them into two 4-bit numbers"""
        split_data = []
        for octet in data:
            upper = octet >> 4
            lower = octet & 0x0f
            print(f"8bit:{octet:02x} upper:{upper:01x} and lower:{lower:01x}")
            split_data.extend([upper, lower])
        return split_data
    

    For the gray scale image to be created the data needs to be converted to a value in the range 0 to 255. However you want to keep only 16 discrete values. This can be done by normalising the 4-bit values in the range of 0 to 1. The multiple the value by 255 to get back to uint8 values.

    def create_square_grayscale(data, data_shape):
        # Normalize data from 0 to 1
        normalized = np.array(data, np.float64) / 0xf
        # fold data to image shape
        pixel_array = normalized.reshape(data_shape)
        # change 16 possible values over 0 to 255 range
        return np.array(pixel_array * 0xff, np.uint8)
    

    My full testcase was:

    from secrets import token_bytes
    import cv2
    import numpy as np
    
    pixel_size = 20
    final_image_size = (120, 120)
    
    
    def gen_data(data_size):
        # Generate some random data
        return token_bytes(data_size)
    
    
    def split_octet(data):
        """
        For each 8-bit number in array, split them into two 4-bit numbers"""
        split_data = []
        for octet in data:
            upper = octet >> 4
            lower = octet & 0x0f
            print(f"8bit:{octet:02x} upper:{upper:01x} and lower:{lower:01x}")
            split_data.extend([upper, lower])
        return split_data
    
    
    def create_square_grayscale(data, data_shape):
        # Normalize data from 0 to 1
        normalized = np.array(data, np.float64) / 0xf
        # fold data to image shape
        pixel_array = normalized.reshape(data_shape)
        # change 16 possible values over 0 to 255 range
        return np.array(pixel_array * 0xff, np.uint8)
    
    
    def main():
        side1, side2 = (int(final_image_size[0]/pixel_size),
                        int(final_image_size[1]/pixel_size))
        rnd_data = gen_data(int((side1 * side2)/2))
        split_data = split_octet(rnd_data)
        img = create_square_grayscale(split_data, (side1, side2))
        print("image data:\n", img)
        new_res = cv2.resize(img, None, fx=pixel_size, fy=pixel_size,
                             interpolation=cv2.INTER_AREA)
        cv2.imwrite("/tmp/rnd.png", new_res)
    
    
    if __name__ == '__main__':
        main()
    

    Which gave a transcript of:

    8bit:34 upper:3 and lower:4
    8bit:d4 upper:d and lower:4
    8bit:bd upper:b and lower:d
    8bit:c3 upper:c and lower:3
    8bit:61 upper:6 and lower:1
    8bit:9e upper:9 and lower:e
    8bit:5f upper:5 and lower:f
    8bit:1b upper:1 and lower:b
    8bit:a5 upper:a and lower:5
    8bit:31 upper:3 and lower:1
    8bit:22 upper:2 and lower:2
    8bit:8a upper:8 and lower:a
    8bit:1e upper:1 and lower:e
    8bit:84 upper:8 and lower:4
    8bit:3a upper:3 and lower:a
    8bit:c0 upper:c and lower:0
    8bit:3c upper:3 and lower:c
    8bit:09 upper:0 and lower:9
    image data:
     [[ 51  68 221  68 187 221]
     [204  51 102  17 153 238]
     [ 85 255  17 187 170  85]
     [ 51  17  34  34 136 170]
     [ 17 238 136  68  51 170]
     [204   0  51 204   0 153]]
    

    And generated the following image:

    enter image description here

    The original data has 18 bytes and there are 36 blocks/"20x20_pixels"

    And if I change the dimensions to 1800, 1000 that you have in the question I get:

    enter image description here