Netpbm and the PGM Format – Part 2

This is the second part of a series on processing PGM format image files in Python. For an overview of what PGM is refer to Part 1. In this part we’ll look at how a PGM file is generated and create a few small images to look at. In part 3 of this series we’ll examine some utility functions to write, read and display PGM image data. Lastly, in part 4, we’ll take a look at a case study of how the PGM format was used for scientific CCD camera testing, and offer some suggestions for how you might be able to use it.

Before we go too much further you will need to get an image viewer that knows how to handle a PGM file. Many don’t. I recommend the excellent ImageJ viewer from the National Institutes of Health. ImageJ is a powerful and well supported open source package written in Java, and although it is primarily aimed at microbiology imaging applications its capabilities are broad enough for it to find use in a variety of situations. I’ve used it to view image data ranging from laser interferograms to images obtained from spacecraft. You can download ImageJ here: http://rsbweb.nih.gov/ij/. Once you have it installed then you can run the example code and look at the output images we’ll be creating.

A 2-D array consisting of data representing the amount of light energy across something like a CCD array (as found in a common CCD camera) is sometimes called a “luminence map”, although it is more commonly referred to as a “grayscale image”. You may also encounter the term “intensity map”, and while it could be used to refer to image data, it’s a more generic term that refers to an array of data representing the intensity of something across the array space. Intensity maps are found in data representation applications, such as a plot of smog density across a city, or temperatures across a grid of points on a printed circuit board, or the intensity levels from the detector in an X-ray machine at the hospital. None of these are “images” in the sense of being something the human eye can see directly, but once the intensities are converted into a grayscale image format (or even color, depending on the application and the type of data) it can be veiwed and interpreted by the good old Mark I eyeball. If you want to learn more, the book “Visualizing Data” by Ben Fry gives some examples of how to convert non-image data into visual representations using the Processing extension for Java.

Then there is what is called the “geometry” of an image. An image is a 2-D array with width and height defined in terms of pixels, with a size, in pixels, that is the product of the width and height. There is also a third dimension, so to speak, which is the size of data used to represent the luminence (or intensity) at each x,y pixel position in the array. Some grayscale images utilize eight bit data, and some use 16-bits per pixel.

You will sometimes see the pixel data size expressed as “8bpp” or “16bpp”. These are just shortcut ways to say n bits per pixel, where n can be 8, 16 or whatever (some color image formats use 24 or 32bpp to hold the color, or chroma, data for each pixel–we’re not going to work with color images here).

Together the width, height and pixel size parameters define the basic geometry of a grayscale luminence map image.

Here’s an example of an intensity map showing the intensity of each pixel in a 3-D surface plot format:

Surface Plot

This was generated in ImageJ from an 8bpp image picked at random from my disk drive.

As with most of the Python code you’ll see around here I’ve made it a point to avoid being clever (Python has some powerful but, to the uninitiated, rather cryptic features which I have avoided–you’re welcome to use them yourself if you wish, of course).

 

Creating a PGM File

As mentioned in the first part of this series a PGM image file is very easy to create. Recall that the “header” of a binary (ID code P5) PGM file consists of:

ID

(optional comments)

Width

Height

Pixel Size

The header is then followed by the binary image data. Everything except for the image data is in plain old ASCII, so creating a header is as simple as creating a formatted string in Python:

outstr = "P5\n%d\n%d\n%d\n" % (width, height, pxsize)

If you want to include comments, then I would suggest inserting them between the ID and the width. Recall that there can only be one whitespace character after the pixel size value, so that limits the choices, and I like to get the comments as close to the top of the file as possible. Putting them at the very start is also a no-go, since most programs that can handle PGM files expect to see the ID characters as the first thing in the file.

The following sample program will create an “image” consisting of random pixel values. The values are randomly selected powers of 2.

# generates an 8bpp "image" of random pixel values

import random

# print ID string (P5)
# print comments (if any)
# print width
# print height
# print size
# print data

