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 hours3,414
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 = []
        decimal_values = []
        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))
            decimal_values.append(value)

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

            if parity != expected:
                all_ok = False

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

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

        print("Decimal lista:")
        print(decimal_values)
        print("lösenord:", last_ascii)

        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()

Card creation code

Python
This code takes 16 values(0-255) and creates an SCAD model that can be exported to STL and printed/ordered
import random

# =========================
# PARAMETERS
# =========================

CARD_THICKNESS = 3

FRAME_HEIGHT = 1
FRAME_WIDTH = 3

HOLE_DIAMETER = 3
HOLE_RADIUS = HOLE_DIAMETER / 2

PITCH = 6

COLS = 16
ROWS = 9  # parity + 8 bits

MARGIN = 6

SIDE_TAB_WIDTH = 10
SIDE_TAB_HEIGHT = 3

# hole in left side tab
SIDE_TAB_HOLE_DIAMETER = 5

# move hole upward
SIDE_TAB_HOLE_Y_OFFSET = 3

# =========================
# CARD DIMENSIONS
# =========================

card_w = (
    MARGIN * 2
    + (COLS - 1) * PITCH
    + HOLE_DIAMETER
)

card_h = (
    MARGIN * 2
    + (ROWS - 1) * PITCH
    + HOLE_DIAMETER
)

# =========================
# READ INPUT DATA
# =========================

values = []

print("")
print("1 = Enter custom bytes")
print("2 = Generate random bytes")
print("")

mode = input("Selection: ").strip()

# ------------------------------------------
# RANDOM BYTES
# ------------------------------------------

if mode == "2":

    values = [
        random.randint(0, 255)
        for _ in range(16)
    ]

    print("")
    print("Random bytes generated:")
    print(values)

# ------------------------------------------
# MANUAL INPUT
# ------------------------------------------

else:

    print("")
    print("Enter 16 values between 0 and 255")

    for i in range(16):

        while True:

            try:

                v = int(input(f"Byte {i + 1}: "))

                if 0 <= v <= 255:
                    values.append(v)
                    break

                print("Value must be between 0 and 255")

            except:
                print("Invalid value")

# =========================
# GENERATE SCAD
# =========================

scad = []

scad.append("$fn=48;")
scad.append("")

scad.append("difference() {")

# ==========================================
# UNION
# ==========================================

scad.append("    union() {")

# ------------------------------------------
# BASE CARD
# ------------------------------------------

scad.append(
    f"        cube([{card_w}, {card_h}, {CARD_THICKNESS}]);"
)

# ------------------------------------------
# FRAME
# ------------------------------------------

# top
scad.append(
    f"""
        translate([0,{card_h - FRAME_WIDTH},{CARD_THICKNESS}])
        cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
    """
)

# bottom
scad.append(
    f"""
        translate([0,0,{CARD_THICKNESS}])
        cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
    """
)

# left
scad.append(
    f"""
        translate([0,0,{CARD_THICKNESS}])
        cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
    """
)

# right
scad.append(
    f"""
        translate([{card_w - FRAME_WIDTH},0,{CARD_THICKNESS}])
        cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
    """
)

# ------------------------------------------
# LEFT SIDE TAB
# ------------------------------------------

scad.append(
    f"""
        translate([-{SIDE_TAB_WIDTH},0,0])
        cube([{SIDE_TAB_WIDTH},{card_h},{SIDE_TAB_HEIGHT}]);
    """
)

# end union
scad.append("    }")

# ==========================================
# CARD HOLES
# ==========================================

for c in range(COLS):

    value = values[c]

    # LSB first
    bits = [
        (value >> i) & 1
        for i in range(8)
    ]

    # parity = 1 if odd number of 1 bits
    parity = 1 if sum(bits) % 2 == 1 else 0

    full = [parity] + bits

    for r in range(ROWS):

        if full[r] == 1:

            x = (
                MARGIN
                + HOLE_RADIUS
                + c * PITCH
            )

            # parity row at top
            y = (
                MARGIN
                + HOLE_RADIUS
                + (ROWS - 1 - r) * PITCH
            )

            scad.append(
                f"""
                    translate([{x},{y},-1])
                    cylinder(
                        h={CARD_THICKNESS + FRAME_HEIGHT + 10},
                        r={HOLE_RADIUS}
                    );
                """
            )

