Bitroller
Published © GPL3+

Optical Punch card reader

This is a OpenCV based optical punch card reader for punch cards of 128 bits.

IntermediateFull instructions provided6 hours6
Optical Punch card reader

Things used in this project

Hardware components

Webcam, Logitech® HD Pro
Webcam, Logitech® HD Pro
×1

Software apps and online services

Pycharm

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Schematics

127 bit punched card

This is a card for experimenting with, in STL format for 3d-printing.

Code

Python code

Python
This code reads the cards using OpenCV and a webcamera
import cv2
import numpy as np

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

print("r=read | wasd=move | WASD=fast | g/h=zoom | G/H=fast | i/o=rotate | q=quit")

base_points = [
    (668, 387), (668, 433), (668, 477), (668, 522),
    (668, 567), (668, 610), (668, 655), (668, 700), (668, 745)
]

columns = []

# 8 columns (45px)
for i in range(8):
    columns.append([(x + i*45, y) for (x, y) in base_points])

# 3 columns (42px)
offset = 8 * 45
for i in range(3):
    columns.append([(x + offset + i*42, y) for (x, y) in base_points])

# 5 columns (45px)
offset += 3 * 42
for i in range(5):
    columns.append([(x + offset + i*45, y) for (x, y) in base_points])

# Fine adjustments
columns[8] = [(x - 1, y) for (x, y) in columns[8]]

for i in range(10, min(16, len(columns))):
    shift = 2
    if i == 11:
        shift = 4
    columns[i] = [(x + shift, y) for (x, y) in columns[i]]

THRESHOLD = 100

poly_pts = np.array([
    [520, 317], [1408, 317], [1408, 811],
    [593, 811], [516, 741]
], np.float32)

# Movement / scale / rotation
offset_x = 0
offset_y = 0
scale = 1.0
rotation = 0.0  # radians

