在Unity3D和OpenCV之间传递图片(Texture2D/WebCamTexture转Mat)

写在前面的话:记录Unity调用opencv里的坑。这是趟了无数的坑之后,写下的满纸的辛酸泪。各种奇怪的错误、闪退折磨了N久之后终于得到的一个好的方法用来在Unity和OpenCV之间传递图片。PS:作为一个长期使用C#的程序猿,弄C++实在是太痛苦了,如果代码有什么不合理的地方也希望各位大佬指正批评。

1. 关于DLL

注意,本文不使用OpenCVforUnity!
关于C#调用C++的DLL,可以参考这里:Unity调用动态链接库dll和so.
写的很详细,非常值得参考。需要注意的是,函数一定要按照链接的方式去写,不然可能会找不到函数入口(这是坑之一)。

2.Texture2D=>Mat

首先,我们一般得到的贴图都是一个Texture,那么怎么转成Texture2D呢?可以使用以下方法:

Texture2D TextureToTexture2D(Texture texture)
{
    Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
    RenderTexture currentRT = RenderTexture.active;
    RenderTexture renderTexture = RenderTexture.GetTemporary(texture.width, texture.height, 32);
    Graphics.Blit(texture, renderTexture);
    RenderTexture.active = renderTexture;
    texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
    texture2D.Apply();
    RenderTexture.active = currentRT;
    RenderTexture.ReleaseTemporary(renderTexture);
    return texture2D;
}

有了这个Texture2D之后,我们需要获取保存图像的指针。代码如下:

pixels = texture2D.GetPixels32();
GCHandle pixelHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
IntPtr pixelPointer = pixelHandle.AddrOfPinnedObject();

关于GCHandle可以参考这里:GCHandle。
这里的pixels是一个Color32[]。
以上的代码需要:

using System;
using System.Runtime.InteropServices;

ok,C#端先到这里。
接下来是C++了。

extern "C" {
	DLLExport uchar* Unity2OpenCVImage(char* inputData, int width, int height,int threshold ,int& size)
	{
		vector<KeyPoint> keypoints;
		Mat opencvImage(height, width, CV_8UC4);
		memcpy(opencvImage.data, inputData, width*height * 4);
		//cvtColor(opencvImage, opencvImage, CV_BGR2RGB);//修改色彩通道BGR=>RGB
		//flip(opencvImage, opencvImage, 0);//翻转图片0为上下翻转,1为左右翻转,-1为01的组合
		Mat dst = opencvImage.clone();
		imshow("result", dst);
		Ptr<FastFeatureDetector> detector = FastFeatureDetector::create(threshold);
		detector->detect(opencvImage, keypoints);
		drawKeypoints(dst, keypoints, dst, Scalar::all(-1), DrawMatchesFlags::DRAW_OVER_OUTIMG);
		size = dst.cols*dst.rows*dst.channels();
		uchar* result = new uchar[dst.cols*dst.rows*4];
		memcpy(result, dst.data, dst.total()*sizeof(uchar)*4);
		return result;
	}
}

到imshow()为止完成了图像的传递,这个函数会将unity里物体上的材质显示到窗口中。
其中注释掉的两行是用来修改色彩通道和翻转图片的,因为Unity和OpenCV的图像存储方式不同,具体可以参考这里:图像计算的像素坐标系差异(这是坑之二)。
另外,需要注意的是memcpy这个函数,他是将inputData(类型为char* ,是函数输入值)里所有的内容拷贝到Mat.data里。第三个参数是拷贝长度,思考一下,一张四通道的图片的大小应该是多少?当然是weight * height * 4咯。(这是坑之三,一定要考虑好需要拷贝的大小,不然图像会不完整)。再多说一句,这里的大小严格的说应该是内存图像行跨度 * 高 * 4。不过我这里内存图像行跨度等于图像宽。关于内存图像行跨度、memcpy的使用以及Mat.data的内容请看这里:Mat::data指针

3.Mat=>Texture2D

在接着上面的DLL,我写了特征点检测。所以dst上面会有一些检测到的特征点,如果不需要当然可以去掉。
如果需要从DLL返回一张图片,则需要先将Mat里的data拷贝到一个数组里。这里依旧是使用memcpy。首先需要一个uchar*用来接收数据,这里的数据大小是 dst.total()*sizeof(uchar)*4,因为我们的Mat是CV_8UC4的,也就是八位Unsigned char(uchar),四通道。如果你使用的图片参数不是这个,那需要修改大小,可以参考这里:图片格式类型,总之要把图片里的所以数据都拷进去。
也许有人会问,我为什么不直接返回dst.data?原因是在退出函数时,Mat里的数据会被释放掉,返回的指针会变成空指针!!!(这是坑之四)。
接下是在Unity里调用DLL函数。

private IntPtr Data = IntPtr.Zero;
Data = Unity2OpenCVImage(pixelPointer, width, height, 80, ref size);

这里的pixelPointer还是第2步获取的那个指针。另外,在C#使用ref int相当于C++里的in&,这里用于获取图片大小。
接下来:

private byte[] buffer = new byte[size];
Marshal.Copy(Data, buffer, 0, size);
Color32[] colors = new Color32[width * height];
for (int i = 0; i < colors.Length; i++)
{
    colors[i] = new Color32(buffer[4 * i], buffer[4 * i + 1], buffer[4 * i + 2],1);
}
Texture2D outputTexture = new Texture2D(640, 640);
outputTexture.SetPixels32(colors);
outputTexture.Apply();
quad2.GetComponent<Renderer>().material.mainTexture = outputTexture;

