DocumentScannerExample是OpenCVForUnity库的一个示例场景,用于演示如何使用OpenCVForUnity库实现文档扫描器应用程序。文档扫描器应用程序是一种常见的应用程序,它可以将纸质文档转换为数字格式,并进行后续处理和存储。
在DocumentScannerExample场景中,OpenCVForUnity库的各种功能被用来实现文档扫描器应用程序。其中包括使用相机捕获图像、预处理图像以准备进行文档检测、检测文档边缘、透视变换以纠正图像倾斜和扭曲。
#if !(PLATFORM_LUMIN && !UNITY_EDITOR)
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using OpenCVForUnity.UnityUtils.Helper;
using OpenCVForUnity.UnityUtils;
using System;
namespace OpenCVForUnityExample
{
/// <summary>
/// Document Scanner Example
/// An example of document scanning (like receipts, business cards etc) using the Imgproc class.
/// 使用Imgproc类扫描文档(如收据、名片等)的示例。
/// </summary>
[RequireComponent(typeof(WebCamTextureToMatHelper))]
public class DocumentScannerExample : MonoBehaviour
{
/// <summary>
/// Determines if debug mode.
/// 确定是否为调试模式。
/// </summary>
public bool isDebugMode = false;
/// <summary>
/// The debug mode toggle.
/// 调试模式切换。
/// </summary>
public Toggle isDebugModeToggle;
Mat yuvMat;//表示一个YUV格式的图像矩阵,原始图像数据将存储在yuvMat中。
Mat yMat;//表示提取自yuvMat的亮度分量(即Y通道)的图像矩阵,用于文档检测和透视变换等操作。
Mat displayMat;//表示用于显示的图像矩阵,该矩阵包含了文档检测和透视变换等操作后的结果图像数据。
Mat inputDisplayAreaMat;//表示输入区域的图像矩阵,用于显示相机捕获的原始图像数据。
Mat outputDisplayAreaMat;//表示输出区域的图像矩阵,用于显示文档扫描器应用程序的结果图像数据。
Scalar CONTOUR_COLOR;//表示轮廓的颜色,用于绘制文档轮廓。
Scalar DEBUG_CONTOUR_COLOR;//表示用于调试轮廓的颜色,用于在调试模式下绘制文档轮廓。
Scalar DEBUG_CORNER_NUMBER_COLOR;//表示用于调试角点数字的颜色,用于在调试模式下绘制文档角点处的数字。
/// <summary>
/// The texture.
/// 贴图
/// </summary>
Texture2D texture;
/// <summary>
/// The webcam texture to mat helper.
/// OpenCVForUnity库中提供的一个帮助类,用于在Unity中将WebCamTexture对象转换为OpenCV中的Mat对象。
/// </summary>
WebCamTextureToMatHelper webCamTextureToMatHelper;
/// <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();
isDebugModeToggle.isOn = isDebugMode;
}
/// <summary>
/// Raises the web cam texture to mat helper initialized event.
/// 将网络摄像头纹理提升到矩阵辅助对象初始化事件。
/// 该函数是WebCamTextureToMatHelper类中的回调函数,用于在WebCamTextureToMatHelper初始化完成后进行一些处理。
/// </summary>
public void OnWebCamTextureToMatHelperInitialized()
{
Debug.Log("OnWebCamTextureToMatHelperInitialized");
// 通过webCamTextureToMatHelper.GetMat()方法获取相机捕获的图像数据
Mat webCamTextureMat = webCamTextureToMatHelper.GetMat();
// 根据图像的宽高比例来设置显示Mat矩阵(displayMat)、输入显示区域Mat矩阵(inputDisplayAreaMat)和输出显示区域Mat矩阵(outputDisplayAreaMat)。
if (webCamTextureMat.width() < webCamTextureMat.height())
{
displayMat = new Mat(webCamTextureMat.rows(), webCamTextureMat.cols() * 2, webCamTextureMat.type(), new Scalar(0, 0, 0, 255));
inputDisplayAreaMat = new Mat(displayMat, new OpenCVForUnity.CoreModule.Rect(0, 0, webCamTextureMat.width(), webCamTextureMat.height()));
outputDisplayAreaMat = new Mat(displayMat, new OpenCVForUnity.CoreModule.Rect(webCamTextureMat.width(), 0, webCamTextureMat.width(), webCamTextureMat.height()));
}
else
{
displayMat = new Mat(webCamTextureMat.rows() * 2, webCamTextureMat.cols(), webCamTextureMat.type(), new Scalar(0, 0, 0, 255));
inputDisplayAreaMat = new Mat(displayMat, new OpenCVForUnity.CoreModule.Rect(0, 0, webCamTextureMat.width(), webCamTextureMat.height()));
outputDisplayAreaMat = new Mat(displayMat, new OpenCVForUnity.CoreModule.Rect(0, webCamTextureMat.height(), webCamTextureMat.width(), webCamTextureMat.height()));
}
// 该Texture2D对象用于在屏幕上显示相机捕获的图像和文档扫描器的结果图像。
texture = new Texture2D(displayMat.cols(), displayMat.rows(), TextureFormat.RGBA32, false);
// 通过将Texture2D对象设置为GameObject的主纹理,可以将图像显示在屏幕上。
gameObject.GetComponent<Renderer>().material.mainTexture = texture;
// 依据图片比例设置gameObject尺寸
gameObject.transform.localScale = new Vector3(displayMat.cols(), displayMat.rows(), 1);
Debug.Log("Screen.width " + Screen.width + " Screen.height " + Screen.height + " Screen.orientation " + Screen.orientation);
//帧率监控器添加元素
if (fpsMonitor != null)
{
fpsMonitor.Add("width", displayMat.width().ToString());
fpsMonitor.Add("height", displayMat.height().ToString());
fpsMonitor.Add("orientation", Screen.orientation.ToString());
fpsMonitor.consoleText = "Please place a document paper (receipt or business card) on a plain background.";
}
/// <summary>
/// 根据屏幕和图像的比例关系来调整相机的正交大小。
/// </summary>
//取displayMat矩阵的宽度和高度,并计算屏幕与图像宽度和高度的比例(widthScale和heightScale)。
float width = displayMat.width();
float height = displayMat.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;
}
yuvMat = new Mat();// 用于存储从相机捕获的图像数据。
yMat = new Mat();// 用于存储yuvMat对象的亮度分量数据。
CONTOUR_COLOR = new Scalar(255, 0, 0, 255);// 表示在绘制文档轮廓时使用的颜色。
DEBUG_CONTOUR_COLOR = new Scalar(255, 255, 0, 255);// 在调试模式下绘制文框角点编号时使用的颜色。
DEBUG_CORNER_NUMBER_COLOR = new Scalar(255, 255, 255, 255);// 在调试模式下绘制文框角点编号时使用的颜色。
// If the WebCam is front facing, flip the Mat horizontally. Required for successful detection of document.
// 如果网络摄像头是正面的,请水平翻转矩阵。成功检测文档所必需的。
if (webCamTextureToMatHelper.IsFrontFacing() && !webCamTextureToMatHelper.flipHorizontal)
{
// 前置相机捕获的图像是镜像翻转的,需要进行水平翻转才能正确地显示图像。
webCamTextureToMatHelper.flipHorizontal = true;
}
else if (!webCamTextureToMatHelper.IsFrontFacing() && webCamTextureToMatHelper.flipHorizontal)
{
// 后置相机捕获的图像不需要进行水平翻转,否则会导致图像被翻转。
webCamTextureToMatHelper.flipHorizontal = false;
}
}
/// <summary>
/// Raises the web cam texture to mat helper disposed event.
/// 用于在webCamTextureToMatHelper对象被销毁时清理资源。
/// </summary>
public void OnWebCamTextureToMatHelperDisposed()
{
Debug.Log("OnWebCamTextureToMatHelperDisposed");
// 检查texture、yuvMat、yMat和displayMat对象是否存在,并释放它们所占用的内存。
if (texture != null)
{
Texture2D.Destroy(texture);
texture = null;
}
if (yuvMat != null)
yuvMat.Dispose();
if (yMat != null)
yMat.Dispose();
if (displayMat != null)
displayMat.Dispose();
}
/// <summary>
/// Raises the web cam 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()
{
if (webCamTextureToMatHelper.IsPlaying() && webCamTextureToMatHelper.DidUpdateThisFrame())// 检查webCamTextureToMatHelper对象是否正在播放并且已经更新了这一帧。
{
//获取当前帧的Mat对象
Mat rgbaMat = webCamTextureToMatHelper.GetMat();
// change the color space to YUV.
// 将其颜色空间从RGBA转换为YUV
Imgproc.cvtColor(rgbaMat, yuvMat, Imgproc.COLOR_RGBA2RGB);
Imgproc.cvtColor(yuvMat, yuvMat, Imgproc.COLOR_RGB2YUV);
// grap only the Y component.
// 提取Y分量
Core.extractChannel(yuvMat, yMat, 0);
// 对Y分量进行高斯模糊和边缘检测
// blur the image to reduce high frequency noises.
Imgproc.GaussianBlur(yMat, yMat, new Size(3, 3), 0);
// find edges in the image.
Imgproc.Canny(yMat, yMat, 50, 200, 3);
// find contours.
// 使用findContours()函数找到所有轮廓,并选择面积最大的轮廓。
List<MatOfPoint> contours = new List<MatOfPoint>();
Find4PointContours(yMat, contours);
// pick the contour of the largest area and rearrange the points in a consistent order.
// 选取最大区域的轮廓,并按一致的顺序重新排列这些点。
MatOfPoint maxAreaContour = GetMaxAreaContour(contours);
maxAreaContour = OrderCornerPoints(maxAreaContour);
// maxAreaContour是一个轮廓对象,size()函数返回轮廓的大小,area()函数返回轮廓的面积。
// 如果轮廓的面积大于0,则将found设置为true,否则设置为false。因此,这行代码的作用是检查是否找到了一个有效的轮廓。
bool found = (maxAreaContour.size().area() > 0);
if (found)
{
// trasform the prospective of original image.
// 变换原始图像的视角。
// 对输入图像进行透视变换,并将变换后的图像复制到输出图像的中心位置,以便进行显示或进一步处理。
using (Mat transformedMat = PerspectiveTransform(rgbaMat, maxAreaContour))// 返回一个经过透视变换后的Mat对象transformedMat。
{
outputDisplayAreaMat.setTo(new Scalar(0, 0, 0, 255));// 使用setTo函数将输出图像outputDisplayAreaMat的所有像素值设置为黑色(0,0,0,255),即清空输出图像。
//检查变换后的图像的大小是否适合放置在输出图像的中心位置。
//如果变换后的图像的宽度和高度都小于等于输出图像的宽度和高度,并且变换后的图像总像素数大于等于输出图像总像素数的1/16,则执行下面的代码块。
if (transformedMat.width() <= outputDisplayAreaMat.width() && transformedMat.height() <= outputDisplayAreaMat.height()
&& transformedMat.total() >= outputDisplayAreaMat.total() / 16)
{
// 计算输出图像和变换后的图像之间的偏移量,以便将变换后的图像居中显示在输出图像中。
int x = outputDisplayAreaMat.width() / 2 - transformedMat.width() / 2;
int y = outputDisplayAreaMat.height() / 2 - transformedMat.height() / 2;
using (Mat dstAreaMat = new Mat(outputDisplayAreaMat, new OpenCVForUnity.CoreModule.Rect(x, y, transformedMat.width(), transformedMat.height())))// 使用一个新的Mat对象dstAreaMat作为输出图像的子区域,并将变换后的图像复制到该子区域中。
{
transformedMat.copyTo(dstAreaMat);
}
}
}
}
// 在调试模式下将处理后的图像显示出来,以便进行调试和验证。
if (isDebugMode)
{
// draw edge image.绘制边缘图像。
Imgproc.cvtColor(yMat, rgbaMat, Imgproc.COLOR_GRAY2RGBA);
// draw all found conours.画出所有找到的轮廓。
Imgproc.drawContours(rgbaMat, contours, -1, DEBUG_CONTOUR_COLOR, 1);
}
// 如果找到了面积最大的轮廓
if (found)
{
// draw max area contour. 绘制最大面积轮廓。
Imgproc.drawContours(rgbaMat, new List<MatOfPoint> { maxAreaContour }, -1, CONTOUR_COLOR, 2);
if (isDebugMode)
{
// draw corner numbers.绘制角编号。
// 使用一个for循环遍历最大面积轮廓的所有点,使用Imgproc.putText函数在每个角点处绘制相应的编号。
for (int i = 0; i < maxAreaContour.toArray().Length; i++)
{
var pt = maxAreaContour.get(i, 0);
Imgproc.putText(rgbaMat, i.ToString(), new Point(pt[0], pt[1]), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, DEBUG_CORNER_NUMBER_COLOR, 1, Imgproc.LINE_AA, false);
}
}
}
rgbaMat.copyTo(inputDisplayAreaMat);
/// 使用Utils.matToTexture2D()函数将输出显示区域的Mat对象转换为Texture2D对象,并将其显示在屏幕上。
Utils.matToTexture2D(displayMat, texture, true, 0, true);
}
}
/// <summary>
/// 在输入图像中查找四边形轮廓,并将它们存储在一个MatOfPoint类型的向量contours中。
/// </summary>
/// <param name="image"></param>输入图像
/// <param name="contours"></param>输出轮廓
private void Find4PointContours(Mat image, List<MatOfPoint> contours)
{
contours.Clear();//清空contours向量
List<MatOfPoint> tmp_contours = new List<MatOfPoint>();
Mat hierarchy = new Mat();
/// <summary>
/// Imgproc.findContours函数查找所有轮廓。
/// 第一个参数是输入图像
/// 第二个参数是存储所有轮廓的向量
/// 第三个参数是轮廓的层次结构
/// 第四个参数是轮廓检索模式,这里使用RETR_EXTERNAL表示只检测最外层轮廓
/// 第五个参数是轮廓近似方法,这里使用CHAIN_APPROX_SIMPLE表示仅保留轮廓的端点。
/// </summary>
Imgproc.findContours(image, tmp_contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
foreach (var cnt in tmp_contours)
{
// 对于每个找到的轮廓,使用Imgproc.convexHull函数计算其凸包。
MatOfInt hull = new MatOfInt();
Imgproc.convexHull(cnt, hull, false);
// 轮廓和凸包转换为Point类型的数组,以便进行多边形逼近。
Point[] cnt_arr = cnt.toArray();
int[] hull_arr = hull.toArray();
Point[] pts = new Point[hull_arr.Length];
for (int i = 0; i < hull_arr.Length; i++)
{
pts[i] = cnt_arr[hull_arr[i]];
}
MatOfPoint2f ptsFC2 = new MatOfPoint2f(pts);
MatOfPoint2f approxFC2 = new MatOfPoint2f();
MatOfPoint approxSC2 = new MatOfPoint();
// 使用Imgproc.approxPolyDP函数对凸包进行多边形逼近,并将结果存储在MatOfPoint类型的approxSC2中。
double arclen = Imgproc.arcLength(ptsFC2, true);
Imgproc.approxPolyDP(ptsFC2, approxFC2, 0.01 * arclen, true);
approxFC2.convertTo(approxSC2, CvType.CV_32S);
// 如果多边形的大小不是4(即不是四边形),则忽略该轮廓。
// 如果大小为4,则将该轮廓添加到contours向量中。
if (approxSC2.size().area() != 4)
continue;
contours.Add(approxSC2);
}
}
/// <summary>
/// 在给定的轮廓列表中找到最大面积的轮廓,并将其返回为MatOfPoint类型。
/// </summary>
/// <param name="contours"></param>
/// <returns></returns>
private MatOfPoint GetMaxAreaContour(List<MatOfPoint> contours)
{
// 如果传入的轮廓列表为空,则返回一个空的MatOfPoint对象。
if (contours.Count == 0)
return new MatOfPoint();
int index = -1;
double area = 0;
// 用一个for循环遍历轮廓列表中的每个轮廓
for (int i = 0; i < contours.Count; i++)
{
// 使用Imgproc.contourArea函数计算每个轮廓的面积。
double tmp = Imgproc.contourArea(contours[i]);
//如果计算出来的面积大于之前计算的面积,则更新最大面积和对应的轮廓索引。
if (area < tmp)
{
area = tmp;
index = i;
}
}
// 返回具有最大面积的轮廓,它是一个MatOfPoint类型的对象。如果轮廓列表中有多个具有相同最大面积的轮廓,则只返回第一个。
return contours[index];
}
/// <summary>
/// 对四个角点进行排序,以便后续的透视变换操作。
/// </summary>
/// <param name="corners"></param>角点矩阵
/// <returns></returns>
private MatOfPoint OrderCornerPoints(MatOfPoint corners)
{
// 代码检查输入的角点矩阵是否为空或大小不足4个,如果是,则直接返回输入的角点矩阵。
if (corners.size().area() <= 0 || corners.rows() < 4)
return corners;
// rearrange the points in the order of upper left, upper right, lower right, lower left.
// 按左上、右上、右下、左下的顺序重新排列这些点。
using (Mat x = new Mat(corners.size(), CvType.CV_32SC1))
using (Mat y = new Mat(corners.size(), CvType.CV_32SC1))
using (Mat d = new Mat(corners.size(), CvType.CV_32SC1))
using (Mat dst = new Mat(corners.size(), CvType.CV_32SC2))
{
// extractChannel函数从角点矩阵中提取x和y坐标通道
Core.extractChannel(corners, x, 0);
Core.extractChannel(corners, y, 1);
// the sum of the upper left points is the smallest and the sum of the lower right points is the largest.
// 左上点的和最小,右下点的和最大。
Core.add(x, y, d);//然后计算每个角点的x坐标和y坐标之和,保存在一个名为d的矩阵中。
Core.MinMaxLocResult result = Core.minMaxLoc(d);// 从矩阵d中获取最小值和最大值,并将结果存储在名为result的MinMaxLocResult对象中。
// 将计算出来的左上角点和右下角点的坐标分别存储在dst矩阵的第0行和第2行,以便后续使用。
dst.put(0, 0, corners.get((int)result.minLoc.y, 0));//左上
dst.put(2, 0, corners.get((int)result.maxLoc.y, 0));//右下
// the difference in the upper right point is the smallest, and the difference in the lower left is the largest.
// 右上角的差异最小,左下角的差异最大。
Core.subtract(y, x, d);// 计算角点矩阵中每个角点的y坐标减去x坐标得到的差值,并将结果存储在名为d的矩阵中。
result = Core.minMaxLoc(d);// 从矩阵d中获取最小值和最大值,并将结果存储在名为result的MinMaxLocResult对象中。
// 将计算出来的右上角点和左下角点的坐标分别存储在dst矩阵的第1行和第3行,以便后续使用。
dst.put(1, 0, corners.get((int)result.minLoc.y, 0));//右上
dst.put(3, 0, corners.get((int)result.maxLoc.y, 0));//左下
// 代码将dst矩阵复制回角点矩阵中,并返回排序后的角点矩阵。
dst.copyTo(corners);
}
return corners;
}
/// <summary>
/// 用于将图像进行透视变换。
/// </summary>
/// <param name="image"></param>输入图像
/// <param name="corners"></param>待变换区域的四个角点
/// <returns></returns>
private Mat PerspectiveTransform(Mat image, MatOfPoint corners)
{
// 代码检查输入的角点矩阵是否为空或大小不足4个,如果是,则直接返回输入的图像矩阵。
if (corners.size().area() <= 0 || corners.rows() < 4)
return image;
//提取出四个角点
Point[] pts = corners.toArray();
Point tl = pts[0];//左上
Point tr = pts[1];//右上
Point br = pts[2];//右下
Point bl = pts[3];//左下
double widthA = Math.Sqrt((br.x - bl.x) * (br.x - bl.x) + (br.y - bl.y) * (br.y - bl.y));//计算底部两个角点的距离
double widthB = Math.Sqrt((tr.x - tl.x) * (tr.x - tl.x) + (tr.y - tl.y) * (tr.y - tl.y));//计算顶部两个角点的距离
int maxWidth = Math.Max((int)widthA, (int)widthB);//确定变换后图像的最大宽度
double heightA = Math.Sqrt((tr.x - br.x) * (tr.x - br.x) + (tr.y - br.y) * (tr.y - br.y));//计算右边两个角点的距离
double heightB = Math.Sqrt((tl.x - bl.x) * (tl.x - bl.x) + (tl.y - bl.y) * (tl.y - bl.y));//计算左边两个角点的距离
int maxHeight = Math.Max((int)heightA, (int)heightB);确定变换后图像的最大高度
//宽度和高度至少为1
maxWidth = (maxWidth < 1) ? 1 : maxWidth;
maxHeight = (maxHeight < 1) ? 1 : maxHeight;
// 将角点矩阵的数据类型转换为CV_32FC2(即32位浮点型的二维矩阵),并创建了一个目标矩阵dst,用于存储变换后的四个角点。
Mat src = new Mat();
corners.convertTo(src, CvType.CV_32FC2);
Mat dst = new Mat(4, 1, CvType.CV_32FC2);
dst.put(0, 0, 0, 0, maxWidth - 1, 0, maxWidth - 1, maxHeight - 1, 0, maxHeight - 1);
// compute and apply the perspective transformation matrix.
// 计算并应用透视变换矩阵。
Mat outputMat = new Mat(maxHeight, maxWidth, image.type(), new Scalar(0, 0, 0, 255));
Mat perspectiveTransform = Imgproc.getPerspectiveTransform(src, dst);//计算透视变换矩阵perspectiveTransform
Imgproc.warpPerspective(image, outputMat, perspectiveTransform, new Size(outputMat.cols(), outputMat.rows()));//原图像image进行透视变换,并将变换后的图像存储在Mat类型的outputMat中。
// return the transformed image.返回变换后的图像
return outputMat;
}
/// <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;
}
/// <summary>
/// Raises the is debug mode toggle value changed event.
/// </summary>
public void OnIsDebugModeToggleValueChanged()
{
if (isDebugMode != isDebugModeToggle.isOn)
{
isDebugMode = isDebugModeToggle.isOn;
}
}
}
}
#endif