Bitroller
Published © CC BY

Automated Hardware random number generator

This is a dice throwing machine using a raspberry pi and a stepper motor. Dice are scanned to get random numbers.

BeginnerShowcase (no instructions)16 hours40
Automated Hardware random number generator

Things used in this project

Story

Read more

Custom parts and enclosures

STL files for the machine parts

This is the STL files for the parts for the machine

Sketchfab still processing.

Code

Python code

Python
This is the code for the machine
from picamera2 import Picamera2
import cv2
import numpy as np
import time
import RPi.GPIO as GPIO
from sklearn.cluster import DBSCAN
from datetime import datetime
import os

# =========================
# GPIO setup
# =========================
STP_PIN = 12
EN_PIN  = 20
SLP_PIN = 16
RST_PIN = 19
DIR_PIN = 21

GPIO.setmode(GPIO.BCM)
GPIO.setup([STP_PIN, EN_PIN, SLP_PIN, RST_PIN, DIR_PIN], GPIO.OUT)

GPIO.output(EN_PIN, GPIO.LOW)
GPIO.output(SLP_PIN, GPIO.HIGH)
GPIO.output(RST_PIN, GPIO.HIGH)

TIMER = 0.0007
STEPS = 380

# =========================
# Camera setup
# =========================
picam2 = Picamera2()
picam2.configure(picam2.create_still_configuration())

picam2.set_controls({
    "AfMode": 0,
    "LensPosition": 6.5
})

picam2.start()
time.sleep(0.2)

# =========================
# Utility functions
# =========================
def resize_with_aspect_ratio(image, max_w, max_h):
    h, w = image.shape[:2]
    scale = min(max_w / w, max_h / h)
    return cv2.resize(image, (int(w * scale), int(h * scale)))

def capture_and_preprocess():
    img = picam2.capture_array()
    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
    img = resize_with_aspect_ratio(img, 800, 600)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (7, 7), 0)
    return img, gray

def apply_square_roi(gray):
    h, w = gray.shape
    side = int(min(w, h) * 0.9)
    cx, cy = w // 2, h // 2

    x1 = max(0, cx - side // 2)
    y1 = max(0, cy - side // 2)
    x2 = min(w, cx + side // 2)
    y2 = min(h, cy + side // 2)

    mask = np.zeros((h, w), np.uint8)
    cv2.rectangle(mask, (x1, y1), (x2, y2), 255, -1)

    return cv2.bitwise_and(gray, mask), (x1, y1, x2, y2)

def detect_and_cluster(gray_masked):
    params = cv2.SimpleBlobDetector_Params()
    params.minThreshold = 5
    params.maxThreshold = 200
    params.filterByArea = True
    params.minArea = 200
    params.maxArea = 1200
    params.filterByColor = True
    params.blobColor = 0
    params.filterByCircularity = True
    params.minCircularity = 0.75
    params.filterByConvexity = True
    params.minConvexity = 0.6
    params.filterByInertia = True
    params.minInertiaRatio = 0.4

    detector = cv2.SimpleBlobDetector_create(params)
    keypoints = detector.detect(gray_masked)

    if not keypoints:
        return None, None

    points = np.array([kp.pt for kp in keypoints])
    db = DBSCAN(eps=60, min_samples=1).fit(points)

    clusters = {}
    for pt, lbl in zip(points, db.labels_):
        clusters.setdefault(lbl, []).append(pt)

    return keypoints, clusters

# =========================
# ORDERED VALIDATION
# =========================
def validate_and_order_dice(clusters):
    """
    Orders dice by vertical position:
    lowest (largest Y) -> first
    """

    dice_info = []

    for pts in clusters.values():
        pts = np.array(pts)
        center_y = pts[:, 1].mean()
        dice_info.append((center_y, len(pts)))

    # Sort: lowest first
    dice_info.sort(key=lambda x: x[0], reverse=True)

    dice_values = [dots for _, dots in dice_info]

    valid = (
        len(dice_values) == 3 and
        all(d <= 6 for d in dice_values) and
        sum(dice_values) <= 18
    )

    return valid, dice_values

def draw_output(image, keypoints, clusters, roi):
    out = image.copy()

    dice_info = []
    for pts in clusters.values():
        pts = np.array(pts)
        center = pts.mean(axis=0).astype(int)
        dice_info.append((center[1], center, len(pts)))

    # Sort lowest -> highest
    dice_info.sort(key=lambda x: x[0], reverse=True)

    for _, center, dots in dice_info:
        cv2.putText(
            out,
            str(dots),
            tuple(center),
            cv2.FONT_HERSHEY_SIMPLEX,
            1.5,
            (0, 255, 0),
            3
        )

    out = cv2.drawKeypoints(
        out,
        keypoints,
        None,
        (0, 0, 255),
        cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
    )

    x1, y1, x2, y2 = roi
    cv2.rectangle(out, (x1, y1), (x2, y2), (255, 0, 0), 2)
    return out

def flipp():
    timer = 0.0004
    stepps = 380

    GPIO.output(DIR_PIN, GPIO.LOW)
    for _ in range(190):
        GPIO.output(STP_PIN, GPIO.HIGH)
        GPIO.output(STP_PIN, GPIO.LOW)
        time.sleep(timer)

    GPIO.output(DIR_PIN, GPIO.HIGH)
    for _ in range(stepps):
        GPIO.output(STP_PIN, GPIO.HIGH)
        GPIO.output(STP_PIN, GPIO.LOW)
        time.sleep(timer)

    GPIO.output(DIR_PIN, GPIO.LOW)
    for _ in range(stepps):
        GPIO.output(STP_PIN, GPIO.HIGH)
        GPIO.output(STP_PIN, GPIO.LOW)
        time.sleep(timer)

    GPIO.output(DIR_PIN, GPIO.HIGH)
    for _ in range(stepps):
        GPIO.output(STP_PIN, GPIO.HIGH)
        GPIO.output(STP_PIN, GPIO.LOW)
        time.sleep(timer)

    GPIO.output(DIR_PIN, GPIO.LOW)
    for _ in range(190):
        GPIO.output(STP_PIN, GPIO.HIGH)
        GPIO.output(STP_PIN, GPIO.LOW)
        time.sleep(0.005)

# =========================
# Save dice results
# =========================
def save_dice_results(dice_values, is_valid,
                      filepath="/home/warlock/results.txt"):
    os.makedirs(os.path.dirname(filepath), exist_ok=True)

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    status = "VALID" if is_valid else "INVALID"
    result_str = " ".join(str(v) for v in dice_values)

    with open(filepath, "a") as f:
        f.write(f"{timestamp} | {status} | {result_str}\n")

# =========================
# MAIN LOOP
# =========================
try:
    while True:
        image, gray = capture_and_preprocess()
        gray_masked, roi = apply_square_roi(gray)

        keypoints, clusters = detect_and_cluster(gray_masked)

        if clusters:
            is_valid, dice_values = validate_and_order_dice(clusters)
            output = draw_output(image, keypoints, clusters, roi)

            print("Dice:", dice_values, "VALID" if is_valid else "INVALID")
            save_dice_results(dice_values, is_valid)
        else:
            output = image

        cv2.imshow("Dice Detection", output)

        key = cv2.waitKey(1000) & 0xFF
        if key == ord('q') or key == 27:
            break

        flipp()
        time.sleep(0.5)

finally:
    cv2.destroyAllWindows()
    picam2.stop()
    picam2.close()
    GPIO.cleanup()

Credits

Bitroller
4 projects • 1 follower

Comments