Bitroller
Published © CC BY

Entropy Dice, an automated dice testing machine

This is a dice testing machine(and random number generator) that uses Raspberry Pi, stepper motors and OpenCV for image recognition.

BeginnerShowcase (no instructions)16 hours55
Entropy Dice, an automated dice testing machine

Things used in this project

Hardware components

Raspberry Pi 5
Raspberry Pi 5
×1
Stepper Motor, Bipolar
Stepper Motor, Bipolar
×1
Camera Module V2
Raspberry Pi Camera Module V2
×1
Driver DRV8825 for Stepper Motors for Theremino System
Driver DRV8825 for Stepper Motors for Theremino System
×1

Software apps and online services

Raspbian
Raspberry Pi Raspbian

Hand tools and fabrication machines

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

Story

Read more

Code

Dice tester

Python
Python program for dice tester, also creats output for webserver output.
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
import matplotlib.pyplot as plt


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




# =========================
# Histogram
# =========================
def histogram():
    filename = "/var/www/html/results.txt"

    # Count dice values 1-6
    counts = {i: 0 for i in range(1, 7)}
    total_dice = 0

    with open(filename, "r") as f:
        for line in f:
            tokens = line.split()
            dice_in_line = 0

            for token in tokens:
                if token.isdigit():
                    value = int(token)
                    if 1 <= value <= 6:
                        counts[value] += 1
                        dice_in_line += 1

            if dice_in_line > 0:
                total_dice += dice_in_line

    values = list(counts.keys())
    frequencies = list(counts.values())

    # Plot histogram
    plt.figure(figsize=(10, 6))
    plt.bar(values, frequencies)
    plt.xlabel("Dice value")
    plt.ylabel("Frequency")
    plt.title(f"Dice Histogram (Total dice: {total_dice})")
    plt.xticks(values)
    plt.grid(axis="y", alpha=0.3)

    # Save as JPG
    output_file = "/var/www/html/dice_histogram.jpg"
    plt.savefig(output_file, dpi=200, bbox_inches="tight")

    # Show the plot
    #plt.show()

    print("Histogram saved as:", output_file)
    print("Total number of dice:", total_dice)
    plt.close()



# =========================
# Save image
# =========================

def save_image_with_status(image, dice_values, is_valid,
                           output_dir="images", quality=85,
                           reason=None):
    import os
    from datetime import datetime
    import cv2

    if image is None:
        print("Warning: no image to save")
        return

    os.makedirs(output_dir, exist_ok=True)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    status_text = "VALID" if is_valid else "INVALID"

    filename = f"{status_text.lower()}_{timestamp}.jpg"
    filepath = os.path.join(output_dir, filename)

    # Build result text
    if dice_values:
        result_text = "Result: " + " ".join(str(v) for v in dice_values)
    else:
        result_text = "Result: none"

    color = (0, 255, 0) if is_valid else (0, 0, 255)

    annotated = image.copy()

    cv2.putText(annotated, status_text, (20, 40),
                cv2.FONT_HERSHEY_SIMPLEX, 1.3, color, 3)

    cv2.putText(annotated, result_text, (20, 85),
                cv2.FONT_HERSHEY_SIMPLEX, 1.0, color, 2)

    if reason:
        cv2.putText(annotated, reason, (20, 130),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.9, color, 2)

    # ---- Save timestamped image ----
    cv2.imwrite(
        filepath,
        annotated,
        [cv2.IMWRITE_JPEG_QUALITY, quality]
    )

    # ---- Save latest image (overwrite every time) ----
    #latest_path = "/home/warlock/imgdice.jpg"
    latest_path = "/var/www/html/imgdice.jpg"
    cv2.imwrite(
        latest_path,
        annotated,
        [cv2.IMWRITE_JPEG_QUALITY, quality]
    )

    print("Saved image:", filepath)
    print("Updated latest image:", latest_path)

# =========================
# Validation
# =========================


def validate_dice(clusters):
    """
    clusters: dict where each value is a list of points for one die
    returns: (is_valid, dice_values)
    """
    # Number of dice
    num_dice = len(clusters)

    # Dots per die
    dice_values = [len(pts) for pts in clusters.values()]

    # Total dots
    total_dots = sum(dice_values)

    valid = True

    if num_dice != 3:
        print("Invalid: number of dice is", num_dice)
        valid = False

    if any(d > 6 for d in dice_values):
        print("Invalid: a die has more than 6 dots")
        valid = False

    if total_dots > 18:
        print("Invalid: total dots exceed 18")
        valid = False

    return valid, dice_values


# =========================
# Log results
# =========================


def save_results(dice_values, filename="/var/www/html/results.txt"):
    """
    dice_values: list of integers, e.g. [3, 4, 1]
    filename: output file name
    """
    with open(filename, "a") as f:
        line = " ".join(str(v) for v in dice_values)
        f.write(line + "\n")

# =========================
# Stepper function
# =========================
def flipp():
        
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(STP_PIN, GPIO.OUT)
    GPIO.setup(EN_PIN, GPIO.OUT)
    GPIO.setup(SLP_PIN, GPIO.OUT)
    GPIO.setup(RST_PIN, GPIO.OUT)
    GPIO.setup(DIR_PIN, GPIO.OUT)

    GPIO.output(EN_PIN, GPIO.LOW)
    GPIO.output(SLP_PIN, GPIO.HIGH)
    GPIO.output(RST_PIN, GPIO.HIGH)
    GPIO.output(DIR_PIN, GPIO.HIGH)
    for _ in range(200):
        GPIO.output(STP_PIN, GPIO.HIGH)
        GPIO.output(STP_PIN, GPIO.LOW)
        time.sleep(0.008)


