前言
本文是博主基于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)
大会时间:2026年2月6-8日
大会地点:中国天津·天津艺龙酒店

一、核心原理
本工具分为「棋盘检测」和「棋子识别」两大核心模块,整体逻辑如下:
- 棋盘检测:通过边缘检测 + 轮廓分析找到棋盘外框→排序四角坐标→插值生成 3×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)

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

- 关键技巧 :
- 霍夫圆变换:
cv2.HoughCircles的param2是核心参数,值越小检测到的圆越多,值越大越精准; - 棋盘内判断:计算圆中心与网格中心点的距离,≤半径 + 5 视为在棋盘上;
- 颜色判断:通过棋子 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. 圆形目标检测的核心技巧
- 预处理:高斯模糊是霍夫圆变换的关键前置步骤,可大幅提升圆检测精度;
- 参数调优 :
param2是霍夫圆检测的核心阈值,需根据实际场景微调; - 后筛选:通过半径、位置等条件过滤无效圆,避免误检测。
3. 进阶扩展方向
- 棋盘角度校正:通过透视变换将倾斜棋盘校正为正矩形,提升网格均匀性;
- 棋子跟踪:为每个棋子分配唯一 ID,跟踪其移动轨迹;
- 落子判断:结合棋盘网格,自动判断棋子落在哪个网格位置;
- 游戏规则校验:基于检测结果,实现井字棋的胜负判断;
- 多棋盘支持:扩展代码,支持同时检测多个棋盘;
- 非圆形棋子识别:通过形状匹配 / 模板匹配识别方形、星形等异形棋子。
七、学习收获
通过本案例的学习,可掌握 OpenCV 在 "结构化目标检测 + 圆形目标识别" 的核心技巧:
- 轮廓拟合与坐标排序在矩形目标定位中的应用;
- 双线性插值实现结构化网格生成的方法;
- 霍夫圆变换检测圆形目标的参数调优思路;
- 基于像素亮度的颜色分类方法;
- 多帧平滑在提升检测稳定性中的应用。
该案例的思路可迁移到象棋棋盘检测、围棋棋盘检测、桌面按钮识别等场景,是 OpenCV 从 "单一目标检测" 到 "复杂场景分析" 的重要学习内容。