这里使用到了Marshal.Copy这个函数,它的作用和C++里的memcpy有点相似,可以将Intptr指向的内容拷贝到buffer里,具体细节参考:Marshal.Copy。一定要注意拷贝内容的大小,否则可能会出现图片大小没对齐等问题,发生闪退或者报错。(这里是坑之五)
在获取buffer之后,我将buffer里的数据转成了Color32的类型,然后使用SetPixels32()将像素保存到一个Texture2D 上,最后将quad2上的材质的主贴图设置为这个Texture2D ,这样quad2上会显示传回来的图片。需要注意的是,我这里对buffer里的数据的转化可能不是最好的处理方式,不过这样做确实是可行的。另外,需要注意在保存像素到Texture2D后,需要使用Texture2D.Apply()刷新,这也算是一个小坑。

总览

最后,将两端的代码完整的展示一下:
C#端:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Runtime.InteropServices;
using System.IO;
public class aaa : MonoBehaviour {
    [HideInInspector]
    public Texture2D texture2D;
    public GameObject quad2;
    
    private Color32[] pixels;
    private IntPtr outputData;
    private int width;
    private int height;
    private IntPtr Data = IntPtr.Zero;
    private byte[] buffer;
    private int size;
    [DllImport("FastDection")]
    public static extern IntPtr Unity2OpenCVImage(IntPtr inputData , int width, int height,int threshold,ref int size);
    void Start () {
        texture2D = TextureToTexture2D(GetComponent<Renderer>().material.mainTexture);
        width = texture2D.width;
        height = texture2D.height;
        pixels = texture2D.GetPixels32();
        GCHandle pixelHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
        IntPtr pixelPointer = pixelHandle.AddrOfPinnedObject();
        int stride = width % 4 == 0 ? width : (width / 4 + 1) * 4;
        Data = Unity2OpenCVImage(pixelPointer, width, height, 80,ref size);
        buffer = new byte[size];
        Marshal.Copy(Data, buffer, 0, size);
        Color32[] colors = new Color32[width * height];
        for (int i = 0; i < colors.Length; i++)
        {
            colors[i] = new Color32(buffer[4 * i], buffer[4 * i + 1], buffer[4 * i + 2],1);
        }
        Texture2D outputTexture = new Texture2D(640, 640);
        outputTexture.SetPixels32(colors);
        outputTexture.Apply();
        quad2.GetComponent<Renderer>().material.mainTexture = outputTexture;
    }

    Texture2D TextureToTexture2D(Texture texture)
    {
        Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
        RenderTexture currentRT = RenderTexture.active;
        RenderTexture renderTexture = RenderTexture.GetTemporary(texture.width, texture.height, 32);
        Graphics.Blit(texture, renderTexture);
        RenderTexture.active = renderTexture;
        texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
        texture2D.Apply();
        RenderTexture.active = currentRT;
        RenderTexture.ReleaseTemporary(renderTexture);
        return texture2D;
    }
}

C++端(里面有一些小的注释,希望也能帮到大家):

#define DLLExport __declspec(dllexport)
#include "opencv2/opencv.hpp"
#include 
#include 
#include 
using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;

//typedef unsigned char byte;

extern "C" {	
	DLLExport uchar* Unity2OpenCVImage(char* inputData, int width, int height,int threshold ,int& size)
	{
		vector<KeyPoint> keypoints;
		Mat opencvImage(height, width, CV_8UC4);
		memcpy(opencvImage.data, inputData, width*height * 4);
		//最后一个参数为拷贝长度,应为图片压成的数组的长度,即高*宽*通道数(RGBA所以是4),
		//需要注意!!=>这里可能应该是内存图像行跨度*高*4!!,(此处宽==内存图像行跨度=640)
		//cvtColor(opencvImage, opencvImage, CV_BGR2RGB);//修改色彩通道BGR=>RGB
		//flip(opencvImage, opencvImage, 0);//翻转图片0为上下翻转,1为左右翻转,-1为01的组合
		//由于这里是把texture2D转Mat,检测特征点之后再把Mat转回去,所以不需要修改色彩通道和翻转图片,
		//否则后面还是要修改,这不是脱裤子放屁么
		Mat dst = opencvImage.clone();
		imshow("result", dst);
		Ptr<FastFeatureDetector> detector = FastFeatureDetector::create(threshold);
		detector->detect(opencvImage, keypoints);
		drawKeypoints(dst, keypoints, dst, Scalar::all(-1), DrawMatchesFlags::DRAW_OVER_OUTIMG);
		size = dst.cols*dst.rows*dst.channels();
		uchar* result = new uchar[dst.cols*dst.rows*4];
		memcpy(result, dst.data, dst.total()*sizeof(uchar)*4);
		////注意!!这里必须要使用memcpy拷贝一遍数据,而不能直接返回dst.data,不然退出函数时Mat里的数据会被释放掉,返回的会变成空指针
		return result;
	}
}

关于WebCamTexture转Mat

由于WebCamTexture是摄像头获取到的贴图,所以会不断刷新,建议放在携程里写以降低卡顿,可以参考这里:unity3d和opencv实时图像传递,处理,高效解决方案,几乎不影响fps,也感谢这位大神的文章,对我帮助很大。

你可能感兴趣的:(在Unity3D和OpenCV之间传递图片(Texture2D/WebCamTexture转Mat))