rnd = random
rnd.seed()

width  = 256
height = 256
pxsize = 255

# create the PGM header
hdrstr = "P5\n%d\n%d\n%d\n" % (width, height, pxsize)

pixels = []
for i in range(0,width):
    for j in range(0,height):
        # generate random values of powers of 2
        pixval = 2**rnd.randint(0,8)
        # some values will be 256, so fix them
        if pixval > pxsize:
            pixval = pxsize
        #endif
        pixels.append(pixval)
    #endfor
#endfor

# convert array to character values
outpix = "".join(map(chr,pixels))

# append the "image" to the header
outstr = hdrstr + outpix

# and write it out to the disk
FILE = open("pgmtest.pgm","w")
FILE.write(outstr)
FILE.close()

 

Analyzing the Image Data

Using ImageJ (or the image viewer of your choice) we can see that the resulting image from the sample code looks like the “snow” on an old-fashioned analog television set when tuned to an empty channel:

random_img

Taking a look at the histogram generated by ImageJ we can see that most of the pixel values seem to be down towards the lower end of the range from 0 to 255:

rnd_img_histo

If you think about it this it makes perfect sense, since we’re using a non-linear exponential function to generate the simulated pixel values (val = 2**n, where n = [0..8]). There are only nine possible pixel values, and five of the possible values will be between 0 and 16. These values don’t really have enough change between them (or luminence delta) to be readily discernable to the naked eye. The pixels with values of 32, 64, 128 and 255 jump right out as readily visible points compared to the low-end values. Hence the image looks dark with a lot of bright dots. The graph below illustrates this:

random_binary_dist

In order to get a more even luminence distribution we can use a different approach to generate random values. If we change the scaling to a linear form the result should be more balanced in terms of light and dark.

We can use the “raw” output of the random function, which ranges from 0.0 to 1.0, as a scaling factor and generate a new 8-bit value in the range [0..255], like so:

pixval = int(255 * rnd.random())

We still need to catch any occurances of 256 and trim them back by one to meet the PGM pixel size limit for an 8-bit image. The resulting image looks like this:

8bitlinrand

The histogram for this image shows a generally random distribution of values, just as one would expect:

8bitlinrand_histo

 

Creating Useful Synthetic Images

An image that is created using an equation or algorithm as the data source is often referred as a “synthetic image”. These types of images are very useful for testing image compressors, verification of MTF (modulation transfer function) analysis algorithms, and other image processing activities. The site http://www.normankoren.com/Tutorials/MTF.html has a good high-level overview of MTF, and there are many other sources of information on the web.

Using the code presented above you might want to try creating a synthetic image with four separate horizontal bands of equal size and with pixel values of 0, 64, 128 and 255, and another with vertical bands of the same values. Then try different types of compression and see how well each handles the synthetic images. What, for example, happens at each “step” between the bands? Is the step transition crisp, or is there some “noise”? Does it matter if the step discontinuity is horizontal or vertical? Since PGM is not a compressed format you can know that it will not itself introduce any unwanted artifacts into the image, but some compression formats will. ImageJ can save the synthetic test images in JPEG, BMP. and PNG, among others, so you can investigate different image file format behaviors. We’ll talk more about this in Part 4 of this series when we explore what can be done with PGM images, and I’ll show you a quick and easy way to get the PSNR (peak signal-to-noise ratio) of an image as well.

 

Until Next time
This concludes Part 2 of this series. Next time we’ll look at how to write a PGM file using arbitrary input data.

Advertisements

1 Response to “Netpbm and the PGM Format – Part 2”


  1. 1 vivekthachil January 10, 2014 at 6:31 am

    Thanks for making me aware of ImageJ. I was thinking of writing something like Image J.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s




Follow Crankycode on WordPress.com

Little Buddy

An awesome little friend

Jordi the Sheltie passed away in 2008 at the ripe old age of 14. He was the most awesome dog I've ever known.


%d bloggers like this: