Bootstrap

2024电赛E题感悟与详解(树莓派opencv视觉部分)

2024电赛E题

1.题目分析

解析

题目如下,一共有六道题,所以一定要在最短的时间内完成对题目的分析,关于整道题,我觉得最麻烦的应该是第三题,因为只要仔细观察大家就会发现,其他五道题棋盘位置不发生变化,所以每个棋格所对应的序列号并不发生变化,但是第三题棋盘被旋转,视觉识别的过程中并不能保证棋格序列号的准确性,所以这一题比较麻烦,其他的题目主要确定好棋子的位置以及棋格的位置即可,后面几道题我笼统的称为人机对弈,只要逻辑代码没有问题,应该都能做完(主要考虑仪器的精度问题)

2.视觉部分

一.代码规划

完成一道题目的代码,首要的是要知道要做什么,通过分析题目,我们可以分析出,我们要实现三子棋的人机对弈,必须要知道九个棋格中点坐标的位置,棋格中有没有棋子以及棋子的颜色,而且要按照从左到右,从上到下的顺序发送关于每个棋格的数据,做好数据包的封装以及树莓派的开机自启动,那么这道题基本上算是完成了

二.解析代码

棋盘识别

棋盘识别我定义了以下三个函数,order_points可以理解为识别矩形,因为我是通过识别最大矩形,然后用最大矩形的数据计算出每个棋格的中点坐标(可以更加准确,无误差),calculate_area的作用相当于滤波,晒出面积过小的矩形,最重要的就是compute_grid_centers,这个函数就是我所说的通过最大矩形的数据计算每个棋格数据的代码,其中的算法大家可以自行理解也可以自己写一个(不难)

def order_points(pts):
    rect = np.zeros((4, 2), dtype="float32")
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]  # 左上角
    rect[2] = pts[np.argmax(s)]  # 右下角
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]  # 右上角
    rect[3] = pts[np.argmax(diff)]  # 左下角
    return rect

def calculate_area(tl, tr, br, bl):
    width = np.linalg.norm(tr - tl)
    height = np.linalg.norm(bl - tl)
    area = width * height / 729  # 面积单位为平方厘米
    return area


def compute_grid_centers(pts):
    rect = order_points(pts)
    (tl, tr, br, bl) = rect
    width = 300
    height = 300
    dst_rect = np.array([
        [0, 0],
        [width, 0],
        [width, height],
        [0, height]
    ], dtype="float32")

    if np.linalg.norm(tl - tr) > 0 and np.linalg.norm(tr - br) > 0 and np.linalg.norm(br - bl) > 0 and np.linalg.norm(bl - tl) > 0:
        M = cv2.getPerspectiveTransform(rect, dst_rect)
        grid_centers = []

        for i in range(3):
            for j in range(3):
                cx = int((j + 0.5) * (width / 3))
                cy = int((i + 0.5) * (height / 3))
                grid_centers.append((cx, cy))

        inv_M = np.linalg.inv(M)
        original_centers = []

        for (cx, cy) in grid_centers:
            original_pt = np.array([cx, cy, 1], dtype="float32").reshape(-1, 1)
            original_pt = np.dot(inv_M, original_pt)
            original_pt = original_pt / original_pt[2]
            original_centers.append((int(original_pt[0]), int(original_pt[1])))

        return original_centers
    else:
        raise ValueError("输入的顶点不能形成有效的四边形。")
棋子识别

主要是判断棋格中点的颜色,以确定是否有棋子

def get_color_category(image, centers):
    categories = []
    for (cx, cy) in centers:
        color = image[cy, cx]
        # 计算灰度值
        gray = int(0.299 * color[2] + 0.587 * color[1] + 0.114 * color[0])
        if gray > 200:  # 白色
            categories.append(0)
        elif gray < 50:  # 黑色
            categories.append(1)
        else:  # 其他颜色
            categories.append(2)
    return categories

 数据传输

数据包封装中设置了帧头帧尾以确保数据的准确性,以字节的形式发送数据

数据包以AA BB 为始,CC DD 为终

def send_data_to_mcu(centers, categories):
    data = bytearray()
    frame_head = b'\xAA\xBB'
    frame_tail = b'\xCC\xDD'
    data.extend(frame_head)
    sequence_number = 1

    for (x, y), category in zip(centers, categories):
        data.extend(struct.pack('>B', sequence_number))
        data.extend(struct.pack('>H', x))
        data.extend(struct.pack('>H', y))
        data.extend(struct.pack('>B', category))
        sequence_number += 1

    data.extend(frame_tail)
    ser.write(data)
    print(f"发送数据: {data}")

三.效果展示  

此处底色为白色,后续换成蓝色以便于白棋识别

 

 

3.视觉代码(完整)

完整代码如下,需要连接摄像头并且打开串口才可以使用,不然会报错,串口大家直接搜树莓派4b原理图找到对应串口的对应引脚就好了