# ==========================================
# HOLE IN LEFT TAB
# ==========================================

hole_x = -SIDE_TAB_WIDTH / 2

hole_y = (
    card_h
    - MARGIN
    - SIDE_TAB_HOLE_DIAMETER
    + SIDE_TAB_HOLE_Y_OFFSET
)

scad.append(
    f"""
        translate([{hole_x},{hole_y},-1])
        cylinder(
            h={CARD_THICKNESS + FRAME_HEIGHT + 10},
            r={SIDE_TAB_HOLE_DIAMETER / 2}
        );
    """
)

# ==========================================
# END DIFFERENCE
# ==========================================

scad.append("}")

# =========================
# SAVE FILE
# =========================

filename = "punchcard.scad"

with open(filename, "w", encoding="utf-8") as f:
    f.write("\n".join(scad))

# =========================
# DISPLAY BYTE INFO
# =========================

print("")
print("Byte information:")
print("")

for i, value in enumerate(values):

    bits = [
        (value >> b) & 1
        for b in range(8)
    ]

    parity = 1 if sum(bits) % 2 == 1 else 0

    print(
        f"Byte {i+1:02d}: "
        f"decimal={value:3d} "
        f"parity={parity} "
        f"bits(LSB->MSB)="
        f"{''.join(map(str, bits))}"
    )

print("")
print(f"SCAD file created: {filename}")
print("")
print("Open the file in OpenSCAD")
print("Press F6")
print("Then export as STL")

MkII card generator 16 bytes

Python
Makes scad code for 16 byte cards, with checksum byte.
import random

# =========================
# PARAMETERS
# =========================

CARD_THICKNESS = 2

FRAME_HEIGHT = 1
FRAME_WIDTH = 3

HOLE_DIAMETER = 3
HOLE_RADIUS = HOLE_DIAMETER / 2

PITCH = 4

# 16 data bytes + 1 checksum byte
COLS = 17

# 8 bits only
ROWS = 8

MARGIN = 4

# ------------------------------------------
# LEFT HANDLE / TAB
# ------------------------------------------

SIDE_TAB_WIDTH = 16
SIDE_TAB_HEIGHT = 3

# Hole in left handle
SIDE_TAB_HOLE_DIAMETER = 6
SIDE_TAB_HOLE_Y_OFFSET = 3

# ------------------------------------------
# STICKER RECESS
# ------------------------------------------

STICKER_RECESS_WIDTH = 12
STICKER_RECESS_HEIGHT = 25
STICKER_RECESS_DEPTH = 0.5

# Positive values move recess downward
STICKER_RECESS_Y_OFFSET = 5

# =========================
# CARD DIMENSIONS
# =========================

card_w = (
    MARGIN * 2
    + (COLS - 1) * PITCH
    + HOLE_DIAMETER
)

card_h = (
    MARGIN * 2
    + (ROWS - 1) * PITCH
    + HOLE_DIAMETER
)

# =========================
# READ INPUT DATA
# =========================

data_values = []

print("")
print("1 = Enter custom bytes")
print("2 = Generate random bytes")
print("")

mode = input("Selection: ").strip()

# ------------------------------------------
# RANDOM BYTES
# ------------------------------------------

if mode == "2":

    data_values = [
        random.randint(0, 255)
        for _ in range(16)
    ]

    print("")
    print("Random bytes generated:")
    print(data_values)

# ------------------------------------------
# MANUAL INPUT
# ------------------------------------------

else:

    print("")
    print("Enter 16 values between 0 and 255")

    for i in range(16):

        while True:

            try:

                v = int(input(f"Byte {i + 1}: "))

                if 0 <= v <= 255:
                    data_values.append(v)
                    break

                print("Value must be between 0 and 255")

            except:
                print("Invalid value")

