
简介面向Unity开发者的触摸屏物体识别桌算法工程针对互动桌面、增强现实或虚拟现实等需要精准判断手指与场景物体对应关系的项目提供了一套基于C#语言和Lean Touch插件的可运行方案。资源包共三千六百四十个文件压缩后为二十四点五四兆主体包含C#脚本、动态链接库、Unity场景、预制体、材质资源、图片贴图以及项目配置文件等其中两百二十个C#脚本给出了触摸监听、物体拾取、手势处理等核心逻辑十二个预制体与三十一个场景可直接用于搭建演示环境。包内还保留了大量元数据与二进制资源便于在Unity编辑器中检查或二次修改目前已有二千四百四十二人学习下载。工程通过继承LeanSelectable重写OnSelect与OnDeselect方法配合Physics.Raycast将屏幕触点转换为世界坐标并投射检测被触摸物体同时给出限制射线检测层级、关联触摸编号、处理界面触摸事件以及使用调试输出进行调试等完整实现思路整体流程从导入Lean Touch插件开始依次覆盖设置触摸监听器、物体识别、性能优化、界面交互到调试与测试脚本中保留了关键节点的注释说明方便按步骤对照。无论是要快速搭建物体识别桌原型还是想学习Unity触摸交互底层写法都可以从中获得可直接落地的参考。1. 互动桌面从哪开始unity c# 触摸屏物体识别桌到底要解决什么一块 55 寸触摸屏平放在台面上摄像头从上方俯拍桌面你把一个积木放到屏上屏幕立刻圈出它并弹出介绍动画。这就是触摸屏物体识别桌一个听起来很“人工智能”的互动装置拆开之后核心只有三件事取图像、识别物体、把识别框和你手指触摸的位置对齐到同一套坐标。做了几台类似设备之后我最大的反直觉结论是物体识别算法很少成为瓶颈OpenCV 跑个颜色阈值就能到 90% 以上识别率真正让项目翻车的永远是坐标对齐和触摸驱动差异。这篇文章沿着硬件选型、识别方案、坐标标定、现场排错的路径讲完整适合正在做互动展桌、儿童教育桌或智能餐桌的 Unity 开发者参考。2. 先定硬件再写代码摄像头、触摸屏与识别桌的三种装配方式识别桌的软件结构其实不复杂复杂的是硬件形态决定了后面所有代码怎么写。摄像头在哪个位置、触摸屏是哪种原理、主机性能到什么程度这三件事没定下来就写识别逻辑几乎必然返工。2.1 摄像头怎么摆决定了你后面三分之二的坑摄像头相对桌面的位置直接决定你拿到的是“正射影像”还是“带透视畸变的斜视影像”。最常见的装配有三种装配方式视野与畸变遮挡风险标定成本适合场景正上方吊装畸变最小接近垂直俯拍人手放物体时容易挡住摄像头低线性缩放基本可用固定展台、演示原型侧上方 45 度支架梯形畸变明显遮挡少人在侧面操作更自然高必须做透视标定商用一体机、教育桌藏在屏幕边框下方畸变最大镜头离物体近基本无遮挡外观干净很高还要做镜头畸变校正一体化桌面硬件产品我一般建议原型期用正上方吊装因为短平快能让你把精力集中在识别和交互上。一旦要对着商用形态做就会改成斜装或者内藏这时候就必须引入单应矩阵做透视校正。别指望“先凑合用线性比例缩放”斜拍角度超过 15 度屏上偏差就会到几十像素手指点上去完全对不上。摄像头选型上普通 USB 摄像头足够但要注意固定焦距、不要自动对焦自动对焦在桌面上扫描时会造成识别框忽大忽小。2.2 触摸屏选红外还是电容识别桌的物理层差异触摸桌的触摸层和普通竖屏触摸一体机不一样它是平放的人的手掌、手腕甚至整条小臂都可能搭在屏幕上。红外触摸框是识别桌的主流选择大尺寸便宜43 到 65 寸都有成熟方案但它害怕强光干扰阳光直射或者高功率白炽灯会让红外信号漂移。电容触摸精度高、边框可以做得更薄但超过 32 寸后成本抬升很快大尺寸成品也少。手感上红外对“大面积手掌误触”处理很差腕部压上去会生成一个巨大的矩形触点需要做防误触过滤把触摸回调里面积异常大的触点直接丢掉。驱动层面的坑也在这里埋着。Windows 下的触摸屏大多走 HID 触摸协议系统层给你的原始坐标是左上原点Unity 的 Input 模块拿到后通常会转成左下原点但部分触摸屏的驱动或者经过中间件透传之后y 轴方向会变得不可预测。这就是后面第 5 章“换了一台屏所有坐标全反了”的根源。做硬件选型时我养成了一个习惯采购前拿样机接一个最小 Unity 工程跑触摸采样把原始坐标打印出来看 30 秒比看任何参数表都管用。2.3 主机配置与 Unity 工程的基础设置主机配置取决于识别跑在哪里。如果只在 Unity 进程内用 OpenCV 做颜色阈值识别i5 级别 CPU 加 8G 内存就够如果要在机内跑 YOLO 模型建议至少 i5 加 GTX 1060 级别显卡否则 640 输入的推理延迟会让你怀疑人生。还有一个更稳的结构是把识别做成独立上位机进程Unity 只负责显示和触摸识别结果通过内存映射或 TCP 送进来。这个结构的好处是识别进程崩溃不影响演示画面也方便单独升级识别算法很多商用互动桌最后都走到这条路上。Unity 工程侧有几个基础设置容易被忽略。安装 Unity 时记得勾选 Windows Build Support否则后面导出 Windows 包会卡住。Player Settings 里把全屏模式设成 Fullscreen Windowed方便触摸屏一体机切换分辨率时不闪黑。7x24 演示设备还要考虑开机自启、进程守护和异常断线恢复这些一般用一个小型启动器脚本包住两个进程Unity 主程序和识别上位机。工程组织上我会把摄像头采集、识别调度、坐标映射、触摸判定拆成四个独立 C# 模块UI 表现层只在收到事件时做响应绝不让 UI 脚本直接碰图像数据。3. 识别核心落地用 OpenCV for Unity 跑通第一版再决定要不要切 YOLO识别算法是标题里的关键词但落地路径比算法本身更重要。对触摸屏识别桌来说识别对象通常是固定品类几类积木、卡片、餐具、标本数量从几类到二十几类不等光照可控桌面区域固定。这种条件下一上来就训练 YOLO 属于杀鸡用牛刀先用 OpenCV for Unity 把整条链路跑通再按需升级是回报最高的顺序。3.1 为什么先从 OpenCV 起步而不是直接上 YOLOOpenCV 在 Asset Store 上有现成插件导入后可以直接操作 WebCamTexture 的 Mat不需要搭 Python 服务不需要训练数据当天就能看到识别框。对于 3 到 8 种颜色和形状差异明显的物体HSV 阈值加轮廓提取完全够用而且在普通 CPU 上单帧处理只要几毫秒。YOLO 的优势是泛化能力强、能区分同类不同款、对遮挡和复杂背景鲁棒代价是要准备几百上千张标注图、训练一轮、导出模型还要在 Unity 侧接推理库。项目时间表如果只有两周OpenCV 版本已经可以交付演示如果有两个月且识别种类超过十类就值得上 YOLO。对比项OpenCV 颜色阈值YOLO 目标检测标注数据不需要每类 300 张起区分相似物体差靠颜色形状硬分好能学纹理细节单帧耗时CPU3~10ms100~300ms抗光照变化差需要固定补光较好项目前期成本半小时跑通一周以上3.2 最小识别流程HSV 阈值、轮廓提取与外接矩形以识别红色积木为例OpenCV for Unity 里最稳的第一步是把图像转到 HSV 空间再做颜色筛选。RGB 阈值对亮度变化太敏感白色台面边缘反光会造成大量误检HSV 把色相和饱和度分开只要目标物体颜色饱和度高亮度漂移影响就小很多。注意 OpenCV 里 H 分量范围是 0 到 179不是 0 到 360拿默认参数的人经常在这里翻车。using OpenCVForUnity.CoreModule; using OpenCVForUnity.ImgprocModule; // frameMat 来自 WebCamTextureToMatHelper.ToMat()是 BGR 格式 Mat hsv new Mat(); Imgproc.cvtColor(frameMat, hsv, Imgproc.COLOR_BGR2HSV); // 红色在 HSV 色相环上跨 0 点和 180 点需要分两段做 InRange 再合并 Mat mask1 new Mat(); Mat mask2 new Mat(); Core.inRange(hsv, new Scalar(0, 60, 60), new Scalar(10, 255, 255), mask1); Core.inRange(hsv, new Scalar(160, 60, 60), new Scalar(179, 255, 255), mask2); Core.bitwise_or(mask1, mask2, mask1); // 开运算先腐蚀后膨胀去掉孤立噪点 Mat kernel Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); Imgproc.morphologyEx(mask1, mask1, Imgproc.MORPH_OPEN, kernel); // 提取外轮廓再做面积和长宽比过滤 ListMatOfPoint contours new ListMatOfPoint(); Imgproc.findContours(mask1, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE); ListRect results new ListRect(); for (int i 0; i contours.Count; i) { Rect r Imgproc.boundingRect(contours[i]); if (r.area() 300) continue; // 识别分辨率 640x360 的经验下限 if (r.width * 3 r.height || r.height * 3 r.width) continue; // 过滤细长噪点 results.Add(r); }这段代码里有几个参数值得单独说。HSV 的 S 和 V 下限都设成 60是为了把暗部和灰白色背景过滤掉如果台面是深色就要把 V 下限调高红色要分两段是因为 HSV 色相环是环形的0 到 10 和 160 到 179 都是红色区域。开运算的 kernel 默认 3x3如果画面噪点密集可以改 5x5但代价是物体边缘会收缩一圈。面积阈值 300 针对 640x360 的识别分辨率如果你把分辨率调到 320x180这个值要相应降到 80 左右。调试时最有效的动作是把 mask 用 Imgproc.imshow 或者转成 Texture2D 显示出来看看目标物体在 mask 里是白还是黑。如果物体区域是黑色说明 S/V 阈值设错了不是 H 的问题。3.3 从 OpenCV 切到 YOLOUnity 侧推理的三种姿势当识别种类超过十类或者物体之间只有纹理差异时OpenCV 就到极限了。常见的升级路径是在 PC 上训练 YOLOv8导出 ONNX然后让 Unity 工程加载推理。Unity 侧跑 ONNX 有三种常见姿势用 Unity 官方的 Sentis 插件用 C# 引用 OnnxRuntime或者把识别完全放到独立的 C# 上位机进程里。老教程里大量出现的 Barracuda 已经被官方归档新项目不建议再入坑Sentis 是它的继任者但集成文档相对少。我真正推荐的是第三种识别进程独立出来Unity 只收结果。理由前面提过崩溃隔离、算法可独立更新、调试时能单独跑识别进程看日志这对商用设备太重要了。不管用哪种方式YOLOv8 的 ONNX 输出解析逻辑是一样的。它的输出是一个 1 x (4N) x 8400 的张量N 是类别数8400 是三个特征层网格点总数。解析时注意 8400 个候选框里绝大多数置信度都接近 0需要先按类别分数过滤再做 NMS。// data 是 ONNX 输出转成的一维数组长度为 1 * 84 * 8400以 COCO 80 类为例 int rows 8400; int cols 84; // 4 个坐标值 80 个类别分数 float confThresh 0.5f; float iouThresh 0.45f; ListDetection detections new ListDetection(); for (int i 0; i rows; i) { int offset i * cols; float cx data[offset]; float cy data[offset 1]; float w data[offset 2]; float h data[offset 3]; float maxScore 0f; int classId -1; for (int c 4; c cols; c) { if (data[offset c] maxScore) { maxScore data[offset c]; classId c - 4; } } if (maxScore confThresh) continue; float x1 cx - w / 2f; float y1 cy - h / 2f; float x2 cx w / 2f; float y2 cy h / 2f; detections.Add(new Detection(x1, y1, x2, y2, maxScore, classId)); } // 按分数降序排序后做 NMS与已保留框的 IoU 超过阈值的候选丢弃 detections.Sort((a, b) b.score.CompareTo(a.score)); ListDetection kept new ListDetection(); foreach (var det in detections) { bool overlap false; foreach (var keep in kept) { float iou ComputeIoU(det, keep); if (iou iouThresh) { overlap true; break; } } if (!overlap) kept.Add(det); }这份解析逻辑有两个必须强调的细节。第一模型训练时通常会把输入图 letterbox 成 640x640也就是等比缩放后两侧补灰边模型输出的坐标是相对于 640x640 输入图的拿到检测框后要按 letterbox 比例还原到原始画面少做这一步所有框都会整体偏移。第二confThresh 和 iouThresh 的取值直接影响体验0.5 的置信度阈值在演示场景偏保守、误报少但遮挡时容易漏检如果你发现静态物体偶尔识别不到可以降到 0.35代价是误报增多这个要靠你的演示环境去权衡。4. 让识别坐标对上触摸点四角标定与 Unity 坐标融合识别模型给出了物体在图像里的位置触摸屏给出了手指的位置这两个位置不在一套坐标系里。整个触摸屏识别桌能不能用全看这一步对齐得准不准。很多团队在这上面耗掉一半工期就是因为没把三个坐标系的事先想清楚。4.1 三个坐标系一个都不能搞混摄像头图像坐标系的原点在图像左上角x 向右、y 向下单位是像素。触摸屏应用层常见的是左上原点但 Unity 的 Input.touch.position 和 mousePosition 是左下原点y 轴向上。第三个是世界坐标系也就是物体在桌面上的物理位置通常用毫米或者归一化坐标表达。三个坐标系纠缠在一起时最保险的做法是约定一个“内部标准坐标系”所有数据进入 Unity 后立刻统一到它下面。坐标系原点位置Y 轴方向来源摄像头图像坐标左上向下OpenCV Mat 直接读取Windows 原始触摸坐标左上向下触摸屏驱动层Unity Input.touch.position左下向上Unity 封装后Unity Canvas 坐标取决于 CanvasScaler取决于设置UI 层我的约定是把“屏幕像素坐标”统一成左下原点。摄像头识别框算出来是左上原点就用y Screen.height - y翻转一次触摸屏拿到 Unity 的 Input 后本来就在左下原点省一次翻转。麻烦在于 Canvas 如果在用 CanvasScaler 拉伸适配UI 元素的 localPosition 不等于像素坐标换算时要除以 canvas.scaleFactor这个经常被忽略。4.2 四角标定与单应矩阵斜拍桌面的一次性校正摄像头斜装时桌面在画面里是一个梯形用线性缩放只能保证中心点勉强对齐四个角越偏越多。正确做法是求一个单应矩阵把图像平面映射到屏幕平面。OpenCV 的 GetPerspectiveTransform 需要四组对应点屏幕四个角在 Unity 里的已知像素坐标和摄像头画面里四个角对应的实际坐标。标定做法是让屏幕依次显示四个角标记点用颜色识别找到标记点的图像坐标再把四组点对传给 GetPerspectiveTransform。// srcPoints摄像头画面里四个角标的位置左上原点像素 // dstPoints屏幕四个角在 Unity 屏幕坐标里的位置左下原点像素 MatOfPoint2f src new MatOfPoint2f( new Point(camTopLeft.x, camTopLeft.y), new Point(camTopRight.x, camTopRight.y), new Point(camBottomRight.x, camBottomRight.y), new Point(camBottomLeft.x, camBottomLeft.y) ); MatOfPoint2f dst new MatOfPoint2f( new Point(screenTopLeft.x, screenTopLeft.y), new Point(screenTopRight.x, screenTopRight.y), new Point(screenBottomRight.x, screenBottomRight.y), new Point(screenBottomLeft.x, screenBottomLeft.y) ); Mat homography Imgproc.getPerspectiveTransform(src, dst);// 把摄像头里任意一个物体中心点映射到屏幕像素坐标 Vector2 MapCameraToScreen(Vector2 pCam, Mat homography) { double h00 homography.get(0, 0)[0]; double h01 homography.get(0, 1)[0]; double h02 homography.get(0, 2)[0]; double h10 homography.get(1, 0)[0]; double h11 homography.get(1, 1)[0]; double h12 homography.get(1, 2)[0]; double h20 homography.get(2, 0)[0]; double h21 homography.get(2, 1)[0]; double h22 homography.get(2, 2)[0]; double x h00 * pCam.x h01 * pCam.y h02; double y h10 * pCam.x h11 * pCam.y h12; double w h20 * pCam.x h21 * pCam.y h22; // 透视映射必须除以 w很多实现漏掉这一步 Vector2 pScreen new Vector2((float)(x / w), (float)(y / w)); return pScreen; }这段代码里的关键点在最后一行齐次坐标转回二维坐标要除以 w。短焦镜头下这个 w 偏离 1 不明显你可能侥幸没翻车一旦换广角镜头或者斜拍角度变大漏除 w 会带来整体偏移和边缘扭曲。标定做完后建议立刻做一次验证在屏幕上五个位置各放一个物体看识别框中心与真实位置的偏差。偏差在 15 像素以内算合格超过这个数优先怀疑角标识别精度而不是矩阵算法。4.3 触摸事件与识别结果融合命中判定和平滑跟随物体识别出来只是第一步演示项目里更常见的手法是物体放上去识别框出现然后用户去触摸那个物体系统弹出详情。识别框如果不做平滑处理会随着每一帧识别结果抖动视觉上非常廉价。我的做法是让 UI 框位置用指数平滑跟随目标坐标速度系数和帧率无关用 Time.deltaTime 计算这是处理“物体速度怎么获取”这类问题最常用的姿势不直接读物理速度而是用插值系数控制跟随响应。// 触摸命中识别框是左下原点坐标系touch.position 也是左下原点直接 Contains public bool HitTest(Rect screenRect, Vector2 touchPos) { return screenRect.Contains(touchPos); } // 平滑跟随指数插值帧率无关 float smoothK 10f; float t 1f - Mathf.Exp(-smoothK * Time.deltaTime); uiRect.position Vector2.Lerp(uiRect.position, targetScreenPos, t);smoothK 这个参数是调试里最常见的调节点。smoothK 越大跟随越快但抖动越明显越小越稳定但看起来反应迟钝。我的经验值是 8 到 12 之间静态放置时做到基本不抖移动物体时有轻微拖尾这个拖尾反而是好事让用户感觉系统在“跟踪”而不是“跳变”。触摸命中的 Rect 建议做一次膨胀把判定范围外扩 5 到 10 像素因为手指尖实际点击位置通常比视觉框边缘更靠里这也是提升主观体验的小技巧。5. 避坑触摸屏识别桌从原型到能演示的四个翻车现场下面四条都是真实项目里反复出现的踩坑记录每条按“现象、原因、解决”讲清楚。这些问题的共同点是单看代码都对合在一起就是不对。5.1 现象识别框在触摸屏上总是偏一只手的位置现象很容易描述摄像头识别到的物体位置和你手指按下的位置之间有一个固定的偏移横竖都可能偏而且偏移量不算小。原因通常是两个叠加摄像头斜拍产生透视变形没有用单应矩阵校正只做了线性缩放同时摄像头图像坐标是左上原点Unity 屏幕坐标是左下原点y 轴没翻转。这两个问题凑在一起中心点误差被放大四角误差更大。解决方法是统一坐标系约定把识别框输出统一到左下原点并且做一次四角标定求单应矩阵。如果标定做完还偏几个像素检查是不是 CanvasScaler 把像素坐标拉伸了别把 UI 的 localPosition 误当成屏幕像素坐标。5.2 现象物体放上去识别框乱跳边缘还全是噪点识别框在静止物体上反复横跳幅度几像素到几十像素多半是图像分割不稳定。原因有几种固定阈值对光照漂移敏感下午阳光斜射进来目标物体的 HSV 值整体偏移一部分区域被分到背景里去摄像头自动白平衡和自动曝光也会让颜色抖动台面上的反光会在物体边缘形成断裂。解决思路是分层处理先限制识别区域到桌面 ROI避开摄像头视野边缘的杂背景再用 HSV 双阈值加形态学开闭运算把边缘孔洞填上最后加面积和宽高比过滤把忽大忽小的手指和影子排除掉。还有一个后悔药是把摄像头自动曝光关掉固定曝光时间颜色分割会稳定一个量级。5.3 现象识别延迟 500ms 起触控和动画对不上物体放下去要半秒之后识别框才出现用户已经把手拿走了。这个延迟在 7x24 演示设备上是致命的体验像幻灯片。原因几乎总是性能堆积主线程做推理纹理上传占 GPU识别分辨率设成 1920x1080 全尺寸每帧都跑帧率被打到 15 以下。我的解决做法是把识别链路和渲染链路彻底分开。摄像头画面降采样到 640x360 再送识别识别频率限制 15 帧每秒用 Stopwatch 做节流推理如果放在 Unity 进程内至少要丢到 background 线程不要在 Update 里同步等结果。还要把触摸响应当作第一优先级用户手指按下瞬间先给 UI 一个反馈识别结果异步到再刷新识别框这样主观延迟感会大幅降低。5.4 现象换了一台触摸屏触摸坐标突然全反了同一套 Unity 工程在同一台主机上换了一块触摸屏y 轴直接反过来或者 x 轴也反。这是血泪经验里最折腾的坑。原因是不同触摸屏的 HID 驱动对坐标方向的定义不一致有的驱动在系统层就做了翻转有的没有Unity 拿到手的 Input.touch.position 自然就不一致。光靠代码里写死翻转是治标不治本的因为你不知道下一块屏会怎样。解决方法是做出厂校准工具应用起来后进入一个调试面板连续打印触摸原始坐标台面上放一个已知位置物体看反馈的触点是否在正确位置然后通过四个配置项“翻转 X、翻转 Y、X 偏移、Y 偏移”现场修正最后把配置写入 PlayerPrefs 或者本地配置文件。另外采购时先拿样机跑触摸采样比到货后和供应商扯皮省时间得多。6. 从演示到交付验证脚本、性能优化与模型剪枝的取舍能演示和能交付之间隔着一套可量化的验收标准。识别桌这类设备一放就是一天用户手上有汗、桌面有反光、现场灯管老化这些都是实验室里看不见的变量。我一般会做一张验收记录表跑三个维度准确率、延迟、稳定性。准确率按物体类别各放 20 次共 100 次统计识别正确率和框中心与真实触摸位置的像素偏差延迟记录从物体放上到高亮出现的耗时分别看 p50 和 p95p50 小于 200ms 算合格p95 不能超过 500ms稳定性是连续开机 72 小时每 8 小时自动跑一轮识别自检记录有没有帧率下跌、内存增长和触摸失灵。交付时把这张表打出来比口头保证“挺稳的”有说服力得多。性能优化上最大的收益来自架构而不是代码微调。识别进程独立后Unity 侧只拿结果数组更新 UI主线程压力几乎为零这是触摸屏一体机上最值得做的一项改造。模型文件放 StreamingAssets避免打进 Unity 包体这是包体优化里最常见的一招对迭代友好换模型不用重新出包。模型本身如果要在低端集成显卡上跑就用 YOLOv8n 这种轻量版本再配合通道剪枝算法把冗余通道剪掉控制在 20% 到 30% 的剪枝比例在验证集上复测 mAP 掉点不超过 2% 就可以接受推理速度通常能再快 30% 到 50%。我现在做识别桌第一步永远是先写触摸采样和摄像头标定工具再谈识别模型。硬件坐标没对齐之前算法再好都是空中楼阁。这个顺序帮我避开了大半的返工希望也能帮到你。本文还有配套的精品资源点击获取