OpenCV摄像头实时处理:九宫格棋盘检测与棋子识别

前言

本文是博主基于24年电赛e题所写。

棋盘检测与棋子识别是计算机视觉在桌面游戏场景的典型应用,本文以「九宫格棋盘(井字棋 / 三子棋)」为例,讲解如何通过轮廓分析定位棋盘、插值生成网格、霍夫变换识别棋子,并通过多帧平滑优化检测稳定性,适合初学者理解 "结构化目标检测 + 圆形目标识别" 的组合应用思路。

目录

前言

一、核心原理

关键概念解析

二、环境准备

三、核心代码实现

四、核心模块解析

[1. 棋盘检测模块(find_qipan)](#1. 棋盘检测模块(find_qipan))

[2. 棋子检测模块(circle_detect)](#2. 棋子检测模块(circle_detect))

[3. 多帧平滑模块](#3. 多帧平滑模块)

五、调试技巧

[1. 关键参数调优](#1. 关键参数调优)

[2. 常见问题与解决方案](#2. 常见问题与解决方案)

六、核心知识点总结

[1. 结构化目标检测的通用思路](#1. 结构化目标检测的通用思路)

[2. 圆形目标检测的核心技巧](#2. 圆形目标检测的核心技巧)

[3. 进阶扩展方向](#3. 进阶扩展方向)

七、学习收获


论文投稿:
第二届仪器仪表与导航控制国际学术研讨会(ISINC 2026)

大会官网:https://ais.cn/u/IjA3Mb

大会时间:2026年2月6-8日

大会地点:中国天津·天津艺龙酒店

一、核心原理

本工具分为「棋盘检测」和「棋子识别」两大核心模块,整体逻辑如下:

  1. 棋盘检测:通过边缘检测 + 轮廓分析找到棋盘外框→排序四角坐标→插值生成 3×3 网格中心点;
  2. 棋子识别:通过霍夫圆变换检测圆形棋子→判断棋子是否在棋盘网格上→区分棋盘内外棋子并标注;
  3. 稳定性优化:多帧缓存棋盘角点 / 中心点坐标,通过均值滤波降低抖动。

关键概念解析

概念 作用
轮廓近似 将不规则棋盘外框轮廓拟合为四边形,精准定位棋盘四角
坐标排序 按 "左上、右上、右下、左下" 排序棋盘角点,为网格插值提供基准
线性插值 基于四角坐标生成 3×3 网格的交点和中心点,实现棋盘网格的结构化划分
霍夫圆变换 检测圆形棋子,通过半径、位置筛选有效棋子
多帧平滑 缓存多帧棋盘坐标,通过均值滤波降低检测抖动,提升稳定性

二、环境准备

仅需 OpenCV 和 NumPy 两个核心库,执行以下命令安装:

bash 复制代码
pip install opencv-python numpy

三、核心代码实现

以下是带详细注释的完整代码,保留核心逻辑的同时,补充注释拆解每个模块的作用,并修复原代码中的小问题(如中心点数据类型、霍夫圆处理):

python 复制代码
import numpy as np
import cv2

class ChessBoardDetector:
    def __init__(self):
        """初始化棋盘检测器:缓存历史坐标用于多帧平滑"""
        self.prev_corners = None   # 棋盘四角历史坐标
        self.prev_centers = None   # 网格中心点历史坐标

    def find_qipan(self, frame):
        """
        检测九宫格棋盘的四角坐标和3×3网格中心点
        :param frame: BGR格式原始帧
        :return: 棋盘四角坐标(list)、3×3网格中心点(list),无则返回None
        """
        # ------------- 1. 参数配置 -------------
        find_center_method = 1  # 中心点计算方法(仅实现方法1)
        area_threshold = 80     # 轮廓最小面积阈值(过滤小噪声)
        pixels_threshold = 50   # 像素差阈值(未使用,保留)

        # ------------- 2. 图像预处理 -------------
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 灰度化
        edged = cv2.Canny(gray, 50, 150)                # Canny边缘检测
        # 膨胀操作:连接断裂的棋盘边缘
        kernel = np.ones((5, 5), np.uint8)
        dilated = cv2.dilate(edged, kernel, iterations=1)

        # ------------- 3. 查找棋盘外轮廓 -------------
        contours, _ = cv2.findContours(dilated.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        if len(contours) == 0:
            return None, None

        # 找面积最大的轮廓(棋盘外框)
        largest_contour = max(contours, key=cv2.contourArea)
        # 轮廓近似:将不规则轮廓拟合为多边形(epsilon=周长×0.02)
        epsilon = 0.02 * cv2.arcLength(largest_contour, True)
        approx = cv2.approxPolyDP(largest_contour, epsilon, True)

        # 仅处理四边形轮廓(棋盘为矩形)
        if len(approx) != 4:
            return None, None

        # 提取四角原始坐标
        corners = approx.reshape((4, 2))
        print("棋盘四角坐标:")
        for corner in corners:
            print(corner)

        # 绘制四角标记(绿色实心圆)
        for corner in corners:
            cv2.circle(frame, tuple(corner.astype(int)), 4, (0, 255, 0), -1)

        # ------------- 4. 棋盘四角坐标排序 -------------
        # 目标顺序:左上、右上、右下、左下
        rect = np.zeros((4, 2), dtype="float32")
        # 求和最小=左上,求和最大=右下
        s = corners.sum(axis=1)
        rect[0] = corners[np.argmin(s)]  # 左上
        rect[2] = corners[np.argmax(s)]  # 右下
        # 差分最小=右上,差分最大=左下(差分=y-x)
        diff = np.diff(corners, axis=1)
        rect[1] = corners[np.argmin(diff)]  # 右上
        rect[3] = corners[np.argmax(diff)]  # 左下
        rect = rect.astype("int")  # 转为整数坐标

        # ------------- 5. 生成3×3网格交点 -------------
        cross_points = []  # 4×4网格交点(3×3棋盘需要4×4交点)
        for i in range(4):  # 列方向插值(0-3)
            for j in range(4):  # 行方向插值(0-3)
                # 双线性插值计算交点坐标
                cross_x = int((rect[0][0] * (3 - i) + rect[1][0] * i) * (3 - j) / 9 +
                              (rect[3][0] * (3 - i) + rect[2][0] * i) * j / 9)
                cross_y = int((rect[0][1] * (3 - i) + rect[1][1] * i) * (3 - j) / 9 +
                              (rect[3][1] * (3 - i) + rect[2][1] * i) * j / 9)
                cross_points.append((cross_x, cross_y))

        # ------------- 6. 计算3×3网格中心点 -------------
        centers = []
        if find_center_method == 1:
            # 方法1:每个网格由4个交点围成,取4点均值为中心点
            for i in range(3):  # 网格行
                for j in range(3):  # 网格列
                    # 提取当前网格的4个交点坐标
                    p1 = cross_points[i * 4 + j]
                    p2 = cross_points[i * 4 + j + 1]
                    p3 = cross_points[(i + 1) * 4 + j]
                    p4 = cross_points[(i + 1) * 4 + j + 1]
                    # 计算4点均值
                    center_x = int((p1[0] + p2[0] + p3[0] + p4[0]) / 4)
                    center_y = int((p1[1] + p2[1] + p3[1] + p4[1]) / 4)
                    centers.append((center_x, center_y))

        # ------------- 7. 多帧平滑(降低坐标抖动) -------------
        if self.prev_corners is not None and self.prev_centers is not None:
            # 历史坐标与当前坐标取均值
            corners = (corners + self.prev_corners) / 2
            centers = (np.array(centers) + np.array(self.prev_centers)) / 2

        # 更新历史坐标缓存
        self.prev_corners = corners
        self.prev_centers = centers
        # 转为整数坐标返回
        corners = [(int(c[0]), int(c[1])) for c in corners]
        centers = [(int(c[0]), int(c[1])) for c in centers] if centers else None

        return corners, centers

    def circle_detect(self, frame, corners, centers):
        """
        检测圆形棋子,区分棋盘内/外棋子,并标注编号和颜色
        :param frame: BGR格式原始帧
        :param corners: 棋盘四角坐标
        :param centers: 3×3网格中心点
        :return: 绘制后的帧、棋盘内棋子列表、棋盘外黑棋列表、棋盘外白棋列表
        """
        cimg = frame.copy()  # 用于绘制结果的副本
        img_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 灰度化
        img_gray = cv2.GaussianBlur(img_gray, (5, 5), 0)    # 高斯模糊(降噪,提升圆检测精度)

        # ------------- 1. 霍夫圆变换检测圆形 -------------
        circles = cv2.HoughCircles(
            img_gray,                    # 输入灰度图
            cv2.HOUGH_GRADIENT,          # 检测方法
            dp=1,                        # 累加器分辨率(与图像分辨率相同)
            minDist=20,                  # 圆中心最小距离(避免重复检测)
            param1=50,                   # Canny边缘检测高阈值
            param2=30,                   # 累加器阈值(值越小检测越多)
            minRadius=10,                # 最小圆半径
            maxRadius=30                 # 最大圆半径
        )

        # 初始化棋子列表
        circles_on_board = []          # 棋盘内棋子
        circles_off_board_black = []   # 棋盘外黑棋
        circles_off_board_white = []   # 棋盘外白棋

        if circles is not None:
            # 格式化圆坐标(取整、降维)
            circles = np.uint16(np.around(circles))
            circles = np.squeeze(circles)
            # 确保circles是二维数组(单圆时会变为一维,需处理)
            if len(circles.shape) == 1:
                circles = np.expand_dims(circles, axis=0)

            # ------------- 2. 筛选有效棋子(半径范围) -------------
            valid_circles = [circle for circle in circles if 10 < circle[2] < 50]
            # 按位置排序(从上到下,从左到右)
            valid_circles.sort(key=lambda x: (x[1], x[0]))

            # 棋盘内棋子编号映射(蛇形排列)
            new_index = [0, 7, 6, 1, 8, 5, 2, 3, 4]
            # 棋盘外棋子计数器
            off_board_black_counter = 1
            off_board_white_counter = 1

            # ------------- 3. 逐圆分析(棋盘内/外 + 颜色判断) -------------
            for circle in valid_circles:
                x, y, r = circle[0], circle[1], circle[2]

                # 判断是否在棋盘网格上(与网格中心点距离≤半径+5)
                on_board = False
                closest_center_idx = -1
                if centers:
                    # 计算与所有网格中心点的距离
                    distances = [np.linalg.norm(np.array(center) - np.array([x, y])) for center in centers]
                    min_dist = min(distances) if distances else float('inf')
                    if min_dist <= r + 5:
                        on_board = True
                        closest_center_idx = np.argmin(distances)

                # ------------- 4. 绘制棋盘内棋子 -------------
                if on_board:
                    # 计算文字标注位置(避免超出画面)
                    text_offset = 15
                    text_x = x + r + text_offset if x + r + text_offset < frame.shape[1] else x - r - text_offset
                    text_y = y + text_offset if y + text_offset < frame.shape[0] else y - text_offset
                    # 棋盘内棋子编号(蛇形映射)
                    grid_num = new_index[closest_center_idx] + 1

                    # 绘制圆轮廓和圆心
                    cv2.circle(cimg, (x, y), r, (255, 0, 0), 2)
                    cv2.circle(cimg, (x, y), 2, (255, 0, 0), 3)
                    # 标注棋子编号
                    cv2.putText(cimg, f"#{grid_num}", (text_x - 20, text_y), 
                                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
                    circles_on_board.append((x, y, r, grid_num))

                # ------------- 5. 绘制棋盘外棋子(区分黑白) -------------
                else:
                    # 计算文字标注位置
                    text_offset = 15
                    text_x = x + r + text_offset if x + r + text_offset < frame.shape[1] else x - r - text_offset
                    text_y = y + text_offset if y + text_offset < frame.shape[0] else y - text_offset

                    # 绘制圆轮廓和圆心
                    cv2.circle(cimg, (x, y), r, (0, 255, 0), 2)
                    cv2.circle(cimg, (x, y), 2, (0, 255, 0), 3)

                    # 提取棋子ROI,通过平均亮度判断颜色(白棋亮,黑棋暗)
                    # 防止ROI越界
                    y1 = max(0, y - r)
                    y2 = min(frame.shape[0], y + r)
                    x1 = max(0, x - r)
                    x2 = min(frame.shape[1], x + r)
                    roi = frame[y1:y2, x1:x2]
                    avg_color = np.mean(roi)  # 平均亮度(0=黑,255=白)

                    # 白棋(平均亮度>150)
                    if avg_color > 150:
                        cv2.putText(cimg, f"White #{off_board_white_counter} @ ({x}, {y})",
                                    (text_x - 140, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
                        circles_off_board_white.append((x, y, r))
                        off_board_white_counter += 1
                    # 黑棋(平均亮度≤150)
                    else:
                        cv2.putText(cimg, f"Black #{off_board_black_counter} @ ({x}, {y})",
                                    (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
                        circles_off_board_black.append((x, y, r))
                        off_board_black_counter += 1

        return cimg, circles_on_board, circles_off_board_black, circles_off_board_white


def main():
    """主程序:摄像头实时检测棋盘和棋子"""
    detector = ChessBoardDetector()
    # 初始化摄像头(1=外接,0=内置,根据实际调整)
    cap = cv2.VideoCapture(1)
    if not cap.isOpened():
        print("错误:无法打开摄像头!")
        return

    print("程序已启动,按 'q' 退出...")
    while True:
        ret, frame = cap.read()
        if not ret:
            print("错误:无法读取摄像头帧!")
            break

        # ------------- 图像预处理:旋转+裁剪为正方形 -------------
        # 逆时针旋转90度(适配摄像头摆放角度)
        frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)
        height, width = frame.shape[:2]
        # 裁剪为正方形(取宽高最小值)
        side_length = min(width, height)
        left = (width - side_length) // 2
        top = (height - side_length) // 2
        frame_cropped = frame[top:top+side_length, left:left+side_length]
        cv2.imshow('Original (Cropped)', frame_cropped)

        # ------------- 检测棋盘和棋子 -------------
        corners, centers = detector.find_qipan(frame_cropped)
        if corners is not None and centers is not None:
            # 检测棋子并绘制结果
            result_frame, _, _, _ = detector.circle_detect(frame_cropped, corners, centers)
            cv2.imshow('Chessboard Detection', result_frame)

        # ------------- 按键退出 -------------
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # 释放资源
    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

四、核心模块解析

1. 棋盘检测模块(find_qipan)

  • 关键技巧
    1. 轮廓近似:通过cv2.approxPolyDP将不规则轮廓拟合为四边形,精准定位棋盘外框;
    2. 坐标排序:通过 "求和 / 差分" 法快速排序四角,避免手动判断;
    3. 双线性插值:基于四角坐标生成均匀的 3×3 网格,适配任意角度的棋盘。

2. 棋子检测模块(circle_detect)

  • 关键技巧
    1. 霍夫圆变换:cv2.HoughCirclesparam2是核心参数,值越小检测到的圆越多,值越大越精准;
    2. 棋盘内判断:计算圆中心与网格中心点的距离,≤半径 + 5 视为在棋盘上;
    3. 颜色判断:通过棋子 ROI 的平均亮度区分黑白(>150 为白,≤150 为黑)。

3. 多帧平滑模块

python 复制代码
if self.prev_corners is not None and self.prev_centers is not None:
    corners = (corners + self.prev_corners) / 2
    centers = (np.array(centers) + np.array(self.prev_centers)) / 2
  • 核心逻辑:将当前帧坐标与历史帧坐标取均值,降低检测抖动;
  • 作用:避免棋盘轻微移动导致坐标大幅变化,提升检测稳定性。

五、调试技巧

1. 关键参数调优

参数 作用 调优建议
Canny 阈值(50/150) 棋盘边缘检测 棋盘边缘模糊→降低低阈值(50→30);噪声多→提高高阈值(150→200)
膨胀核大小(5×5) 连接断裂的棋盘边缘 边缘断裂严重→增大核(5×5→7×7);噪声多→减小(5×5→3×3)
霍夫圆 param2(30) 圆检测阈值 检测不到棋子→降低(30→20);误检测多→提高(30→40)
圆半径范围(10-30) 筛选棋子大小 棋子大→增大 maxRadius(30→40);棋子小→减小 minRadius(10→5)
棋盘内距离阈值(r+5) 判断棋子是否在棋盘上 棋子偏移大→增大(r+5→r+10);误判多→减小(r+5→r+3)
颜色判断阈值(150) 区分黑白棋子 光照强→提高(150→180);光照弱→降低(150→120)

2. 常见问题与解决方案

问题现象 原因分析 解决方案
检测不到棋盘 轮廓拟合非四边形 / 边缘检测不完整 调整 Canny 阈值;增大膨胀核;确保棋盘在画面中完整且边缘清晰
棋盘坐标抖动 多帧平滑未生效 / 棋盘轻微移动 增加历史缓存帧数(当前仅 2 帧,可扩展为 3-4 帧);减小摄像头抖动
检测不到棋子 霍夫圆 param2 过高 / 半径范围不对 降低 param2;调整 minRadius/maxRadius;增加高斯模糊核大小(5×5→7×7)
棋子颜色判断错误 光照不均 / ROI 越界 调整颜色阈值;增加 ROI 越界保护(如代码中 y1/y2/x1/x2);固定光照环境
棋盘网格不均匀 四角坐标排序错误 / 插值参数不对 检查坐标排序逻辑;调整插值的 i/j 范围(确保 4×4 交点)

六、核心知识点总结

1. 结构化目标检测的通用思路

本案例中,棋盘是 "结构化矩形目标",通过 "边缘→轮廓→拟合→插值" 的思路,实现从 "整体定位" 到 "内部网格划分" 的完整检测。

2. 圆形目标检测的核心技巧

  1. 预处理:高斯模糊是霍夫圆变换的关键前置步骤,可大幅提升圆检测精度;
  2. 参数调优param2是霍夫圆检测的核心阈值,需根据实际场景微调;
  3. 后筛选:通过半径、位置等条件过滤无效圆,避免误检测。

3. 进阶扩展方向

  1. 棋盘角度校正:通过透视变换将倾斜棋盘校正为正矩形,提升网格均匀性;
  2. 棋子跟踪:为每个棋子分配唯一 ID,跟踪其移动轨迹;
  3. 落子判断:结合棋盘网格,自动判断棋子落在哪个网格位置;
  4. 游戏规则校验:基于检测结果,实现井字棋的胜负判断;
  5. 多棋盘支持:扩展代码,支持同时检测多个棋盘;
  6. 非圆形棋子识别:通过形状匹配 / 模板匹配识别方形、星形等异形棋子。

七、学习收获

通过本案例的学习,可掌握 OpenCV 在 "结构化目标检测 + 圆形目标识别" 的核心技巧:

  1. 轮廓拟合与坐标排序在矩形目标定位中的应用;
  2. 双线性插值实现结构化网格生成的方法;
  3. 霍夫圆变换检测圆形目标的参数调优思路;
  4. 基于像素亮度的颜色分类方法;
  5. 多帧平滑在提升检测稳定性中的应用。

该案例的思路可迁移到象棋棋盘检测、围棋棋盘检测、桌面按钮识别等场景,是 OpenCV 从 "单一目标检测" 到 "复杂场景分析" 的重要学习内容。

相关推荐
Yff_world2 小时前
网络安全与 Web 基础笔记
前端·笔记·web安全
YangYang9YangYan2 小时前
2026高职大数据专业数据分析学习必要性
大数据·学习·数据分析
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][gpio[[gpiolib]
linux·笔记·学习
RFCEO2 小时前
学习前端编程:DOM 树、CSSOM 树、渲染树详解
前端·学习·渲染树·dom 树·cssom 树·浏览器的渲染流程·回流/重绘
孞㐑¥2 小时前
算法—哈希表
开发语言·c++·经验分享·笔记·算法
Jackyzhe2 小时前
从零学习Kafka:配置参数
分布式·学习·kafka
传说故事2 小时前
【论文阅读】Being-H0.5:规模化以人为中心的机器人学习以实现跨具身化泛化
论文阅读·学习·机器人·具身智能
Jack___Xue2 小时前
LangGraph学习笔记(四)---LangGraph检查点和Send机制
jvm·笔记·学习
今儿敲了吗2 小时前
计算机网络第四章笔记(六)
笔记·计算机网络