# =========================
# Resize with aspect ratio
# =========================
def resize_with_aspect_ratio(image, max_width, max_height):
    h, w = image.shape[:2]
    scale = min(max_width / w, max_height / h)
    new_w = int(w * scale)
    new_h = int(h * scale)
    return cv2.resize(image, (new_w, new_h))

x = 0

while x < 1:
    # =========================
    # Camera (Camera 2)
    # =========================
    picam2 = Picamera2()
    config = picam2.create_still_configuration()
    picam2.configure(config)

    picam2.start()
    time.sleep(1.0)

    image = picam2.capture_array()

    picam2.stop()
    picam2.close()

    # =========================
    # Image processing
    # =========================
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    image = resize_with_aspect_ratio(image, 800, 600)

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (7, 7), 0)

    # =========================
    # Circular ROI mask
    # =========================
    h, w = gray.shape
    center_x = w // 2 -60
    center_y = h // 2
    radius = int(min(w, h) * 0.27)

    mask = np.zeros((h, w), dtype=np.uint8)
    cv2.circle(mask, (center_x, center_y), radius, 255, -1)

    gray_masked = cv2.bitwise_and(gray, gray, mask=mask)

    # =========================
    # Blob detector parameters
    # =========================
    params = cv2.SimpleBlobDetector_Params()

    params.minThreshold = 5
    params.maxThreshold = 200
    params.thresholdStep = 5

    params.filterByArea = True
    params.minArea = 200
    params.maxArea = 1200

    params.filterByColor = True
    params.blobColor = 0

    params.filterByCircularity = True
    params.minCircularity = 0.75
    params.maxCircularity = 1.0

    params.filterByConvexity = True
    params.minConvexity = 0.6

    params.filterByInertia = True
    params.minInertiaRatio = 0.4

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

    print("Total detected dots:", len(keypoints))

    # =========================
    # DBSCAN clustering
    # =========================
    if len(keypoints) == 0:
        print("No dots detected")
        GPIO.cleanup()
        exit()
        
    # Convert keypoints to array
    points = np.array([kp.pt for kp in keypoints])

    # DBSCAN: distance ~ dot spacing on a die
    db = DBSCAN(
        eps=60,          # adjust if dice size changes
        min_samples=1
    ).fit(points)

    labels = db.labels_

    # Group points by cluster
    clusters = {}
    for point, label in zip(points, labels):
        clusters.setdefault(label, []).append(point)

    num_dice = len(clusters)

    print("Detected dice:", num_dice)
    dice_values = [len(pts) for pts in clusters.values()]
    dice_values = sorted(
        [len(pts) for pts in clusters.values()]
    )

    # =========================
    # Draw result
    # =========================
    output = image.copy()

    for i, (label, pts) in enumerate(clusters.items(), 1):
        pts = np.array(pts)
        center = pts.mean(axis=0).astype(int)
        dots = len(pts)

        cv2.putText(
            output,
            f"{dots}",
            tuple(center),
            cv2.FONT_HERSHEY_SIMPLEX,
            1.5,
            (0, 255, 0),
            3
        )

    # Draw dots
    output = cv2.drawKeypoints(
        output,
        keypoints,
        None,
        (0, 0, 255),
        cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
    )

    is_valid, dice_values = validate_dice(clusters)



    # Draw ROI
    cv2.circle(output, (center_x, center_y), radius, (255, 0, 0), 2)

    #cv2.imshow("Dice Detection with DBSCAN", output)

    if is_valid:
        print("Valid dice:", dice_values)
        save_results(dice_values)
        save_image_with_status(output, dice_values, is_valid=True)
    else:
        print("Dice result rejected")
        save_image_with_status(output, dice_values, is_valid=False)
    #cv2.waitKey(1000)
    cv2.destroyAllWindows()
    flipp()
    histogram()
    GPIO.cleanup()

Webpage for results

HTML
This webpage shows the results of the tests
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dice Images</title>

    <meta http-equiv="refresh" content="2">

    <style>
        body {
            background-color: #111;
            color: #fff;
            font-family: Arial, sans-serif;
            text-align: center;
        }

        .container {
            display: flex;
            justify-content: center;
            gap: 40px;
            margin-top: 40px;
        }

        img {
            max-width: 45%;
            border: 3px solid #444;
            border-radius: 8px;
        }

        a {
            color: #4da6ff;
            text-decoration: none;
            font-size: 1.1em;
        }

        a:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>

    <h1>Dice Testing Unit</h1>

    <div class="container">
        <img id="dice" src="imgdice.jpg" alt="Latest dice image">
        <img id="hist" src="dice_histogram.jpg" alt="Statistics">
    </div>

    <p style="margin-top: 30px;">
        <a href="results.txt" target="_blank">View results.txt</a>
    </p>

    <script>
        const now = Date.now();
        document.getElementById("dice").src = "imgdice.jpg?t=" + now;
        document.getElementById("hist").src = "dice_histogram.jpg?t=" + now;
    </script>

</body>
</html>

Credits

Bitroller
3 projects • 1 follower

Comments