ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity+C#打造触摸屏物体识别桌:从颜色识别到坐标映射

2026/10/8 8:58:39 拓冰建站 浏览量
Unity+C#打造触摸屏物体识别桌:从颜色识别到坐标映射 简介面向 Unity C# 开发者和互动桌/AR/VR 项目人员这份工程源码包演示了如何利用 Lean Touch 插件在触摸屏上稳定识别并交互场景物体。压缩包内含 3640 个文件以 220 个 C# 脚本、106 个动态库为主体配合预制体、场景、材质、着色器与说明文档另有大量 info、meta、asset 等资源管理文件可直接在 Unity 中打开运行整体大小约 24.54MB。核心逻辑覆盖 LeanSelectable 触摸监听、屏幕坐标转世界坐标、Physics.Raycast 射线检测、UI 触摸响应以及通过层级过滤提升命中性能的优化方法。对于需要实现互动桌面、展览展示或触摸识别功能的学习者除了可直接复用的脚本还能从工程结构中理解单指/多指事件与物体选择的关联方式掌握如何为交互物体添加选中状态、隔离无效射线区域等细节无论是快速搭建互动原型还是深入理解触摸射线检测与事件分发原理都能获得可直接运行的参考代码与排错视角。资源目前已有 2442 人学习下载适合从入门到进阶的 Unity 开发人员作为参考工程使用。1. 触摸屏物体识别桌Unity 与 C# 断不了的那根视觉引线一个普通摄像头俯拍桌面桌上放一个红色积木屏幕上立刻弹出关于积木的动画手指一点还能拖拽缩放。这个场景不是科幻片而是博物馆互动桌、儿童教育桌、餐饮智能桌的常见玩法。这套触摸屏物体识别桌算法核心就是用 Unity 和 C# 把“视觉识别”和“触摸交互”串成一条线摄像头识别物体的像素坐标经坐标映射换算成桌面坐标再叠加 Unity 的触摸响应。资源适合想做互动桌原型却不想从零啃图像处理的 Unity 开发者也适合给展览、产品展示做快速验证。我拆完第一感受是识别本身不难真正坑人的是坐标映射和光照。2. 先从颜色识别开始HSV 过滤与轮廓质心算法拆解2.1 为什么选 HSV把“颜色”从光照里拆出来做物体识别桌第一件事是“看见”物体。最简单可靠的一类方案是颜色识别而颜色识别里又分 RGB 和 HSV 两条路。RGB 的致命问题是三个通道一起受亮度影响识别红色时你要写R G R B可白天窗口光一强整个画面被洗白R、G、B 同时升高这个条件瞬间失效晚上灯光一变暗三个值一起降红色又变成一团黑。HSV 模型把“色相 H”“饱和度 S”“明度 V”拆开色相对光照变化相对稳定红色在 H 通道 0 附近不管明暗怎么变只要物体确实是红色H 基本不会漂。所以做触摸桌识别我一般第一版就优先用 HSV 做阈值分割。OpenCV for Unity 里的 HSV 范围和 Unity 自带的Color.HSVToRGB完全不同OpenCV 的 H 是 0~179S、V 是 0~255而 Unity 的 H 是 0~1S、V 也是 0~1。很多新手把 Unity 的 Hue 0.0f~1.0f 直接塞给Scalar结果 mask 永远全黑。这个单位换算的坑我见过不止一次。所以在写InRange之前最好先写一个工具函数把前端传进来的 0~1 范围参数转成 OpenCV 的 0~179 和 0~255。private Scalar ToOpenCvHsv(float h, float s, float v) { return new Scalar(h * 179f, s * 255f, v * 255f); }这个函数没有技术含量但能把参数面板上的单位统一成设计师习惯的 0~1 或 0~180现场调试时少被换算折腾。参数最终还是会写成 OpenCV 的整数范围放在配置文件里。2.2 InRange 与 FindContours最小可行的识别回路HSV 过滤之后得到一张黑白 mask白色区域就是目标颜色。接下来在 mask 上找轮廓再通过轮廓面积过滤掉噪声最后用图像矩计算质心。这套流程我拆出来的实现大致长这样public ListDetection Detect(Mat frame) { Mat blurred new Mat(); Cv2.GaussianBlur(frame, blurred, new Size(5, 5), 0); // 先降噪 Mat hsv new Mat(); Cv2.CvtColor(blurred, hsv, ColorConversionCodes.BGR2HSV); Mat mask new Mat(); Cv2.InRange(hsv, _lower, _upper, mask); Mat morph new Mat(); Mat kernel Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3)); Cv2.MorphologyEx(mask, morph, MorphTypes.Open, kernel); Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(morph, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); ListDetection results new ListDetection(); foreach (Point[] contour in contours) { double area Cv2.ContourArea(contour); if (area _minArea) continue; Moments m Cv2.Moments(contour); if (m.M00 0) continue; float cx (float)(m.M10 / m.M00); float cy (float)(m.M01 / m.M00); results.Add(new Detection(cx, cy, (float)area)); } return results; }先对原图做GaussianBlur目标是把传感器噪点压下去否则后续轮廓数量可能成百上千。InRange的输出是二值图MorphologyEx用开运算把小白斑去掉同时保留大面积目标形态。FindContours用External模式只拿最外层轮廓少算内环ApproxSimple用最少的点描述轮廓质心计算不受影响。后面Moments的 M10/M00 和 M01/M00 就是轮廓质心的横纵坐标。这段代码里最容易翻车的是忘掉MorphologyEx摄像头噪点稍大mask 上就会出现很多针孔一个针孔就是一个轮廓面积过滤又挡不住小噪点最后结果里都是闪烁的碎片。2.3 参数表H/S/V 与 minArea 怎么定参数不是拍脑袋定的。以最常见的红色圆球为例实测比较稳的参数组合是H 取 0~10 和 170~179 两个区间因为红色在 HSV 色环上跨越了 0 的两侧S 取 100~255把偏灰的浅红排掉V 取 80~255避免暗红色被当成背景。minArea我用的是相对值而不是绝对值比如画面总像素的 0.1%这样摄像头远近变化时不用重新调。如果识别蓝色物体H 在 100~130 之间S 和 V 保持 100 以上。参数红色球参考值蓝色球参考值说明H Min0 和 170100OpenCV H 范围 0~179H Max10 和 179130红色要合并两个区间S Min100100低于这个值颜色太灰V Min8080低于这个值偏黑minArea0.1% 画面0.1% 画面用比例避免距离影响morphSize33开运算内核大小现场调试时不要直接改代码把 H、S、V 做成运行时 Slider一边拖一边看 mask 预览。这个习惯能省下大量改代码重编译的时间。我一般先固定 V 和 S只拖 H 找色相找到主色相区后再慢慢收窄 S 和 V最后才调 minArea直到画面里只留下目标物体。3. 坐标映射才是触摸桌的灵魂像素坐标到桌面世界坐标3.1 四角标定与单应矩阵一个点怎么跨坐标系摄像头俯拍桌面识别出来的质心坐标是“图像像素坐标”比如 (312, 240)。可触摸桌真正需要的是“桌面物理坐标”比如这个物体位于桌面左上角往右 40cm、往下 25cm 的位置。如果不做映射直接拿像素坐标当 UI 坐标用画面里看起来是圆的放到投影坐标上就歪了尤其是镜头斜着装或桌面不是矩形时误差更明显。如果桌面是正四边形且摄像头严格垂直俯拍线性映射就够了图像中的 dx 对应桌面上的 dx比例固定。但实际安装时摄像头总有几度倾斜甚至桌面上放着抬高的托盘透视变形会让线性映射在边缘产生几厘米偏差。比如桌面上 10cm 等距的三个点在斜视角摄像头下图像间距可能是 48 像素、52 像素直接乘系数算出的桌面距离前后不一。单应矩阵能把透视变形也一起纠正掉。最稳的标定方法是四角标定在桌面放四个标记点比如四张 A4 纸的角依次按“左上、右上、右下、左下”的顺序点选或自动识别得到四个图像坐标同时量出它们在桌面坐标系下的坐标然后用单应矩阵把两个平面连接起来。OpenCV 的GetPerspectiveTransform输入四个点对就能算出 3×3 单应矩阵 H之后任意图像点都能用PerspectiveTransform映射到桌面坐标。Point2f[] src new Point2f[] { new Point2f(152f, 128f), // 图像中的左上角 new Point2f(538f, 126f), // 图像中的右上角 new Point2f(552f, 392f), // 图像中的右下角 new Point2f(148f, 396f) // 图像中的左下角 }; Point2f[] dst new Point2f[] { new Point2f(0f, 0f), // 桌面左上角设为原点 new Point2f(800f, 0f), new Point2f(800f, 500f), new Point2f(0f, 500f) }; Mat H Cv2.GetPerspectiveTransform(src, dst);这里有个必须强调的细节四个点的顺序必须完全一致你写 src 时用左上、右上、右下、左下dst 也必须按相同顺序写。如果 src 是顺时针写dst 是逆时针写算出来的矩阵会把桌面映射成一个交叉的翻转平面触摸点和识别物体永远对不上。代码里我把桌面坐标又直接写成像素单位 800×500这样跟 UI 层对接方便如果你要接真实物理单位可以把 dst 写成毫米效果一样。映射一个识别点时Point2f[] input new Point2f[] { new Point2f(cx, cy) }; Point2f[] output Cv2.PerspectiveTransform(input, H); float worldX output[0].X; float worldY output[0].Y;为什么不直接WarpPerspective把整张图矫正因为识别质心只需要一个点PerspectiveTransform只做一个点乘几乎零开销而矫正整张图是把交给 GPU/CPU 的活又拿回来在触摸桌这种实时交互场景里很不划算。标定点采集我一般做成 Unity 编辑器工具打开一个标定面板把 RawImage 显示的画面放大鼠标点击四个标记点自动写入 srcPoints桌面坐标直接在面板里填数字。算完 H 后把鼠标移到画面上看小球看是否贴合不贴合就重新点。注意点顺序每点一个在画面里画一个编号避免自己记混。3.2 触摸屏坐标与摄像头坐标的融合姿势摄像头识别物体只是工作的一半触摸桌还要响应人的手指。这里牵扯到两套坐标系触摸屏产生的触摸点在 Unity 里是屏幕坐标左下角原点往上增长摄像头出的是图像坐标左上角原点。两套原点方向不同直接套用 H 矩阵会整体上下颠倒。常见做法是做一次 Y 翻转把触摸点先转成图像坐标系再套同一个 H 映射。int camWidth 640; int camHeight 480; if (Input.touchCount 0) { Touch t Input.GetTouch(0); float tx t.position.x / Screen.width * camWidth; float ty camHeight - t.position.y / Screen.height * camHeight; Point2f[] touchImg new Point2f[] { new Point2f(tx, ty) }; Point2f[] touchWorld Cv2.PerspectiveTransform(touchImg, H); }这里的关键是ty camHeight - ...这一行。Unity 触摸屏坐标 Y 轴朝上摄像头图像 Y 轴朝下如果不翻转触摸点会跑到镜像位置。翻转之后触摸点的坐标系规则就和摄像头图像一致了。还有一个选择如果你让摄像头画面在屏幕上是“镜像显示的”那触摸点反而不需要翻转但那样识别物体的拖拽方向也会镜像非常别扭。我的建议是全程保持非镜像只在触摸点进来时做一次翻转。如果摄像头画面没有全屏而是放在一个固定区域触摸点转图像坐标时要先算出 RawImage 在屏幕上的 Rect你只需要把触摸点相对于 RawImage 左下角的位置映射到图像分辨率而不是用全屏比例。RectTransform rt rawImage.rectTransform; Vector2 localPos; RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, t.position, null, out localPos); float px (localPos.x rt.rect.width / 2f) / rt.rect.width * camWidth; float py camHeight - (localPos.y rt.rect.height / 2f) / rt.rect.height * camHeight;用ScreenPointToLocalPointInRectangle拿到的 localPos 是相对锚点的局部坐标需要对半才能转成左下角起点。这个写法在 RawImage 不是全屏、画面上有标题栏或黑边时特别有用比粗暴地用Screen.width换算准得多。触摸点和物体识别结果都映射到桌面世界坐标后交互就顺了物体质心坐标生成 UI 元素触摸点坐标用来判断手指是否按在物体附近。距离小于一个阈值比如 UI 上的 50px就判定为选中随后把触摸位移喂给物体实现拖拽。这里注意物体识别和触摸是两个并行通道识别结果通常跑 30 FPS触摸屏可能跑 60 FPS需要给识别结果做一次缓冲不然后续帧率不同步。4. 工程复现资源包文件结构、场景搭建与参数面板4.1 资源包文件清单与各自职责资源包的核心文件按职责拆分成五个脚本我以表格列出脚本文件主要职责公开参数CameraCapture.cs打开摄像头、把 Texture2D 转成 Mat、逐帧提供图像deviceIndex,requestWidth,requestHeightHsvFilter.cs把 BGR 转 HSV生成 mask对外暴露 H/S/V 范围hMin,hMax,sMin,sMax,vMin,vMaxObjectDetector.cs找轮廓、算质心和面积输出ListDetectionminAreaRatio,morphSize,blurSizeCoordinateMapper.cs四角标定、求单应矩阵、把图像点映射到桌面坐标dstWidth,dstHeight,srcPointsInteractionManager.cs接收触摸点、判断选中、拖拽物体、触发事件selectRadius这五个脚本的依赖方向是单向的CameraCapture 喂给 HsvFilterHsvFilter 喂给 ObjectDetectorObjectDetector 的结果和 InteractionManager 里的触摸点一起交给 CoordinateMapper。不要写成互相来回引用的上帝脚本否则现场换摄像头或者改坐标映射时你会被迫拆一堆耦合逻辑。场景里还需要一个 RawImage 用来显示摄像头画面一个 Canvas 用来叠加 UI 物体以及一个空物体挂InteractionManager接收触摸事件。Canvas 的渲染模式建议用 Screen Space - Overlay因为触摸点直接就是屏幕像素坐标不需要考虑相机投影。RawImage 作为画面的可视层本身不接收射线raycastTarget要关掉不然它会拦截 Canvas 上的点击触摸事件会被 UI 吃掉。这是一个很隐蔽的坑。4.2 场景搭建步骤从摄像头到 RawImage 再到交互层第一步新建 Unity 工程导入 OpenCV for Unity 插件。第二步创建一个CameraRig空物体挂CameraCapture.cs把 RawImage 动态填进去这样摄像头画面能实时显示。第三步挂HsvFilter.cs和ObjectDetector.cs用同一个 Mat 走通“取帧—过滤—识别”。第四步挂CoordinateMapper.cs录入四角标定坐标。第五步创建 Canvas往里面放一个空物体作为交互层挂InteractionManager.cs。void InitCamera() { WebCamDevice[] devices WebCamTexture.devices; _webcamTexture new WebCamTexture(devices[_deviceIndex].name, 640, 480, 30); _webcamTexture.Play(); _rawImage.texture _webcamTexture; _rawImage.uvRect new Rect(0, 0, 1, 1); }WebCamTexture拿到的是 YUV 或 RGB 数据直接转成 Mat 时要注意通道顺序。OpenCV 默认 BGR而WebCamTexture拿到的是 RGB如果直接Cv2.CvtColor到 HSV 会偏色。正确做法是先转成TextureFormat.RGB24的 Texture2D把像素拷贝成byte[]再交给Cv2.ImDecode或Cv2.CvtColor然后调一次ColorConversionCodes.RGB2HSV。我见过有人不改通道顺序红色球被识别成黄色调了半天 HSV 参数都没用最后发现是通道顺序反了。摄像头分辨率我建议锁在 640×480而不是追求 1080p。识别一个桌面上 5~10cm 的物体VGA 分辨率足够把分辨率抬高到 1080p处理耗时翻两三倍但识别精度提升极其有限。触摸桌首要目标是低延迟不是照片级清晰度。选择支持手动曝光的 USB 摄像头部署时优先锁死曝光。如果只能用笔记本内置摄像头也要在系统相机设置里尽量关掉自动增强否则 HSV 参数会反复漂移。4.3 可调参数面板现场校准不再靠玄学识别参数黑白盒调试完全我习惯在场景右下角放一排 SliderH Min、H Max、S Min、S Max、V Min、V Max、Min Area。每个 Slider 直接绑定到对应的识别脚本属性在 OnValueChanged 里赋值。这样现场换一个颜色的球拖几分钟 Slider 就能锁定新参数。public class ParameterPanel : MonoBehaviour { public HsvFilter hsvFilter; public ObjectDetector detector; public Slider hMinSlider; public Slider hMaxSlider; public Slider sMinSlider; void Start() { hMinSlider.onValueChanged.AddListener(v hsvFilter.hMin Mathf.RoundToInt(v)); hMaxSlider.onValueChanged.AddListener(v hsvFilter.hMax Mathf.RoundToInt(v)); sMinSlider.onValueChanged.AddListener(v hsvFilter.sMin Mathf.RoundToInt(v)); } }参数面板只是调试工具最终要存成 JSON。用 Unity 的 JsonUtility 写一个 ParamData 类启动时读取如果文件不存在就用默认值。这样一来现场人员不需要懂代码只需在面板上拖好参数后点保存下次开机自动加载。部署多个场地时每个场地一份配置文件互不干扰。JSON 的序列化很简单[System.Serializable] public class ParamData { public int hMin, hMax, sMin, sMax, vMin, vMax; public float minAreaRatio; public float selectRadius; }但注意 H 范围在红色时是双区间单靠一个 Slider 只覆盖 0~10 一侧170~179 的另一侧还得写死。我一般把hMax拆成两个当hMax hMin时自动拆成两段区间合并 mask。Label 上的数值用 0~180 的整数方便现场口述记录。拖完参数记得按一次“保存”否则下次启动又是默认值之前的调参就白费了。5. 避坑指南光照、坐标系与性能的翻车现场5.1 识别框抖得像帕金森先锁曝光再滤波现象物体明明静止屏幕上的识别框却在几个像素到十几个像素之间来回跳动严重时质心坐标输出是一条噪声曲线。原因两件事叠加。一是摄像头自动曝光和自动白平衡在慢速变化同一块颜色在不同帧的 HSV 值不完全一致二是 mask 边缘的噪点导致轮廓面积和质心微微摆动。解决先把相机手动曝光、手动白平衡锁死。WebCamTexture 没有直接暴露曝光控制但多数摄像头驱动允许通过相机属性设置锁不了就用物理遮挡降低光照波动。然后在质心输出端做一次指数平滑filteredCx Mathf.Lerp(filteredCx, rawCx, 0.3f); filteredCy Mathf.Lerp(filteredCy, rawCy, 0.3f);0.3f越大跟手越强但越抖越小越稳但延迟越高。触摸桌这种交互场景我一般取 0.25~0.35既不过于漂移也不至于完全不听使唤。不要迷信“卡尔曼滤波万能”很多场景下最简单的 Lerp 已经够用。5.2 触摸点和识别物体隔着一个“身位”Y 轴方向的锅现象手指按在屏幕上的物体上物体没有响应手指点在物体上方一段距离反而选中了物体。原因Unity 的触摸坐标原点在左下角OpenCV 的图像坐标原点在左上角。你拿触摸点直接套单应矩阵等于把触摸点当成了图像坐标Y 轴正好相反结果触摸映射位置和真实位置上下颠倒。解决触摸转图像坐标时必须做ty camHeight - ty。同时检查单应矩阵标定时四角点的顺序是不是从左上开始顺时针记录。一旦顺序错了不只是镜像可能还会旋转 90 度或产生交叉映射那个离谱程度更不好排查。float ty camHeight - t.position.y / Screen.height * camHeight;5.3 帧率暴跌别再在 Update 里同步跑 Cv2现象接上摄像头后 Unity 编辑器从 60 FPS 掉到 15 FPS拖拽物体时明显卡顿。原因Cv2的 Mat 操作是 CPU 密集型的而且同步方法会阻塞主线程。在 Update 里直接调用Detect一帧要花 20~50msUnity 主线程被占住什么都不顺。解决把识别放到Task.Run里跑只在主线程更新需要显示的结果。注意 Mat 不能在子线程和主线程之间直接共享我的做法是把WebCamTexture的像素先拷贝成byte[]在子线程做Cv2.ImDecode或Cv2.CvtColor返回的只是一个包含质心坐标和面积的普通结构体再回到主线程更新 UI。async TaskDetection DetectAsync(byte[] rgbPixels, int width, int height) { return await Task.Run(() { Mat src new Mat(height, width, MatType.CV_8UC3); Marshal.Copy(rgbPixels, 0, src.DataPointer, rgbPixels.Length); Cv2.CvtColor(src, src, ColorConversionCodes.RGB2BGR); // 复用 HsvFilter / ObjectDetector 的逻辑 return detection; }); }Unity 中WebCamTexture主线程访问不受限但拷贝像素要在主线程完成识别任务丢到后台线程后UI 显示依然流畅。Task.Run的代价是每帧新建任务和线程切换对于触摸桌这种 30 FPS 完全够用没必要上复杂的并发模型。如果你在子线程里用了 Mat记得在Task.Run结束时Dispose否则内存会被吃到爆。5.4 小物体识别不到面积过滤与形态学处理现象拳头大的球能识别手指头大小的积木偶尔消失或者 A4 纸边缘反光被当成识别结果。原因minArea用了绝对像素值物体离摄像头远时面积急剧缩小小于阈值就被过滤掉同时反光区域在 HSV 里 S 值低很容易窜到其他颜色的区间。解决minArea改成相对画面比例比如maxArea frameWidth * frameHeight * 0.001f随分辨率自适应。对 mask 先做开运算再做一个闭运算把目标内部的孔洞填掉避免同一物体被FindContours拆成多块。float minArea frame.Cols * frame.Rows * 0.0008f; Cv2.MorphologyEx(mask, morph, MorphTypes.Close, kernel);闭运算对红色球这类实心目标很有效。如果目标表面有纹理或印刷图案同一色块会被纹理打断闭运算能补上细缝。但内核过大也会把小目标直接抹没morphSize从 3 开始调别一上来就 11×11。5.5 阴影和反光被当成目标加一道形状过滤现象目标物体周围出现几个次级目标质心不稳定甚至把影子识别进来。原因强光下物体在地面的投影色相与物体相近但饱和度和亮度偏低高光反射区域则在物体内部形成空洞让轮廓变成月牙形质心也被带偏。解决在 HSV 过滤后对 mask 做一遍形状过滤计算轮廓的圆形度4π*area/perimeter^2小于 0.5 的直接丢弃。圆形度过滤对规则小球非常有效因为影子的形状通常很碎圆形度不高。同时把 V 的最小值抬高比如从 80 调到 100能把偏暗的阴影边缘排掉。double perimeter Cv2.ArcLength(contour, true); double circularity 4 * Math.PI * area / (perimeter * perimeter); if (circularity 0.5) continue;如果目标本身不规则比如积木、玩偶圆形度过滤就不能用了。那种场景我一般改成轮廓凸包面积占比凸包面积和轮廓面积比接近 1说明轮廓饱满属于真实物体阴影往往细长占比会很低。这个指标比圆形度更宽容适合桌面常见的拼块和卡片。6. 进阶技巧多目标识别与误差自检6.1 多目标排队给每个物体一个 ID单目标识别只是起步实际桌面通常同时放好几个物体。最简单有效的多目标方案不是上目标跟踪网络而是做最近邻匹配当前帧所有轮廓质心和上一帧的质心一一比较距离小于某个阈值就认为是同一个物体继承上一帧的 ID没有匹配上的就分配新 ID。这个方案在 30 FPS 下已经足够稳。ListDetection current detector.Detect(frame); foreach (Detection curr in current) { Detection prev FindNearest(previous, curr, 30f); if (prev ! null) { curr.id prev.id; } else { curr.id _nextId; } } previous current;30f 这个阈值要跟分辨率挂钩640×480 下物体每帧位移一般不超过 20~30 像素你手速再快也难在一帧内飞出屏幕。ID 稳定后拖拽交互就可以只认 ID不认坐标避免切换物体时 UI 突然跳走。多点同时拖拽则需要把识别结果和触摸点都按 ID 分组复杂度会上一档但原理一样。如果场景里物体移动很快或者互相遮挡再考虑卡尔曼预测位置但这是后话。6.2 验证流程一张网格纸量化标定误差坐标映射到底准不准不要靠肉眼“看着差不多”要有一个可量化的验证方法。我每次部署触摸桌都打一张 A4 网格纸每个格子代表 10cm平铺在桌面上。启动识别后把网格交叉点一个个放上去记录识别输出的世界坐标和真实网格坐标的差整理成一张表。误差超过 1cm 就优先怀疑标定点顺序和触摸点 Y 翻转误差集中在边缘则说明单应矩阵采样点不够中间区域准而边缘漂可以考虑采集四角以外的点用FindHomography做多点拟合。输出坐标用 Unity 的Debug.Log和File.WriteAllText写到 CSV现场改完参数后快速扫一遍误差趋势比反复目测准得多。我自己的习惯是每次启动项目后先跑一遍网格标定误差表格不删留着跟下一次对比。这样一来光是判断“这次的光照是不是让识别漂了”就不需要猜了。从那以后我遇到类似触摸桌项目第一件事永远是拿网格纸和标定流程先走一遍确认坐标系和映射矩阵没问题再去调颜色参数和交互玩法。这个顺序帮我避开了至少一半的“玄学问题”。希望帮到你。本文还有配套的精品资源点击获取