Unity AR项目实战:OpenCVSharp实时摄像头轮廓检测全流程
1. 项目概述与核心价值
最近在做一个Unity的AR项目,需要实时处理摄像头画面,提取画面中特定物体的轮廓。一开始想用Unity自带的图像处理API,但试了试发现功能太基础,稍微复杂一点的边缘检测和轮廓分析做起来非常吃力。后来把目光投向了OpenCV,这个计算机视觉领域的“瑞士军刀”。但Unity是C#环境,直接调用OpenCV的C++库很麻烦,直到发现了OpenCVSharp这个宝藏——它是一个C#封装的OpenCV库,能和Unity无缝集成。今天这个教程,就是把我从环境搭建、插件导入、代码编写到最终实现摄像头实时轮廓检测的全过程,毫无保留地分享出来。整个过程踩了不少坑,也总结了很多能让代码跑得更稳、效果更好的技巧,我会在每一步都详细说明。无论你是想做人机交互、AR滤镜、智能监控还是简单的图像处理实验,这套方案都能给你一个扎实的起点。教程里附带了完整的、可直接运行的C#脚本,你完全可以“抄作业”拿去用。
2. 环境准备与核心工具选型
2.1 Unity版本与OpenCVSharp插件选择
首先说环境。我用的Unity版本是2021.3 LTS,这是一个长期支持版,比较稳定。理论上2019.4及以后的版本都兼容,但为了避免一些未知的DLL依赖问题,建议使用较新的LTS版本。
核心是OpenCVSharp。这里有个关键点:我们不能直接用NuGet上的OpenCVSharp,因为那主要是给Windows桌面应用或控制台程序用的。Unity需要的是针对其运行时(尤其是跨平台需求)编译好的版本。我推荐使用GitHub上“shimat/opencvsharp”仓库的Releases页面中,专门为Unity预编译的包,文件名通常类似opencvsharp-4.8.0-unitypackage。这个包里面已经包含了针对Windows、macOS、Android、iOS等平台编译好的原生库(OpenCV的DLL或.so文件)以及C#封装层,开箱即用,省去了自己编译的麻烦。
注意:一定要下载带“unitypackage”后缀的。直接导入NuGet的包,在Unity编辑器里可能能跑,但打包到移动端(如Android)百分百会报“DllNotFoundException”。
2.2 项目初始化与插件导入
- 创建新项目:打开Unity Hub,创建一个新的3D项目(URP或Built-in渲染管线均可,本教程不涉及特殊着色器)。
- 导入OpenCVSharp:在Assets文件夹上右键,选择
Import Package > Custom Package...,找到你下载的.unitypackage文件,点击打开。在导入窗口中,确保所有文件都被勾选,然后点击Import。导入后,你的Project窗口里应该会出现OpenCVSharp和OpenCvSharpExample等文件夹。 - 验证导入:为了快速验证环境是否正常,你可以打开
OpenCvSharpExample文件夹下的示例场景(如果有的话)运行一下。更直接的方法是,我们马上自己写个测试脚本。
2.3 核心依赖与前置知识
这个项目主要依赖两个核心:
- Unity的WebCamTexture类:这是Unity用于捕获摄像头视频流的原生类。它使用起来非常简单,能直接获取到摄像头拍摄的每一帧图像数据,并以
Texture2D的形式提供给我们。 - OpenCVSharp库:我们将用它来处理
Texture2D转换而来的图像矩阵(Mat对象),进行灰度化、模糊、边缘检测和轮廓查找等一系列操作。
你需要对C#有基本了解,并且最好知道一点图像处理的基础概念,比如什么是灰度图、什么是高斯模糊、什么是Canny边缘检测。不过别担心,即使不清楚,跟着我的步骤和代码注释,你也能完全理解每一步在做什么。
3. 核心流程与代码架构设计
3.1 整体工作流拆解
整个实时轮廓检测的流程,可以抽象为以下几个核心步骤,它们会在每一帧循环执行:
- 捕获帧:使用
WebCamTexture获取当前摄像头画面的一帧,存为一个Texture2D。 - 格式转换:将Unity的
Texture2D转换为OpenCVSharp能处理的Mat对象。这里涉及到颜色通道顺序的转换(Unity通常是RGBA,OpenCV默认是BGR)。 - 图像预处理:对
Mat进行预处理,为轮廓检测做准备。这通常包括:- 转为灰度图:减少计算量,将三通道彩色图转为单通道灰度图。
- 应用模糊:使用高斯模糊或中值模糊来平滑图像,减少噪声和微小细节对边缘检测的干扰。
- 边缘检测:使用Canny算法在预处理后的灰度图上找出明显的边缘。Canny算法会输出一个二值图像(黑白图),其中白色线条代表检测到的边缘。
- 查找轮廓:在Canny算法输出的二值图像上,使用
FindContours函数找出所有闭合或开放的边缘,即轮廓。每个轮廓由一系列点(Point)组成。 - 轮廓筛选与绘制:对所有找到的轮廓进行筛选(比如根据面积、周长过滤掉太小的噪点),然后将筛选后的轮廓在原图上绘制出来。
- 渲染回Unity:将绘制好轮廓的
Mat对象,再转换回Unity的Texture2D,并赋值给一个RawImageUI组件或Material的贴图,从而在屏幕上实时显示处理结果。
3.2 单例管理器设计模式
对于这种需要全局管理摄像头、图像处理流程的项目,我强烈建议使用一个单例模式的管理器类。这样做的好处是:
- 访问方便:在任何脚本中都可以通过
ContourDetectionManager.Instance来调用核心方法。 - 生命周期可控:可以集中管理摄像头的初始化、开始、停止和释放。
- 避免资源泄漏:确保在游戏退出或场景切换时,能正确释放摄像头和OpenCV相关的资源。
我们的核心脚本ContourDetectionManager就将采用这种设计。它会负责协调WebCamTexture、图像处理管道以及最终的画面渲染。
4. 详细实现步骤与代码解析
4.1 步骤一:创建UI与管理器脚本
- 在Unity场景中创建一个UI Canvas。
- 在Canvas下创建两个
RawImage组件:RawImage_Source:用于显示原始的摄像头画面。RawImage_Processed:用于显示经过OpenCV处理并绘制了轮廓的画面。
- 创建一个空的GameObject,命名为
ContourDetectionManager,并挂载我们即将编写的同名C#脚本。
4.2 步骤二:编写轮廓检测管理器核心脚本
以下是ContourDetectionManager.cs的完整代码,我会分段进行详细解释。
using UnityEngine; using UnityEngine.UI; using OpenCvSharp; using System.Collections.Generic; public class ContourDetectionManager : MonoBehaviour { // 单例实例 public static ContourDetectionManager Instance { get; private set; } // UI引用 [Header("UI References")] public RawImage rawImageSource; public RawImage rawImageProcessed; // 摄像头控制 [Header("Camera Settings")] public int requestedWidth = 640; public int requestedHeight = 480; public int requestedFPS = 30; private WebCamTexture _webCamTexture; private bool _isCamAvailable = false; // OpenCV 处理相关变量 private Mat _sourceMat; private Mat _processedMat; private Mat _grayMat; private Mat _blurMat; private Mat _cannyMat; // 轮廓检测参数(可在Inspector中调整) [Header("Contour Detection Parameters")] [Range(0, 255)] public double cannyThreshold1 = 50; // Canny低阈值 [Range(0, 255)] public double cannyThreshold2 = 150; // Canny高阈值 public int apertureSize = 3; // Canny算子孔径大小,必须是奇数 [Range(0, 255)] public int blurKernelSize = 5; // 高斯模糊核大小,必须是奇数 public double contourAreaThreshold = 500.0; // 轮廓面积过滤阈值,小于此值的轮廓将被忽略 // 绘制轮廓参数 [Header("Contour Drawing Parameters")] public Scalar contourColor = new Scalar(0, 255, 0); // 轮廓颜色 (B, G, R) public int contourThickness = 2; void Awake() { // 实现单例模式 if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); // 如果需要跨场景,则保留 } else { Destroy(gameObject); } } void Start() { InitializeWebCam(); InitializeMats(); } void Update() { if (!_isCamAvailable || _webCamTexture == null || !_webCamTexture.isPlaying) return; // 核心处理流程:每一帧都执行 ProcessFrame(); } void OnDestroy() { // 释放所有OpenCV的Mat资源,防止内存泄漏 ReleaseMats(); if (_webCamTexture != null && _webCamTexture.isPlaying) { _webCamTexture.Stop(); } } }代码解析(第一部分:基础框架):
Instance:实现了简单的单例模式,确保全局只有一个管理器。WebCamTexture _webCamTexture:Unity用于捕获摄像头视频流的对象。Mat对象:这是OpenCVSharp的核心数据结构,代表一个图像矩阵。我们声明了多个Mat用于存储处理过程中的中间结果(源图像、灰度图、模糊图、Canny结果等),这是一种常见的优化手段,避免在每帧都new新的对象,减少GC(垃圾回收)压力。- 参数公开:将Canny阈值、模糊核大小、轮廓面积阈值等关键参数暴露为
public变量,并加上[Range]或[Header]属性。这样我们就可以在Unity编辑器的Inspector窗口中实时拖动滑块来调整效果,非常方便调试。 Awake,Start,Update,OnDestroy:标准的Unity生命周期函数,用于初始化、每帧更新和清理资源。
4.3 步骤三:初始化摄像头与Mat对象
接下来,我们在类中添加初始化方法。
private void InitializeWebCam() { // 检查设备是否有摄像头 if (WebCamTexture.devices.Length == 0) { Debug.LogError("No camera devices found!"); _isCamAvailable = false; return; } // 通常使用第一个摄像头(后置),你可以根据需要选择 WebCamDevice selectedDevice = WebCamTexture.devices[0]; Debug.Log($"Selected Camera: {selectedDevice.name}"); // 创建WebCamTexture _webCamTexture = new WebCamTexture(selectedDevice.name, requestedWidth, requestedHeight, requestedFPS); // 将原始纹理显示在第一个RawImage上 if (rawImageSource != null) { rawImageSource.texture = _webCamTexture; } // 开始播放摄像头 _webCamTexture.Play(); _isCamAvailable = true; // 等待几帧让摄像头完全启动 StartCoroutine(WaitForCameraStart()); } private System.Collections.IEnumerator WaitForCameraStart() { yield return new WaitForSeconds(0.5f); // 等待0.5秒 // 可以在这里检查_webCamTexture.width是否大于10,以确保摄像头数据已就绪 if (_webCamTexture != null && _webCamTexture.width > 10) { Debug.Log($"Camera started. Resolution: {_webCamTexture.width}x{_webCamTexture.height}"); } else { Debug.LogWarning("Camera may not have started properly."); } } private void InitializeMats() { // 注意:这里Mat的尺寸先随意创建,在ProcessFrame中会根据实际纹理尺寸重新创建或调整大小 // 提前创建是为了避免在Update循环中频繁分配内存 _sourceMat = new Mat(); _processedMat = new Mat(); _grayMat = new Mat(); _blurMat = new Mat(); _cannyMat = new Mat(); }代码解析(第二部分:初始化):
InitializeWebCam:获取摄像头设备列表,默认选择第一个(通常是后置摄像头)。你可以扩展这里,让用户在前置和后置摄像头之间切换。创建WebCamTexture时传入了我们期望的宽、高和帧率,但实际分辨率可能受硬件限制,最终以_webCamTexture.width/height为准。WaitForCameraStart:这是一个简单的协程,用于等待摄像头硬件初始化完成。因为Play()方法是异步的,立即访问纹理尺寸可能为0。等待半秒是一个经验值,能解决大部分启动问题。InitializeMats:预先创建Mat对象。虽然尺寸不对,但创建空对象比在每帧的循环里new Mat()性能更好。我们会在每帧处理时,用正确尺寸的数据填充它们。
4.4 步骤四:核心帧处理流程
这是最核心的部分,ProcessFrame方法实现了我们之前拆解的工作流。
private void ProcessFrame() { // 1. 从WebCamTexture获取当前帧并转换为Texture2D Texture2D frameTexture = new Texture2D(_webCamTexture.width, _webCamTexture.height, TextureFormat.RGBA32, false); frameTexture.SetPixels32(_webCamTexture.GetPixels32()); frameTexture.Apply(); // 必须调用Apply使SetPixels生效 // 2. 将Texture2D转换为OpenCV的Mat (注意颜色通道顺序) // Unity的Texture2D是RGBA,OpenCV默认是BGR,所以需要转换 _sourceMat = OpenCvSharp.Unity.TextureToMat(frameTexture, _sourceMat); // 因为我们后续要绘制彩色轮廓,所以需要一份副本用于最终显示 _processedMat = _sourceMat.Clone(); // 3. 图像预处理 // 3.1 转换为灰度图 Cv2.CvtColor(_sourceMat, _grayMat, ColorConversionCodes.BGR2GRAY); // 3.2 高斯模糊,减少噪声 // 确保模糊核大小是正奇数 int blurSize = blurKernelSize | 1; // 位运算确保为奇数 Cv2.GaussianBlur(_grayMat, _blurMat, new Size(blurSize, blurSize), 0); // 4. Canny边缘检测 Cv2.Canny(_blurMat, _cannyMat, cannyThreshold1, cannyThreshold2, apertureSize, true); // 5. 查找轮廓 Point[][] contours; HierarchyIndex[] hierarchy; // RetrievalModes.External 只检测最外层轮廓, Tree会检测所有层级轮廓 // ContourApproximationModes.ApproxSimple 压缩水平、垂直、对角方向的线段,只保留端点 Cv2.FindContours(_cannyMat, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); // 6. 筛选并绘制轮廓 for (int i = 0; i < contours.Length; i++) { double area = Cv2.ContourArea(contours[i]); // 根据面积过滤掉太小的轮廓(可能是噪声) if (area > contourAreaThreshold) { // 绘制轮廓到处理后的图像上 Cv2.DrawContours(_processedMat, contours, i, contourColor, contourThickness); // 可选:计算并绘制轮廓的最小外接矩形或旋转矩形,用于物体定位 // OpenCvSharp.Rect rect = Cv2.BoundingRect(contours[i]); // Cv2.Rectangle(_processedMat, rect, new Scalar(255, 0, 0), 2); } } // 7. 将处理后的Mat转换回Texture2D并显示 Texture2D processedTexture = OpenCvSharp.Unity.MatToTexture(_processedMat, frameTexture); // 复用之前的Texture2D if (rawImageProcessed != null) { rawImageProcessed.texture = processedTexture; } // 销毁临时创建的Texture2D,防止内存泄漏(MatToTexture如果复用则不用销毁) Destroy(frameTexture); }代码解析(第三部分:处理流程):
- 获取帧:
_webCamTexture.GetPixels32()获取当前帧的颜色数组,然后创建一个临时的Texture2D来存放它。这一步是性能瓶颈之一,因为GetPixels32是阻塞调用且涉及内存拷贝。对于更高帧率的需求,可以考虑使用WebCamTexture.GetPixels32的非阻塞版本或异步GPU读取,但本教程以简单稳定为首要目标。 - 格式转换:
OpenCvSharp.Unity.TextureToMat是OpenCVSharp for Unity提供的工具函数,它内部处理了RGBA到BGR的转换,非常方便。我们将原始帧存到_sourceMat,并克隆一份到_processedMat用于绘制轮廓。 - 预处理:
Cv2.CvtColor:将BGR彩色图转为灰度图。灰度图只有一个通道,后续处理计算量小。Cv2.GaussianBlur:高斯模糊。blurSize通过blurKernelSize | 1确保是奇数。模糊能有效平滑图像,抑制高频噪声,让Canny检测到的边缘更干净、连续。SigmaX设为0表示根据核大小自动计算标准差。
- Canny边缘检测:这是轮廓检测的关键步骤。
cannyThreshold1和cannyThreshold2是两个阈值。梯度强度高于threshold2的被认为是强边缘,低于threshold1的丢弃,介于两者之间的,如果连接到强边缘则保留。apertureSize是Sobel算子的大小。L2gradient设为true表示使用更精确的L2范数计算梯度幅度。 - 查找轮廓:
Cv2.FindContours在二值图像_cannyMat上查找轮廓。RetrievalModes.External只找最外层的轮廓,忽略内部嵌套的轮廓(比如一个白色圆圈里面的黑色圆圈,只检测外圈)。如果你需要所有轮廓层级关系,可以用Tree。ApproxSimple会压缩轮廓,减少点的数量,提升后续处理效率。 - 筛选与绘制:遍历所有找到的轮廓,计算其面积(
Cv2.ContourArea)。面积太小的轮廓很可能是图像噪声,我们用contourAreaThreshold将其过滤掉。对于符合条件的轮廓,使用Cv2.DrawContours将其以指定颜色和粗细绘制到_processedMat上。注释掉的代码展示了如何获取轮廓的外接矩形,这在物体定位和测量中非常有用。 - 渲染回UI:使用
OpenCvSharp.Unity.MatToTexture将绘制好的_processedMat转换回Texture2D,并赋值给rawImageProcessed的texture属性,从而在UI上实时显示。这里复用了之前创建的frameTexture对象,避免重复创建销毁带来的GC开销。
4.5 步骤五:资源释放
最后,别忘了在管理器销毁时释放所有资源,这是良好编程习惯,对移动端尤其重要。
private void ReleaseMats() { _sourceMat?.Dispose(); _processedMat?.Dispose(); _grayMat?.Dispose(); _blurMat?.Dispose(); _cannyMat?.Dispose(); }5. 参数调优与效果调试实战
代码写完了,直接运行可能效果并不理想。轮廓可能断断续续,或者检测到太多杂乱无章的线条。这时就需要调参。我们的优势在于,所有关键参数都暴露在了Inspector面板上,可以实时调整并看到效果。
5.1 Canny阈值调优
这是影响边缘检测效果最关键的参数。
cannyThreshold1(低阈值):值设得太低,会保留很多弱边缘和噪声;设得太高,可能会丢失真实的弱边缘。通常从50左右开始尝试。cannyThreshold2(高阈值):值设得太低,会导致强边缘不连续;设得太高,只有梯度非常强的边缘才会被保留。经验上,threshold2大约是threshold1的2到3倍。- 调试技巧:在Unity编辑器中运行游戏,选中
ContourDetectionManager对象,然后在Game视图和Inspector视图之间来回切换。慢慢拖动两个阈值的滑块,观察画面中边缘线条的变化。目标是让你关心的物体的轮廓清晰、连续,同时背景噪声尽可能少。对于光照均匀、对比度高的场景,阈值可以设高一些;对于昏暗或低对比度场景,阈值需要设低。
5.2 模糊核大小调整
blurKernelSize决定了模糊的强度。
- 值越大,图像越模糊,噪声抑制效果越好,但物体的边缘也会变得更“粗”和模糊,可能导致Canny检测到的边缘位置偏移或细节丢失。
- 值越小,图像越清晰,但噪声也更明显。
- 调试技巧:如果画面中有很多“雪花点”似的噪声,尝试增大
blurKernelSize(例如从5调到7或9)。如果发现物体的精细边缘(如文字边缘)丢失了,就适当调小。通常5或7是一个不错的起点。必须确保是奇数。
5.3 轮廓面积阈值过滤
contourAreaThreshold用于过滤噪声轮廓。
- 运行程序后,先不要过滤,观察原始检测到的所有轮廓。你会发现屏幕上有很多一闪而过的小白点或短线。
- 逐渐增大
contourAreaThreshold,这些小的噪声轮廓会逐渐消失,只留下面积较大的、你真正关心的物体轮廓(比如你的手、一个杯子、一本书)。 - 这个值需要根据你摄像头画面的实际尺寸和你想检测的物体大小来定。可以先设一个很小的值(如10),看看所有轮廓,然后逐步增加,直到噪声轮廓基本消失,而目标轮廓依然稳定存在。
5.4 一个实用的调试技巧:分步可视化
为了更直观地理解每一步处理的效果,我强烈建议你在场景中多创建几个RawImage,分别显示_grayMat、_blurMat和_cannyMat。这样你就能清晰地看到,图像是如何从彩色变成灰度,再被模糊,最后变成只有边缘的二值图的。这对于精准定位问题(比如是模糊不够导致噪声多,还是Canny阈值没设对导致边缘断裂)有巨大帮助。你只需要在ProcessFrame方法的相应步骤后,添加类似的转换和赋值代码即可。
6. 常见问题排查与性能优化
在实际操作中,你几乎一定会遇到下面这些问题。这里是我踩过坑后的解决方案。
6.1 问题一:编辑器运行正常,打包后报错“DllNotFoundException: opencv_core...”
- 原因:这是最常见的问题。你导入的OpenCVSharp包可能不包含目标平台(如Android、iOS)的原生库,或者打包时没有正确包含。
- 解决方案:
- 确认插件:确保你导入的是专门为Unity编译的
.unitypackage,并且其Plugins文件夹下包含了对应平台(如x86_64,Android/arm64-v8a,iOS)的库文件。 - 检查Player Settings:在
File -> Build Settings -> Player Settings...中,找到Other Settings部分,确保Scripting Backend对于Android平台,优先使用IL2CPP,并且Target Architectures勾选了ARM64(现代手机基本都是64位)。 - 检查Plugins导入设置:在Project窗口,找到OpenCVSharp插件目录下的
.dll或.so文件,选中它们,在Inspector中查看Platform Settings。确保为你的目标平台正确勾选了(例如,Android的.so文件应该勾选Android平台,并选择正确的CPU架构)。
- 确认插件:确保你导入的是专门为Unity编译的
6.2 问题二:画面卡顿,帧率很低
- 原因:图像处理是计算密集型任务,尤其是在移动设备上。
GetPixels32和TextureToMat/MatToTexture的转换以及OpenCV运算本身都很耗CPU。 - 优化方案:
- 降低分辨率:将
requestedWidth和requestedHeight从640x480降到320x240,性能会有显著提升,虽然画面会变模糊,但对于轮廓检测有时够用。 - 降低处理频率:不一定每帧都处理。可以创建一个计数器,每2帧或3帧处理一次。在
Update中修改:private int _frameCounter = 0; void Update() { if (!_isCamAvailable) return; _frameCounter++; if (_frameCounter % 2 == 0) // 每2帧处理一次 { ProcessFrame(); } else { // 即使不处理,也要更新原始画面(如果显示的话) // 或者直接什么都不做 } } - 简化处理流程:如果场景简单,可以尝试去掉高斯模糊步骤,或者使用更小的模糊核。
FindContours的模式使用External而不是Tree也能节省计算。 - 使用Job System和Burst Compiler(高级):对于极致的性能需求,可以考虑使用Unity的C# Job System来并行处理图像数据,并用Burst Compiler编译,但这需要将部分OpenCV算法用Job重写,复杂度较高。
- 降低分辨率:将
6.3 问题三:轮廓闪烁或不稳定
- 原因:光照变化、摄像头自动对焦/自动白平衡、以及Canny阈值固定不变,导致边缘梯度强度波动。
- 解决方案:
- 固定摄像头参数:如果平台支持,尝试锁定摄像头的焦点、曝光和白平衡。但这在Unity的
WebCamTexture中控制比较麻烦。 - 使用自适应阈值:放弃固定的Canny阈值,改用自适应阈值算法,如
Cv2.AdaptiveThreshold。它会根据图像局部区域的像素值分布来计算阈值,对光照变化更鲁棒。但这通常用于二值化,替代的是“灰度->二值”步骤,而不是Canny。你可以先尝试用自适应阈值二值化,再在其结果上FindContours,有时效果更好。 - 轮廓跟踪与滤波:对连续帧间检测到的轮廓进行关联和滤波(如卡尔曼滤波)。比如,比较前后两帧轮廓的位置和面积,如果变化在合理范围内,则认为它们是同一个物体,使用平滑后的位置进行绘制。这属于更高级的跟踪范畴。
- 固定摄像头参数:如果平台支持,尝试锁定摄像头的焦点、曝光和白平衡。但这在Unity的
6.4 问题四:在部分Android设备上崩溃或无画面
- 原因:设备兼容性问题。某些设备的摄像头可能不支持你请求的分辨率或帧率。
- 解决方案:
- 不指定分辨率:创建
WebCamTexture时不传宽高参数,让Unity使用设备的默认分辨率。然后我们的Mat在初始化时也不指定大小,在ProcessFrame中根据_webCamTexture.width/height动态调整Mat的尺寸。 - 添加异常捕获:在
InitializeWebCam和ProcessFrame的关键步骤添加try-catch,并将错误信息打印到UI上,方便调试。 - 测试主流设备:在开发过程中,尽量用几款不同品牌和型号的安卓手机进行测试。
- 不指定分辨率:创建
6.5 内存泄漏排查
- 现象:游戏运行时间越长,内存占用越高,最终可能崩溃。
- 检查点:
- 确保
OnDestroy中调用了ReleaseMats()和_webCamTexture.Stop()。 - 确保
ProcessFrame中创建的临时Texture2D对象(frameTexture)在最后被Destroy。注意,如果MatToTexture复用了这个纹理,则不需要销毁,我们的代码已经处理了。 - 在Unity Profiler的Memory模块中,观察
Texture2D和ManagedHeap的增长情况。如果发现Texture2D数量持续增长,说明有纹理没有被正确释放。
- 确保
7. 功能扩展与进阶思路
基础轮廓检测跑通后,你可以在此基础上做很多有趣的扩展:
- 轮廓分析:不止是画出来,还可以计算轮廓的属性。
Cv2.ArcLength:计算轮廓周长。Cv2.BoundingRect/Cv2.MinAreaRect:获取轮廓的外接矩形或最小外接旋转矩形,用于物体定位和方向判断。Cv2.ApproxPolyDP:用多边形逼近轮廓,可以判断物体形状(比如通过顶点数判断是三角形、矩形还是圆形)。
- 颜色过滤:在找轮廓前,先根据颜色筛选区域。使用
Cv2.InRange函数在HSV颜色空间中进行阈值化,只保留特定颜色范围的区域(比如追踪一个红色的球),然后再进行轮廓检测,这样能极大排除背景干扰。 - 轮廓间关系:利用
FindContours返回的hierarchy(层级信息),可以分析轮廓之间的嵌套关系(比如一个轮廓是否在另一个轮廓内部)。 - 凸包与凸性缺陷:使用
Cv2.ConvexHull计算轮廓的凸包,再用Cv2.ConvexityDefects计算凸性缺陷。这常用于手势识别,比如识别伸出手指的数量(每个手指间的凹陷就是一个凸性缺陷)。 - 与Unity物理系统交互:将检测到的主要轮廓点,转换成Unity世界空间中的一系列点,然后用这些点生成一个
Polygon Collider 2D,这样虚拟物体就能与现实摄像头画面中的轮廓进行物理交互了。 - 多线程处理:将耗时的图像处理过程(从
CvtColor到FindContours)放到另一个线程中,避免阻塞主线程导致游戏卡顿。这需要处理好线程间的数据同步(如使用双缓冲机制)。
实现这些扩展功能,本质上就是在ProcessFrame方法的第5步(查找轮廓)之后,添加相应的分析和处理代码。OpenCVSharp提供了非常丰富的函数,几乎涵盖了OpenCV C++版本的所有功能,文档也比较齐全,遇到问题多查查OpenCV的官方文档和社区,大部分都能找到答案。
