loading...马上就出来rua!

基于OpenCVForUnity的图像美化系统


前言介绍

​ 这篇文章的起因是数字图像处理这门课程的作业需要做一个基于OpenCV的图像美化系统,感觉OpenCV的各种环境配置有点麻烦,之前看到朋友用过插件实现在Unity中使用OpenCV的接口,我就寻思为什么不用Unity做这个作业呢,于是找到了OpenCVForUnity这个插件开整。上手后发现网上关于该插件的文章较少。虽说该插件是Java版OpenCV的复制,理论上使用起来几乎没有区别,但是经实际使用还是出现了一些问题,于是我打算将我的使用经历及踩坑情况一一记录下来。

​ 本文使用的Unity版本为2020.3.40f1c1版,OpenCVForUnity插件版本为2.5.8

项目地址(不包含插件)

成品地址

成品展示图

实验内容

图片的读取与保存

​ 作业要求是自选图片地址打开,而Unity本身并没有对资源管理器中操作的相关支持。查询资料后发现需要引入c#中Comdlg.dll中的方法实现打开任务管理器并获取到用户选择文件路径。故通过此方法附加上OpenCV中自带的imread方法读取图像,通过imwrite方法保存图像。在此插件中两方法都位于Imgcodecs类中。代码与使用效果如下:

参考博客地址

