
1. 项目概述与核心思路做这个自动飞镖计分系统最初的想法很简单每次和朋友玩飞镖总得有人盯着靶子算分不仅容易出错还打断了游戏的流畅性。作为一个喜欢折腾树莓派和计算机视觉的玩家我就琢磨着能不能用摄像头和AI把这活儿给自动化了。核心目标很明确让机器“看懂”飞镖扎在靶子的哪个区域并自动、实时地算出分数。整个系统的骨架我把它拆解成“眼、脑、手”三个部分。“眼”就是摄像头负责捕捉飞镖靶的实时画面。“脑”是核心的AI处理单元这里我选择了性能更强的笔记本电脑来担当因为它要运行训练好的YOLOv8模型进行实时的目标检测和位置分析计算量不小。“手”则是树莓派它负责接收“脑”处理好的结果并驱动LCD显示屏把分数清晰、即时地展示给玩家看。这个分工的好处在于各司其职笔记本全力攻坚复杂的视觉算法树莓派则专注于稳定可靠的硬件交互和显示避免了让树莓派单核硬扛深度学习推理可能带来的卡顿和延迟。你可能会问为什么不用树莓派直接跑YOLOv8不是有TensorFlow Lite或者ONNX Runtime可以部署轻量模型吗这个问题我深度测试过。对于YOLOv8n纳米级这类超轻量模型树莓派5勉强能跑但帧率FPS很低往往只有2-3帧这对于需要实时反馈的飞镖游戏来说体验很差会有明显的延迟感。而如果使用精度更高的YOLOv8s或以上模型树莓派的推理速度就完全无法满足实时性要求了。因此采用“笔记本边缘服务器树莓派客户端”的架构是目前在有限预算下兼顾高精度检测和低延迟反馈的最优解。它本质上是一个小型的边缘计算系统把重计算任务上抛本地只做轻量级的数据展示和控制。2. 核心组件选型与硬件搭建硬件是项目的基石选对组件能避免后期无数麻烦。我的核心清单和选型理由如下你可以根据预算和手头资源灵活调整。2.1 计算与控制单元树莓派 5 (Raspberry Pi 5)我选择了树莓派5作为系统的控制与显示中心。相比前代它的CPU和GPU性能有显著提升I/O带宽也翻倍这对于驱动LCD屏、处理串口通信以及未来可能的扩展如声音模块都更加游刃有余。关键点务必配备一个主动散热风扇和散热片。树莓派5在高负载下发热量不小过热会导致降频影响系统稳定性。我用的是一款低噪音的涡轮风扇加铝合金散热套件实测长时间运行核心温度能控制在50°C以下。笔记本电脑充当系统的“AI大脑”。我的是一台搭载了NVIDIA GTX 1660 Ti显卡的游戏本。选择它是因为YOLOv8训练和推理都能利用CUDA进行GPU加速速度比纯CPU快几十倍。如果你的笔记本没有独立显卡使用CPU也能运行但需要做好心理准备推理速度会慢很多可能无法实现真正的“实时”。对于模型训练阶段GPU几乎是必需品否则一个epoch可能就要跑几个小时。2.2 视觉采集单元2K USB 网络摄像头我选用的是LECTRk的2K摄像头。选择2K分辨率2560x1440而非1080p是经过深思熟虑的。飞镖靶的双倍环、三倍环区域非常窄高分辨率能提供更多的像素点来精确定位镖针的落点。一个经验公式假设一个标准镖靶的直径在图像中约占1000像素那么靶面上最小的分区比如20分的三倍区的像素宽度可能只有10-15个像素。1080p图像下这个值会更小定位误差会明显增大。摄像头务必使用固定支架确保其与镖靶平面保持平行并正对靶心这是后续所有图像处理算法能正常工作的前提。2.3 交互与显示单元16x2 LCD 显示屏 (带I2C接口)为了简化接线我强烈推荐购买已经焊好了I2C转接板的16x2 LCD屏。传统的16引脚LCD需要连接树莓派多达6-7个GPIO口而I2C版本只需要连接4根线VCC, GND, SDA, SCL极大地简化了硬件连接也节省了宝贵的GPIO资源。蓝色背光在室内环境下显示清晰。杜邦线与 GPIO扩展板准备足够的公对公、公对母杜邦线。使用一个GPIO扩展板Breakout Board或T型扩展板会非常方便可以避免直接将杜邦线插在树莓派脆弱的排针上也便于后续的调试和修改。2.4 硬件连接与供电连接示意图与实操要点树莓派供电使用官方推荐的5V/5A USB-C电源适配器。供电不足会导致树莓派运行不稳定尤其是在连接了外设的情况下。LCD屏连接VCC - 树莓派 5V 引脚 (Pin 2 或 4)GND - 树莓派 GND 引脚 (Pin 6, 9, 14, 20, 25, 30, 34, 39 等任意一个)SDA - 树莓派 GPIO2 (SDA, Pin 3)SCL - 树莓派 GPIO3 (SCL, Pin 5)注意连接前需要先用sudo raspi-config命令进入树莓派配置界面启用I2C接口。摄像头连接直接插入笔记本电脑的USB 3.0接口通常为蓝色确保获得最大的数据传输带宽。树莓派与笔记本通信两者处于同一局域网下。我让树莓派通过网线连接路由器笔记本连接Wi-Fi。通信采用Socket网络编程实现。在代码中笔记本作为服务器Server树莓派作为客户端Client通过TCP协议传输得分数据。这种方式比USB串口通信更灵活不受距离限制。硬件搭建避坑指南供电是万恶之源树莓派或外设的任何诡异问题如随机重启、LCD乱码首先检查电源是否达标线路是否虚接。I2C地址冲突如果LCD屏不显示在树莓派终端输入sudo i2cdetect -y 1命令扫描I2C设备地址。常见的LCD I2C地址是0x27或0x3F。确保代码中使用的地址与扫描结果一致。摄像头对焦很多USB摄像头是自动对焦的在飞镖击中靶面时如果靶子有轻微震动可能会导致瞬间失焦。建议在代码初始化时通过OpenCV的set函数将摄像头的自动对焦模式关闭如果驱动支持并设置为手动或无限远模式或者选用定焦摄像头。3. 数据集构建与YOLOv8模型训练详解这是整个项目最核心、也最耗时的一步。模型的精度直接决定了计分系统的可用性。我采用了双模型策略一个用于语义分割Segmentation锁定靶心另一个用于关键点检测Keypoint Detection精确定位飞镖尖。3.1 数据采集模拟真实场景的多样性我用了两个下午在不同的光照条件白天自然光、晚上室内暖光/冷光、从多个角度正面、轻微侧方、拍摄了大约500张飞镖靶的照片。关键技巧包含空靶至少1/3的照片是空靶状态这能让模型学会什么是“背景”。覆盖所有分区确保1到20分区、单倍区、双倍区、三倍区、红心Bullseye、绿心Single Bull都有大量飞镖命中的样本。制造干扰有些照片中我让手、飞镖筒或其他物品部分出现在镜头边缘让模型学会排除这些干扰。飞镖状态多样包括单支镖、多支镖聚集、飞镖斜插、飞镖掉落挂在靶上等状态。3.2 数据标注精度是模型的基石我使用Roboflow这个在线平台进行标注它对于个人和小项目非常友好。创建分割数据集上传图片后使用“多边形”工具Polygon精确地勾勒出整个镖靶红心Bullseye的区域。这个模型的目标只有一个在任何情况下都能找到靶心。标签名设为bullseye。创建关键点数据集在另一份数据集中使用“点”工具Point标注每一支飞镖的镖尖Tip精确位置。标签名设为dart_tip。这里有个至关重要的细节对于斜插的飞镖要想象镖尖穿透靶面的那个点而不是标注可见的镖身某处。数据增强Augmentation在Roboflow中我应用了以下增强策略来增加数据的多样性提高模型鲁棒性旋转±15度模拟摄像头轻微歪斜。亮度与对比度随机调整模拟光照变化。模糊轻微的高斯模糊模拟对焦不准。注意添加裁剪、剪切Shear等可能严重改变几何位置的增强要谨慎因为这会直接影响飞镖位置的物理意义。3.3 模型训练从本地到云端的策略Roboflow支持一键导出为YOLOv8格式并生成训练代码。我最初尝试在本地笔记本GTX 1660 Ti上训练。分割模型训练输入图像尺寸设为640x640训练了150个epoch。这个模型相对简单收敛很快。关键点模型训练这是重点。YOLOv8的关键点检测需要指定每个关键点的数量。我这里就是1个点镖尖。我使用了预训练的yolov8s-pose.pt权重进行迁移学习训练了300个epoch。batch size根据显卡内存设为16。本地训练的痛点笔记本风扇狂转训练时几乎无法做其他事情且一次实验周期很长。后来我转向了Google Colab的付费套餐Colab Pro。它提供更强大的GPU如V100或T4训练速度大幅提升且不占用本地资源。操作流程将Roboflow导出的数据集上传到Google Drive在Colab中挂载Drive然后运行修改后的训练脚本。Colab的断点续训功能也很好用。3.4 模型评估与优化训练完成后不能只看最后的损失值loss必须分析验证集上的关键指标分割模型主要看mAP50-95(B)即在不同IoU阈值下的平均精度。我的模型在0.5 IoU下达到了0.98以上说明识别靶心非常可靠。关键点模型除了目标检测的mAP更要关注关键点精度指标OKSObject Keypoint Similarity类似于目标检测的IoU。我的模型在验证集上的OKS达到了0.92。同时我使用YOLOv8提供的val.py脚本在验证集上运行并保存预测结果图片人工逐一检查那些预测偏差较大的案例。发现主要问题集中在边缘三倍区的密集区域和飞镖重叠的情况。针对这些问题我额外采集了约50张类似场景的图片加入数据集进行增量训练使模型精度得到进一步提升。模型训练心得数据质量 数据数量100张标注精准的图片远胜于1000张标注粗糙的图片。在标注阶段多花一小时可能在调试阶段节省十小时。预训练权重是捷径务必使用官方预训练权重进行迁移学习这能极大加快收敛速度提升最终性能。耐心调参学习率lr0是最重要的超参数之一。我使用YOLOv8的Linear学习率调度器并设置了cos退火。如果训练过程中损失波动很大可以尝试降低学习率。模型并非越大越好对于树莓派或笔记本部署需要在精度和速度间权衡。我测试过yolov8n-pose纳米级速度极快但精度在复杂场景下下降明显。最终选择yolov8s-pose小规模取得了较好的平衡。4. 系统软件架构与核心代码实现整个系统的软件部分分为两大模块运行在笔记本电脑上的AI推理与计分服务以及运行在树莓派上的显示与通信客户端。两者通过Socket进行数据交换。4.1 笔记本电脑端AI推理服务器app.py这个Python脚本是系统的大脑它需要完成摄像头读取、模型推理、分数计算和网络发送。import cv2 from ultralytics import YOLO import numpy as np import socket import json class DartScoringServer: def __init__(self, seg_model_path, kp_model_path, camera_index0): # 加载训练好的模型 self.seg_model YOLO(seg_model_path) # 分割模型用于找靶心 self.kp_model YOLO(kp_model_path) # 关键点模型用于找镖尖 self.cap cv2.VideoCapture(camera_index) # 设置摄像头参数分辨率、关闭自动对焦如果支持 self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 2560) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1440) # 尝试设置手动对焦取决于摄像头驱动 # self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 0) # self.cap.set(cv2.CAP_PROP_FOCUS, 0) # 镖靶几何参数需根据实际图像测量标定 self.bullseye_radius_pixels None # 将在运行时从分割结果计算 self.board_radius_pixels None # 靶面半径像素 self.scores_ring [6, 13, 4, 18, 1, 20, 5, 12, 9, 14, 11, 8, 16, 7, 19, 3, 17, 2, 15, 10] # 标准镖靶20个分区从顶部开始顺时针顺序 # Socket通信设置 self.server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.bind((0.0.0.0, 65432)) # 绑定到所有网络接口端口65432 self.server_socket.listen(1) print(等待树莓派连接...) self.client_socket, self.client_address self.server_socket.accept() print(f连接来自: {self.client_address}) def calculate_score(self, dart_point, bullseye_center): 根据镖尖相对于靶心的极坐标计算分数 if self.bullseye_radius_pixels is None: return 0, N/A dx dart_point[0] - bullseye_center[0] dy dart_point[1] - bullseye_center[1] distance np.sqrt(dx**2 dy**2) angle np.arctan2(dy, dx) # 弧度制范围(-pi, pi] # 转换为角度制并调整到从正上方12点钟方向开始顺时针递增 angle_deg np.degrees(angle) angle_deg (90 - angle_deg) % 360 # 调整坐标系 # 判断分数区 if distance self.bullseye_radius_pixels * 0.1: # 假设红心半径占靶心半径10% return 50, BULL elif distance self.bullseye_radius_pixels: return 25, S-BULL else: # 确定分区索引 (0-19) sector_index int((angle_deg 9) // 18) % 20 # 每个分区18度偏移9度使边界在分区中间 base_score self.scores_ring[sector_index] # 判断倍区 board_radius self.board_radius_pixels # 需要预先标定 if distance board_radius * 0.95: # 外缘双倍环 return base_score * 2, fD{base_score} elif distance board_radius * 0.85: # 单倍区外 return base_score, f{base_score} elif distance board_radius * 0.55: # 三倍环 return base_score * 3, fT{base_score} elif distance board_radius * 0.45: # 单倍区内 return base_score, f{base_score} else: # 内中心区已处理红心 return 0, MISS # 理论上不会进入这里 def run(self): bullseye_center None while True: ret, frame self.cap.read() if not ret: break # 第一步使用分割模型锁定靶心 seg_results self.seg_model(frame, conf0.7, verboseFalse) if seg_results[0].masks is not None: # 获取靶心区域的轮廓或中心点这里简化处理取第一个检测框的中心 for box in seg_results[0].boxes: x1, y1, x2, y2 box.xyxy[0].cpu().numpy() bullseye_center (int((x1x2)/2), int((y1y2)/2)) # 估算靶心半径可根据框大小或分割掩码计算 self.bullseye_radius_pixels int((y2 - y1) / 2) # 绘制靶心 cv2.circle(frame, bullseye_center, self.bullseye_radius_pixels, (0, 255, 0), 2) break # 第二步如果找到靶心则运行关键点模型检测飞镖 dart_scores [] if bullseye_center is not None: kp_results self.kp_model(frame, conf0.5, verboseFalse) if kp_results[0].keypoints is not None: for kpts in kp_results[0].keypoints.data: # kpts.shape: [num_darts, num_kpts(1), 3] (x, y, conf) for dart in kpts: x, y, conf dart.cpu().numpy() if conf 0.5: # 置信度阈值 dart_point (int(x), int(y)) score, score_text self.calculate_score(dart_point, bullseye_center) dart_scores.append(score) # 在图像上标注 cv2.circle(frame, dart_point, 5, (0, 0, 255), -1) cv2.putText(frame, score_text, (dart_point[0]10, dart_point[1]), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2) # 计算本轮总分例如最近三镖 total_score sum(dart_scores[-3:]) if dart_scores else 0 # 通过Socket发送数据到树莓派 data_to_send { current_scores: dart_scores[-3:], # 最近三镖分数 total_score: total_score, dart_count: len(dart_scores) } try: self.client_socket.sendall(json.dumps(data_to_send).encode(utf-8) b\n) # 添加换行符作为消息分隔 except: print(连接断开尝试重连...) # 这里应添加重连逻辑 break # 显示实时画面可选用于调试 cv2.imshow(Dart Scoring, frame) if cv2.waitKey(1) 0xFF ord(q): break self.cap.release() cv2.destroyAllWindows() self.client_socket.close() self.server_socket.close() if __name__ __main__: server DartScoringServer(best_seg.pt, best_kp.pt, camera_index0) server.run()4.2 树莓派端显示客户端final_code.py这个脚本运行在树莓派上负责接收数据并驱动LCD显示。import socket import json import time import board import busio import adafruit_character_lcd.character_lcd_i2c as character_lcd class DartDisplayClient: def __init__(self, server_ip192.168.1.100, server_port65432): # 初始化I2C LCD (地址通常是0x27或0x3f) i2c busio.I2C(board.SCL, board.SDA) self.lcd character_lcd.Character_LCD_I2C(i2c, 16, 2, address0x27) self.lcd.clear() self.lcd.message Dart System\nInitializing... # 设置Socket连接 self.server_ip server_ip self.server_port server_port self.sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) def connect_to_server(self): while True: try: print(f尝试连接到 {self.server_ip}:{self.server_port}...) self.sock.connect((self.server_ip, self.server_port)) print(连接成功) self.lcd.clear() self.lcd.message Connected!\nReady to Play. time.sleep(2) break except socket.error as e: print(f连接失败: {e}. 5秒后重试...) self.lcd.message Connecting...\nRetry in 5s time.sleep(5) def run(self): self.connect_to_server() buffer while True: try: # 接收数据 data self.sock.recv(1024).decode(utf-8) if not data: print(服务器断开连接) self.lcd.message Server Down!\nReconnecting... self.connect_to_server() continue buffer data # 处理可能粘包的数据以换行符分隔 while \n in buffer: line, buffer buffer.split(\n, 1) if line.strip(): self.process_data(line.strip()) except socket.error as e: print(f接收错误: {e}) self.lcd.message Network Err\nReconnecting... time.sleep(2) self.connect_to_server() except KeyboardInterrupt: print(用户中断) break self.sock.close() self.lcd.clear() def process_data(self, json_str): try: data json.loads(json_str) scores data.get(current_scores, []) total data.get(total_score, 0) count data.get(dart_count, 0) # 格式化LCD显示信息 (16字符x2行) if scores: # 第一行显示最近三镖例如: D20 T15 50 line1 .join([str(s) for s in scores[-3:]]) line1 line1.center(16) # 居中显示 else: line1 No Darts.center(16) # 第二行显示总数和镖数例如: Total: 85 (5) line2 fTotal:{total:3d} ({count}) line2 line2.center(16) self.lcd.clear() self.lcd.message f{line1}\n{line2} except json.JSONDecodeError: print(fJSON解析错误: {json_str}) if __name__ __main__: # 注意这里的IP需要替换为运行app.py的笔记本电脑的IP地址 client DartDisplayClient(server_ip192.168.1.100) client.run()4.3 双模型协同与“靶心锁定”机制在最初的测试中我同时运行两个模型发现了一个严重问题关键点检测模型在定位镖尖时严重依赖一个稳定的坐标系原点即靶心。如果分割模型在某一帧中未能检测到靶心或者检测框有轻微抖动那么即使飞镖位置没变计算出的极坐标距离和角度也会漂移导致分数计算错误。解决方案我引入了“靶心锁定”机制。系统启动后前N帧例如10帧会持续运行分割模型将检测到的靶心坐标进行移动平均滤波得到一个稳定的初始靶心位置(cx0, cy0)。在后续帧中除非连续M帧例如5帧都检测不到靶心否则将固定使用这个初始位置作为坐标原点进行分数计算。分割模型仍然在每一帧运行但它的作用变成了“监控”一旦发现靶心位置持续丢失可能因为遮挡或光线剧变系统会报警并尝试重新初始化。这个策略的代码体现在app.py的run循环中。在实际实现时我创建了一个BullseyeTracker类它内部维护一个靶心坐标的队列进行滤波处理并对外提供一个稳定的get_stable_center()方法。关键点检测和计分逻辑都调用这个方法而不是直接使用每一帧的原始检测结果。这样即使分割模型有单帧的噪声也不会影响计分的稳定性。5. 系统集成、测试与问题排查当硬件连好代码写完真正的挑战才刚刚开始。集成测试是暴露问题、打磨系统的关键阶段。5.1 分阶段集成测试单元测试LCD测试先在树莓派上写一个简单的脚本让LCD循环显示一些字符确保硬件连接和I2C通信正常。Socket通信测试写一个简单的回显Echo程序笔记本发送消息树莓派接收并显示验证网络连通性和数据格式。模型独立测试在笔记本上用静态图片分别测试分割模型和关键点模型确保它们能正确输出预期结果。子系统联调视觉-计算联调关闭Socket发送让笔记本端的程序运行并在OpenCV窗口中实时显示检测框、关键点和计算出的分数。用一支飞镖在靶上不同位置移动观察分数计算是否正确。重点验证靶心锁定机制和分数分区逻辑。计算-显示联调暂时将笔记本端的AI推理部分注释掉改为模拟生成一些分数数据通过Socket发送给树莓派确保LCD能正确接收并显示。全系统压力测试进行连续的游戏对局模拟持续运行系统1-2小时观察是否有内存泄漏程序占用内存是否持续增长、通信是否偶发中断、LCD显示是否卡顿。在不同光照条件下测试开灯、关灯、拉窗帘评估模型的鲁棒性。5.2 遇到的典型问题与解决方案以下是我在测试中踩过的坑及其解决方法整理成排查清单问题现象可能原因排查步骤与解决方案LCD屏无显示或乱码1. I2C地址错误2. 接线错误或松动3. 背光未开启4. 树莓派I2C未启用1. 运行sudo i2cdetect -y 1扫描地址确认是0x27还是0x3F并修改代码。2. 检查VCC、GND、SDA、SCL四根线是否接对、接牢。3. 在代码中尝试lcd.backlight True。4. 运行sudo raspi-config在Interface Options中启用I2C。树莓派无法连接到笔记本1. IP地址错误2. 防火墙阻止3. 笔记本端服务未启动1. 在笔记本上运行ipconfig(Windows) 或ifconfig(Linux/macOS) 查看无线局域网IP确保树莓派代码中的IP与此一致。2. 临时关闭笔记本的防火墙进行测试。3. 确保笔记本的app.py先于树莓派的final_code.py运行并处于监听状态。摄像头画面卡顿或延迟高1. USB带宽不足2. 图像分辨率过高3. 模型推理速度慢1. 将摄像头插到USB 3.0蓝色接口。2. 在代码中降低cv2.VideoCapture的分辨率如改为1280x720。3. 考虑使用更小的YOLO模型如nano或开启OpenCV的cv2.dnn后端进行TensorRT加速如果使用NVIDIA GPU。飞镖检测时有时无或置信度低1. 光照条件变化2. 飞镖与背景对比度低3. 模型未见过此类场景1. 增加训练数据的光照多样性或在现场添加补光灯。2. 尝试在图像预处理时增加对比度或使用CLAHE算法。3. 采集当前环境下的“困难样本”对模型进行微调Fine-tuning。分数计算错误分区错乱1. 靶心定位不准2. 镖靶图像存在透视畸变3. 分数环半径比例标定错误1. 强化“靶心锁定”机制使用更稳定的滤波算法如卡尔曼滤波。2. 在摄像头正对镖靶的前提下使用OpenCV的findHomography或warpPerspective进行透视校正。3. 制作一个标定工具在镖靶上贴几个已知物理位置的标记点通过图像测量精确计算像素距离与实际距离的比例关系。程序运行一段时间后崩溃1. 内存泄漏2. Socket连接异常未处理3. 树莓派过热1. 检查代码中是否有未释放的资源如OpenCV窗口、Socket连接。使用try...except...finally确保资源释放。2. 增加Socket通信的异常捕获和重连机制如代码中的connect_to_server循环。3. 检查树莓派温度vcgencmd measure_temp确保散热风扇工作正常。5.3 精度提升的进阶技巧在基本系统跑通后如果你对精度有更高要求可以尝试以下方法透视校正如果摄像头无法完全正对镖靶图像会产生梯形畸变。可以在镖靶的四个角或外圈金属丝上贴四个高对比度标记点。系统启动时先检测这四个点计算出一个从图像平面到正对平面的透视变换矩阵并将每一帧图像都应用这个变换进行校正然后再进行检测。这能极大提高角度计算的准确性。多帧融合与去抖飞镖击中靶面后可能会轻微振动。可以对连续多帧如5帧检测到的同一个飞镖位置进行加权平均以稳定最终坐标避免因单帧抖动导致的分数跳变。使用更专业的摄像头可以考虑使用工业相机它们通常提供更稳定的驱动、更高的帧率和更灵活的参数设置如手动调焦、固定光圈但成本也更高。模型量化与加速如果希望最终部署到树莓派上可以研究将YOLOv8模型转换为TensorFlow Lite或ONNX Runtime格式并进行动态量化在精度损失可控的前提下大幅提升推理速度。6. 项目总结与扩展思考经过从硬件采购、模型训练、代码编写到反复调试的完整周期这个自动飞镖计分系统终于能比较稳定地工作了。最大的成就感来自于看到飞镖击中靶面后LCD屏上几乎实时地显示出正确的分数完全替代了人工计分。这个过程里我深刻体会到嵌入式AI项目是一个典型的多学科交叉工程它要求你不仅懂软件和算法还要能搞定硬件连接、网络通信甚至简单的机械固定。几个关键体会问题分解是关键不要把“做一个自动计分系统”当成一个任务而是拆解成“如何看到靶子”、“如何识别飞镖”、“如何计算坐标”、“如何显示结果”、“如何让两部分对话”等一系列小问题逐个击破。数据决定上限模型的表现天花板在数据标注完成的那一刻就基本确定了。在数据采集和标注上多花的时间会在后期调试中加倍地回报你。稳定比炫技更重要最初我总想用最复杂的算法但后来发现一个简单的移动平均滤波靶心锁定比复杂的跟踪算法更稳定可靠。在工程中简单且鲁棒的方案往往是最好的方案。完整的闭环测试不可或缺单元测试通过不代表系统能工作。必须搭建一个从物理世界扔飞镖到数字世界LCD显示的完整闭环测试环境才能发现那些在仿真中想不到的问题比如光线变化、网络延迟、硬件偶发故障等。未来可以玩的方向 这个项目就像一个乐高底座有很强的扩展性。比如可以增加一个语音播报模块用树莓派连接一个USB声卡和小音箱每次计分后播报“Triple Twenty!”可以开发一个简单的Web界面用Flask框架在树莓派上跑一个本地网页服务器让玩家通过手机浏览器就能看到更详细的分数历史和统计甚至可以加入玩家识别功能用另一个轻量级模型识别是谁扔的飞镖自动为多人游戏计分。这些扩展都能在现有的硬件和架构基础上通过增删软件模块来实现这正是嵌入式AI项目迷人的地方——你的想法就是系统的边界。