import cv2
import numpy as np
import serial
import struct

# 打开摄像头
cap = cv2.VideoCapture(0)
if not cap.isOpened():
    raise IOError("无法打开摄像头。")

# 配置串口
try:
    ser = serial.Serial('/dev/ttyAMA0', 115200, timeout=1)  # 根据实际情况修改串口号和波特率
    print("串口成功打开。")
except Exception as e:
    print(f"打开串口失败: {e}")
    exit()

def order_points(pts):
    rect = np.zeros((4, 2), dtype="float32")
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]  # 左上角
    rect[2] = pts[np.argmax(s)]  # 右下角
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]  # 右上角
    rect[3] = pts[np.argmax(diff)]  # 左下角
    return rect

def calculate_area(tl, tr, br, bl):
    width = np.linalg.norm(tr - tl)
    height = np.linalg.norm(bl - tl)
    area = width * height / 729  # 面积单位为平方厘米
    return area

def compute_grid_centers(pts):
    rect = order_points(pts)
    (tl, tr, br, bl) = rect
    width = 300
    height = 300
    dst_rect = np.array([
        [0, 0],
        [width, 0],
        [width, height],
        [0, height]
    ], dtype="float32")

    if np.linalg.norm(tl - tr) > 0 and np.linalg.norm(tr - br) > 0 and np.linalg.norm(br - bl) > 0 and np.linalg.norm(bl - tl) > 0:
        M = cv2.getPerspectiveTransform(rect, dst_rect)
        grid_centers = []

        for i in range(3):
            for j in range(3):
                cx = int((j + 0.5) * (width / 3))
                cy = int((i + 0.5) * (height / 3))
                grid_centers.append((cx, cy))

        inv_M = np.linalg.inv(M)
        original_centers = []

        for (cx, cy) in grid_centers:
            original_pt = np.array([cx, cy, 1], dtype="float32").reshape(-1, 1)
            original_pt = np.dot(inv_M, original_pt)
            original_pt = original_pt / original_pt[2]
            original_centers.append((int(original_pt[0]), int(original_pt[1])))

        return original_centers
    else:
        raise ValueError("输入的顶点不能形成有效的四边形。")

def get_color_category(image, centers):
    categories = []
    for (cx, cy) in centers:
        color = image[cy, cx]
        # 计算灰度值
        gray = int(0.299 * color[2] + 0.587 * color[1] + 0.114 * color[0])
        if gray > 200:  # 白色
            categories.append(0)
        elif gray < 50:  # 黑色
            categories.append(1)
        else:  # 其他颜色
            categories.append(2)
    return categories

def send_data_to_mcu(centers, categories):
    data = bytearray()
    frame_head = b'\xAA\xBB'
    frame_tail = b'\xCC\xDD'
    data.extend(frame_head)
    sequence_number = 1

    for (x, y), category in zip(centers, categories):
        data.extend(struct.pack('>B', sequence_number))
        data.extend(struct.pack('>H', x))
        data.extend(struct.pack('>H', y))
        data.extend(struct.pack('>B', category))
        sequence_number += 1

    data.extend(frame_tail)
    ser.write(data)
    print(f"发送数据: {data}")

while True:
    ret, frame = cap.read()
    if not ret:
        print("读取摄像头帧失败。")
        break

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    edges = cv2.Canny(blurred, 50, 150)
    contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    largest_area = 0
    largest_contour = None

    for cnt in contours:
        epsilon = 0.02 * cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, epsilon, True)

        if len(approx) == 4:
            approx = np.squeeze(approx)
            rect = order_points(approx)
            (tl, tr, br, bl) = rect
            area = calculate_area(tl, tr, br, bl)
            print(f"检测到的矩形面积: {area} 平方厘米")
            if area > 50:  # 实际面积大于50平方厘米
                largest_area = area
                largest_contour = approx

    if largest_contour is not None:
        ordered_points = order_points(largest_contour)
        try:
            grid_centers = compute_grid_centers(ordered_points)

            print("九个矩形框的中心点坐标(按行列顺序排序):")
            for i, (cx, cy) in enumerate(grid_centers):
                print(f"矩形 {i + 1}: ({int(cx)}, {int(cy)})")

            categories = get_color_category(frame, grid_centers)
            print("对应颜色类别(按行列顺序排序):")
            for i, category in enumerate(categories):
                print(f"矩形 {i + 1}: 类别 {category}")

            for (cx, cy) in grid_centers:
                cv2.circle(frame, (int(cx), int(cy)), 5, (0, 255, 0), -1)
                cv2.putText(frame, f"({int(cx)},{int(cy)})", (int(cx) - 20, int(cy) - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA)

            send_data_to_mcu(grid_centers, categories)
        except ValueError as ve:
            print(ve)

    cv2.imshow('Detected Chessboard Grid', frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()
ser.close()

4.。。。。。。。

如果有不懂的可以关注私信我,也可以留言,看到会回的,谢谢大家

;