# =========================
# CHECKSUM BYTE
# =========================

checksum = sum(data_values) % 256

values = data_values + [checksum]

print("")
print(f"Checksum byte: {checksum}")

# =========================
# GENERATE SCAD
# =========================

scad = []

scad.append("$fn=48;")
scad.append("")

scad.append("difference() {")

# ==========================================
# MAIN UNION
# ==========================================

scad.append("    union() {")

# ------------------------------------------
# BASE CARD
# ------------------------------------------

scad.append(
    f"""
    cube([{card_w}, {card_h}, {CARD_THICKNESS}]);
    """
)

# ------------------------------------------
# FRAME
# ------------------------------------------

# top
scad.append(
    f"""
    translate([0,{card_h - FRAME_WIDTH},{CARD_THICKNESS}])
    cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
    """
)

# bottom
scad.append(
    f"""
    translate([0,0,{CARD_THICKNESS}])
    cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
    """
)

# left
scad.append(
    f"""
    translate([0,0,{CARD_THICKNESS}])
    cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
    """
)

# right
scad.append(
    f"""
    translate([{card_w - FRAME_WIDTH},0,{CARD_THICKNESS}])
    cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
    """
)

# ------------------------------------------
# LEFT HANDLE
# ------------------------------------------

scad.append(
    f"""
    translate([-{SIDE_TAB_WIDTH},0,0])
    cube([{SIDE_TAB_WIDTH},{card_h},{SIDE_TAB_HEIGHT}]);
    """
)

# end union
scad.append("    }")

# ==========================================
# CARD DATA HOLES
# ==========================================

for c in range(COLS):

    value = values[c]

    # LSB first
    bits = [
        (value >> i) & 1
        for i in range(8)
    ]

    for r in range(ROWS):

        if bits[r] == 1:

            x = (
                MARGIN
                + HOLE_RADIUS
                + c * PITCH
            )

            y = (
                MARGIN
                + HOLE_RADIUS
                + (ROWS - 1 - r) * PITCH
            )

            scad.append(
                f"""
                translate([{x},{y},-1])
                cylinder(
                    h={CARD_THICKNESS + FRAME_HEIGHT + 10},
                    r={HOLE_RADIUS}
                );
                """
            )

# ==========================================
# HOLE IN LEFT HANDLE
# ==========================================

hole_x = -SIDE_TAB_WIDTH / 2

hole_y = (
    card_h
    - MARGIN
    - SIDE_TAB_HOLE_DIAMETER
    + SIDE_TAB_HOLE_Y_OFFSET
)

scad.append(
    f"""
    translate([{hole_x},{hole_y},-1])
    cylinder(
        h={CARD_THICKNESS + FRAME_HEIGHT + 10},
        r={SIDE_TAB_HOLE_DIAMETER / 2}
    );
    """
)

# ==========================================
# STICKER RECESS
# ==========================================

recess_x = -SIDE_TAB_WIDTH + 2

recess_y = (
    (card_h / 2)
    - (STICKER_RECESS_HEIGHT / 2)
    - STICKER_RECESS_Y_OFFSET
)

scad.append(
    f"""
    translate([
        {recess_x},
        {recess_y},
        {CARD_THICKNESS - STICKER_RECESS_DEPTH}
    ])
    cube([
        {STICKER_RECESS_WIDTH},
        {STICKER_RECESS_HEIGHT},
        {STICKER_RECESS_DEPTH + 1}
    ]);
    """
)

# ==========================================
# END DIFFERENCE
# ==========================================

scad.append("}")

# =========================
# SAVE FILE
# =========================

filename = "punchcard.scad"

with open(filename, "w", encoding="utf-8") as f:
    f.write("\n".join(scad))

# =========================
# DISPLAY BYTE INFO
# =========================

print("")
print("Byte information:")
print("")

