The programs we wrote for ‘O’-level were written on coding sheets and then taken up to Nene College by our teacher. There, they were transformed into punched cards and run on the computer. A week later, the resulting line printer output and punched card decks were returned to us.

I still have some of the punched card decks for these programs. Since I don’t have access to a punched card reader, I decided to read them by photographing them and then writing some image processing software to recover the data on each of them.

## Taking the Photographs¶

The key information we need from an image of a punched card is which parts of the image are background and which are punched card. The ideal image would consist of a punched card silhouetted against a background of completely blown-out white.

I started with a softbox on the floor pointing upwards. This provided a very bright source of diffuse light.

Above the softbox I placed a A3-sized piece of clear perspex from a picture frame to provide a stable horizontal surface on which to place the punched card.

Above this, a camera mounted on a tripod and angled to point vertically downwards. This camera was placed as high above the perspex as possible to provide the best chance of keeping the sensor plane parallel with the plane of the punched card.

The focal length was chosen so that the card filled about half of the frame so that precise position of the card was not important, allowing fast placing of the card, and to reduce any pincushion distortion of the card image.

The light intensity was adjusted to blow out the background with an exposure at f/8 and to provide plenty of depth of field so the card would be nicely sharp all over even if the plane of the card was not parallel to the plane of the sensor.

Finally, The camera was connected to a PC to allow tethered shooting of images automatically captured every three seconds. Once this was set going, all that I had to do was remove the old card and place the new one in position within three seconds. (This rate of 20 card per minute is only one order of magnitude slower than the 300cpm rate of the real ICL card reader that originally read them.)

The results look like this:

As you can see, I wasn’t completely successful in blowing out the background all over the image but the results were easily fit for purpose.

## Writing the Software¶

The rest of this page presents a python script that reads in a single card and prints out the corresponding test. I wrapped this in a main function that wild-carded over a directory of card images representing a single card deck and processed each image in turn.

We’ll need the following python packages:

• The Python Imaging Library (PIL), to manipulate the pixels in the photographs and apply the perspective transformation to the card
• matplotlib.pyplot, a collection of command style functions for creating graphics. This isn’t needed to extract the information from the card, we just use it to to annotate the images to illustrate the effects of our processing
• numpy, a package for matrix handling that we will use to calculate the perspective transformation needed to project the image of the card back into its correct form

so let’s start off by importing them:

import matplotlib.pyplot as plt
import Image
import ImageDraw
import sys
import numpy


### The Geometry of a Standard Punched Card¶

We use the following sizes, in thousands of an inch, of a standard punched card with dimensions of 7⅜” by 3¼”:

CARDWIDTH=7375            # The width of the card
CARDHEIGHT=3250           # The height of the card
HOLEWIDTH=54              # The width of a punched hole
HOLEHEIGHT=124            # The height of a punched hole
COLSPACING=87             # Horizontal separation of hole centres
ROWSPACING=CARDHEIGHT/13  # Vertical separation of hole centres
FIRSTCOLUMN=250           # Centre of first column of holes


We will assume that the card is positioned close enough to the centre of the image that the central 100px × 100px area is all within the bounds of the punched card.

### How Characters are Encoded¶

There are 80 columns on the card, representing eighty characters. Each column contains twelve rows where holes may be punched. The top two rows are referred to as zones and labelled by ICL as ‘R’ (for the top one) and ‘X’ for the one below it. The remaining ten rows are labelled 0 to 9.

We represent each encoding of a character with a twelve-bit number, bit 0 representing the 0 column and bit 11 representing the ‘R’ column. The following function converts a list of columns (such as [‘R’, 1]) into its twelve bit equivalent:

def C(l):
m = {0:  0b001000000000, 1:   0b000100000000, 2:   0b000010000000,
3:  0b000001000000, 4:   0b000000100000, 5:   0b000000010000,
6:  0b000000001000, 7:   0b000000000100, 8:   0b000000000010,
9:  0b000000000001, 'R': 0b100000000000, 'X': 0b010000000000
}

return sum([m[i] for i in l])


and we can use it to create a dictionary mapping these codes onto the corresponding ICL character:

codes = {
C([]):        ' ',
C([0]):       '0',
C([1]):       '1',
C([2]):       '2',
C([3]):       '3',
C([4]):       '4',
C([5]):       '5',
C([6]):       '6',
C([7]):       '7',
C([8]):       '8',
C([9]):       '9',
C(['R']):     '&',
C(['R',0]):   '&', # Undocumented but seems to match our cards
C([3,8]):     '#',
C([4,8]):     '@',
C([5,8]):     '(',
C([6,8]):     ')',
C([7,8]):     ']',
C(['R',1]):   'A',
C(['R',2]):   'B',
C(['R',3]):   'C',
C(['R',4]):   'D',
C(['R',5]):   'E',
C(['R',6]):   'F',
C(['R',7]):   'G',
C(['R',8]):   'H',
C(['R',9]):   'I',
C(['X',1]):   'J',
C(['X',2]):   'K',
C(['X',3]):   'L',
C(['X',4]):   'M',
C(['X',5]):   'N',
C(['X',6]):   'O',
C(['X',7]):   'P',
C(['X',8]):   'Q',
C(['X',9]):   'R',
C([0,2]):     'S',
C([0,3]):     'T',
C([0,4]):     'U',
C([0,5]):     'V',
C([0,6]):     'W',
C([0,7]):     'X',
C([0,8]):     'Y',
C([0,9]):     'Z',
C(['X']):     '-',
C(['X',0]):   '"',
C([0,1]):     '/',
C(['R',2,8]): '+',
C(['R',3,8]): '.',
C(['R',4,8]): ';',
C(['R',5,8]): ':',
C(['R',6,8]): ',',
C(['R',7,8]): '!',
C(['X',2,8]): '[',