PSE Entertainment Corp
February 06, 2012, 03:08:56 PM *
Welcome, Guest. Please login or register.

Login with username, password and session length
News: Enjoy PSE Tunes!  They're great!
 
   Home   Help Search Login Register  
Pages: [1]
  Print  
Author Topic: Generate a BMP image file with Python (BMP Writer)  (Read 3705 times)
webmast
Guest
« on: February 15, 2010, 10:59:18 PM »

As an exercise, I decided to see if I could create an bmp file using pure python.  I know how to write raw bytes to the disk using the very handy struct module so I figured it was just a matter of getting the image specification down.  Turns out I was right.  The BMP format (like most image formats) contains some header bytes followed by data bytes.  A decent explanation of the format can be found on the Wikipedia (check it out!).

This code generates a 200 x 200 bmp file with a random color for each pixel.  It uses a very minimal header.  The output will look something like this:


Enjoy!

Code:
#Some key imports.
#Struct is used to create the actual bytes.
#It is super handy for this type of thing.
import struct, random

#Function to write a bmp file.  It takes a dictionary (d) of
#header values and the pixel data (bytes) and writes them
#to a file.  This function is called at the bottom of the code.
def bmp_write(d,the_bytes):
    mn1 = struct.pack('<B',d['mn1'])
    mn2 = struct.pack('<B',d['mn2'])
    filesize = struct.pack('<L',d['filesize'])
    undef1 = struct.pack('<H',d['undef1'])
    undef2 = struct.pack('<H',d['undef2'])
    offset = struct.pack('<L',d['offset'])
    headerlength = struct.pack('<L',d['headerlength'])
    width = struct.pack('<L',d['width'])
    height = struct.pack('<L',d['height'])
    colorplanes = struct.pack('<H',d['colorplanes'])
    colordepth = struct.pack('<H',d['colordepth'])
    compression = struct.pack('<L',d['compression'])
    imagesize = struct.pack('<L',d['imagesize'])
    res_hor = struct.pack('<L',d['res_hor'])
    res_vert = struct.pack('<L',d['res_vert'])
    palette = struct.pack('<L',d['palette'])
    importantcolors = struct.pack('<L',d['importantcolors'])
    #create the outfile
    outfile = open('bitmap_image.bmp','wb')
    #write the header + the_bytes
    outfile.write(mn1+mn2+filesize+undef1+undef2+offset+headerlength+width+height+\
                  colorplanes+colordepth+compression+imagesize+res_hor+res_vert+\
                  palette+importantcolors+the_bytes)
    outfile.close()

###################################    
def main():
    #Here is a minimal dictionary with header values.
    #Of importance is the offset, headerlength, width,
    #height and colordepth.
    #Edit the width and height to your liking.
    #These header values are described in the bmp format spec.
    #You can find it on the internet. This is for a Windows
    #Version 3 DIB header.
    d = {
        'mn1':66,
        'mn2':77,
        'filesize':0,
        'undef1':0,
        'undef2':0,
        'offset':54,
        'headerlength':40,
        'width':200,
        'height':200,
        'colorplanes':0,
        'colordepth':24,
        'compression':0,
        'imagesize':0,
        'res_hor':0,
        'res_vert':0,
        'palette':0,
        'importantcolors':0
        }

    #Function to generate a random number between 0 and 255
    def rand_color():
        x = random.randint(0,255)
        return x

    #Build the byte array.  This code takes the height
    #and width values from the dictionary above and
    #generates the pixels row by row.  The row_mod and padding
    #stuff is necessary to ensure that the byte count for each
    #row is divisible by 4.  This is part of the specification.
    the_bytes = ''
    for row in range(d['height']-1,-1,-1):# (BMPs are L to R from the bottom L row)
        for column in range(d['width']):
            b = rand_color()
            g = rand_color()
            r = rand_color()
            pixel = struct.pack('<BBB',b,g,r)
            the_bytes = the_bytes + pixel
        row_mod = (d['width']*d['colordepth']/8) % 4
        if row_mod == 0:
            padding = 0
        else:
            padding = (4 - row_mod)
        padbytes = ''
        for i in range(padding):
            x = struct.pack('<B',0)
            padbytes = padbytes + x
        the_bytes = the_bytes + padbytes
        
    #call the bmp_write function with the
    #dictionary of header values and the
    #bytes created above.
    bmp_write(d,the_bytes)

if __name__ == '__main__':
    main()




« Last Edit: May 26, 2010, 10:07:32 PM by webmaster » Report to moderator   Logged
webmast
Guest
« Reply #1 on: May 01, 2010, 10:21:59 AM »

I'm pleased to announce that the above BMP writer has found it's way into the very cool GOLLY project.  One of the contributors, Tim Hutton, needed a quick and dirty way to output BMP images without requiring the user to install the Python Image Library (PIL).