# Persistent overlay
last_matrix = None
last_ascii = ""
last_status = ""
last_color = (255,255,255)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    h, w = frame.shape[:2]
    cx, cy = w // 2, h // 2

    display = frame.copy()

    # ---------- Transform with rotation ----------
    def transform(x, y):
        # scale first
        x0 = (x - cx) * scale
        y0 = (y - cy) * scale

        # rotate
        xr = x0 * np.cos(rotation) - y0 * np.sin(rotation)
        yr = x0 * np.sin(rotation) + y0 * np.cos(rotation)

        # translate back
        x2 = cx + xr + offset_x
        y2 = cy + yr + offset_y

        return int(x2), int(y2)

    # Draw polygon
    poly_scaled = np.array([transform(x, y) for (x, y) in poly_pts])
    cv2.polylines(display, [poly_scaled.reshape((-1,1,2))], True, (0,255,0), 2)

    # Draw columns
    shifted_columns = []
    for col in columns:
        new_col = []
        for (x, y) in col:
            tx, ty = transform(x, y)
            new_col.append((tx, ty))
            cv2.circle(display, (tx, ty), 5, (0,0,255), -1)
        shifted_columns.append(new_col)

    # ---------- Input ----------
    key = cv2.waitKey(1) & 0xFF

    # Move
    if key == ord('w'): offset_y -= 1
    elif key == ord('s'): offset_y += 1
    elif key == ord('a'): offset_x -= 1
    elif key == ord('d'): offset_x += 1

    # Fast move
    elif key == ord('W'): offset_y -= 5
    elif key == ord('S'): offset_y += 5
    elif key == ord('A'): offset_x -= 5
    elif key == ord('D'): offset_x += 5

    # Zoom
    elif key == ord('g'): scale *= 0.995
    elif key == ord('h'): scale /= 0.995

    # Fast zoom
    elif key == ord('G'): scale *= 0.97
    elif key == ord('H'): scale /= 0.97

    # ---------- Rotation ----------
    elif key == ord('i'): rotation -= 0.002
    elif key == ord('o'): rotation += 0.002

    # Fast rotation
    elif key == ord('I'): rotation -= 0.01
    elif key == ord('O'): rotation += 0.01

    # ---------- Read ----------
    elif key == ord('r'):
        matrix = []
        ascii_chars = []
        all_ok = True

        for col in shifted_columns:
            bits = []

            for (x, y) in col:
                if 0 <= x < w and 0 <= y < h:
                    b, g, r_pix = frame[y, x]
                    gray = int(0.299*r_pix + 0.587*g + 0.114*b)
                    bits.append(1 if gray < THRESHOLD else 0)
                else:
                    bits.append(0)

            matrix.append(bits)

            parity = bits[0]
            data_bits = bits[1:]

            value = sum(b << i for i, b in enumerate(data_bits))

            ones = sum(data_bits)
            expected = 1 if ones % 2 == 0 else 0

            if parity != expected:
                all_ok = False

            ascii_chars.append(chr(33 + (value % 94)))

        last_matrix = matrix
        last_ascii = ''.join(ascii_chars)

        if all_ok:
            last_status = "PARITY OK"
            last_color = (0,255,0)
        else:
            last_status = "PARITY ERROR"
            last_color = (0,0,255)

    elif key == ord('q'):
        break

    # ---------- Overlay ----------
    if last_matrix is not None:
        cell = 18
        rows = 9
        cols = len(last_matrix)

        tx = w - cols * cell - 20
        ty = 20

        cv2.rectangle(display,
                      (tx-10, ty-10),
                      (tx + cols*cell + 10, ty + rows*cell + 60),
                      (0,0,0),
                      -1)

        for c in range(cols):
            for r in range(rows):
                val = last_matrix[c][r]
                color = (0,255,0) if val else (100,100,100)

                cv2.putText(display, str(val),
                            (tx + c*cell, ty + r*cell + 14),
                            cv2.FONT_HERSHEY_SIMPLEX,
                            0.5, color, 1)

        cv2.putText(display, last_ascii,
                    (tx, ty + rows*cell + 20),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.7, (255,255,255), 2)

        cv2.putText(display, last_status,
                    (tx, ty + rows*cell + 45),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.7, last_color, 2)

    cv2.imshow("Camera", display)

cap.release()
cv2.destroyAllWindows()

Decoding program with Reed-Solomon

Python
Decoding punch card with Reed-Solomon encoding
import cv2
import numpy as np
from reedsolo import RSCodec

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

print("r=ASCII read | p=RS decode | wasd=move | WASD=fast | g/h=zoom | G/H=fast | i/o=rotate | q=quit")

# ==========================================================
# BASE POINTS
# ==========================================================

base_points = [
    (668, 387),
    (668, 433),
    (668, 477),
    (668, 522),
    (668, 567),
    (668, 610),
    (668, 655),
    (668, 700),
    (668, 745)
]

# ==========================================================
# BUILD COLUMNS
# ==========================================================

columns = []

# 8 columns (45 px spacing)
for i in range(8):
    columns.append([
        (x + i * 45, y)
        for (x, y) in base_points
    ])

# 3 columns (42 px spacing)
offset = 8 * 45

for i in range(3):
    columns.append([
        (x + offset + i * 42, y)
        for (x, y) in base_points
    ])

# 5 columns (45 px spacing)
offset += 3 * 42

for i in range(5):
    columns.append([
        (x + offset + i * 45, y)
        for (x, y) in base_points
    ])

# ==========================================================
# FINE CALIBRATION
# ==========================================================

# Column 9 slightly left
columns[8] = [
    (x - 1, y)
    for (x, y) in columns[8]
]

# Columns 11-16 slightly right
for i in range(10, min(16, len(columns))):

    shift = 2

    # Column 12 extra shift
    if i == 11:
        shift = 4

    columns[i] = [
        (x + shift, y)
        for (x, y) in columns[i]
    ]

# ==========================================================
# SETTINGS
# ==========================================================

THRESHOLD = 100

