Unity是由Unity Technologies研发的跨平台游戏引擎,ARKit是苹果公司在2017年发布的在ios上运行的增强现实SDK。Unity-ARKit-Plugin 是Unity3D研发的库,可以在Unity环境下编写有趣的ARKit小程序。现在的ARKit2.0功能可谓十分强大,可以追踪面部表情,识别图像,识别物体等等。然而,这一切的功能开发非常受制于苹果公司,这让人不禁想到,我们是否可以把强大的OpenCV和Unity-ARKit结合起来呢?今天我们就来教大家如何把三者结合起来。先来看看完成后是什么样子:
首先是一些必要软件和库的安装,包括Unity和Unity-ARKit-Plugin。安装Unity时,主要选项有Unity主体,IOS支持和Visual Studio IDE,如下图所示:
Vuforia是另一个AR识别库,这里可以不安装。接下来下载Unity-ARKit-Plugin (https://bitbucket.org/Unity-Technologies/unity-arkit-plugin/downloads/?tab=downloads). 解压并用Unity打开。
三者结合的关键是搭建沟通的桥梁。以Unity为主体,我们首先获取苹果设备中的视频帧。打开Assets ▸ UnityARKitPlugin ▸ Plugins ▸ iOS ▸ UnityARKit ▸ NativeInterface下的ARKitDefines.h. 这里我们要定义一个新的类型。注意因为大多数OpenCV操作都在灰度图像上进行,所以这里的类型中只包括了灰度图像,需要彩色图像的小伙伴可以直接使用上面的UnityPixelBuffer。
接下来打开ARSessionNative.mm以做修改,mm是一类源代码文件,带有这种扩展名的文件,除了可以包含Objective-C和C代码以外还可以包含C++代码。打开并加入以下高亮部分。
细心的小伙伴会发现,这里OpenCVPixelBuffer模仿了UnityPixelBuffer的定义。第一部分生成一个对象,第二部分将设备的视频帧拷贝到对象的YPixelbytes,YPixelByte是一个指针,将会在Unity中定义。
有的同学可能会问,为什么不直接用s_UnityPixelBuffer呢?因为s_UnityPixelBuffer指向背景图像,然而我们不希望改动背景图像,所以建立了一个新的函数。那为什么不使用OpenCV里获取视频帧的函数呢?我尝试用OpenCV教程里的方法但失败了,有兴趣的同学可以亲自试一下。
在回到Unity前,我们先解决OpenCV和Unity的链接。这里我给大家准备好了一个pakcage,可以直接导入Unity
(https://github.com/TerryLiu007/ARCV2.0/blob/master/ARCV2.0_opencv_starter.unitypackage)导入后你会在Assets ▸ UnityOpenCVPlugin ▸ Plugins ▸ iOS ▸ NativeInterface下看到NativeInterface.mm. 里面的重要部分包括:
// OpenCV interface 界面定义
@interface videoCapture : NSObject
{
int width;
int height;
}
@end
// OpenCV implementation 类定义
@implementation videoCapture
// 初始函数
- (instancetype)initWithWidth:(int)w height:(int)h {
if (self) {
width = w;
height = h;
}
return self;
}
// 每一帧的更新
- (void)updateWithWidth: (int)inputWidth height: (int)inputHeight input: (unsigned char*)inputData output: (unsigned char*)outputData {
Mat img(inputHeight, inputWidth, CV_8UC1, inputData);
// Resized to specified size
Mat gray(360, 640, img.type());
resize(img, gray, gray.size(), cv::INTER_AREA);
// Canny edge
Mat edge;
Canny(gray, edge, 100, 200);
edge = ~edge;
// Convert to Unity's texture format (RGBA)
Mat argb;
cvtColor(edge, argb, CV_GRAY2RGBA);
// Copy to buffer secured by Unity side
memcpy(outputData, argb.data, argb.total() * argb.elemSize());
}
// Declare functions to export to C# 桥接到C#的定义
extern "C" {
void* allocateVideoCapture(int width, int height);
void releaseVideoCapture(void* capture);
void updateVideoCapture(void* capture, int width, int height, unsigned char* inputImage, unsigned char* outputImage);
}
// Generate objects 对象初始化,_bridge_retained 指手动内存管理
void* allocateVideoCapture(int width, int height) {
videoCapture* capture = [[videoCapture alloc] initWithWidth:width height:height];
return (__bridge_retained void*)capture;
}
// destroy object 对象终止,_bridge_transfer 指内存交还系统(ARC)管理
void releaseVideoCapture(void* capture) {
videoCapture* cap = (__bridge_transfer videoCapture*)capture;
cap = nil;
}
// for calling every frame 每一帧的更新
void updateVideoCapture(void* capture, int width, int height, unsigned char* inputImage, unsigned char* outputImage) {
videoCapture* cap = (__bridge videoCapture*)capture;
[cap updateWithWidth:width height:height input:inputImage output:outputImage];
}
(左右滑动试试)
函数定义的框架使用Object-C,而对于OpenCV部分使用C++,对于每一帧的更新,大家可以自由发挥,这里以Canny edge为例。
接下来我们回到Unity环境中,对接上面定义的四个函数,打开Assets ▸ UnityOpenCVPlugin ▸ Plugins ▸ iOS ▸ Helper下的VideoCaptureSimple.cs
// Import external library 对接四个函数
[DllImport("__Internal")]
private static extern IntPtr allocateVideoCapture(int width, int height);
[DllImport("__Internal")]
private static extern void releaseVideoCapture(IntPtr capture);
[DllImport("__Internal")]
private static extern void updateVideoCapture(IntPtr capture, int width, int height, IntPtr inputImage, IntPtr outputImage);
[DllImport("__Internal")]
private static extern void OpenCVPixelData (int enable, IntPtr YPixelBytes);
接下来就像引用Unity环境内的函数一样,可以直接引用以上函数,首先定义指针
// Pointer to device capture object 定义指向OpenCV界面的指针
private IntPtr nativeCapture;
// Video texture 设备视频帧的指针
private byte[] textureYBytes;
private GCHandle textureYHandle;
private IntPtr textureYInputPtr;
// Output render image 输出图像的指针
private Texture2D texture;
private Color32[] pixels;
private GCHandle pixelsHandle;
private IntPtr pixelsOutputPtr;
接着初始化指针,这里以输出图像为例
void Start () {
#if UNITY_IOS
UnityARSessionNativeInterface.ARFrameUpdatedEvent += UpdateCamera;
texturesInitialized = false;
// 初始化OpenCV界面指针
nativeCapture = allocateVideoCapture(inputWidth, inputHeight);
// 初始化输出图像指针
texture = new Texture2D(640, 360, TextureFormat.ARGB32, false);
pixels = texture.GetPixels32();
pixelsHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
pixelsOutputPtr = pixelsHandle.AddrOfPinnedObject();
#endif
//渲染目标的纹理即是输出图像
renderTarget.material.mainTexture = texture;
}
pixels在每一帧会被修改,要想在渲染目标上显示,需要apply()。在Update()函数中
void Update() {
#if UNITY_IOS
if (!texturesInitialized)
return;
//Fetch the video texture 获取新的视频帧
SetOpenCVPixelData (true, textureYInputPtr);
//Display video 处理视频帧并在渲染目标上显示
updateVideoCapture(nativeCapture, inputWidth, inputHeight, textureYInputPtr, pixelsOutputPtr);
texture.SetPixels32(pixels);
texture.Apply();
#endif
}
最后还有一些释放指针的处理,这里就不细讲了。
程序的桥接部分已经基本完成,接下来是完成Unity项目。打开UnityARKitScene,只保留图中所示的三项,并创建新的平面,把VideoCaptureSimple和Capture材料拖进去。这里因为Unity坐标原点位于左下,OpenCV坐标原点位于左上,为了让图像旋转准确,对于X轴对称,X的Scale为负。
在导出项目前还有重要的一步,就是导入opencv2.framework。 这一步在Xcode Project中也可以做,但是每次都要手动设置很麻烦。Assets ▸ UnityOpenCVPlugin ▸ Plugins ▸ iOS ▸ Editor下的UnityOpenCVBuildPostprocessor.cs帮我们自动完成了opencv2.framework的导入。
proj.AddFrameworkToProject(proj.TargetGuidByName("Unity-iPhone"), "opencv2.framework", false);
注意这里Unity会在默认文件夹找这个framework,我们需要提前把opencv2.framework放到下面这个文件夹。其中iPhoneOSxx.x.sdk是你安装的sdk版本。关于如何编译opencv2.framework,网站上有很多教程,这里就不赘述了。
Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOSxx.x.sdk/System/Library/Frameworks/
最后一步就是build project,在打开的Xcode Project中使用你自己的账号,安装到手机即可。
这个小demo到这里就结束了,然而这仅仅是一个框架,小伙伴们可以自由发挥,填充进去更多的东西。笔者在一年前使用OpenCV标定图像放置虚拟物品,只可惜几个月后苹果发布了ARKit1.5,实现了同样的功能。然而理论上OpenCV可以做到的东西远不止图像处理,使用这个框架加上Unity的渲染引擎,能做到的就靠大家的想象力了。
关于标定图像放置物品的场景也已上传,有兴趣的同学自行研究,做出来是这个样子的: