写在前面的话:记录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 <opencv2/xfeatures2d.hpp>
#include <opencv2/core/core.hpp>
#include <iostream>
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,也感谢这位大神的文章,对我帮助很大。