(不知道为什么选c#没有代码高亮 这边就选java了 ouo)

引入dll

/// <summary>
/// 调用系统的窗口,数据接收类
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class OpenFileName
{
    public int structSize = 0;
    public IntPtr dlgOwner = IntPtr.Zero;
    public IntPtr instance = IntPtr.Zero;
    public String filter = null;
    public String customFilter = null;
    public int maxCustFilter = 0;
    public int filterIndex = 0;
    public String file = null;
    public int maxFile = 0;
    public String fileTitle = null;
    public int maxFileTitle = 0;
    public String initialDir = null;
    public String title = null;
    public int flags = 0;
    public short fileOffset = 0;
    public short fileExtension = 0;
    public String defExt = null;
    public IntPtr custData = IntPtr.Zero;
    public IntPtr hook = IntPtr.Zero;
    public String templateName = null;
    public IntPtr reservedPtr = IntPtr.Zero;
    public int reservedInt = 0;
    public int flagsEx = 0;
}

public class WindowDll
{
    [DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
    public static extern bool GetOpenFileName([In, Out] OpenFileName ofn);
    [DllImport("Comdlg32.dll", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Auto)]
    public static extern bool GetSaveFileName([In, Out] OpenFileName ofd);
}

读取图片

​ 除了上述问题,在这部分还出了一个小插曲。由于正版插件很贵,我也只用于学习用途,我原本是在网上随便找了一个版本的插件,用imread读取图片时完全没有出现问题。后来由于该版本在噪声添加方面有不可解决的问题,我又找到了现在的2.5.8版本。替换后发现读取完的图片直接用matToTexture2D转为Texture显示后突然出现倒置现象。找了很久的问题也没有解决,最后采用了读取后手动转正的方法。在保存时也出现了类似的问题,可能和插件中实现方式有关。

public Mat LoadPic()
    {
        OpenFileName ofn = new OpenFileName();
        ofn.structSize = Marshal.SizeOf(ofn);
        ofn.filter = "图片文件(*.jpg*.png)\0*.jpg;*.png"; //显示的可选文件
        ofn.file = new string(new char[256]);
        ofn.maxFile = ofn.file.Length;
        ofn.fileTitle = new string(new char[64]);
        ofn.maxFileTitle = ofn.fileTitle.Length;
        string path = Application.streamingAssetsPath;   //默认路径
        path = path.Replace('/', '\\');
        ofn.initialDir = path;
        ofn.title = "Open Project";
        ofn.defExt = "JPG";
                           //注意 以下项目不一定要全选 但是0x00000008项不要缺少
        ofn.flags = 0x00080000 | 0x00001000 | 0x00000800 | 0x00000200 | 0x00000008;//OFN_EXPLORER|OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST| OFN_ALLOWMULTISELECT|OFN_NOCHANGEDIR
        //点击Windows窗口时开始加载选中的图片
        if (WindowDll.GetOpenFileName(ofn))
        {
            Debug.Log("Selected file with full path: " + ofn.file);
            Utils.setDebugMode(true);

            Mat read = Imgcodecs.imread(Utils.getFilePath(ofn.file), Imgcodecs.IMREAD_UNCHANGED); //得到图片,读取图片通道类型遵循原图片类型
            //手动反转两次
            Core.flip(read, read, 0);
            Core.flip(read, read, 1);
            //将原本读取的RGB颜色类型转换成Mat存储适用的BGR颜色类型
            Imgproc.cvtColor(read, read, Imgproc.COLOR_RGBA2BGRA);

            return read;
        }
        return null;
    }

保存图片

public void SavePic()
    {
        OpenFileName ofn = new OpenFileName();
        ofn.structSize = Marshal.SizeOf(ofn);
        ofn.filter = "图片文件(*.jpg*.png)\0*.jpg;*.png";
        ofn.file = new string(new char[256]);
        ofn.maxFile = ofn.file.Length;
        ofn.fileTitle = new string(new char[64]);
        ofn.maxFileTitle = ofn.fileTitle.Length;
        string path = Application.streamingAssetsPath;
        path = path.Replace('/', '\\');
        ofn.initialDir = path;
        ofn.title = "Open Project";
        ofn.defExt = "JPG";
        ofn.flags = 0x00080000 | 0x00001000 | 0x00000800 | 0x00000200 | 0x00000008;
        if (WindowDll.GetSaveFileName(ofn))
        {
            CancelOption();
            
            Mat toSave = savedMat.clone();
            Core.flip(toSave, toSave, 0); //手动反转
            Imgproc.cvtColor(toSave, toSave, Imgproc.COLOR_BGRA2RGBA);

            Imgcodecs.imwrite(ofn.file, toSave);
        }
    }

效果展示

打开

保存

调整明亮度与对比度

​ 亮度和对比度可以通过公式$\alpha * f(i,j) + \beta$来调整。其中$\alpha$可以代表对比度,$\beta$代表亮度。在OpenCV中既可以使用遍历像素的方法实现,也可以使用自带的ConvertTo方法快速调整。

public void AdjustBrightness()
    {
        Mat curMat = PictureLoader.Instance.savedMat.clone();
        Mat newMat = new Mat(curMat.size(), curMat.type());

        float value = brightness.value;
        curMat.convertTo(newMat, curMat.type(), 1, value);

        PictureLoader.Instance.curMat = newMat;
        curMat.Dispose();
    }

    public void AdjustAlpha()
    {
        Mat curMat = PictureLoader.Instance.savedMat.clone();
        Mat newMat = new Mat(curMat.size(), curMat.type());

        float value = alpha.value;
        curMat.convertTo(newMat, curMat.type(), value, 0);

        PictureLoader.Instance.curMat = newMat;
        curMat.Dispose();
    }

img

img

饱和度调整

算法步骤:

  1. 计算RGB三通道的最大最小值,并进一步得到delta和value。
    $$
    delta = (Max - Min)/255
    $$

    $$
    value = (Max+Min)/255
    $$

  2. 若最大最小一致,即delta=0,则表明为灰点,不需继续操作,直接处理下个像素。

  3. 通过value算出HSL中L的值
    $$
    L=(Max-Min)/(2*255)
    $$

  4. S值为
    $$
    \begin{cases}
    S=delta/value,L<0.5\S=delta/(2-value),L\ge0.5
    \end{cases}
    $$

  5. 当percent大于等于0时,即提高色彩饱和度,那么alpha值为:
    $$
    \begin{cases}
    alpha = S, percent+S\ge1\
    alpha=1-percent,else
    \end{cases}
    $$
    此时,调整后的图像RGB三通道值为:
    $$
    RGB=RGB+(RGB-L*255)*alpha
    $$

  6. 若percent小于0时,即降低色彩饱和度,则alpha=percent,此时调整后的图像RGB三通道值为:
    $$
    RGB=L255+(RGB-L255)*(1-alpha)
    $$

算法来源

​ 当时不知道为什么这段代码一运行程序就卡死了。后来发现是这个遍历的过程特别慢,可能算法比较复杂,在每个像素上运行计算量就很大。于是我就给它加了个协程顺便记了个时,发现它是真慢。每次运行大概要8s到12s左右。推测跟插件的效率也有关?

    IEnumerator Sat()
    {
        Mat curMat = PictureLoader.Instance.savedMat.clone();
        Mat newMat = new Mat(curMat.size(), curMat.type());

        float stime = Time.time;
        float saturation = sat.value;
        Debug.Log("s");
        Debug.Log(curMat);
        float increment = (saturation - 80) * 1.0f / 200f;
        for (int col = 0; col < curMat.cols(); col++)
        {
            for (int row = 0; row < curMat.rows(); row++)
            {
                // R,G,B 分别对应数组中下标的 2,1,0
                float r = (float)curMat.get(row, col)[2];
                float g = (float)curMat.get(row, col)[1];
                float b = (float)curMat.get(row, col)[0];

                float maxn = Mathf.Max(r, Mathf.Max(g, b));
                float minn = Mathf.Max(r, Mathf.Max(g, b));

                float delta, value;
                delta = (maxn - minn) / 255;
                value = (maxn + minn) / 255;

                float new_r, new_g, new_b;

                float light, sat, alpha;
                light = value / 2;

                if (light < 0.5)
                    sat = delta / value;
                else
                    sat = delta / (2 - value);

                if (increment >= 0)
                {
                    if ((increment + sat) >= 1)
                        alpha = sat;
                    else
                    {
                        alpha = 1 - increment;
                    }
                    alpha = 1 / alpha - 1;
                    new_r = r + (r - light * 255) * alpha;
                    new_g = g + (g - light * 255) * alpha;
                    new_b = b + (b - light * 255) * alpha;
                }
                else
                {
                    alpha = increment;
                    new_r = light * 255 + (r - light * 255) * (1 + alpha);
                    new_g = light * 255 + (g - light * 255) * (1 + alpha);
                    new_b = light * 255 + (b - light * 255) * (1 + alpha);
                }
                newMat.put(row, col, new double[] { new_b, new_g, new_r, curMat.get(row, col)[3] });
            }
            yield return null;
        }
        PictureLoader.Instance.curMat = newMat;
        Debug.Log("f" + (Time.time - stime));
    }

img

添加边框

​ 先加载边框图片,然后resize到原图大小,生成边框的灰度图作为mask,然后用copyTo方法附加到原图上。

public void MakeBorder()
    {
        Mat border = PictureLoader.Instance.LoadPic(); //加载边框图片
        Mat curMat = PictureLoader.Instance.savedMat.clone();

        Imgproc.resize(border, border, curMat.size()); //调整边框大小适应图片

        Mat mask = border.clone();
        Imgproc.cvtColor(mask, mask, Imgproc.COLOR_BGRA2GRAY); //生成灰度图作为mask
        Imgproc.threshold(mask, mask, 10, 255, Imgproc.THRESH_BINARY); 

        border.copyTo(curMat, mask); //复制边框到原图

        PictureLoader.Instance.curMat = curMat;
    }

img

img

浮雕效果

​ 经查阅浮雕效果的原理是每个像素的RGB值都设置为该位置的初始值减去其右下方第二的像素的差,最后统一加上128用于控制灰度,显示出类似浮雕的灰色。这样处理的思路是,将图像上的每个点与它的对角线的像素点形成差值,这样淡化相似的颜色,突出不同的颜色、边缘,从而使图像产生纵深感,产生类似于浮雕的效果。(解释来自

public void FuDiao()
    {
        Mat src = PictureLoader.Instance.savedMat.clone();
        if (src.type() == CvType.CV_8UC1) return;

        Mat dst = src.clone();
        int rowNumber = dst.rows();
        int colNumber = dst.cols();

        for (int i = 1; i < rowNumber - 1; ++i)
        {
            for (int j = 1; j < colNumber - 1; ++j)
            {
                dst.put(i, j, new double[] { (src.get(i + 1, j + 1)[0] - src.get(i - 1, j - 1)[0] + 128), (src.get(i + 1, j + 1)[1] - src.get(i - 1, j - 1)[1] + 128) , (src.get(i + 1, j + 1)[2] - src.get(i - 1, j - 1)[2] + 128), 255 });
            }

        }
        PictureLoader.Instance.curMat = dst;
    }

img

简单倒影效果

​ 采用随机将像素偏移一定位置的方法创造出类似倒影的效果,再将原图与倒影图上下拼接完成效果。代码如下。

public void Reflection()
    {
        Mat img = PictureLoader.Instance.savedMat.clone();
        Mat dstImg = new Mat(2 * img.rows(), img.cols(), img.type());
        img.copyTo(new Mat(dstImg, new Rect(0, img.rows(), img.cols(), img.rows())));

        int rowNumber = img.rows();
        int colNumber = img.cols();

        for (int i = 1; i < rowNumber - 1; ++i)
        {
            for (int j = 1; j < colNumber - 1; ++j)
            {
                int deltax = UnityEngine.Random.Range(0, 50);
                int deltay = UnityEngine.Random.Range(0, 50);

                while (j + deltax >= colNumber)
                {
                    deltax = UnityEngine.Random.Range(0, 50);
                }
                while (i + deltay >= rowNumber)
                {
                    deltay = UnityEngine.Random.Range(0, 50);
                }

                img.put(i, j, new double[] { img.get(i + deltay, j + deltax)[0], img.get(i + deltay, j + deltax)[1], img.get(i + deltay, j + deltax)[2], 255 });
            }

        }

        Core.flip(img, img, 0);
        img.copyTo(new Mat(dstImg, new Rect(0, 0, img.cols(), img.rows())));

        PictureLoader.Instance.curMat = dstImg;
    }

img

实验总结

​ 感觉大部分时间都花在查资料和api以及代码的调试上了,实际上并没有很多产出。还有很多内容由于一些无法解决的问题没有实现。希望之后能继续调试并完善这个项目。


文章作者: mashimaro kumo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 mashimaro kumo !
评论
  目录