Tuesday, August 28, 2012

jQuery/Raphael Virtual Card Punch

I've been brushing up on javascript and jQuery. I'm not really a web developer. These days I mostly work in the Java/Linux space deep inside the server, but sometimes a little web development is in the mix. I was working on a future post about backing up Flickr images and data to a static HTML slideshow. While setting up the slideshow, it occurred to me that I could use jQuery to create a Virtual Card Punch that would run entirely in the browser. So back to punch cards one final time.

A minimal version of the page is available here for anyone who wishes to crawl around the source and find out exactly how it was coded.
Programming using a card-punch was a noisy affair, you can hear what it was like at http://ibm-1401.info, listen to http://ibm-1401.info/IBM026KeyPunch.mp3.

The card-punch could also be programmed to do things like duplicate a deck. This was achieved by punching instructions onto a card and installing the card on the card-punch's program-cylinder.  The cylinder was installed in the card-punch and as it turned little cogs engaged with the holes and bumped little leavers signalling instructions to the card-punch. Duplicating a deck took the noise to a whole new level.

The following javascript libraries were used:
  • Raphael - SVG (Scalable Vector Graphics) javascript library.
  • jQuery - the write less, do more, javascript library.
  • Fancybox - floating lightbox.
The jQuery library grabs and edits the input text as it is typed. The Raphael library dynamically adds SVG elements to the page. The Fancybox library creates a popup window large enough to produce a scannable image of a card. I've run the code in chrome, firefox and IE8. IE8 seems a little buggy, but it's OK once you start typing. I used Raphael and SVG to learn a bit about something new. For true portability it would probably be better off using a table of precomputed images, one per character - that would also be very easy to code - jQuery could be used to dynamically update the visible images. Yet another approach can be seen at www.kloth.net - that site uses an http server to generate jpg's or png's for a wide variety of card encodings.

Wednesday, August 1, 2012

Punch Card Reader - the FAQ

Can I have a card to scan?
You could use a screen grab from my Javascript Virtual Cardpunch. Or you use the following python punchcardgen.py script that generates card images from text read from stdin (how about a t-shirt with a message punched into it):
#!/usr/bin/env python
# punchcardgen.py 
# Copyright (C) 2011: Michael Hamilton
# The code is GPL 3.0(GNU General Public License) ( http://www.gnu.org/copyleft/gpl.html )
import Image
import sys


# found measurements at http://www.quadibloc.com/comp/cardint.htm
CARD_WIDTH = 7.0 + 3.0/8.0 # Inches
CARD_HEIGHT = 3.25 # Inches
CARD_COL_WIDTH = 0.087 # Inches
CARD_HOLE_WIDTH = 0.055 # Inches IBM, 0.056 Control Data
CARD_ROW_HEIGHT = 0.25 # Inches
CARD_HOLE_HEIGHT = 0.125 # Inches
CARD_TOPBOT_MARGIN = 3.0/16.0 # Inches at top and bottom
CARD_SIDE_MARGIN = 0.2235 # Inches on each side

DARK = (0,0,0)
BRIGHT = (255,255,255)  # pixel brightness value (i.e. (R+G+B)/3)

    /&-0123456789ABCDEFGHIJKLMNOPQR/STUVWXYZ:#@'="`.<(+|!$*);^~,%_>? |
12 / O           OOOOOOOOO                        OOOOOO             |
11|   O                   OOOOOOOOO                     OOOOOO       |
 0|    O                           OOOOOOOOO                  OOOOOO |
 1|     O        O        O        O                                 |
 2|      O        O        O        O       O     O     O     O      |
 3|       O        O        O        O       O     O     O     O     |
 4|        O        O        O        O       O     O     O     O    |
 5|         O        O        O        O       O     O     O     O   |
 6|          O        O        O        O       O     O     O     O  |
 7|           O        O        O        O       O     O     O     O |
 8|            O        O        O        O OOOOOOOOOOOOOOOOOOOOOOOO |
 9|             O        O        O        O                         | 

translate = None
if translate == None:
    translate = {}
    # Turn the ASCII art sideways and build a hash look up for 
    # column values, for example:
    #   A:(O, , ,O, , , , , , , , )
    #   B:(O, , , ,O, , , , , , , )
    #   C:(O, , , , ,O, , , , , , )
    rows = IBM_MODEL_029_KEYPUNCH[1:].split('\n');
    rotated = [[ r[i] for r in rows[0:13]] for i in range(5, len(rows[0]) - 1)]
    for v in rotated:
        translate[v[0]] = tuple(v[1:])