poly_pts = np.array([
    [520, 317],
    [1408, 317],
    [1408, 811],
    [593, 811],
    [516, 741]
], np.float32)

# ==========================================================
# TRANSFORM VARIABLES
# ==========================================================

offset_x = 0
offset_y = 0

scale = 1.0
rotation = 0.0

# ==========================================================
# OVERLAY DATA
# ==========================================================

last_matrix = None
last_ascii = ""
last_status = ""
last_color = (255, 255, 255)

# ==========================================================
# MAIN LOOP
# ==========================================================

while True:

    ret, frame = cap.read()

    if not ret:
        break

    h, w = frame.shape[:2]

    cx = w // 2
    cy = h // 2

    display = frame.copy()

    # ======================================================
    # TRANSFORM FUNCTION
    # ======================================================

    def transform(x, y):

        # Scale around center
        x0 = (x - cx) * scale
        y0 = (y - cy) * scale

        # Rotate
        xr = (
            x0 * np.cos(rotation)
            - y0 * np.sin(rotation)
        )

        yr = (
            x0 * np.sin(rotation)
            + y0 * np.cos(rotation)
        )

        # Translate
        x2 = cx + xr + offset_x
        y2 = cy + yr + offset_y

        return int(x2), int(y2)

    # ======================================================
    # DRAW POLYGON
    # ======================================================

    poly_scaled = np.array([
        transform(x, y)
        for (x, y) in poly_pts
    ])

    cv2.polylines(
        display,
        [poly_scaled.reshape((-1, 1, 2))],
        True,
        (0, 255, 0),
        2
    )

    # ======================================================
    # DRAW SAMPLE POINTS
    # ======================================================

    shifted_columns = []

    for col in columns:

        new_col = []

        for (x, y) in col:

            tx, ty = transform(x, y)

            new_col.append((tx, ty))

            cv2.circle(
                display,
                (tx, ty),
                5,
                (0, 0, 255),
                -1
            )

        shifted_columns.append(new_col)

    # ======================================================
    # INPUT
    # ======================================================

    key = cv2.waitKey(1) & 0xFF

    # ------------------------------------------------------
    # MOVE
    # ------------------------------------------------------

    if key == ord('w'):
        offset_y -= 1

    elif key == ord('s'):
        offset_y += 1

    elif key == ord('a'):
        offset_x -= 1

    elif key == ord('d'):
        offset_x += 1

    # ------------------------------------------------------
    # FAST MOVE
    # ------------------------------------------------------

    elif key == ord('W'):
        offset_y -= 5

    elif key == ord('S'):
        offset_y += 5

    elif key == ord('A'):
        offset_x -= 5

    elif key == ord('D'):
        offset_x += 5

    # ------------------------------------------------------
    # ZOOM
    # ------------------------------------------------------

    elif key == ord('g'):
        scale *= 0.995

    elif key == ord('h'):
        scale /= 0.995

    # ------------------------------------------------------
    # FAST ZOOM
    # ------------------------------------------------------

    elif key == ord('G'):
        scale *= 0.97

    elif key == ord('H'):
        scale /= 0.97

    # ------------------------------------------------------
    # ROTATION
    # ------------------------------------------------------

    elif key == ord('i'):
        rotation -= 0.002

    elif key == ord('o'):
        rotation += 0.002

    # ------------------------------------------------------
    # FAST ROTATION
    # ------------------------------------------------------

    elif key == ord('I'):
        rotation -= 0.01

    elif key == ord('O'):
        rotation += 0.01

    # ======================================================
    # ASCII CARD MODE
    # ======================================================

    elif key == ord('r'):

        matrix = []
        ascii_chars = []
        all_ok = True

        for col in shifted_columns:

            bits = []

            for (x, y) in col:

                if 0 <= x < w and 0 <= y < h:

                    b, g, r_pix = frame[y, x]

                    gray = int(
                        0.299 * r_pix +
                        0.587 * g +
                        0.114 * b
                    )

                    bit = 1 if gray < THRESHOLD else 0

                    bits.append(bit)

                else:
                    bits.append(0)

            matrix.append(bits)

            # row 0 = parity
            # row 1 = LSB
            parity = bits[0]

            data_bits = bits[1:]

            value = 0

            for i, b in enumerate(data_bits):
                value |= (b << i)

            # even parity
            ones = sum(data_bits)

            expected = 1 if ones % 2 == 0 else 0

            if parity != expected:
                all_ok = False

            ascii_chars.append(
                chr(33 + (value % 94))
            )

        last_matrix = matrix
        last_ascii = ''.join(ascii_chars)

        if all_ok:
            last_status = "PARITY OK"
            last_color = (0, 255, 0)
        else:
            last_status = "PARITY ERROR"
            last_color = (0, 0, 255)

        print("\nASCII:", last_ascii)

    # ======================================================
    # REED SOLOMON MODE
    # ======================================================

    elif key == ord('p'):

        rs_values = []
        rs_matrix = []

        print("\n--- REED SOLOMON READ ---")

        for col_idx, col in enumerate(shifted_columns):

            bits = []

            for (x, y) in col:

                if 0 <= x < w and 0 <= y < h:

                    b, g, r_pix = frame[y, x]

                    gray = int(
                        0.299 * r_pix +
                        0.587 * g +
                        0.114 * b
                    )

                    bit = 1 if gray < THRESHOLD else 0

                    bits.append(bit)

                else:
                    bits.append(0)

            rs_matrix.append(bits)

            # row 0 = parity
            # rows 1-8 = data
            data_bits = bits[1:]

            # LSB at bottom
            reversed_bits = list(reversed(data_bits))

            value = 0

            for i, b in enumerate(reversed_bits):
                value |= (b << i)

            rs_values.append(value)

            print(f"Column {col_idx}: {value}")

        print("RS Values:", rs_values)

        # --------------------------------------------------
        # REED SOLOMON DECODE
        # --------------------------------------------------

        try:

            rsc = RSCodec(6)

            decoded = rsc.decode(
                bytearray(rs_values)
            )

            decoded_bytes = decoded[0]

            decoded_text = decoded_bytes.decode(
                'utf-8',
                errors='replace'
            )

            print("Decoded text:", decoded_text)

            last_matrix = rs_matrix
            last_ascii = decoded_text
            last_status = "RS DECODE OK"
            last_color = (0, 255, 255)

        except Exception as e:

            print("RS decode failed:", e)

            last_matrix = rs_matrix
            last_ascii = "RS DECODE FAILED"
            last_status = "RS ERROR"
            last_color = (0, 0, 255)

    # ======================================================
    # QUIT
    # ======================================================

    elif key == ord('q'):
        break

    # ======================================================
    # OVERLAY
    # ======================================================

    if last_matrix is not None:

        cell = 18

        rows = 9
        cols = len(last_matrix)

        tx = w - cols * cell - 20
        ty = 20

        # Background
        cv2.rectangle(
            display,
            (tx - 10, ty - 10),
            (tx + cols * cell + 10,
             ty + rows * cell + 60),
            (0, 0, 0),
            -1
        )

        # Matrix
        for c in range(cols):

            for r in range(rows):

                val = last_matrix[c][r]

                color = (
                    (0, 255, 0)
                    if val else
                    (100, 100, 100)
                )

                cv2.putText(
                    display,
                    str(val),
                    (tx + c * cell,
                     ty + r * cell + 14),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.5,
                    color,
                    1
                )

        # Result text
        cv2.putText(
            display,
            last_ascii,
            (tx, ty + rows * cell + 20),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.7,
            (255, 255, 255),
            2
        )

        # Status
        cv2.putText(
            display,
            last_status,
            (tx, ty + rows * cell + 45),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.7,
            last_color,
            2
        )

    cv2.imshow("Camera", display)

cap.release()
cv2.destroyAllWindows()

Credits

Bitroller
5 projects • 1 follower

Comments