首先打开场景定位到主脚本:ComicFilterExample,然后定位到主函数:Update,会发现他首先把摄像头mat取出来,再用cvtColor函数将rgba转gray参考:https://blog.csdn.net/keith_bb/article/details/53470170
先转成了灰度图,然后高斯滤波GaussianBlur(处理图像之前一般都要高斯滤波处理一下,对整幅图像进行加权平均的过程)参考:https://blog.csdn.net/vblittleboy/article/details/9187447
然后进行一个简单处理,灰度值小于60的直接变0,60-120之间的采用斜线填充,120以上的变255这样整个图就变成了黑白灰三色。
然后进行边缘检测,再进行颜色反转(输出的边缘为白色,转为黑色),这样最终的效果就出来了!
参考了博客:https://blog.csdn.net/eevee_1/article/details/118632540?ops_request_misc=&request_id=&biz_id=102&utm_term=ComicFilter&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-3-118632540.nonecase&spm=1018.2226.3001.4187
可能是版本变了,所以略有不同。
#if !(PLATFORM_LUMIN && !UNITY_EDITOR)
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using OpenCVForUnity.UnityUtils;
using OpenCVForUnity.UnityUtils.Helper;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace OpenCVForUnityExample
{
/// <summary>
/// Comic Filter Example
/// An example of image processing (comic filter) using the Imgproc class.
/// 使用Imgproc类进行图像处理(漫画滤镜)的示例。
/// Referring to http://dev.classmethod.jp/smartphone/opencv-manga-2/.
/// </summary>
[RequireComponent(typeof(WebCamTextureToMatHelper))]
public class ComicFilterExample : MonoBehaviour
{
/// <summary>
/// The comic filter.
/// 漫画滤镜。
/// </summary>
ComicFilter comicFilter;
/// <summary>
/// The texture.
/// 贴图
/// </summary>
Texture2D texture;
/// <summary>
/// The webcam texture to mat helper.
/// 相机提额度到矩阵的助手脚本。
/// </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).
// 避免仅在某些 Android 设备(例如 Google Pixel、Pixel2)中出现的前置摄像头低光问题。
webCamTextureToMatHelper.avoidAndroidFrontCameraLowLightIssue = true;
#endif
webCamTextureToMatHelper.Initialize();
}
/// <summary>
/// Raises the web cam texture to mat helper initialized event.
/// 将网络摄像头纹理提升到 mat 助手初始化事件。
/// </summary>
public void OnWebCamTextureToMatHelperInitialized()
{
Debug.Log("OnWebCamTextureToMatHelperInitialized");
// 从辅助类获取Mat。
Mat webCamTextureMat = webCamTextureToMatHelper.GetMat();
// 从Mat创建一个新的Texture2D对象,并将其分配给gameObject的材质的主纹理。
texture = new Texture2D(webCamTextureMat.cols(), webCamTextureMat.rows(), TextureFormat.RGBA32, false);
Utils.matToTexture2D(webCamTextureMat, texture);
gameObject.GetComponent<Renderer>().material.mainTexture = texture;
// 将gameObject的缩放设置为与Mat的尺寸匹配。
gameObject.transform.localScale = new Vector3(webCamTextureMat.cols(), webCamTextureMat.rows(), 1);
Debug.Log("Screen.width " + Screen.width + " Screen.height " + Screen.height + " Screen.orientation " + Screen.orientation);
// 记录有关屏幕尺寸和方向的信息,并将此信息添加到fpsMonitor对象中(如果存在)。
if (fpsMonitor != null)
{
fpsMonitor.Add("width", webCamTextureMat.width().ToString());
fpsMonitor.Add("height", webCamTextureMat.height().ToString());
fpsMonitor.Add("orientation", Screen.orientation.ToString());
}
float width = webCamTextureMat.width();
float height = webCamTextureMat.height();
float widthScale = (float)Screen.width / width;
float heightScale = (float)Screen.height / height;
//根据屏幕尺寸和Mat的尺寸计算相机正交大小的比例因子。
if (widthScale < heightScale)
{
Camera.main.orthographicSize = (width * (float)Screen.height / (float)Screen.width) / 2;
}
else
{
Camera.main.orthographicSize = height / 2;
}
// 用预设的边缘检测算法值创建新的ComicFilter对象。
int thickness = (Mathf.Max(width, height) <= 640) ? 3 : 5;
comicFilter = new ComicFilter(60, 120, thickness);
}
/// <summary>
/// Raises the web cam texture to mat helper disposed event.
/// 将网络摄像头纹理提升到 mat helper 处理事件。用于将网络摄像头纹理转换为Mat的辅助类被销毁时调用。
/// </summary>
public void OnWebCamTextureToMatHelperDisposed()
{
// 记录日志,表示辅助类已被销毁。
Debug.Log("OnWebCamTextureToMatHelperDisposed");
// 销毁ComicFilter对象。
comicFilter.Dispose();
// 如果texture对象不为空,则销毁它。
if (texture != null)
{
Texture2D.Destroy(texture);
texture = null;
}
}
/// <summary>
/// Raises the web cam texture to mat helper error occurred event.
/// 将网络摄像头纹理提升到 mat helper 错误发生事件。
/// </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())//检查是否正在播放网络摄像头纹理,并且是否已经更新了当前帧。
{
Mat rgbaMat = webCamTextureToMatHelper.GetMat();// 获取Mat对象,以便进行处理。
comicFilter.Process(rgbaMat, rgbaMat);//调用ComicFilter对象的Process方法对Mat进行处理。
//Imgproc.putText(rgbaMat, "W:" + rgbaMat.width() + " H:" + rgbaMat.height() + " SO:" + Screen.orientation, new Point(5, rgbaMat.rows() - 10), Imgproc.FONT_HERSHEY_SIMPLEX, 1.0, new Scalar(255, 255, 255, 255), 2, Imgproc.LINE_AA, false);
Utils.matToTexture2D(rgbaMat, texture);//将处理后的Mat转换为Texture2D对象。
}
}
/// <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
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using OpenCVForUnity.UtilsModule;
using System;
using UnityEngine;
namespace OpenCVForUnityExample
{
public class ComicFilter
{
/// <summary>
/// grayMat:用于存储灰度化后的图像的Mat对象。
/// maskMat:用于存储图像掩膜的Mat对象。
/// screentoneMat:用于存储图像半色调效果的Mat对象。
/// grayDstMat:用于存储灰度化后的图像的目标Mat对象。
/// grayLUT:用于存储灰度化查找表的Mat对象。
/// contrastAdjustmentsLUT:用于存储对比度调整查找表的Mat对象。
/// kernel_dilate和kernel_erode:用于存储膨胀和腐蚀过程中的内核的Mat对象。
/// blurSize:用于存储应用高斯模糊时的内核大小的Size对象。
/// blackThresh:用于存储黑色阈值的整数值。
/// drawMainLine:一个布尔值,指示是否绘制主要线条。
/// useNoiseFilter:一个布尔值,指示是否使用噪声过滤
/// </summary>
Mat grayMat;
Mat maskMat;
Mat screentoneMat;
Mat grayDstMat;
Mat grayLUT;
Mat contrastAdjustmentsLUT;
Mat kernel_dilate;
Mat kernel_erode;
Size blurSize;
int blackThresh;
bool drawMainLine;
bool useNoiseFilter;
/// <summary>
/// ComicFilter.cs文件中的构造函数。构造函数使用传递给它的参数来初始化类的各种成员变量。
/// </summary>
/// <param name="blackThresh"></param> 黑色阈值,用于确定图像中哪些像素应该被处理为黑色。默认值为60。
/// <param name="grayThresh"></param>灰度阈值,用于确定图像中哪些像素应该被处理为灰色。默认值为120。
/// <param name="thickness"></param>线条粗细,用于确定生成的卡通效果中线条的粗细。默认值为5。
/// <param name="useNoiseFilter"></param>一个布尔值,指示是否使用噪声过滤。默认值为true。
public ComicFilter(int blackThresh = 60, int grayThresh = 120, int thickness = 5, bool useNoiseFilter = true)
{
// 将传递给它的参数值分配给类的相应成员变量,并使用这些值初始化其它成员变量。
this.blackThresh = blackThresh;
this.drawMainLine = (thickness != 0);
this.useNoiseFilter = useNoiseFilter;
// 计算灰度查找表grayLUT对象,该查找表将图像中的每个像素值映射到0到255之间的灰度级别。
// 如果当前元素的值在blackThresh和grayThresh之间,则将LUT中对应像素的值设置为255,否则保持默认值(0)不变。
// 这个过程的目的是将图像中黑色到灰色之间的像素映射到白色,其他像素映射到黑色。
grayLUT = new Mat(1, 256, CvType.CV_8UC1);
byte[] lutArray = new byte[256];
for (int i = 0; i < lutArray.Length; i++)
{
if (blackThresh <= i && i < grayThresh)//i大于黑色阈值但小于灰度阈值
lutArray[i] = 255;
}
MatUtils.copyToMat(lutArray, grayLUT);
// 如果线条粗细不为0,则还会计算用于膨胀、腐蚀和高斯模糊的内核,并用它们初始化相应的成员变量。
if (drawMainLine)
{
kernel_dilate = new Mat(thickness, thickness, CvType.CV_8UC1, new Scalar(1));//这个内核用于膨胀操作,可以增加图像中线条的粗细。
int erode = (thickness >= 5) ? 2 : 1;//根据thickness的值计算腐蚀内核的大小erode。
kernel_erode = new Mat(erode, erode, CvType.CV_8UC1, new Scalar(1));//这个内核用于腐蚀操作,可以平滑图像中的噪声。
int blur = (thickness >= 4) ? thickness - 1 : 3;//根据thickness的值计算高斯模糊的内核大小blur。如果thickness大于等于4,则将blur设置为thickness-1,否则将其设置为3。
blurSize = new Size(blur, blur);//使用blur的值创建一个大小为blur x blur的Size对象blurSize,该对象用于指定高斯模糊的内核大小。
//使用一个名为contrastAdjustmentsLUTArray的byte数组来初始化对比度调整查找表contrastAdjustmentsLUT。
contrastAdjustmentsLUT = new Mat(1, 256, CvType.CV_8UC1);
byte[] contrastAdjustmentsLUTArray = new byte[256];
for (int i = 0; i < contrastAdjustmentsLUTArray.Length; i++)
{
int a = (int)(i * 1.5f);
contrastAdjustmentsLUTArray[i] = (a > byte.MaxValue) ? (byte)255 : (byte)a;
}
MatUtils.copyToMat(contrastAdjustmentsLUTArray, contrastAdjustmentsLUT);//这个查找表将用于调整卡通图像的对比度。
}
}
/// <summary>
/// 该方法对源图像(src)进行处理,并将结果保存到目标图像(dst)中。该方法应用各种图像处理技术以创建漫画风格的效果。
/// </summary>
/// <param name="src"></param>源图像
/// <param name="dst"></param>目标图像
/// <param name="isBGR"></param>布尔标志
public void Process(Mat src, Mat dst, bool isBGR = false)
{
//首先检查源图像和目标图像是否不为空。
if (src == null)
throw new ArgumentNullException("src == null");
if (dst == null)
throw new ArgumentNullException("dst == null");
// 检查grayMat对象是否为空或其维度是否与源图像匹配。
// 如果任何条件为真,则该方法会释放grayMat、maskMat、screentoneMat和grayDstMat对象并将它们设置为null。
if (grayMat != null && (grayMat.width() != src.width() || grayMat.height() != src.height()))
{
grayMat.Dispose();
grayMat = null;
maskMat.Dispose();
maskMat = null;
screentoneMat.Dispose();
screentoneMat = null;
grayDstMat.Dispose();
grayDstMat = null;
}
// grayMat、maskMat和grayDstMat对象使用源图像的维度进行实例化。
// 这段代码使用了null合并运算符(??),它检查grayMat、maskMat和grayDstMat对象是否为null。
// 如果是null,则使用指定的高度、宽度和类型(CV_8UC1)创建一个新的Mat对象,并将其赋值给grayMat、maskMat和grayDstMat。
// 如果对象不为null,则不会创建新的Mat对象。这个代码段的作用是确保grayMat、maskMat和grayDstMat对象已经被正确初始化,并且它们的维度与源图像(src)相同。
grayMat = grayMat ?? new Mat(src.height(), src.width(), CvType.CV_8UC1);
maskMat = maskMat ?? new Mat(src.height(), src.width(), CvType.CV_8UC1);
grayDstMat = grayDstMat ?? new Mat(src.height(), src.width(), CvType.CV_8UC1);
// 如果screentoneMat对象为空,则该方法创建一个带条纹的半色调并将其保存到screentoneMat中。即条纹填充
if (screentoneMat == null)
{
// create a striped screentone.创建一个条纹网点。
screentoneMat = new Mat(src.height(), src.width(), CvType.CV_8UC1, new Scalar(255));
for (int i = 0; i < screentoneMat.rows() * 2.5f; i = i + 4)
{
Imgproc.line(screentoneMat, new Point(0, 0 + i), new Point(screentoneMat.cols(), -screentoneMat.cols() + i), new Scalar(0), 1);//绘制填充
}
}
/// 检查源图像的类型。
/// 如果它是灰度图像(CV_8UC1),则将源图像复制到grayMat。
/// 如果它是彩色图像(CV_8UC3或CV_8UC4),则使用适当的颜色转换函数(COLOR_BGR2GRAY、COLOR_RGB2GRAY、COLOR_BGRA2GRAY或COLOR_RGBA2GRAY)将源图像转换为灰度图像,并将其保存到grayMat。
if (src.type() == CvType.CV_8UC1)
{
src.copyTo(grayMat);
}
//使用Imgproc.cvtColor函数进行颜色空间转换后,得到的图像只有一种颜色,即黑白灰阶。
//因为该函数将源图像从BGR或RGB模式转换为灰度模式,灰度图像只有一个通道,并且每个像素的值仅表示其亮度或灰度,而不包含颜色信息。
//在灰度图像中,每个像素的颜色值表示它在黑白灰阶中的灰度级别,通常在0到255之间,其中0表示黑色,255表示白色,中间的灰度级别代表着不同程度的灰度。
//因此,灰度图像可以看作是一种单通道的黑白图像,每个像素的值仅表示其亮度或灰度,而不包含颜色信息。
else if (src.type() == CvType.CV_8UC3)
{
Imgproc.cvtColor(src, grayMat, (isBGR) ? Imgproc.COLOR_BGR2GRAY : Imgproc.COLOR_RGB2GRAY);
}
else
{
Imgproc.cvtColor(src, grayMat, (isBGR) ? Imgproc.COLOR_BGRA2GRAY : Imgproc.COLOR_RGBA2GRAY);
}
// binarize.使用阈值(blackThresh)对灰度图像进行二值化,并将结果保存到grayDstMat中。
Imgproc.threshold(grayMat, grayDstMat, blackThresh, 255.0, Imgproc.THRESH_BINARY);
// draw striped screentone.使用查找表(LUT)和maskMat在二值化图像上绘制了一个带条纹的半色调。
/// <summary>
/// 灰度查找表(LUT)是一种用于将输入像素值映射到输出像素值的技术。在这里,grayLUT是一个256x1的Mat矩阵,其中每个像素值代表了输入像素值的映射输出值。
/// </summary>
Core.LUT(grayMat, grayLUT, maskMat);// 对灰度图像grayMat进行颜色映射操作,将灰度值转换为新的颜色值,保存在maskMat中。黑白之间的部分为1,在screentoneMat.copyTo(grayDstMat, maskMat);会被复制。
//maskMat = new Mat(src.height(), src.width(), CvType.CV_8UC1, new Scalar(0)); 可以注释掉这行或者取消注释,能看出差异。
//screentoneMat为得到的填充图像,根据grayLUT求得的maskMat用于指示哪些部分需要填充。
//screentoneMat为一张全部为填充条纹的Mat,依据maskMat使用copyTo将条纹复制到grayDstMat实现填充效果。
screentoneMat.copyTo(grayDstMat, maskMat);//使用copyTo函数将screentoneMat复制到grayDstMat中,但只在掩码图像maskMat中对应的像素位置上进行复制。如果掩码图像maskMat中某个像素值为0,则不进行复制,因此输出图像中的颜色数量可能减少。
//screentoneMat.copyTo(grayDstMat);
// draw main line.
if (drawMainLine)
{
// 使用LUT函数将grayMat进行对比度调整
Core.LUT(grayMat, contrastAdjustmentsLUT, maskMat); // = grayMat.convertTo(maskMat, -1, 1.5, 0);
// 使用模糊滤波和膨胀操作对maskMat进行处理。
if (useNoiseFilter)
{
Imgproc.blur(maskMat, grayMat, blurSize);// 模糊滤波可以平滑图像并减少噪声。
Imgproc.dilate(grayMat, maskMat, kernel_dilate);// 膨胀操作可以将白色区域扩张,使图像中的线条变得更加粗细。
}
else
{
Imgproc.dilate(maskMat, grayMat, kernel_dilate);
}
// 使用absdiff函数计算grayMat和maskMat之间的差异,并使用threshold函数将结果二值化为二进制掩码。
Core.absdiff(grayMat, maskMat, grayMat);
Imgproc.threshold(grayMat, maskMat, 25, 255.0, Imgproc.THRESH_BINARY);
// 如果启用了噪声过滤,则对掩码进行侵蚀和位反转操作,并使用copyTo函数将掩码应用于grayDstMat。
if (useNoiseFilter)
{
Imgproc.erode(maskMat, grayMat, kernel_erode);// 使用了OpenCV的erode函数对二值化的掩码图像(maskMat)进行腐蚀操作,以去除噪声并使线条更加细长
Core.bitwise_not(grayMat, maskMat);// 使用bitwise_not函数对grayMat进行位反转,将掩码中的白色部分变为黑色,黑色部分变为白色。
maskMat.copyTo(grayDstMat, grayMat);// 使用copyTo函数将处理后的掩码(grayMat)应用于grayDstMat中,以过滤掉原始图像中的非线条部分,只保留线条部分。
}
// 否则,只需使用bitwise_not函数反转掩码,并将结果应用于grayDstMat。
else
{
Core.bitwise_not(maskMat, grayMat);
grayMat.copyTo(grayDstMat, maskMat);
}
}
// 用于将处理后的图像grayDstMat复制到目标图像dst中。这个过程的作用是将处理后的图像应用于目标图像中,以便显示和保存。
// 它检查目标图像的类型是否为CV_8UC1(灰度图像)。
// 如果是,则直接将grayDstMat复制到dst中,因为它们的类型相同。
if (dst.type() == CvType.CV_8UC1)
{
grayDstMat.copyTo(dst);
}
// 如果目标图像的类型为CV_8UC3(三通道BGR或RGB图像),则使用cvtColor函数将grayDstMat从灰度图像转换为BGR或RGB图像,并将结果保存到dst中。转换时,可以根据isBGR标志变量选择BGR或RGB模式。
else if (dst.type() == CvType.CV_8UC3)
{
Imgproc.cvtColor(grayDstMat, dst, (isBGR) ? Imgproc.COLOR_GRAY2BGR : Imgproc.COLOR_GRAY2RGB);
}
// 如果目标图像的类型不是CV_8UC1或CV_8UC3,则使用cvtColor函数将grayDstMat从灰度图像转换为BGRA或RGBA图像,并将结果保存到dst中。
else
{
Imgproc.cvtColor(grayDstMat, dst, (isBGR) ? Imgproc.COLOR_GRAY2BGRA : Imgproc.COLOR_GRAY2RGBA);
}
}
public void Dispose()
{
foreach (var mat in new[] { grayMat, maskMat, screentoneMat, grayDstMat, grayLUT, kernel_dilate, kernel_erode, contrastAdjustmentsLUT })
if (mat != null) mat.Dispose();
grayDstMat =
screentoneMat =
maskMat =
grayMat =
grayLUT =
kernel_dilate =
kernel_erode =
contrastAdjustmentsLUT = null;
}
}
}