From the GOLLY WEB SITE:  (http://golly.sourceforge.net)

"Golly is an open source, cross-platform application for exploring Conway's Game of Life and other cellular automata. The primary authors are Andrew Trevorrow and Tomas Rokicki, with code contributions by Tim Hutton, Dave Greene and Jason Summers."

Right on!
Report to moderator   Logged
riseofthedecays
Newbie
*
Posts: 1


« Reply #2 on: May 24, 2010, 04:19:11 PM »

Hi !

Thanks for this code, it will be very useful for me.
For some reasons, it did not work with my python v3.1.2 :
  • It did not like naming a variable 'bytes' (the IDLE editor detects it as a function name)
  • It didn't recognize '' as an empty bytes object but as an empty string. I replaced it by bytes() and it went fine
You'll find below the modified version of your code that runs ok on my computer.

Code:
#Some key imports.
#Struct is used to create the actual bytes.
#It is super handy for this type of thing.
import struct, random

#Function to write a bmp file.  It takes a dictionary (d) of
#header values and the pixel data (bytes) and writes them
#to a file.  This function is called at the bottom of the code.
def bmp_write(d,byte):
    mn1 = struct.pack('<B',d['mn1'])
    mn2 = struct.pack('<B',d['mn2'])
    filesize = struct.pack('<L',d['filesize'])
    undef1 = struct.pack('<H',d['undef1'])
    undef2 = struct.pack('<H',d['undef2'])
    offset = struct.pack('<L',d['offset'])
    headerlength = struct.pack('<L',d['headerlength'])
    width = struct.pack('<L',d['width'])
    height = struct.pack('<L',d['height'])
    colorplanes = struct.pack('<H',d['colorplanes'])
    colordepth = struct.pack('<H',d['colordepth'])
    compression = struct.pack('<L',d['compression'])
    imagesize = struct.pack('<L',d['imagesize'])
    res_hor = struct.pack('<L',d['res_hor'])
    res_vert = struct.pack('<L',d['res_vert'])
    palette = struct.pack('<L',d['palette'])
    importantcolors = struct.pack('<L',d['importantcolors'])
    #create the outfile
    outfile = open('bitmap_image.bmp','wb')
    #write the header + the bytes
    outfile.write(mn1+mn2+filesize+undef1+undef2+offset+headerlength+width+height+\
                  colorplanes+colordepth+compression+imagesize+res_hor+res_vert+\
                  palette+importantcolors+byte)
    outfile.close()

###################################   
def main():
    #Here is a minimal dictionary with header values.
    #Of importance is the offset, headerlength, width,
    #height and colordepth.
    #Edit the width and height to your liking.
    #These header values are described in the bmp format spec.
    #You can find it on the internet. This is for a Windows
    #Version 3 DIB header.
    d = {
        'mn1':66,
        'mn2':77,
        'filesize':0,
        'undef1':0,
        'undef2':0,
        'offset':54,
        'headerlength':40,
        'width':200,
        'height':200,
        'colorplanes':0,
        'colordepth':24,
        'compression':0,
        'imagesize':0,
        'res_hor':0,
        'res_vert':0,
        'palette':0,
        'importantcolors':0
        }

    #Function to generate a random number between 0 and 255
    def rand_color():
        x = random.randint(0,255)
        return x

    #Build the byte array.  This code takes the height
    #and width values from the dictionary above and
    #generates the pixels row by row.  The row_mod and padding
    #stuff is necessary to ensure that the byte count for each
    #row is divisible by 4.  This is part of the specification.
    byte = bytes()
    for row in range(d['height']-1,-1,-1):# (BMPs are L to R from the bottom L row)
        for column in range(d['width']):
            b = rand_color()
            g = rand_color()
            r = rand_color()
            pixel = struct.pack('<BBB',b,g,r)
            byte = byte + pixel
        row_mod = (d['width']*d['colordepth']/8) % 4
        if row_mod == 0:
            padding = 0
        else:
            padding = (4 - row_mod)
        padbytes = bytes()
        for i in range(padding):
            x = struct.pack('<B',0)
            padbytes = padbytes + x
        byte = byte + padbytes
       
    #call the bmp_write function with the
    #dictionary of header values and the
    #bytes created above.
    bmp_write(d,byte)

if __name__ == '__main__':
    main()

I must tell you how much I'm delighted to have solved this problem, especially because I have been learning python (which is my first language apart from R and a little Matlab at school) for only 2 weeks !!!
Thanks again
Etienne
Report to moderator   Logged
webmast
Guest
« Reply #3 on: May 26, 2010, 09:41:27 PM »

Good catch, Etienne!

Indeed, Python 3.x uses "bytes" as new object type.  I wrote this script in Python v2.6 and didn't have that problem.

The original source code has been modified and "bytes" has been replace with "the_bytes" everywhere to make sure it works across python versions.

JDM2
Report to moderator   Logged
david.hilton
Guest
« Reply #4 on: February 17, 2011, 06:36:44 AM »

PIL was quite slow for my purposes, and it had some issues running under Parallel Python.

I've created a few modifications that should make it easier for others to simply import and start using this.

I should point out that this is by no means infallible, and if you are creating dynamically sized images, be sure to test the extremes to make sure the files are readable.  I have had problems with some programs reading generated files around 200k in size.  I suspect simply modifying the header using something like this (http://www.fastgraph.com/help/bmp_header_format.html) as a reference would fix that.

Code:
import BMPWriter
header = BMPWriter.default_bmp_header
...(as in test code)


Code:
#!/usr/bin/python
import struct, random

# modified from http://pseentertainmentcorp.com/smf/index.php?topic=2034.0

# important values: offset, headerlength, width, height and colordepth
# This is for a Windows Version 3 DIB header
# You will likely want to customize the width and height
default_bmp_header = {'mn1':66,
                      'mn2':77,
                      'filesize':0,
                      'undef1':0,
                      'undef2':0,
                      'offset':54,
                      'headerlength':40,
                      'width':200,
                      'height':200,
                      'colorplanes':0,
                      'colordepth':24,
                      'compression':0,
                      'imagesize':0,
                      'res_hor':0,
                      'res_vert':0,
                      'palette':0,
                      'importantcolors':0}

def bmp_write(header, pixels, filename):
    '''It takes a header (based on default_bmp_header),
    the pixel data (from structs, as produced by get_color and row_padding),
    and writes it to filename'''
    header_str = ""
    header_str += struct.pack('<B', header['mn1'])
    header_str += struct.pack('<B', header['mn2'])
    header_str += struct.pack('<L', header['filesize'])
    header_str += struct.pack('<H', header['undef1'])
    header_str += struct.pack('<H', header['undef2'])
    header_str += struct.pack('<L', header['offset'])
    header_str += struct.pack('<L', header['headerlength'])
    header_str += struct.pack('<L', header['width'])
    header_str += struct.pack('<L', header['height'])
    header_str += struct.pack('<H', header['colorplanes'])
    header_str += struct.pack('<H', header['colordepth'])
    header_str += struct.pack('<L', header['compression'])
    header_str += struct.pack('<L', header['imagesize'])
    header_str += struct.pack('<L', header['res_hor'])
    header_str += struct.pack('<L', header['res_vert'])
    header_str += struct.pack('<L', header['palette'])
    header_str += struct.pack('<L', header['importantcolors'])
    #create the outfile
    outfile = open(filename, 'wb')
    #write the header + pixels
    outfile.write(header_str + pixels)
    outfile.close()

def row_padding(width, colordepth):
    '''returns any necessary row padding'''
    byte_length = width*colordepth/8
    # how many bytes are needed to make byte_length evenly divisible by 4?
    padding = (4-byte_length)%4
    padbytes = ''
    for i in range(padding):
        x = struct.pack('<B',0)
        padbytes += x
    return padbytes

def pack_color(red, green, blue):
    '''accepts values from 0-255 for each value, returns a packed string'''
    return struct.pack('<BBB',blue,green,red)

def pack_hex_color(hex_color):
    '''accepts RGB hex colors like '#ACE024', returns a packed string'''
    base = 16
    red = int(hex_color[1:3], base)
    green = int(hex_color[3:5], base)
    blue = int(hex_color[5:7], base)
    return pack_color(red, green, blue)
    

###################################    
def test():
    header = default_bmp_header
    header["width"] = 150
    header["height"] = 50
    #Function to generate a random number between 0 and 255
    def rand_color():
        x = random.randint(0,255)
        return x

    #Build the byte array.  This code takes the height
    #and width values from the dictionary above and
    #generates the pixels row by row.  The row_padding
    #stuff is necessary to ensure that the byte count for each
    #row is divisible by 4.  This is part of the specification.
    pixels = ''
    for row in range(header['height']-1,-1,-1):# (BMPs are L to R from the bottom L row)
        for column in range(header['width']):
            r = rand_color()
            g = rand_color()
            b = rand_color()
            pixels += pack_color(r, g, b)
        pixels += row_padding(header['width'], header['colordepth'])
        
    #call the bmp_write function with the
    #dictionary of header values and the
    #pixels as created above.
    bmp_write(header, pixels, "test.bmp")

if __name__ == '__main__':
    test()
« Last Edit: February 17, 2011, 07:21:25 AM by david.hilton » Report to moderator   Logged
webmast
Guest
« Reply #5 on: February 19, 2011, 12:38:52 PM »

Sweet!  Thanks for the update David.

JDM2
Report to moderator   Logged
Pages: [1]
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.14 | SMF © 2006-2008, Simple Machines LLC Valid XHTML 1.0! Valid CSS!