1.介绍
开发环境:Hololens2 + unity2020.3 +vs2019+python3.8
最近一直在研究如何让hololens与电脑通信,查阅资料发现使用Socket可以实现通信。
让hololens与电脑通信,其实就是做到让unity和电脑通信,然后根据需求开发hololens端的UI即可。
项目流程大概是在unity中编写C#脚本捕获和发送图片,电脑端用python接收图片并保存。
在UI方面,设计两个Hololens Button,用来挂载两个方法,一个控制捕获照片,另一个控制将捕获的照片发送到python服务器端。后续工作是想用接收来的图片来做图像处理等。
下面是一张unity项目图
2.项目代码
客户端代码:
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System;
using System.Net.Sockets;
using System.Net;
using System.Collections.Generic;
using UnityEngine.Windows.WebCam;
using Microsoft.MixedReality.Toolkit.UI;
public class CaptureAndSend : MonoBehaviour
{
// Hololens摄像头对象
PhotoCapture photoCaptureObject = null;
// 捕获的图片对象
Texture2D targetTexture = null;
// 图片的宽和高
int width = 1280;
int height = 720;
// 用于控制图片捕获和发送的按钮
public Interactable CaptureButton;
public Interactable SendButton;
// 服务器IP地址和端口号
string serverIP = "172.22.90.152";
int serverPort = 8888;
private TcpClient client;
// 图片数据的字节数组
byte[] imageBytes = null;
void Start()
{
client = new TcpClient(serverIP, serverPort);
// 获取按钮组件并添加点击事件
CaptureButton.OnClick.AddListener(CaptureImage);
SendButton.OnClick.AddListener(SendImage);
}
void OnDestroy()
{
// 释放摄像头资源
if (photoCaptureObject != null)
{
photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
photoCaptureObject.Dispose();
photoCaptureObject = null;
}
}
void CaptureImage()
{
// 初始化PhotoCapture对象并设置参数
PhotoCapture.CreateAsync(true, OnPhotoCaptureCreated);
}
void SendImage()
{
if (imageBytes != null)
{
try
{
// 创建TcpClient连接服务器
// 获取网络流并发送图片数据
NetworkStream stream = client.GetStream();
stream.Write(imageBytes, 0, imageBytes.Length);
Debug.Log("Image sent to server.");
// 关闭网络流和TcpClient连接
stream.Close();
client.Close();
}
catch (Exception e)
{
Debug.Log(e.ToString());
}
}
else
{
Debug.Log("imageBytes变量为空");
}
}
void OnPhotoCaptureCreated(PhotoCapture captureObject)
{
// 将PhotoCapture对象赋值给成员变量
photoCaptureObject = captureObject;
// 创建CameraParameters对象
CameraParameters camParameters = new CameraParameters();
camParameters.hologramOpacity = 0.0f;
camParameters.cameraResolutionWidth = width;
camParameters.cameraResolutionHeight = height;
camParameters.pixelFormat = CapturePixelFormat.PNG;
// 捕获图片并保存到Texture2D对象中
captureObject.StartPhotoModeAsync(camParameters, OnPhotoModeStarted);
}
void OnPhotoModeStarted(PhotoCapture.PhotoCaptureResult result)
{
if (result.success)
{
targetTexture = new Texture2D(width, height, TextureFormat.RGB24, false);
// 拍摄图像并保存到Texture2D对象中
photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemory);
}
else
{
Debug.LogError("Unable to start photo mode!");
}
}
void OnCapturedPhotoToMemory(PhotoCapture.PhotoCaptureResult result, PhotoCaptureFrame photoCaptureFrame)
{
if (result.success)
{
// 将图片数据转换为字节数组
imageBytes = ConvertToByteArray(photoCaptureFrame);
// 将图片数据显示到屏幕上
targetTexture.LoadImage(imageBytes);
//GetComponent<Renderer>().material.mainTexture = targetTexture;
Debug.Log("Image captured.");
}
else
{
Debug.LogError("Failed to capture photo.");
}
// 停止PhotoCapture模式并释放资源
photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
}
void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result)
{
// 释放PhotoCapture资源
photoCaptureObject.Dispose();
photoCaptureObject = null;
}
byte[] ConvertToByteArray(PhotoCaptureFrame photoCaptureFrame)
{
// 获取图像数据缓冲区
List<byte> buffer = new List<byte>();
photoCaptureFrame.CopyRawImageDataIntoBuffer(buffer);
// 将List<byte>转换为byte[]
byte[] bytes = buffer.ToArray();
return bytes;
}
}
服务器端代码:
import socket
import cv2
import numpy as np
import os
from datetime import datetime
# 创建socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取本地主机名
host = '172.22.90.152'
# 设置一个端口
port = 8888
# 绑定端口号
server_socket.bind((host, port))
# 设置最大连接数,超过后排队
server_socket.listen(5)
print('等待客户端连接...')
# 接受客户端连接
client_socket, address = server_socket.accept()
print('连接地址:', address)
# 接收图片数据
data = b''
while True:
packet = client_socket.recv(4096)
if not packet:
break
data += packet
# 将接收到的字节数据转换为OpenCV格式的图片
img_data = np.frombuffer(data, dtype=np.uint8)
img = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
print(img_data)
print('Received image size:', img.shape)
# 在窗口中显示接收到的图片
cv2.imshow('Received Image', img)
print("开始保存文件")
# 将接收到的图片保存到指定文件夹中
save_folder = './received_images'
if not os.path.exists(save_folder):
os.makedirs(save_folder)
print("创建文件夹")
current_time = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
filename = os.path.join(save_folder, f'received_image_{current_time}.jpg')
cv2.imwrite(filename, img)
print(f'Saved received image to {filename}')
cv2.waitKey()
# 关闭连接
client_socket.close()
server_socket.close()
3.开发流程
3.1客户端
创建一个Unity项目,转为UWP平台,将MRTK工具包导入项目工程,添加一个默认的MR场景。
导入XR Plug-in Management,按照下图设置
创建一个C#脚本文件,将上面客户端代码复制进去。代码中string serverIP可设为同一局域网下电脑的IP,如果在unity中调试可先设为127.0.0.1。
创建一个空节点命名为HololensCamera,来挂载刚刚创建的脚本。
接下来给脚本关联两个Button。
打开Toolbox,里面有很多Hololens按钮,我选择的是NearMenus下面的第一个
可以给按钮菜单栏重命名为Menu,在ButtonCollection节点下面有三个按钮,因为只用两个按钮,所以我删除了一个。可以给这两个按钮节点重命名为Capture个Send来方便区分。在按钮的Inspector里面可以设置图标,在IconAndText中可以设置显示文字。
将两个按钮拖拽到设置的空节点挂载的脚本的两个所需的Button引用上
这样以后unity项目就建立完成
3.2服务器端
在pycharm建立工程将python代码复制进去,导入所需的包。其中的ip也要设为自己电脑的ip。如果本机调试就可以先设为127.0.0.1 端口号和C#中同步。
4.调试
先运行服务器端代码,再运行unity启动Game模式,此时服务器端运行窗口会输出客户端连接的提示信息。在unity先点击拍摄按钮,在发送按钮。此时电脑端会受到图片,并将图片保存在了python项目同一文件夹下的received_image文件夹下。
在unity调试没有问题的话,再部署到hololens里面。
运行服务器,打开holo程序,点击捕捉按钮,然后发送,看一看图片有没有保存到文件夹里面。