for i, value in enumerate(values):

    bits = [
        (value >> b) & 1
        for b in range(8)
    ]

    label = "CHECKSUM" if i == 16 else f"BYTE {i+1:02d}"

    print(
        f"{label}: "
        f"decimal={value:3d} "
        f"bits(LSB->MSB)="
        f"{''.join(map(str, bits))}"
    )

print("")
print(f"SCAD file created: {filename}")
print("")
print("Open the file in OpenSCAD")
print("Press F6")
print("Then export as STL")

Card reader MkII 16 bytes

Python
Code for reading 16 byte cards with checksum
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")

# ---------- 8 bits per column ----------
base_points = [
    (668, 387), (668, 433), (668, 477), (668, 522),
    (668, 567), (668, 610), (668, 655), (668, 700)
]

columns = []

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

offset = 8 * 45

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

offset += 3 * 42

for i in range(6):
    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(17, len(columns))):

    shift = 2

    if i == 11:
        shift = 4

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

THRESHOLD = 100

# ---------- Rectangle card ----------
poly_pts = np.array([
    [430, 317],
    [1435, 317],
    [1435, 771],
    [430, 771]
], np.float32)

offset_x = 0
offset_y = 0
scale = 1.0
rotation = 0.0

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

decimal_values = []
checksum_value = 0

while True:

    ret, frame = cap.read()

    if not ret:
        break

    h, w = frame.shape[:2]

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

    display = frame.copy()

    # ---------- Left-side decimal overlay ----------
    if last_matrix is not None:

        overlay_x = 20
        overlay_y = 60

        cv2.rectangle(
            display,
            (10, 20),
            (320, 500),
            (0, 0, 0),
            -1
        )

        cv2.putText(
            display,
            "Decimal values:",
            (overlay_x, overlay_y),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.7,
            (0, 255, 0),
            2
        )

        # ---------- Bytes 1-16 ----------
        for i, val in enumerate(decimal_values):

            cv2.putText(
                display,
                f"{i+1}: {val}",
                (overlay_x, overlay_y + 35 + i * 22),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.6,
                (255, 255, 255),
                1
            )

        # ---------- Byte 17 = checksum ----------
        cv2.putText(
            display,
            f"17 (checksum): {checksum_value}",
            (overlay_x, overlay_y + 35 + len(decimal_values) * 22 + 15),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.6,
            (0, 255, 255),
            2
        )

    # ---------- Transform ----------
    def transform(x, y):

        x0 = (x - cx) * scale
        y0 = (y - cy) * scale

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

        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
    )

    # ---------- Orientation circle ----------
    circle_center = transform(505, 402)

    cv2.circle(
        display,
        circle_center,
        20,
        (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 = []
        decimal_values = []

        # ---------- First 16 columns ----------
        for col in shifted_columns[:16]:

            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)

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

            decimal_values.append(value)

        # ---------- Checksum column ----------
        checksum_bits = []

        for (x, y) in shifted_columns[16]:

            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)

                checksum_bits.append(1 if gray < THRESHOLD else 0)

            else:
                checksum_bits.append(0)

        matrix.append(checksum_bits)

        checksum_value = sum(b << i for i, b in enumerate(checksum_bits))

        expected_checksum = sum(decimal_values) % 256

        if checksum_value == expected_checksum:

            last_status = "CHECKSUM OK"
            last_color = (0, 255, 0)

        else:

            last_status = f"CHECKSUM ERROR ({checksum_value} != {expected_checksum})"
            last_color = (0, 0, 255)

        last_matrix = matrix

        print("Decimal lista:")
        print(decimal_values)

        print("Checksum:")
        print(checksum_value)

        print("Expected checksum:")
        print(expected_checksum)

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

    # ---------- Right overlay ----------
    if last_matrix is not None:

        cell = 18
        rows = 8
        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 + 35),
            (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_status,
            (tx, ty + rows * cell + 20),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.7,
            last_color,
            2
        )

    cv2.imshow("Camera", display)

cap.release()
cv2.destroyAllWindows()

Credits

Bitroller
5 projects • 3 followers

Comments