基于树莓派与OpenCV的智能飞镖计分系统:从机器视觉到嵌入式实践

发布时间:2026/7/28 6:29:38

基于树莓派与OpenCV的智能飞镖计分系统:从机器视觉到嵌入式实践 1. 项目概述从传统飞镖到智能计分的跨越如果你玩过飞镖或者看过飞镖比赛肯定对那个圆圆的、布满数字和分区的镖盘不陌生。传统的玩法无论是501还是米老鼠都需要一个记分员或者至少需要玩家自己停下来走到靶子前仔细辨认飞镖扎中的是单倍区、双倍区还是三倍区然后心算或者用笔记下分数。这个过程其实打断了游戏的流畅性和紧张感。尤其是在朋友聚会或者小型比赛时频繁的计分、验靶、争论常常让乐趣大打折扣。“快乐智造营 | 17 电子计分标靶”这个项目瞄准的就是这个痛点。它的核心目标是打造一个能够自动、实时、准确识别飞镖命中位置并计算得分的智能标靶系统。这不仅仅是把纸质记分板换成电子显示屏那么简单它涉及到传感器技术、信号处理、算法识别和用户体验设计的综合应用。想象一下飞镖出手命中靶盘的瞬间得分和剩余分数就立刻显示在旁边的屏幕上甚至通过语音播报出来整个过程无需人工干预游戏体验的流畅度和科技感会得到质的提升。这个项目非常适合对硬件DIY、嵌入式开发以及机器视觉入门感兴趣的朋友。它不像一些纯软件项目那样抽象你能实实在在地触摸到电路板、传感器看到飞镖击中时LED灯亮起听到蜂鸣器提示音成就感非常直接。同时它又包含了足够的挑战性如何让系统在各种角度、力度下都能稳定识别如何区分紧挨着的两个分区如何处理飞镖未扎稳掉镖的情况解决这些问题的过程正是“智造”的乐趣所在。接下来我将以一个完整项目从构思到实现的角度拆解这个电子计分标靶的核心技术、设计思路、实操步骤以及那些只有亲手做过才会知道的“坑”。2. 核心方案选型与设计思路拆解做一个电子计分标靶听起来方法很多但每种路径背后的成本、精度和复杂度天差地别。在项目启动前我们必须明确核心需求并据此选择最合适的技术路线。2.1 需求定义与方案对比首先我们要明确这个“电子计分标靶”需要做到什么程度核心功能自动识别飞镖命中的分区包括单倍、双倍、三倍区、红心。实时性识别和计分应在飞镖命中后1秒内完成。准确性识别准确率需高于95%这是保证游戏可玩性的底线。稳定性能承受飞镖反复冲击且长时间工作可靠。成本可控整体硬件成本应控制在爱好者可接受的范围内例如数百元级别。基于这些需求市面上主流的方案有以下几种方案类型实现原理优点缺点本项目适用性分析压力/振动传感器矩阵在靶盘每个分区背面安装独立的压力或振动传感器。飞镖击中时对应传感器被触发。原理简单响应快电路直观。1. 传感器数量极多20个分区*单/双/三倍区布线复杂成本高。2. 难以精确区分紧邻分区如20和1区交界。3. 对传感器安装精度和一致性要求极高。不推荐。适用于分区少、结构简单的靶子对于标准飞镖盘过于复杂。电容/电阻触摸矩阵将靶面视为一个触摸屏通过检测飞镖镖针导体与靶面网格的接触点来定位。无需分区传感器结构可能简化。1. 需要镖针是导体标准飞镖多为钢制可行。2. 靶面需要制作复杂的导电网格工艺难度大。3. 易受环境湿度、灰尘干扰稳定性存疑。探索性方案工艺门槛高不适合作为首个实现项目。摄像头机器视觉在靶盘前方或侧方架设摄像头拍摄飞镖命中后的图像通过图像处理算法识别镖针位置。非接触式不改变靶盘本身结构灵活性高。1. 对光照条件敏感环境光变化会影响识别。2. 算法复杂度较高需要一定的图像处理知识。3. 实时性受处理器性能影响可能有延迟。强力候选方案。随着开源视觉库如OpenCV和廉价高性能单板机如树莓派普及该方案可行性大增。激光扫描定位在靶盘周围布置激光发射器和接收器阵列通过飞镖镖针遮挡激光束来三角定位。精度极高响应速度极快。1. 光学系统复杂校准极其困难。2. 成本非常高昂。3. 对镖针的粗细、反光性有要求。不适用。属于工业或高端商用方案远超业余DIY范畴。综合比较摄像头机器视觉方案在成本、实现难度和功能灵活性之间取得了最佳平衡。它允许我们使用一个标准的、廉价的纸质或植绒镖盘无需进行破坏性改造。核心工作从“如何布置传感器”转变为“如何写好识别算法”这对于学习嵌入式开发和图像处理来说是更有价值的挑战。因此本项目将采用基于树莓派或类似性能的单板计算机和USB摄像头的机器视觉方案。2.2 系统架构设计确定了视觉方案整个系统的架构就清晰了感知层由一个固定在靶盘正前方、视角能完整覆盖整个镖盘的USB摄像头组成。为了保证图像质量建议选择带自动对焦、分辨率在720P以上的摄像头。处理层树莓派4B或更新型号的单板机。它负责运行我们的计分程序进行图像采集、处理和识别计算。选择树莓派是因为其社区支持强大有完善的Python和OpenCV生态。输出层显示一块HDMI接口的显示屏用于实时显示摄像头画面可选、得分、回合信息、剩余分数等。语音一个USB音箱或通过音频接口连接的音箱用于播报得分、回合切换等提示音。软件层基于Python使用OpenCV库进行核心图像处理使用Tkinter或PyQt等库构建简单的图形用户界面GUI。整个工作流程是摄像头持续捕获靶盘图像 - 飞镖命中后程序检测到图像发生显著变化 - 对变化区域进行图像处理定位镖针尖端 - 根据镖针尖端坐标映射到预定义的镖盘分区地图上 - 查询得分更新分数并显示/播报。3. 核心细节解析与实操要点采用视觉方案最大的挑战在于算法的稳定性和鲁棒性。下面我将拆解几个最关键的图像处理环节。3.1 镖盘校准与坐标映射这是整个系统准确计分的基础。我们不能指望每次摆放摄像头的位置都分毫不差因此需要一个校准步骤让程序“认识”当前摄像头视角下的镖盘。实操步骤制作一张“校准图”。可以在纸上打印一个标准的镖盘线框图或者直接在真实的镖盘上标记出几个关键点。程序启动后进入校准模式。在GUI上提示用户依次用鼠标点击图像中镖盘的几个特征点。最少需要4个点我推荐选取20分区的顶部中心12点钟方向、6分区底部中心6点钟方向、15分区左侧中心9点钟方向和3分区右侧中心3点钟方向。点击时程序会记录下这些点在摄像头图像中的像素坐标(x_pixel, y_pixel)。我们已知这些点在标准镖盘模型上的“世界坐标”(x_world, y_world)。这个模型可以是一个以靶心为原点、半径为单位1的归一化圆盘。利用OpenCV的cv2.findHomography()函数计算从“图像像素坐标”到“镖盘世界坐标”的单应性变换矩阵Homography Matrix。这个矩阵H是一个3x3的矩阵它能将图像上的任意一点映射到标准镖盘模型上的对应位置。为什么用单应性变换因为摄像头拍摄的镖盘图像是一个透视投影它不是一个完美的正视图可能会存在梯形畸变。单应性变换正是用来描述这种平面到平面的透视映射关系比简单的仿射变换只能处理旋转、缩放、平移更准确。校准完成后保存这个变换矩阵H。之后任何飞镖命中的像素点p_pixel都可以通过p_world H * p_pixel变换到标准模型坐标进而判断它落在哪个分区。3.2 飞镖命中检测与定位系统需要知道“什么时候该去分析图像”。最简单有效的方法是帧间差分法。背景建模在游戏开始前连续采集若干帧如30帧没有飞镖的靶盘图像计算其平均图像作为“背景帧”。这可以消除一些固定的噪声。实时差分游戏过程中对于每一帧新捕获的图像先将其转换为灰度图然后与“背景帧”做绝对值差分diff |current_frame - background_frame|。二值化与阈值处理对差分图像应用一个阈值例如像素值变化大于30的认为是显著变化将其转化为黑白二值图像。飞镖命中的区域由于金属镖针和镖尾的颜色、阴影与背景差异大会形成一个白色的斑点或短线。轮廓查找与筛选使用OpenCV的cv2.findContours()查找二值图像中的所有白色轮廓。然后根据轮廓的面积、长宽比等特征进行筛选。一个刚命中的飞镖其轮廓通常是一个小面积、近似圆形的斑点镖针撞击点或者一个细长的形状如果镖尾也入镜。我们需要过滤掉面积过大可能是人影晃动或过小噪声的轮廓。定位镖尖找到最可能是飞镖的轮廓后需要精确定位镖针的尖端。对于圆形斑点可以取其中心。对于细长形状可以计算其最小外接矩形取矩形的一个短边中心假设飞镖是垂直或近似垂直扎入的。这个点就是我们需要的p_pixel。3.3 分区识别与得分计算得到镖尖的世界坐标p_world (x, y)后我们需要判断它属于哪个分区。计算极坐标将世界坐标(x, y)转换为相对于靶心(0,0)的极坐标(r, theta)。其中r是到靶心的距离theta是与垂直向上方向12点钟方向的夹角弧度或角度。判断分区根据r判断倍数区如果r 红心半径则为靶心50分或25分。如果r在三倍区环的内外半径之间则为三倍区。如果r在双倍区环的内外半径之间则为双倍区。如果r在单倍区范围内则为单倍区。如果r超出镖盘最大半径则为脱靶0分。根据theta判断数字分区标准镖盘有20个数字分区每个分区占18度360/20。我们需要将theta角度通常需要转换并规范化到0-360度映射到对应的数字。这里要注意镖盘的数字排列不是顺序的需要预先定义一个查找表。例如12点钟方向-10度到10度区间是20分区顺时针下一个18度区间是1分区再下一个是18分区以此类推。计算得分将数字分区的分值乘以倍数区的倍数即为本次投掷得分。例如坐标落在“20分区”的“三倍区”内得分就是 20 * 3 60分。4. 实操过程与核心环节实现下面我将以树莓派4B和Python为例展示核心代码片段和实现细节。假设你已经完成了树莓派的基础设置安装Raspbian系统、开启SSH、VNC等。4.1 环境准备与依赖安装首先通过SSH连接到树莓派更新系统并安装必要的软件包。# 更新系统包列表 sudo apt-get update sudo apt-get upgrade -y # 安装Python3开发环境和pip如果尚未安装 sudo apt-get install python3-dev python3-pip -y # 安装编译OpenCV所需的系统依赖库这是一步关键且耗时的操作 sudo apt-get install build-essential cmake pkg-config -y sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng-dev -y sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev -y sudo apt-get install libxvidcore-dev libx264-dev -y sudo apt-get install libfontconfig1-dev libcairo2-dev -y sudo apt-get install libgdk-pixbuf2.0-dev libpango1.0-dev -y sudo apt-get install libgtk2.0-dev libgtk-3-dev -y sudo apt-get install libatlas-base-dev gfortran -y sudo apt-get install libhdf5-dev libhdf5-serial-dev libhdf5-103 -y sudo apt-get install libqtgui4 libqtwebkit4 libqt4-test python3-pyqt5 -y sudo apt-get install python3-pyqt5.qtmultimedia -y # 安装Python虚拟环境工具推荐便于管理 sudo pip3 install virtualenv virtualenvwrapper然后创建一个虚拟环境并在其中安装Python库。# 编辑 ~/.bashrc添加virtualenvwrapper配置如果未添加过 echo export WORKON_HOME$HOME/.virtualenvs ~/.bashrc echo export VIRTUALENVWRAPPER_PYTHON/usr/bin/python3 ~/.bashrc echo source /usr/local/bin/virtualenvwrapper.sh ~/.bashrc source ~/.bashrc # 创建名为‘darts’的虚拟环境 mkvirtualenv darts -p python3 workon darts # 激活环境 # 在虚拟环境中安装NumPy和OpenCV # 安装NumPy pip install numpy # 安装OpenCV。对于树莓派推荐使用预编译的wheel速度更快。 # 可以搜索适用于你Python版本和系统架构的OpenCV wheel文件或者从源码编译耗时。 # 这里使用一个常用的预编译版本版本号请根据情况调整 pip install opencv-contrib-python-headless4.5.3.56 # 注意-headless版本不包含GUI相关功能更适合无桌面环境的树莓派。 # 如果你需要在树莓派桌面环境下显示图像可以安装 opencv-contrib-python # 安装其他辅助库 pip install imutils # 简化OpenCV操作 pip install pyttsx3 # 文本转语音用于播报4.2 核心代码实现解析以下是精简后的核心类DartsScorer的部分代码展示了校准、检测和计分的主要逻辑。import cv2 import numpy as np import json from imutils.video import VideoStream import time class DartsScorer: def __init__(self, camera_src0, calib_filecalibration.json): 初始化计分器。 :param camera_src: 摄像头索引默认0。 :param calib_file: 存储校准数据的文件路径。 self.vs VideoStream(srccamera_src).start() time.sleep(2.0) # 让摄像头预热 self.calib_file calib_file self.homography_matrix None self.board_radius 1.0 # 标准镖盘世界坐标半径 self.load_calibration() self.background_frame None self.setup_score_areas() # 初始化分区定义 def setup_score_areas(self): 定义标准镖盘的分区角度和半径范围。这是一个简化的静态定义。 # 数字分区从20区开始顺时针排列的角度度中间值 self.sector_angles [0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 252, 270, 288, 306, 324, 342] self.sector_values [20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5] # 对应的分值 # 半径分区 (基于世界坐标半径r, 假设靶盘半径1) self.radius_zones { BULLSEYE: (0.0, 0.03), # 靶心 (50分) INNER_BULL: (0.03, 0.07), # 内牛眼 (25分) - 根据实际标准调整 TRIPLE: (0.07, 0.15), # 三倍区 (简化模型实际是环) DOUBLE: (0.85, 1.0), # 双倍区 (外环) SINGLE: (0.15, 0.85), # 单倍区主体 } def calibrate(self): 交互式校准用户点击图像上的4个已知点。 print([INFO] 开始校准。请按顺序点击图像上的4个点) print( 1. 20分区顶部 (12点方向)) print( 2. 6分区底部 (6点方向)) print( 3. 15分区左侧 (9点方向)) print( 4. 3分区右侧 (3点方向)) points_pixel [] def click_event(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: points_pixel.append([x, y]) cv2.circle(frame, (x, y), 5, (0, 255, 0), -1) cv2.imshow(Calibration, frame) print(f已记录点 {len(points_pixel)}: ({x}, {y})) frame self.vs.read() if frame is None: print([ERROR] 无法从摄像头读取帧) return cv2.imshow(Calibration, frame) cv2.setMouseCallback(Calibration, click_event) cv2.waitKey(0) cv2.destroyAllWindows() if len(points_pixel) ! 4: print([ERROR] 需要恰好4个点进行校准。) return # 世界坐标 (归一化圆盘上的点) points_world np.array([ [0.0, -1.0], # 12点方向 (20分区顶部) [0.0, 1.0], # 6点方向 [-1.0, 0.0], # 9点方向 [1.0, 0.0] # 3点方向 ], dtypenp.float32) points_pixel np.array(points_pixel, dtypenp.float32) # 计算单应性矩阵 self.homography_matrix, status cv2.findHomography(points_pixel, points_world) if self.homography_matrix is None: print([ERROR] 无法计算单应性矩阵。) return # 保存校准数据 calib_data { homography_matrix: self.homography_matrix.tolist(), calibration_points_pixel: points_pixel.tolist() } with open(self.calib_file, w) as f: json.dump(calib_data, f, indent4) print(f[INFO] 校准完成数据已保存至 {self.calib_file}) def load_calibration(self): 从文件加载校准数据。 try: with open(self.calib_file, r) as f: data json.load(f) self.homography_matrix np.array(data[homography_matrix]) print([INFO] 校准数据加载成功。) except FileNotFoundError: print([INFO] 未找到校准文件请先运行校准程序。) self.homography_matrix None def capture_background(self, num_frames30): 捕获背景帧。确保靶盘上无飞镖。 print([INFO] 正在捕获背景帧请确保靶盘为空...) frames [] for i in range(num_frames): frame self.vs.read() if frame is not None: gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frames.append(gray) time.sleep(0.05) self.background_frame np.median(frames, axis0).astype(np.uint8) print([INFO] 背景帧已更新。) def detect_and_score_dart(self, frame): 检测一帧图像中的飞镖并计分。 :param frame: 输入的BGR图像帧。 :return: (score, (x_world, y_world)) 得分和世界坐标如果未检测到则返回(None, None) if self.homography_matrix is None or self.background_frame is None: print([WARNING] 系统未校准或无背景帧。) return None, None # 1. 转换为灰度图 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 2. 帧间差分 diff cv2.absdiff(gray, self.background_frame) # 3. 阈值化与形态学操作 thresh cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)[1] # 使用开运算去除小噪声闭运算连接邻近区域 kernel np.ones((5,5), np.uint8) thresh cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) thresh cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel) # 4. 查找轮廓 cnts, _ cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not cnts: return None, None # 5. 筛选轮廓假设飞镖轮廓是面积在一定范围内的小区域 dart_contour None for c in cnts: area cv2.contourArea(c) if 50 area 500: # 面积阈值需要根据摄像头距离和分辨率调整 dart_contour c break if dart_contour is None: return None, None # 6. 定位镖尖这里简化处理取轮廓的质心 M cv2.moments(dart_contour) if M[m00] 0: return None, None cX int(M[m10] / M[m00]) cY int(M[m01] / M[m00]) dart_tip_pixel np.array([[[cX, cY]]], dtypenp.float32) # 需要是3维数组 # 7. 映射到世界坐标 dart_tip_world cv2.perspectiveTransform(dart_tip_pixel, self.homography_matrix) x_w, y_w dart_tip_world[0][0] # 8. 转换为极坐标 r np.sqrt(x_w**2 y_w**2) theta_rad np.arctan2(-y_w, x_w) # 注意图像y轴向下这里取反以符合数学坐标系 theta_deg np.degrees(theta_rad) % 360 # 转换为0-360度 # 9. 判断得分 score self._calculate_score(r, theta_deg) return score, (x_w, y_w) def _calculate_score(self, r, theta_deg): 根据极坐标(r, theta_deg)计算得分。 # 判断倍数区 multiplier 1 if r self.radius_zones[BULLSEYE][1]: # 靶心区域 if r self.radius_zones[BULLSEYE][0]: # 实际应为0分这里处理边界 base_score 50 # 假设为红心 else: base_score 25 # 内牛眼 # 靶心区不分单双倍直接返回 return base_score elif self.radius_zones[TRIPLE][0] r self.radius_zones[TRIPLE][1]: multiplier 3 elif self.radius_zones[DOUBLE][0] r self.radius_zones[DOUBLE][1]: multiplier 2 elif r self.radius_zones[SINGLE][1] or r self.radius_zones[SINGLE][0]: # 超出单倍区范围但在靶盘内可能是三倍或双倍区外的无效区或脱靶 # 简化处理如果r1脱靶否则可能是无效区计0分标准规则是看镖尖所在分区 # 这里为简化r1计0分否则按单倍区处理实际需更精确的半径分区 if r 1.0: return 0 # 否则继续按单倍区逻辑 # 判断数字分区 # 找到theta_deg最接近的预定义分区角度 sector_idx np.argmin([abs(theta_deg - ang) for ang in self.sector_angles]) base_score self.sector_values[sector_idx] return base_score * multiplier def run(self): 主循环持续读取视频流检测飞镖并计分。 print([INFO] 启动计分系统。按 q 退出按 b 重新捕获背景按 c 重新校准。) self.capture_background() total_score 0 while True: frame self.vs.read() if frame is None: break score, world_pos self.detect_and_score_dart(frame) display_frame frame.copy() if score is not None: total_score score # 在图像上绘制结果 cv2.putText(display_frame, fScore: {score}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.putText(display_frame, fTotal: {total_score}, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) print(f[DART] 命中得分: {score}, 累计: {total_score}) cv2.imshow(Darts Scoring System, display_frame) key cv2.waitKey(1) 0xFF if key ord(q): break elif key ord(b): self.capture_background() elif key ord(c): self.calibrate() self.vs.stop() cv2.destroyAllWindows() if __name__ __main__: scorer DartsScorer() scorer.run()4.3 硬件搭建与部署建议摄像头固定使用一个小的三脚架或自制支架将USB摄像头固定在镖盘正前方约0.5-1米处。确保摄像头光轴尽可能垂直于镖盘平面以减少透视畸变。调整焦距使整个镖盘清晰对焦并充满大部分画面。树莓派连接将树莓派、显示屏、音箱、摄像头连接好。如果使用树莓派官方7寸触摸屏可以直接组装在一起形成一个一体化的显示终端。供电与放置为树莓派准备一个可靠的5V/3A电源。将整个系统放置在镖盘侧方或下方的桌子上确保连接线不会妨碍玩家投掷。软件自启动为了让系统开机即用可以将主程序设置为开机自启动。编辑/etc/rc.local文件在exit 0之前添加一行假设程序在虚拟环境中运行su - pi -c cd /path/to/your/project /home/pi/.virtualenvs/darts/bin/python /path/to/your/project/main.py 注意替换为你的实际路径。5. 常见问题与排查技巧实录在实际制作和调试过程中你几乎一定会遇到下面这些问题。这里记录了我的排查经验和解决方案。5.1 飞镖检测不稳定误检或漏检问题现象系统有时会把光影变化、人影晃动误认为是飞镖或者飞镖明明扎上了系统却没反应。排查思路检查背景帧确保捕获背景帧时靶盘区域光照均匀且没有移动物体。可以在晚上或拉上窗帘的室内进行减少自然光变化干扰。调整差分阈值代码中的30是差分二值化的阈值。如果环境光较暗或摄像头质量一般可以尝试降低如20如果环境光变化大可以适当提高如40。可以通过实时调整来观察效果。优化形态学操作kernel np.ones((5,5), np.uint8)中的核大小影响去噪和连接效果。如果飞镖轮廓总是被断开可以增大核大小如(7,7)或增加闭运算次数。如果误检太多可以增大开运算的核或先进行开运算。调整轮廓面积过滤if 50 area 500:这个范围需要根据你的摄像头分辨率和距离来调整。最好的方法是打印出检测到的轮廓面积观察一个正确命中的飞镖轮廓面积大概是多少然后以此为基础设定范围。我的经验在窗边测试时下午阳光移动会导致大面积误检。最终方案是a) 将摄像头安装在室内光源如顶灯正下方保证光照稳定b) 使用cv2.createBackgroundSubtractorMOG2()这类动态背景减除器替代简单的帧间差分它对缓慢的光照变化更鲁棒。5.2 计分不准确分区判断错误问题现象飞镖明明扎在20分的三倍区系统却识别为1分的单倍区。排查思路校准精度这是最常见的原因。确保校准时点击的4个点尽可能精确地对应镖盘的实际位置12点、6点、9点、3点方向的分区中心。可以打印一张带有十字标记的校准图贴在镖盘上辅助点击。检查单应性矩阵校准后可以做一个测试。在图像上随意点击一点程序将其映射到世界坐标并打印出来。你可以手动判断这个映射是否合理例如点击靶心附近映射后的坐标(x,y)应该非常接近(0,0)。验证极坐标转换打印出计算得到的(r, theta_deg)。r应该在0到1之间theta_deg应该能正确反映方向。例如命中12点方向附近theta_deg应接近0度或360度。分区定义数据仔细核对setup_score_areas函数中的sector_angles和sector_values数组。角度顺序必须是顺时针且与镖盘数字排列严格对应。一个像素级的偏差可能导致分区错误。我的经验最初我的角度顺序弄反了用了逆时针导致所有分区错位。后来我写了一个调试函数在图像上绘制出根据当前映射和分区定义计算出的所有分区边界线叠加在摄像头画面上一目了然地发现了问题。5.3 系统延迟或卡顿问题现象飞镖命中后要等1-2秒甚至更久才有反应。排查思路降低图像分辨率USB摄像头默认分辨率可能很高如1080p。对于这个应用640x480的分辨率已经足够且能大幅减少处理时间。在初始化VideoStream时设置分辨率self.vs VideoStream(srccamera_src, resolution(640, 480)).start()。优化处理流程在主循环中并非每一帧都需要进行完整的检测。可以设定一个“冷却时间”例如一次检测到飞镖后暂停检测0.5秒避免对同一支飞镖重复处理。检查树莓派性能使用htop命令查看CPU使用率。如果持续接近100%说明处理能力不足。考虑使用更高效的图像处理函数或者将部分计算如背景减除改用OpenCV的C后端或使用NumPy向量化操作。关闭不必要的图形显示cv2.imshow是比较耗资源的操作。在最终部署版本中可以考虑关闭实时视频显示只保留分数显示界面。我的经验将分辨率从1280x720降到640x480后单帧处理时间从约120ms降到了30ms以内实时性得到巨大提升。同时我移除了调试用的轮廓绘制代码也节省了一些时间。5.4 飞镖紧挨或遮挡识别问题现象靶盘上已经有多支飞镖新投掷的飞镖紧挨着旧飞镖系统可能无法正确分割出新飞镖的轮廓或者将多支飞镖识别为一个轮廓。解决方案动态背景更新不要永远使用最初的背景帧。每次成功计分后可以将当前帧包含已扎在靶上的飞镖的一部分区域“融合”到背景中。这样旧的飞镖就成了“背景”的一部分只有新飞镖的闯入才会被检测为变化。可以使用加权平均background_frame 0.95*background_frame 0.05*current_gray_frame。注意只更新靶盘区域避免其他环境变化被学习。轮廓分析进阶如果多支飞镖被识别为一个轮廓可以通过分析轮廓的凸性缺陷或使用更高级的分水岭算法来尝试分割。但这会显著增加算法复杂度。对于业余项目动态背景更新是更实用有效的策略。制作这个电子计分标靶的过程是一个典型的“发现问题 - 设计方案 - 实现调试 - 优化改进”的工程循环。它没有唯一的正确答案我的方案和代码也只是一个起点。你可以在此基础上增加更多功能比如支持多种游戏规则501、米老鼠、玩家档案管理、通过网络进行多人对战、甚至添加一个机械装置来自动拔镖。每一个功能的添加都会带来新的挑战和学习的乐趣。最重要的是当你和朋友对着这个自己打造的智能标靶投出飞镖并听到它自动报出分数时那种“智造”带来的快乐和成就感是无可替代的。

相关新闻