KeyFrameGreenScreenExample是一个演示如何使用OpenCVForUnity库和Unity引擎实现绿幕抠像的示例项目。该项目使用了多张图像作为关键帧,并通过对关键帧进行透视变换和色彩校正等操作,将绿幕背景替换为指定的背景图像。
具体来说,KeyFrameGreenScreenExample中的主要步骤如下:
- 加载关键帧图像和背景图像,并获取绿幕区域的掩码。
- 对关键帧图像进行透视变换,将其转换为一个四边形区域。
- 对关键帧图像进行色彩校正,以便将其颜色与背景图像匹配。
- 将关键帧图像中的绿幕区域替换为背景图像中的对应区域。
- 将处理后的关键帧图像添加到输出序列中。
- 重复步骤2-5,对所有关键帧图像进行处理,生成最终的输出序列1. 。
#if !(PLATFORM_LUMIN && !UNITY_EDITOR)
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using OpenCVForUnity.UnityUtils;
using OpenCVForUnity.UnityUtils.Helper;
using OpenCVForUnity.PhotoModule;
namespace OpenCVForUnityExample
{
/// <summary>
/// Key Frame Green Screen Example
/// Greenscreen effect without a physical green screen, via OpenCV.
/// 通过OpenCV实现无物理绿屏的绿屏效果。
/// This performs background subtraction, and sets the background to "green" for use with "key frame" video editing software.
/// 这将执行背景减法,并将背景设置为“绿色”,以便与“关键帧”视频编辑软件一起使用。
/// Referring to https://gist.github.com/drscotthawley/2d6bbffce9dda5f3057b4879c3bd4422.
/// </summary>
[RequireComponent(typeof(WebCamTextureToMatHelper))]
public class KeyFrameGreenScreenExample : MonoBehaviour
{
/// <summary>
/// The thresh.
/// 阈值
/// </summary>
[Range(0, 255)]
public float thresh = 50.0f;
/// <summary>
/// The background raw image.
/// 背景原始图像。
/// </summary>
public RawImage bgRawImage;
/// <summary>
/// The texture.
/// 贴图
/// </summary>
Texture2D texture;
/// <summary>
/// The webcam texture to mat helper.
/// OpenCVForUnity库中提供的一个帮助类,用于在Unity中将WebCamTexture对象转换为OpenCV中的Mat对象。
/// </summary>
WebCamTextureToMatHelper webCamTextureToMatHelper;
/// <summary>
/// The background mat.
/// 背景矩阵
/// </summary>
Mat bgMat;
/// <summary>
/// The foreground mask mat.
/// 前景掩膜矩阵
/// </summary>
Mat fgMaskMat;
/// <summary>
/// The background mask mat.
/// 北京掩膜矩阵
/// </summary>
Mat bgMaskMat;
/// <summary>
/// The green mat.
/// 绿色矩阵
/// </summary>
Mat greenMat;
/// <summary>
/// The background texture.
/// 背景贴图
/// </summary>
Texture2D bgTexture;
/// <summary>
/// The kernel for morphologyEx method.
/// morphologyEx 方法的内核。
/// </summary>
Mat kernel;
bool isTouched;
/// <summary>
/// The FPS monitor.
/// 帧率监控器
/// </summary>
FpsMonitor fpsMonitor;
// Use this for initialization
void Start()
{
fpsMonitor = GetComponent<FpsMonitor>();
webCamTextureToMatHelper = gameObject.GetComponent<WebCamTextureToMatHelper>();
#if UNITY_ANDROID && !UNITY_EDITOR
// Avoids the front camera low light issue that occurs in only some Android devices (e.g. Google Pixel, Pixel2).
webCamTextureToMatHelper.avoidAndroidFrontCameraLowLightIssue = true;
#endif
webCamTextureToMatHelper.Initialize();
//使用Imgproc.getStructuringElement方法创建了一个3x3的椭圆形结构元素,将其赋值给了kernel变量
// 该结构元素在后续的图像处理中用于形态学操作,例如膨胀和腐蚀等
kernel = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(3, 3));
}
/// <summary>
/// Raises the webcam texture to mat helper initialized event.
/// </summary>
public void OnWebCamTextureToMatHelperInitialized()
{
Debug.Log("OnWebCamTextureToMatHelperInitialized");
// 通过webCamTextureToMatHelper.GetMat()方法获取相机捕获的图像数据
Mat webCamTextureMat = webCamTextureToMatHelper.GetMat();
// 将OpenCV Mat格式的webCamTextureMat转换为Unity的Texture2D格式,并将其赋值给变量texture。
texture = new Texture2D(webCamTextureMat.cols(), webCamTextureMat.rows(), TextureFormat.RGBA32, false);
Utils.matToTexture2D(webCamTextureMat, texture);
// 通过将Texture2D对象设置为GameObject的主纹理,可以将图像显示在屏幕上。
gameObject.GetComponent<Renderer>().material.mainTexture = texture;
gameObject.transform.localScale = new Vector3(webCamTextureMat.cols(), webCamTextureMat.rows(), 1);
Debug.Log("Screen.width " + Screen.width + " Screen.height " + Screen.height + " Screen.orientation " + Screen.orientation);
if (fpsMonitor != null)
{
fpsMonitor.Add("width", webCamTextureMat.width().ToString());
fpsMonitor.Add("height", webCamTextureMat.height().ToString());
fpsMonitor.Add("orientation", Screen.orientation.ToString());
fpsMonitor.consoleText = "SPACE KEY or TOUCH SCREEN: Reset backgroud image.";
}
/// <summary>
/// 根据屏幕和图像的比例关系来调整相机的正交大小。
/// </summary>
//取displayMat矩阵的宽度和高度,并计算屏幕与图像宽度和高度的比例(widthScale和heightScale)。
float width = webCamTextureMat.width();
float height = webCamTextureMat.height();
float widthScale = (float)Screen.width / width;
float heightScale = (float)Screen.height / height;
if (widthScale < heightScale)
{
Camera.main.orthographicSize = (width * (float)Screen.height / (float)Screen.width) / 2;
}
else
{
Camera.main.orthographicSize = height / 2;
}
bgMat = new Mat(webCamTextureMat.rows(), webCamTextureMat.cols(), CvType.CV_8UC4);// 用于存储背景图像的Mat对象
fgMaskMat = new Mat(webCamTextureMat.rows(), webCamTextureMat.cols(), CvType.CV_8UC1);//用于存储前景掩码的Mat对象
bgMaskMat = new Mat(webCamTextureMat.rows(), webCamTextureMat.cols(), CvType.CV_8UC1);//用于存储背景掩码的Mat对象
greenMat = new Mat(webCamTextureMat.rows(), webCamTextureMat.cols(), CvType.CV_8UC4, new Scalar(0, 255, 0, 255));// 用于存储绿色图像
bgTexture = new Texture2D(bgMat.cols(), bgMat.rows(), TextureFormat.RGBA32, false);// 用于将bgMat对象中的图像转换为纹理的Texture2D对象
}
/// <summary>
/// Raises the webcam texture to mat helper disposed event.
/// 用于在webCamTextureToMatHelper对象被销毁时清理资源。
/// </summary>
public void OnWebCamTextureToMatHelperDisposed()
{
Debug.Log("OnWebCamTextureToMatHelperDisposed");
if (bgMat != null)
{
bgMat.Dispose();
bgMat = null;
}
if (fgMaskMat != null)
{
fgMaskMat.Dispose();
fgMaskMat = null;
}
if (bgMaskMat != null)
{
bgMaskMat.Dispose();
bgMaskMat = null;
}
if (greenMat != null)
{
greenMat.Dispose();
greenMat = null;
}
if (texture != null)
{
Texture2D.Destroy(texture);
texture = null;
}
}
/// <summary>
/// Raises the webcam texture to mat helper error occurred event.
/// 将网络摄像头纹理提升到矩阵辅助对象发生错误事件。
/// </summary>
/// <param name="errorCode">Error code.</param>
public void OnWebCamTextureToMatHelperErrorOccurred(WebCamTextureToMatHelper.ErrorCode errorCode)
{
Debug.Log("OnWebCamTextureToMatHelperErrorOccurred " + errorCode);
}
// Update is called once per frame
void Update()
{
/// <summary>
/// 该代码基于平台的不同使用不同的输入方式:在Unity编辑器中使用鼠标,而在Android和iOS设备上使用触摸屏。
/// </summary>
// 它将下面的代码块仅在Android或iOS平台且不在Unity编辑器中运行时编译。
#if ((UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR)
//Touch
// 在Android和iOS平台下,该代码会检查是否有一个触摸输入,并且该触摸输入为结束状态(TouchPhase.Ended)。如果满足这些条件并且当前不在UI对象上,则将isTouched变量设置为true。
if (Input.touchCount == 1) {
Touch t = Input.GetTouch(0);
if (t.phase == TouchPhase.Ended && !EventSystem.current.IsPointerOverGameObject (t.fingerId)) {
isTouched = true;
}
}
#else
// 该代码会检查是否按下了空格键(KeyCode.Space)。如果按下了空格键,则将isTouched变量设置为true。
//Mouse
if (Input.GetKeyUp(KeyCode.Space))
{
isTouched = true;
}
#endif
//检查WebCamTextureToMatHelper组件是否正在播放,并且是否已更新当前帧。如果满足这些条件,就会执行以下步骤:
if (webCamTextureToMatHelper.IsPlaying() && webCamTextureToMatHelper.DidUpdateThisFrame())
{
Mat rgbaMat = webCamTextureToMatHelper.GetMat();//获取当前帧的Mat对象rgbaMa
// 每次点击屏幕或空格都会将当前图片记为背景,以便之后采用帧差法提取出前景
if (isTouched)
{
rgbaMat.copyTo(bgMat);// 将rgbaMat对象复制到bgMat对象中,并将其用作背景图像
setBgTexture(bgMat);//使用setBgTexture函数将背景图像转换为纹理
isTouched = false;// isTouched变量设置为false
}
//set fgMaskMat
// 计算前景掩码fgMaskMat
// 使用当前帧的rgbaMat和背景图像bgMat作为输入,并使用阈值参数thresh来控制前景掩码的计算
findFgMaskMat(rgbaMat, bgMat, thresh);
//set bgMaskMat
//计算背景掩码bgMaskMat
// 将fgMaskMat取反
Core.bitwise_not(fgMaskMat, bgMaskMat);
//copy greenMat using bgMaskMat
// 使用bgMaskMat作为蒙版,将greenMat复制到rgbaMat中
// 将在背景掩码所表示的区域中添加绿色图像
greenMat.copyTo(rgbaMat, bgMaskMat);
//Imgproc.putText (rgbaMat, "SPACE KEY or TOUCH SCREEN: Reset backgroud image.", new Point (5, rgbaMat.rows () - 10), Imgproc.FONT_HERSHEY_SIMPLEX, 0.6, new Scalar (255, 255, 255, 255), 2, Imgproc.LINE_AA, false);
// 使用Utils.matToTexture2D函数将处理后的rgbaMat对象转换为纹理,并将其存储在texture变量中
// 该纹理将被用于渲染到屏幕上
Utils.matToTexture2D(rgbaMat, texture);
}
}
/// <summary>
/// Finds the foreground mask mat.
/// 寻找前景掩码
/// 计算前景掩码,以便将绿色图像放置在背景下方,只在前景区域中显示。
/// </summary>
/// <param name="fgMat">Fg mat.</param>当前帧的fgMat对象
/// <param name="bgMat">Background mat.</param>背景图像的bgMat对象
/// <param name="thresh">Thresh.</param>可选的阈值参数thresh
private void findFgMaskMat(Mat fgMat, Mat bgMat, float thresh = 13.0f)
{
// 创建两个Mat对象diff1和diff2,分别用于存储fgMat和bgMat之间的差异。
// 使用Core.absdiff函数计算diff1和diff2。
/// <summary>
/// 函数 cv::absdiff 的作用是计算两个数组或一个数组和一个标量之间的每个元素的绝对值差。
/// 当两个数组的大小和类型相同时,该函数计算两个数组对应元素之间的绝对值差,并将结果存储在输出数组 dst 中。计算绝对值差的公式为:
/// dst(I) = saturate( | src1(I) - src2(I) | )
/// 其中,I 是数组元素的多维索引,saturate 是一个函数,用于将值限制在输出数组数据类型的范围内。
/// 当其中一个输入是标量时,该函数计算数组中每个元素和标量之间的绝对值差,并将结果存储在输出数组 dst 中。
/// 如果标量是由 Scalar 对象构造的或与数组中的通道数相同,则函数计算对应元素之间的绝对值差。计算绝对值差的公式为:
/// dst(I) = saturate( | src1(I) - src2 | )
/// 其中,I 是数组元素的多维索引。
/// 如果数组的深度为 CV_32S,则不会应用饱和规则,可能会得到负值以处理溢出。
/// 多通道数组的每个通道都会被单独处理。
/// </summary>
Mat diff1 = new Mat();
Core.absdiff(fgMat, bgMat, diff1);//该函数将fgMat和bgMat中对应位置的像素值相减,得到差异矩阵,并将结果存储到diff1中。
Mat diff2 = new Mat();
Core.absdiff(bgMat, fgMat, diff2);
Mat diff = diff1 + diff2;// 将diff1和diff2相加,得到总差异图像diff。
// 使用Imgproc.threshold函数将diff中小于阈值thresh的像素值设置为0,其余像素值保持不变。
Imgproc.threshold(diff, diff, thresh, 0, Imgproc.THRESH_TOZERO);
// 将diff转换为灰度图像,并将其存储在fgMaskMat中。
Imgproc.cvtColor(diff, fgMaskMat, Imgproc.COLOR_RGBA2GRAY);
// 再次使用Imgproc.threshold函数将fgMaskMat中小于10的像素值设置为0,其余像素值保持不变。
Imgproc.threshold(fgMaskMat, fgMaskMat, 10, 0, Imgproc.THRESH_TOZERO);
// 再次使用Imgproc.threshold函数将fgMaskMat中小于等于0的像素值设置为0,其余像素值设置为255。
Imgproc.threshold(fgMaskMat, fgMaskMat, 0, 255, Imgproc.THRESH_BINARY);
// Small area removal from binary images by opening
// 使用Imgproc.morphologyEx函数对fgMaskMat进行形态学操作,以去除二值图像中的小区域。
// 该函数使用一个核(kernel)对fgMaskMat进行开运算操作。
Imgproc.morphologyEx(fgMaskMat, fgMaskMat, Imgproc.MORPH_OPEN, kernel);
//释放diff1、diff2和diff对象所占用的内存空间
diff1.Dispose();
diff2.Dispose();
diff.Dispose();
}
/// <summary>
/// Sets the background texture.
/// 设置背景纹理。
/// 将传入的Mat对象bgMat转换为纹理,并将其设置为背景RawImage的纹理
/// </summary>
/// <param name="bgMat">Background mat.</param>
private void setBgTexture(Mat bgMat)
{
// In order to reuse the bgMat, the upside-down state caused by the matToTexture2D method should be restored.
// 为了重用bgMat,应该恢复由matToTexture2D方法引起的颠倒状态。
// 使用Utils.matToTexture2D函数将bgMat转换为纹理,并将其存储在bgTexture变量中。
// 同时,由于该函数会导致纹理上下颠倒,因此需要将其翻转回去,即使用第三个参数为true的重载函数,并将第四个参数设置为0,表示不旋转纹理。
Utils.matToTexture2D(bgMat, bgTexture, true, 0, true);
// 将bgTexture设置为背景RawImage的纹理,即将其赋值给bgRawImage.texture。
bgRawImage.texture = bgTexture;
// 设置bgRawImage的缩放比例,以保持纹理的纵横比
bgRawImage.rectTransform.localScale = new Vector3(1.0f, (float)bgMat.height() / (float)bgMat.width(), 1.0f);
}
/// <summary>
/// Raises the destroy event.
/// </summary>
void OnDestroy()
{
webCamTextureToMatHelper.Dispose();
}
/// <summary>
/// Raises the back button click event.
/// </summary>
public void OnBackButtonClick()
{
SceneManager.LoadScene("OpenCVForUnityExample");
}
/// <summary>
/// Raises the play button click event.
/// </summary>
public void OnPlayButtonClick()
{
webCamTextureToMatHelper.Play();
}
/// <summary>
/// Raises the pause button click event.
/// </summary>
public void OnPauseButtonClick()
{
webCamTextureToMatHelper.Pause();
}
/// <summary>
/// Raises the stop button click event.
/// </summary>
public void OnStopButtonClick()
{
webCamTextureToMatHelper.Stop();
}
/// <summary>
/// Raises the change camera button click event.
/// </summary>
public void OnChangeCameraButtonClick()
{
webCamTextureToMatHelper.requestedIsFrontFacing = !webCamTextureToMatHelper.requestedIsFrontFacing;
}
}
}
#endif