if __name__ == '__main__':

    scale = 1000
    margin = 200
    card_x_pixels = int(CARD_WIDTH * scale)
    card_y_pixels = int(CARD_HEIGHT * scale)

    img_size = (2 * margin + card_x_pixels, 2 * margin + card_y_pixels)

    side_margin_pixels = int(CARD_SIDE_MARGIN * scale)
    col_width_pixels = int(CARD_COL_WIDTH * scale)

    top_bot_margin = int(CARD_TOPBOT_MARGIN * scale)
    row_height_pixels = int(CARD_ROW_HEIGHT * scale)

    hole_width = int(CARD_HOLE_WIDTH * scale)
    hole_height = int(CARD_HOLE_HEIGHT * scale)

    card_area = (margin, margin, margin + card_x_pixels, margin + card_y_pixels)
    proto_img = Image.new('RGB', img_size, BRIGHT)
    proto_pix = proto_img.load()
    proto_img.paste(DARK, card_area)
    # Remove the top left corner (don't know the standard for this - guess)
    i = 0
    for x in xrange(margin, margin + side_margin_pixels):
        for y in xrange(margin, margin + top_bot_margin + hole_height - i):
            proto_pix[x,y] = BRIGHT
        i += 2
    card_number = 1
    for line in sys.stdin:
        img = proto_img.copy()
        x = margin + side_margin_pixels
        for char in line:
            if char in translate:
                values = translate[char] 
                y = margin + top_bot_margin
                for row in xrange(0, CARD_ROWS):
                    if values[row] == 'O':
                        img.paste(BRIGHT, (x, y, x + hole_width, y + hole_height))
                    y += row_height_pixels
            x += col_width_pixels
            if x > margin + card_x_pixels:
        img = img.resize((img_size[0]/REDUCE_IN_SIZE, img_size[1]/REDUCE_IN_SIZE))
        filename =  "%010.10d.jpg" % ( card_number )
        print filename, line
        card_number += 1

The script has no command line options, just feed it uppercase text, for example:
% python punchcardgen.py
In this case the script produces a single image:
The full-sized image can be rescanned to text by using my original punchcard script, for example:
% python punchcard.py 0000000001.jpg > prog.f90
% gfortran prog.f90                    
% ./a.out 

If you have your own cards, you can just hold them up to an even light and take their picture, for example you might use a monitor displaying white or a cloudy sky - just make sure the resulting image background is smooth and the picture is straight and square - and hold it by the bottom corner, for example:

To get the best scan, try the python script with the -d or -i options for debug info, use -b N to change the threshold light levels.  Use a full sized image - if the image is too small the calculations introduce errors.

Why not use an auto-feed scanner with a straight through paper path?
That would be the way to go if I wanted to spend money on it – and didn't want to learn a little electronics.  

Why not add a motor and automate the feed?
I was worried about jams on older decks of cards. I think I could build a better feed by copying my photo printer's paper feed in Lego. (possible patent violation?)  

I did consider using my photo-printer as a feeder, that would probably have worked quite well.  I would have to figure out how to collect cards as they exit the printer.

Some way along I figured I could get the job done with stuff at hand without out buying anything.  Once I set that constraint, options narrowed considerably and decisions were easier to make.

There was also the case of the minicomputer with a crank fitted over the instruction single-step toggle-switch – variable speed debugging – consider my approach a homage to that earlier clever hardware hack.

Why not use an array of detectors connected to the Arduino and eliminate the camera?
That would be cool but - this is my first attempt at electronics. Advancing each column past a single column scan would seem hard to calibrate correctly. I imagine that could be solved with a grid/wheel of calibration holes moving with the card or moving with the scanner.  

If the card moved at a constant speed it might be possible to detect start and end of card, and from the timing figure out what went past and what row it belonged to. 

Why not just read the text printed at the top of the card?
I didn't think the text would be good enough for OCR. Some cards were quite worn. I did not want to manually read and enter each card. OCR seems a tough problem compared to reading the holes.

What is the Arduino for exactly?
The Arduino stops the card, detects that a card has stopped, signals the camera to focus and shoot, opens the servo to let the card go.  It plays a key role in keeping the cards in order, both the order of the images in the camera, and the order of the physical cards in the output bin.  It could do more, such as run a feed motor.   But really, the Card Reader is a integration with an Arduino in the mix - it's not a pure Arduino project.  

Why not use a webcam, Android camera?
The Canon S2 IS employed here is old, but produces reasonable distortion free images - with the CHDK firmware hack it seemed a shame not to use it.  It would be nice to feed directly to the PC - Android or a webcam would accomplish this.  Perhaps a wireless capable SD-card might also work.

Why would anyone want to go to this much time/effort?
For me, learning by doing works best. This was a well bounded problem that look solvable. It wasn't all that much effort, I just kept the problem in the back of my mind over the last year. There was only the occasional burst of activity when ideas solidified – I was not working to a deadline.

Why not tweak-it/finish-it/enhance-it in some respect?
I've scanned the cards I wanted to - some MIX, some FORTRAN.  In the process achieved my goal of learning a little about Arduino, electronics, CHDK, fritzing, and PIL.  I hope to apply some of what I've learnt to some of my other interests, for example, nature photography: